From 6a2f41ab7c39cdff0324dd5c2200b68d405548a1 Mon Sep 17 00:00:00 2001 From: santoshkumarradha Date: Thu, 17 Sep 2026 00:46:02 -0400 Subject: [PATCH 001/173] internal/catalog: keep all three artificial-analysis indexes and the open-weights fact on a model row Co-Authored-By: codeaf --- internal/catalog/catalog.go | 58 ++++++++++++++-------- internal/catalog/catalog_test.go | 83 ++++++++++++++++++++++++++++++++ 2 files changed, 121 insertions(+), 20 deletions(-) diff --git a/internal/catalog/catalog.go b/internal/catalog/catalog.go index 198c4f8ac6..af30d5cdb5 100644 --- a/internal/catalog/catalog.go +++ b/internal/catalog/catalog.go @@ -107,22 +107,29 @@ type Model struct { // tops the webapps board and sits mid-table on 3d has a real strength an // average would report as mediocrity. Zero means nobody published one. ArenaElo float64 `json:"arena_elo,omitempty"` - // IntelligenceIndex is the one published score in the catalog, carried - // verbatim and never computed here. + // The three Artificial Analysis scores the catalog keeps, carried verbatim + // and never computed here. // // OpenRouter's rows may carry a `benchmarks` block, and inside it an // `artificial_analysis` object with `intelligence_index`, `coding_index` // and `agentic_index` — Artificial Analysis's numbers, republished. 155 of // 528 rows had one on 2026-08-11. Zero means NOBODY published a score, and - // never a model that scored zero: a surface showing this must render the + // never a model that scored zero: a surface showing these must render the // zero as absence the way it renders an absent price. // - // Only the intelligence index is kept. The other two are the same source - // saying the same thing at a different angle, and a catalog row is not the - // place to hold a benchmark suite. - IntelligenceIndex float64 `json:"intelligence_index,omitempty"` - InputModalities []string `json:"input_modalities,omitempty"` - OutputModalities []string `json:"output_modalities,omitempty"` + // All three are kept because they are read seat by seat: the agentic and + // coding indexes describe a long tool loop, the intelligence index a single + // reasoning call, and the seat asking decides which one it needs. + IntelligenceIndex float64 `json:"intelligence_index,omitempty"` + CodingIndex float64 `json:"coding_index,omitempty"` + AgenticIndex float64 `json:"agentic_index,omitempty"` + // OpenWeights says the row's weights are published — OpenRouter's + // `hugging_face_id`, kept as the one-word answer to whether the weights are + // public. A row cached before this field existed reads false, which every + // reader must take as unknown rather than closed. + OpenWeights bool `json:"open_weights,omitempty"` + InputModalities []string `json:"input_modalities,omitempty"` + OutputModalities []string `json:"output_modalities,omitempty"` // Parameters is which request fields the provider says this model accepts — // OpenRouter's `supported_parameters`, lowercased and deduped. // @@ -1023,7 +1030,10 @@ func fetch(ctx context.Context, options Options) ([]Model, error) { CacheReadPrice: cacheRead, PriceUnknown: !promptOK || !completionOK, ArenaElo: arenaElo(item.Benchmarks), - IntelligenceIndex: intelligenceIndex(item.Benchmarks), + IntelligenceIndex: analysisScore(item.Benchmarks, "intelligence_index"), + CodingIndex: analysisScore(item.Benchmarks, "coding_index"), + AgenticIndex: analysisScore(item.Benchmarks, "agentic_index"), + OpenWeights: item.HuggingFaceID != "", InputModalities: cleanLowerList(item.Architecture.Input), OutputModalities: cleanLowerList(item.Architecture.Output), Parameters: cleanLowerList(item.SupportedParameters), @@ -1083,6 +1093,10 @@ type modelWire struct { SupportedEfforts []string `json:"supported_efforts"` DefaultEffort string `json:"default_effort"` } `json:"reasoning"` + // HuggingFaceID is the row's `hugging_face_id`, read for the one fact it + // carries — the weights behind the row are published — and not kept on the + // Model beside that fact. + HuggingFaceID string `json:"hugging_face_id"` // Benchmarks stays raw so its shape cannot break the row around it. It // carried an object beside a LIST on 2026-08-11 (`design_arena: []` next // to `artificial_analysis: {…}`), which is exactly the kind of thing that @@ -1090,24 +1104,28 @@ type modelWire struct { Benchmarks json.RawMessage `json:"benchmarks"` } -// intelligenceIndex digs the one published score out of a raw benchmarks -// block, and answers zero for every shape it does not recognize. Nothing here -// is allowed to fail loudly: a score is a nicety on a row, and a catalog that -// refused to load because a benchmark changed shape would have traded four -// hundred models for one number. -func intelligenceIndex(raw json.RawMessage) float64 { +// analysisScore digs one published Artificial Analysis score out of a raw +// benchmarks block by its field name, and answers zero for every shape it does +// not recognize. Nothing here is allowed to fail loudly: a score is a nicety on +// a row, and a catalog that refused to load because a benchmark changed shape +// would have traded four hundred models for one number. The block is decoded to +// raw messages and only the asked-for field parsed, so one drifted score costs +// itself and never its siblings. +func analysisScore(raw json.RawMessage, field string) float64 { if len(raw) == 0 { return 0 } var block struct { - ArtificialAnalysis struct { - IntelligenceIndex float64 `json:"intelligence_index"` - } `json:"artificial_analysis"` + ArtificialAnalysis map[string]json.RawMessage `json:"artificial_analysis"` } if json.Unmarshal(raw, &block) != nil { return 0 } - if score := block.ArtificialAnalysis.IntelligenceIndex; score > 0 { + var score float64 + if json.Unmarshal(block.ArtificialAnalysis[field], &score) != nil { + return 0 + } + if score > 0 { return score } return 0 diff --git a/internal/catalog/catalog_test.go b/internal/catalog/catalog_test.go index d980b28061..5121ec8376 100644 --- a/internal/catalog/catalog_test.go +++ b/internal/catalog/catalog_test.go @@ -2,9 +2,12 @@ package catalog import ( "context" + "encoding/json" "errors" "io" "net/http" + "os" + "path/filepath" "strings" "sync/atomic" "testing" @@ -198,6 +201,86 @@ const cachePricePayload = `{"data":[ "pricing":{"prompt":"-1","completion":"-1"}} ]}` +// WHAT A SEAT ASKS DECIDES WHICH SCORE IT NEEDS. The three indexes the +// artificial_analysis block publishes describe different work — the agentic and +// coding scores a long tool loop, the intelligence score a single reasoning +// call — so all three travel on the row, and every shape the block can arrive +// in costs only the score it broke. OpenWeights rides the same row: a +// hugging_face_id is the published answer to whether the weights are public. +const benchmarkPayload = `{"data":[ + {"id":"bench/all","name":"All","architecture":{"input_modalities":["text"],"output_modalities":["text"]}, + "benchmarks":{"artificial_analysis":{"intelligence_index":56,"coding_index":41,"agentic_index":33}}, + "hugging_face_id":"deepseek-ai/DeepSeek-V3"}, + {"id":"bench/coding-null","name":"CodingNull","architecture":{"input_modalities":["text"],"output_modalities":["text"]}, + "benchmarks":{"artificial_analysis":{"intelligence_index":40,"coding_index":null,"agentic_index":"high"}}, + "hugging_face_id":""}, + {"id":"bench/no-benchmarks","name":"NoBenchmarks","architecture":{"input_modalities":["text"],"output_modalities":["text"]}} +]}` + +func TestACarriesTheThreeArtificialAnalysisIndexesAndOpenWeights(t *testing.T) { + dir := t.TempDir() + c := Load(context.Background(), Options{ + BaseURL: "https://openrouter.example/api/v1", Dir: dir, + HTTPClient: catalogClient(t, http.StatusOK, benchmarkPayload, nil), + }) + + all, ok := c.Model("bench/all") + if !ok { + t.Fatal("bench/all is missing from the catalog") + } + if all.IntelligenceIndex != 56 || all.CodingIndex != 41 || all.AgenticIndex != 33 { + t.Fatalf("bench/all indexes = %v/%v/%v, want 56/41/33", all.IntelligenceIndex, all.CodingIndex, all.AgenticIndex) + } + if !all.OpenWeights { + t.Fatal("a row with a hugging_face_id read as closed-weights") + } + + // Zero keeps meaning nobody published a score: a null and a non-number are + // absence, exactly like the missing block beside them, and an empty + // hugging_face_id is not a published one. + partial, _ := c.Model("bench/coding-null") + if partial.IntelligenceIndex != 40 || partial.CodingIndex != 0 || partial.AgenticIndex != 0 { + t.Fatalf("bench/coding-null indexes = %v/%v/%v, want 40/0/0", partial.IntelligenceIndex, partial.CodingIndex, partial.AgenticIndex) + } + if partial.OpenWeights { + t.Fatal("an empty hugging_face_id read as open-weights") + } + none, _ := c.Model("bench/no-benchmarks") + if none.IntelligenceIndex != 0 || none.CodingIndex != 0 || none.AgenticIndex != 0 { + t.Fatalf("bench/no-benchmarks invented indexes: %v/%v/%v", none.IntelligenceIndex, none.CodingIndex, none.AgenticIndex) + } + + // A second process reads the file the first one wrote; a cache that dropped + // the scores or the fact would hand it rows the fetch had filled in. + second := Load(context.Background(), Options{ + BaseURL: "https://openrouter.example/api/v1", Dir: dir, + HTTPClient: catalogClient(t, http.StatusInternalServerError, "", nil), + }) + if got, _ := second.Model("bench/all"); got.IntelligenceIndex != 56 || got.CodingIndex != 41 || got.AgenticIndex != 33 || !got.OpenWeights { + t.Fatalf("cached row lost the scores or the fact: %+v", got) + } + + // An older cache predating the fields serves the same rows: absence there + // is unknown, not zero-published, and nothing about loading refuses it. + legacy := t.TempDir() + day := time.Date(2026, 9, 9, 10, 0, 0, 0, time.UTC) + old, err := json.Marshal(cache{FetchedAt: day, Models: []Model{{ID: "legacy/model", IntelligenceIndex: 22}}}) + if err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(legacy, cacheName), old, 0o600); err != nil { + t.Fatal(err) + } + fromOld := Load(context.Background(), Options{ + BaseURL: DefaultBaseURL, Dir: legacy, Now: func() time.Time { return day.Add(time.Hour) }, + HTTPClient: failedCatalogClient(), + }) + got, ok := fromOld.Model("legacy/model") + if !ok || got.IntelligenceIndex != 22 || got.CodingIndex != 0 || got.AgenticIndex != 0 { + t.Fatalf("older cache row = %+v found %t", got, ok) + } +} + // WHAT A CACHE READ COSTS IS THE WHOLE OF WHAT A CACHE IS WORTH. A session // re-sends its transcript on every step, so the gap between the prompt price and // the cache-read price is the only number that says what the prefix saved — and From 837eb0801a34a5aceed3e08326d13efe836d91d0 Mon Sep 17 00:00:00 2001 From: santoshkumarradha Date: Thu, 17 Sep 2026 00:38:48 -0400 Subject: [PATCH 002/173] config, tui3: the crew reads the whole catalog by default, and both tables move onto the 2026-09-16 front The three preset words resolved the open-weight table unless the family row said otherwise; they resolve the catalog-wide one now, and the five shipped tier defaults are that family's balanced row. The worker, careful and mastermind seats are re-read off one plot of expected task bill against seat quality, priced under each seat's own call shape, in both families. The manual's tables, preset table, crew lines and the paragraph about how the ids were picked follow. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01Sb7knukoGMPXo8kK876MsU --- cmd/codeaf/chatv3_crew_test.go | 2 +- cmd/codeaf/seats_wiring_test.go | 5 +- .../1090-crew-default-family-is-all.md | 27 +++ internal/config/crew.go | 187 ++++++++++-------- internal/config/crew_test.go | 60 +++--- internal/config/seats.go | 6 +- internal/config/seats_test.go | 10 +- internal/config/settings.go | 54 ++--- internal/manual/chat/commands.md | 16 +- internal/manual/chat/models-and-cost.md | 119 +++++------ internal/tui3/crew.go | 8 +- internal/tui3/crew_test.go | 68 ++++--- internal/tui3/onboarding.go | 6 +- internal/tui3/settings.go | 8 +- 14 files changed, 333 insertions(+), 243 deletions(-) create mode 100644 docs/changes/unreleased/1090-crew-default-family-is-all.md diff --git a/cmd/codeaf/chatv3_crew_test.go b/cmd/codeaf/chatv3_crew_test.go index a615140e11..38c4cdcb66 100644 --- a/cmd/codeaf/chatv3_crew_test.go +++ b/cmd/codeaf/chatv3_crew_test.go @@ -122,7 +122,7 @@ func TestABrokenRowLeavesTheLastGoodCrewInPlace(t *testing.T) { t.Fatal(err) } want, _ := config.CrewModels(config.CrewMax) - if model, _ := roles.Resolve(roles.Source(source), roles.RolePlanner, "vendor/conversation"); model != "moonshotai/kimi-k3" { + if model, _ := roles.Resolve(roles.Source(source), roles.RolePlanner, "vendor/conversation"); model != want[config.ModelTierMastermind] { t.Fatalf("after the row parsed again the planner resolves to %q, want the max crew's %q", model, want[config.ModelTierMastermind]) } diff --git a/cmd/codeaf/seats_wiring_test.go b/cmd/codeaf/seats_wiring_test.go index 033d56ef15..6e6c78ce87 100644 --- a/cmd/codeaf/seats_wiring_test.go +++ b/cmd/codeaf/seats_wiring_test.go @@ -148,7 +148,10 @@ func TestAnErrandWithNoFlagsRunsTheProfilesCrewAndSaysSo(t *testing.T) { t.Fatalf("the errand did not settle cleanly: %v\nstderr:\n%s", err, stderr.String()) } - work := config.TierModelAt(script.dir, config.ModelTierLow) + // THE WORK SEAT IS THE WORKER ROW, which is what [config.ResolveSeats] fills + // it from. It used to be read off the small-work row here and pass anyway, + // because the frugal preset happened to put one id on both. + work := config.TierModelAt(script.dir, config.ModelTierWorker) plan := config.TierModelAt(script.dir, config.ModelTierMastermind) for _, model := range []string{work, plan} { found := false diff --git a/docs/changes/unreleased/1090-crew-default-family-is-all.md b/docs/changes/unreleased/1090-crew-default-family-is-all.md new file mode 100644 index 0000000000..73d7b1f980 --- /dev/null +++ b/docs/changes/unreleased/1090-crew-default-family-is-all.md @@ -0,0 +1,27 @@ +--- +kind: changed +title: The crew's default family is `all`, and both families' worker, careful and mastermind seats moved +pr: 1090 +surface: [chat, engine] +invalidates: + - "`models.crew.source` defaulted to `open`, so a profile that never answered the row resolved the open-weight table. The default is now `all`: `DefaultCrewSource = CrewSourceAll`, and a profile that never chose a family resolves the all family's balanced row — `google/gemini-2.5-flash`, `deepseek/deepseek-v4-flash-0731`, `z-ai/glm-5.3-flash`, `anthropic/claude-fable-5.1`, `anthropic/claude-fable-5.1`." + - "The five shipped tier defaults were the open table's balanced row. `DefaultReflexModel` is now `google/gemini-2.5-flash`, `DefaultHighModel` and `DefaultMastermindModel` are `anthropic/claude-fable-5.1`; `DefaultLowModel` and `DefaultWorkerModel` are unchanged. The identity the crew row rests on still holds: the five defaults are exactly the DEFAULT family's balanced row." + - "`config.CrewModels` and `config.CrewLine` answered the OPEN family. They answer the default family, which is now `all`; the open table is reached through `CrewModelsForSource(CrewSourceOpen, …)` and `CrewLineFor(CrewSourceOpen, …)`. A blank, misspelt or retired family word still folds to the default, which is `all` rather than `open`." + - "A profile that applied a preset under an older build now reads `custom`, because the worker, careful and mastermind seats moved in both families. The crew word, the `/crew` highlight and the seat rung all say `custom` until the preset is applied again, which writes the new five." + - "The open table's worker column is unchanged and its mastermind column is not: frugal now plans on `z-ai/glm-5.3-flash`, and balanced and max both plan on `z-ai/glm-5.3` rather than `moonshotai/kimi-k3`. The all table's worker holds `z-ai/glm-5.3-flash` through balanced and `openai/gpt-5.6-sol` and `anthropic/claude-opus-5` are in no preset of it any more." + - "The `/crew` listing named the family whenever the row was not `open`. It names it whenever the row is not the default family, so an `open` listing carries the `family · open models` line and an `all` one does not." +--- + +Both tables are read off one plot, computed seat by seat on 2026-09-16: the +expected bill a seat's own call shape runs up, built from the catalog's +published prompt, completion and cache-read prices, against that seat's quality +from its published intelligence, coding and agentic indexes. The worker and the +careful seats are priced as long cached loops and the mastermind as one-shot +calls, which is why the columns no longer climb together in either family. + +In the all family the worker stays on `glm-5.3-flash` through balanced because +the worker seat carries most of a task's tokens: a step there multiplies through +the whole bill, where a step on the careful or the mastermind seat is paid a +handful of times. The laws around the tables are untouched — the careful seat is +a different vendor from the worker in every preset of both families and still +sees images, and the reflex and small-work columns still never vary. diff --git a/internal/config/crew.go b/internal/config/crew.go index 0e9ed04c9b..6c007ee153 100644 --- a/internal/config/crew.go +++ b/internal/config/crew.go @@ -22,17 +22,14 @@ import ( // asked to think. // // TWO FAMILIES BEHIND THE SAME THREE WORDS. Every preset exists twice: once in -// the open family, every seat an open-weight model, which is the property that -// makes a shipped default defensible, because nobody's default crew should be -// a bet on one vendor's pricing; and once in the all family, the same three -// words resolved over the whole catalog with closed and frontier models in it, -// for the person who has chosen to spend what those cost. Which family the -// words draw from is one row, [KeyCrewSource], and open is what it reads when -// nobody has answered it: the all family is a thing a person asks for, never -// one they get by saying nothing. Both the derived reading and the write -// resolve through the row ([CrewSourceAt]), so the word on the sheet can never -// mean one family while the five values it summarizes were drawn from the -// other. +// the all family, the same three words resolved over the whole catalog with +// closed and frontier models in it; and once in the open family, every seat an +// open-weight model, for the person who wants the three words to stay on that +// shelf whatever the catalog does around it. Which family the words draw from +// is one row, [KeyCrewSource], and all is what it reads when nobody has +// answered it. Both the derived reading and the write resolve through the row +// ([CrewSourceAt]), so the word on the sheet can never mean one family while +// the five values it summarizes were drawn from the other. // // THE PRESET IS DERIVED AND NEVER STORED. [CrewAt] reads the five live tier // values and answers which preset they are, or "custom". A stored word would be @@ -63,101 +60,125 @@ const ( var CrewPresets = []string{CrewFrugal, CrewBalanced, CrewMax} // DefaultCrew is what a profile nobody has touched reads. It is balanced because -// the five shipped tier defaults ARE the balanced row — see [crewModels] — and -// that identity is asserted by a test rather than trusted. +// the five shipped tier defaults ARE the balanced row of the DEFAULT FAMILY — +// see [crewAllModels] — and that identity is asserted by a test rather than +// trusted. const DefaultCrew = CrewBalanced // The two families the preset words can draw from. They are the values // [KeyCrewSource] takes, spelled here once and read by the row, the resolver // and the manual. const ( - // CrewSourceOpen is the open-weight family and the shipped default. + // CrewSourceOpen is the open-weight family. CrewSourceOpen = "open" - // CrewSourceAll is the whole catalog, closed and frontier models included. + // CrewSourceAll is the whole catalog, closed and frontier models included, + // and the family a profile that has answered nothing resolves. CrewSourceAll = "all" ) -// CrewSources lists them, open first, which is also the default and the order -// the row widens in. +// CrewSources lists them, open first, which is the order the row widens in: the +// narrower shelf, then the whole catalog. The default is the second of them, +// [DefaultCrewSource], because this list is about width and not about which one +// a profile starts on. var CrewSources = []string{CrewSourceOpen, CrewSourceAll} -// DefaultCrewSource is open, for the same reason the shipped crew is open -// weights: a family nobody chose is the one this build can defend. -const DefaultCrewSource = CrewSourceOpen +// DefaultCrewSource is all: the three words are read off the whole catalog +// unless the row says otherwise, and the five shipped tier defaults are that +// family's balanced row. +const DefaultCrewSource = CrewSourceAll -// crewModels is the whole table: one row per preset, one model per class. +// crewModels is the open-weight table: one row per preset, one model per class. // -// THE WORKER COLUMN IS THE DIAL. It climbs the open-weight pareto front one step -// per preset — deepseek-v4-flash, glm-5.3-flash, glm-5.3 — because it is the -// seat that pays most of a task's bill, and a preset that moved every other seat -// while leaving it alone would change everything about a task except its cost. -// The mastermind column buys a bigger planning model without choosing its -// generation behavior. The careful column is always a DIFFERENT VENDOR from -// the worker and always sees images (the vision role rides it). The reflex and -// low columns never vary: they are the same -// near-free models in all three presets, and a column that never varies is not -// a dial. +// THE WORKER COLUMN IS THE DIAL. It climbs one step per preset — +// deepseek-v4-flash, glm-5.3-flash, glm-5.3 — because it is the seat that pays +// most of a task's bill, and a preset that moved every other seat while leaving +// it alone would change everything about a task except its cost. The careful +// column is always a DIFFERENT VENDOR from the worker and always sees images +// (the vision role rides it). The reflex and low columns never vary: they are +// the same near-free models in all three presets, and a column that never +// varies is not a dial. // -// Every id is an open-weight model, picked off the catalog's own published -// scores against blended price: 2026-09-01 for the roster as a whole, with the -// frugal mastermind and the balanced careful and mastermind re-picked on -// 2026-09-16 (the manual's open table lists every seat; settings.go's -// DefaultWorkerModel says how). No closed model is here: the open family is the -// shelf that has to stand on price alone, and the `all` family is where a closed -// model goes. +// HOW THE IDS WERE READ OFF, on 2026-09-16 and seat by seat. Every open-weight +// row of the catalog was placed on two axes: the expected bill that seat's own +// call shape runs up, built from the catalog's published prompt, completion and +// cache-read prices, against that seat's quality, taken from its published +// intelligence, coding and agentic indexes. THE CALL SHAPE IS PART OF THE +// PRICE. The worker and the careful seats were costed as LONG CACHED LOOPS — a +// large prompt read back turn after turn, so the cache-read price carries most +// of the weight — and the mastermind as ONE-SHOT CALLS, where the prompt is +// paid in full each time and there are few of them. Each preset then takes, for +// each seat, a point on the pareto front of that plot at the bill it is willing +// to run: nothing on the front costs less at the same quality, and nothing at +// the same bill scores higher. +// +// That is why the columns do not climb together. Under one-shot pricing +// glm-5.3-flash is on the front at frugal's bill and glm-5.3 is the next point +// above it, so the mastermind column reads flash, glm-5.3, glm-5.3; under +// long-loop pricing the same plot puts kimi-k3 on the careful seat from +// balanced upward, which is also the second vendor that seat has to be. +// +// No closed model is here: the open family is the shelf that stands on the +// open-weight rows alone, and the `all` family is where a closed model goes. var crewModels = map[string]map[string]string{ CrewFrugal: { ModelTierReflex: "mistralai/mistral-nemo", ModelTierLow: "deepseek/deepseek-v4-flash-0731", ModelTierWorker: "deepseek/deepseek-v4-flash-0731", ModelTierHigh: "z-ai/glm-5.3-flash", - ModelTierMastermind: "z-ai/glm-5.3", + ModelTierMastermind: "z-ai/glm-5.3-flash", }, CrewBalanced: { ModelTierReflex: "mistralai/mistral-nemo", ModelTierLow: "deepseek/deepseek-v4-flash-0731", ModelTierWorker: "z-ai/glm-5.3-flash", ModelTierHigh: "moonshotai/kimi-k3", - ModelTierMastermind: "moonshotai/kimi-k3", + ModelTierMastermind: "z-ai/glm-5.3", }, CrewMax: { ModelTierReflex: "mistralai/mistral-nemo", ModelTierLow: "deepseek/deepseek-v4-flash-0731", ModelTierWorker: "z-ai/glm-5.3", ModelTierHigh: "moonshotai/kimi-k3", - ModelTierMastermind: "moonshotai/kimi-k3", + ModelTierMastermind: "z-ai/glm-5.3", }, } // crewAllModels is the same three presets answered from the whole catalog // rather than its open-weight shelf, which is what the `all` family of -// [KeyCrewSource] draws from. The careful column is the same law here as +// [KeyCrewSource] draws from AND WHAT A PROFILE THAT HAS ANSWERED NOTHING +// RESOLVES ([DefaultCrewSource]). The careful column is the same law here as // there: a DIFFERENT VENDOR from the worker in every preset, and the reflex -// and low columns still never vary. The ids are locked the way the open ones -// are, off the catalog's own published scores against blended price, and -// closed models live here and only here. +// and low columns still never vary. Closed models live here and only here. +// +// The ids come off the same plot [crewModels] describes, run on 2026-09-16 over +// every row of the catalog rather than the open-weight ones: expected task bill +// against seat quality, the bill built from the published prompt, completion +// and cache-read prices under each seat's own call shape — the worker and the +// careful seats as long cached loops, the mastermind as one-shot calls — and +// the quality from the published intelligence, coding and agentic indexes. // -// TWO COLUMNS SPEND DIFFERENTLY HERE. The worker column is [crewModels]'s dial -// but moves once, not per preset: gpt-5.6-sol holds frugal and balanced, and -// only max buys claude-fable-5.1, because on the catalog the coding ceiling is -// the expensive step and the two steps below it are the same model. What -// frugal-to-balanced buys here is the careful seat (gemini-3.8-flash to -// claude-opus-5) and a smarter mastermind, not a dearer worker. And max's -// mastermind sits on the worker's own model on purpose: fable-5.1 is the -// catalog's ceiling, so there is no better planner to buy above it. +// THE WORKER STAYS ON glm-5.3-flash THROUGH BALANCED, and that is the whole +// shape of this table. The worker seat carries most of a task's tokens, so a +// step there multiplies through the entire bill while a step on the careful or +// the mastermind seat is paid a handful of times. The money therefore goes to +// the two low-volume seats first: frugal to balanced moves careful from +// gemini-3.8-flash to claude-fable-5.1 and the mastermind from glm-5.3-flash to +// claude-fable-5.1, and only max moves the worker itself. Max's mastermind then +// sits on the worker's own model because at that bill the one-shot front has no +// separate planner above it. var crewAllModels = map[string]map[string]string{ CrewFrugal: { ModelTierReflex: "google/gemini-2.5-flash", ModelTierLow: "deepseek/deepseek-v4-flash-0731", - ModelTierWorker: "openai/gpt-5.6-sol", + ModelTierWorker: "z-ai/glm-5.3-flash", ModelTierHigh: "google/gemini-3.8-flash", - ModelTierMastermind: "anthropic/claude-opus-5", + ModelTierMastermind: "z-ai/glm-5.3-flash", }, CrewBalanced: { ModelTierReflex: "google/gemini-2.5-flash", ModelTierLow: "deepseek/deepseek-v4-flash-0731", - ModelTierWorker: "openai/gpt-5.6-sol", - ModelTierHigh: "anthropic/claude-opus-5", + ModelTierWorker: "z-ai/glm-5.3-flash", + ModelTierHigh: "anthropic/claude-fable-5.1", ModelTierMastermind: "anthropic/claude-fable-5.1", }, CrewMax: { @@ -169,15 +190,15 @@ var crewAllModels = map[string]map[string]string{ }, } -// crewTableFor is the family one source word names. It is total: `all` names -// the catalog-wide family, and every other reading (blank, misspelt, a word a -// later build retired) names the open one, because a family nobody asked for -// is the one this build can defend. +// crewTableFor is the family one source word names. It is total: `open` names +// the open-weight family, and every other reading (blank, misspelt, a word a +// later build retired) names the catalog-wide one, because an unreadable answer +// resolves the same family an absent answer does ([DefaultCrewSource]). func crewTableFor(source string) map[string]map[string]string { - if normalCrewSource(source) == CrewSourceAll { - return crewAllModels + if normalCrewSource(source) == CrewSourceOpen { + return crewModels } - return crewModels + return crewAllModels } // knownCrewSource folds a word and says whether it is one of the families this @@ -206,8 +227,8 @@ func normalCrewSource(source string) string { // CrewSourceAt is which family the preset words draw from on this profile, // [DefaultCrewSource] when the row is absent. A word this build does not know -// reads as open, silently, the way a retired choice reads everywhere else on -// this sheet. +// reads as the default family, silently, the way a retired choice reads +// everywhere else on this sheet. func CrewSourceAt(profileDir string) string { if value, ok := persistedString(profileDir, KeyCrewSource); ok { return normalCrewSource(value) @@ -237,37 +258,37 @@ func CrewModelsForSource(source, preset string) (map[string]string, bool) { // contradiction the chooser exists to prevent. var crewLines = map[string]map[string]string{ CrewSourceOpen: { - CrewFrugal: "deepseek works, glm-5.3 thinks · pennies a day", - CrewBalanced: "glm-flash works, kimi-k3 checks and thinks", - CrewMax: "glm-5.3 works, kimi-k3 thinks and checks", + CrewFrugal: "deepseek works, glm-flash checks and thinks · pennies a day", + CrewBalanced: "glm-flash works, kimi-k3 checks, glm-5.3 thinks", + CrewMax: "glm-5.3 works and thinks, kimi-k3 checks", }, CrewSourceAll: { - CrewFrugal: "gpt-5.6-sol works, gemini-flash checks, opus thinks", - CrewBalanced: "gpt-5.6-sol works, opus checks, fable thinks", + CrewFrugal: "glm-flash works and thinks, gemini-flash checks", + CrewBalanced: "glm-flash works, fable checks and thinks", CrewMax: "fable works and thinks, astra checks", }, } -// CrewLine is one preset's own line in the OPEN family, empty for a word that is -// not a preset. The family-aware spelling is [CrewLineFor]. +// CrewLine is one preset's own line in the DEFAULT family, empty for a word +// that is not a preset. The family-aware spelling is [CrewLineFor]. func CrewLine(preset string) string { - return CrewLineFor(CrewSourceOpen, preset) + return CrewLineFor(DefaultCrewSource, preset) } // CrewLineFor is one preset's own line in one family, empty for a word that is -// not a preset. A family this build does not know reads as open, the way the -// row does. +// not a preset. A family this build does not know reads as the default one, the +// way the row does. func CrewLineFor(source, preset string) string { return crewLines[normalCrewSource(source)][strings.ToLower(strings.TrimSpace(preset))] } -// CrewModels is the five models one preset would set in the OPEN family, by +// CrewModels is the five models one preset would set in the DEFAULT family, by // tier word: the family a profile nobody has touched reads, and the spelling // the callers hold. The family-aware spelling is // [CrewModelsForSource]. It returns a copy, because a caller printing the // table must not be able to edit it. func CrewModels(preset string) (map[string]string, bool) { - return CrewModelsForSource(CrewSourceOpen, preset) + return CrewModelsForSource(DefaultCrewSource, preset) } // CrewAt is the crew as the five live tier values make it: the preset they are, @@ -282,8 +303,8 @@ func CrewModels(preset string) (map[string]string, bool) { // // The comparison runs against the family [CrewSourceAt] names, so the reading // moves with the row and never behind it: flip the family and a crew the old -// family wrote matches nothing, which reads as custom and is true, because -// five open ids are not any all-family preset either. +// family wrote matches nothing, which reads as custom and is true, because one +// family's five ids are not any preset of the other. func CrewAt(profileDir string) string { family := CrewSourceAt(profileDir) live := make(map[string]string, len(ModelTiers)) @@ -386,7 +407,7 @@ func SetCrewSource(profileDir, source string) error { // CrewSummary is the one line a crew change confirms itself with: // -// crew → balanced · brain kimi-k3 · hands glm-5.3-flash · checks kimi-k3 +// crew → balanced · brain claude-fable-5.1 · hands glm-5.3-flash · checks claude-fable-5.1 // // The three names are the classes a person actually asked about — what thinks, // what works, what checks — and HANDS IS THE WORKER: the seat that does the @@ -401,7 +422,7 @@ func CrewSummary(profileDir string) string { // CrewClasses is the three class names alone: // -// brain kimi-k3 · hands glm-5.3-flash · checks kimi-k3 +// brain claude-fable-5.1 · hands glm-5.3-flash · checks claude-fable-5.1 // // It is the tail of [CrewSummary] lifted out because a second surface prints the // crew now — /status, where the word already has a label of its own and "crew →" @@ -417,7 +438,7 @@ func CrewClasses(profileDir string) string { // CrewClassModels is the three ids [CrewClasses] names, in that order and // without the role words in front of them: // -// kimi-k3, glm-5.3-flash, kimi-k3 +// claude-fable-5.1, glm-5.3-flash, claude-fable-5.1 // // It exists because a surface drawing the crew line has to be able to say which // runs of it are the ANSWER — the ids a person typed /crew to change — and which diff --git a/internal/config/crew_test.go b/internal/config/crew_test.go index 017a406845..64dc1ce484 100644 --- a/internal/config/crew_test.go +++ b/internal/config/crew_test.go @@ -38,6 +38,10 @@ func TestTheShippedTierDefaultsAreExactlyTheBalancedCrew(t *testing.T) { } } +// THE OPEN FAMILY IS LOCKED TOO, and this is its page. It is read through +// [CrewModelsForSource] and not [CrewModels], because the bare spelling answers +// the DEFAULT family and the default family is the catalog-wide one — the all +// table's own page is [TestTheAllFamilyNamesTheLockedModels]. func TestCrewPresetsNameTheApprovedModels(t *testing.T) { want := map[string]map[string]string{ CrewFrugal: { @@ -45,25 +49,25 @@ func TestCrewPresetsNameTheApprovedModels(t *testing.T) { ModelTierLow: "deepseek/deepseek-v4-flash-0731", ModelTierWorker: "deepseek/deepseek-v4-flash-0731", ModelTierHigh: "z-ai/glm-5.3-flash", - ModelTierMastermind: "z-ai/glm-5.3", + ModelTierMastermind: "z-ai/glm-5.3-flash", }, CrewBalanced: { ModelTierReflex: "mistralai/mistral-nemo", ModelTierLow: "deepseek/deepseek-v4-flash-0731", ModelTierWorker: "z-ai/glm-5.3-flash", ModelTierHigh: "moonshotai/kimi-k3", - ModelTierMastermind: "moonshotai/kimi-k3", + ModelTierMastermind: "z-ai/glm-5.3", }, CrewMax: { ModelTierReflex: "mistralai/mistral-nemo", ModelTierLow: "deepseek/deepseek-v4-flash-0731", ModelTierWorker: "z-ai/glm-5.3", ModelTierHigh: "moonshotai/kimi-k3", - ModelTierMastermind: "moonshotai/kimi-k3", + ModelTierMastermind: "z-ai/glm-5.3", }, } for preset, expected := range want { - got, ok := CrewModels(preset) + got, ok := CrewModelsForSource(CrewSourceOpen, preset) if !ok { t.Fatalf("there is no %s preset", preset) } @@ -248,15 +252,15 @@ func TestTheAllFamilyNamesTheLockedModels(t *testing.T) { CrewFrugal: { ModelTierReflex: "google/gemini-2.5-flash", ModelTierLow: "deepseek/deepseek-v4-flash-0731", - ModelTierWorker: "openai/gpt-5.6-sol", + ModelTierWorker: "z-ai/glm-5.3-flash", ModelTierHigh: "google/gemini-3.8-flash", - ModelTierMastermind: "anthropic/claude-opus-5", + ModelTierMastermind: "z-ai/glm-5.3-flash", }, CrewBalanced: { ModelTierReflex: "google/gemini-2.5-flash", ModelTierLow: "deepseek/deepseek-v4-flash-0731", - ModelTierWorker: "openai/gpt-5.6-sol", - ModelTierHigh: "anthropic/claude-opus-5", + ModelTierWorker: "z-ai/glm-5.3-flash", + ModelTierHigh: "anthropic/claude-fable-5.1", ModelTierMastermind: "anthropic/claude-fable-5.1", }, CrewMax: { @@ -289,13 +293,13 @@ func TestTheAllFamilyNamesTheLockedModels(t *testing.T) { // pick is the contradiction the chooser exists to prevent. wantLines := map[string]map[string]string{ CrewSourceOpen: { - CrewFrugal: "deepseek works, glm-5.3 thinks · pennies a day", - CrewBalanced: "glm-flash works, kimi-k3 checks and thinks", - CrewMax: "glm-5.3 works, kimi-k3 thinks and checks", + CrewFrugal: "deepseek works, glm-flash checks and thinks · pennies a day", + CrewBalanced: "glm-flash works, kimi-k3 checks, glm-5.3 thinks", + CrewMax: "glm-5.3 works and thinks, kimi-k3 checks", }, CrewSourceAll: { - CrewFrugal: "gpt-5.6-sol works, gemini-flash checks, opus thinks", - CrewBalanced: "gpt-5.6-sol works, opus checks, fable thinks", + CrewFrugal: "glm-flash works and thinks, gemini-flash checks", + CrewBalanced: "glm-flash works, fable checks and thinks", CrewMax: "fable works and thinks, astra checks", }, } @@ -308,12 +312,13 @@ func TestTheAllFamilyNamesTheLockedModels(t *testing.T) { } } -// A SOURCE NOBODY SET IS THE OPEN FAMILY. The all table may exist, but a profile -// that never answered the row resolves the open family: the shipped defaults, -// the open presets under the same words, and a write that lands the open ids. -// This is the default-preservation law for the FAMILY. An unknown word is held -// to the same law, silently, the way a retired choice reads everywhere else. -func TestASourceNobodyAnsweredReadsTheOpenFamily(t *testing.T) { +// A SOURCE NOBODY SET IS THE DEFAULT FAMILY, which is the catalog-wide one. The +// open table exists beside it, but a profile that never answered the row +// resolves `all`: the shipped defaults, the all presets under the same words, +// and a write that lands the all ids. This is the default-preservation law for +// the FAMILY. An unknown word is held to the same law, silently, the way a +// retired choice reads everywhere else. +func TestASourceNobodyAnsweredReadsTheDefaultFamily(t *testing.T) { dir := t.TempDir() if got := CrewSourceAt(dir); got != DefaultCrewSource { t.Fatalf("an untouched profile reads the source as %q, want %q", got, DefaultCrewSource) @@ -324,20 +329,21 @@ func TestASourceNobodyAnsweredReadsTheOpenFamily(t *testing.T) { if err := ApplyCrew(dir, CrewMax); err != nil { t.Fatal(err) } - want, _ := CrewModels(CrewMax) + want, _ := CrewModelsForSource(DefaultCrewSource, CrewMax) for _, tier := range ModelTiers { if got := TierModelAt(dir, tier); got != want[tier] { - t.Errorf("with no source set, max wrote %s to %q, want the open %q", tier, got, want[tier]) + t.Errorf("with no source set, max wrote %s to %q, want the default family's %q", tier, got, want[tier]) } } - // A WORD THIS BUILD DOES NOT KNOW READS AS OPEN, silently. Being told a - // preference from an older build is now an error is the one thing a - // retirement must never do, and a family is not a thing to guess either. + // A WORD THIS BUILD DOES NOT KNOW READS AS THE DEFAULT FAMILY, silently. + // Being told a preference from an older build is now an error is the one + // thing a retirement must never do, and a family is not a thing to guess + // either. if err := writeProfileValue(dir, KeyCrewSource, "wide"); err != nil { t.Fatal(err) } - if got := CrewSourceAt(dir); got != CrewSourceOpen { - t.Fatalf("an unknown source word reads as %q, want %q", got, CrewSourceOpen) + if got := CrewSourceAt(dir); got != DefaultCrewSource { + t.Fatalf("an unknown source word reads as %q, want %q", got, DefaultCrewSource) } if got := CrewAt(dir); got != CrewMax { t.Fatalf("an unknown source word moved the crew to %q", got) @@ -388,7 +394,7 @@ func TestFlippingToAllMakesTheWordsMeanTheAllFamily(t *testing.T) { if got := CrewAt(dir); got != CrewBalanced { t.Fatalf("the crew reads %q after balanced was applied under open again", got) } - open, _ := CrewModels(CrewBalanced) + open, _ := CrewModelsForSource(CrewSourceOpen, CrewBalanced) for _, tier := range ModelTiers { if got := TierModelAt(dir, tier); got != open[tier] { t.Errorf("after re-applying under open, %s reads %q, want the open %q", tier, got, open[tier]) diff --git a/internal/config/seats.go b/internal/config/seats.go index 71b0e5a90a..4066a73c1b 100644 --- a/internal/config/seats.go +++ b/internal/config/seats.go @@ -411,7 +411,7 @@ func (s Seat) Report() string { return withNotice(s.Line(), s.Notice()) } // Describe is one seat in a receipt's voice: // -// work deepseek/deepseek-v4-flash-0731 (crew frugal) +// work z-ai/glm-5.3-flash (crew frugal) // plan follows the work model (default) // // THE EMPTINESS LAW, as the crew row already keeps it: an unfilled plan seat is @@ -441,7 +441,7 @@ type Seats struct { // Sentence is both seats, unlabelled, for a door whose opening lines have a // label column of their own: // -// work deepseek/deepseek-v4-flash-0731 (crew frugal) · plan z-ai/glm-5.3 (crew frugal) +// work z-ai/glm-5.3-flash (crew frugal) · plan z-ai/glm-5.3-flash (crew frugal) // // ONE SHAPE AND NOT TWO. It would read a little better to collapse a run whose // seats came from the same crew into one clause, and it would mean a script @@ -453,7 +453,7 @@ func (s Seats) Sentence() string { // Line is the one line a headless run opens with: // -// models: work deepseek/deepseek-v4-flash-0731 (crew frugal) · plan z-ai/glm-5.3 (crew frugal) +// models: work z-ai/glm-5.3-flash (crew frugal) · plan z-ai/glm-5.3-flash (crew frugal) func (s Seats) Line() string { return modelsLabel + s.Sentence() } // Notice is the inheritance line for whichever seat was filled by an older row, diff --git a/internal/config/seats_test.go b/internal/config/seats_test.go index d863b3e2ba..223f5fcd4a 100644 --- a/internal/config/seats_test.go +++ b/internal/config/seats_test.go @@ -43,8 +43,8 @@ func TestTheSeatLadderAnswersInItsOwnOrder(t *testing.T) { { name: "the crew answers when it is the only thing said", crew: CrewFrugal, - work: "deepseek/deepseek-v4-flash-0731", workRung: "crew frugal", - plan: "z-ai/glm-5.3", planRung: "crew frugal", + work: "z-ai/glm-5.3-flash", workRung: "crew frugal", + plan: "z-ai/glm-5.3-flash", planRung: "crew frugal", }, { name: "the environment outranks the crew", @@ -78,8 +78,8 @@ func TestTheSeatLadderAnswersInItsOwnOrder(t *testing.T) { // An operator can still put a level on a flag or tier row. name: "the crew leaves generation behavior to the model", crew: CrewMax, - work: "z-ai/glm-5.3", workRung: "crew max", - plan: "moonshotai/kimi-k3", planRung: "crew max", + work: "anthropic/claude-fable-5.1", workRung: "crew max", + plan: "anthropic/claude-fable-5.1", planRung: "crew max", }, { // And a flag carrying one is not shortened either, so the two rungs @@ -98,7 +98,7 @@ func TestTheSeatLadderAnswersInItsOwnOrder(t *testing.T) { // it: the same row a task handed off in conversation rides. hand: map[string]string{ModelTierWorker: "vendor/my-own-worker"}, work: "vendor/my-own-worker", workRung: "crew custom", - plan: "z-ai/glm-5.3", planRung: "crew custom", + plan: "z-ai/glm-5.3-flash", planRung: "crew custom", }, { // A row cleared on purpose means "follow the conversation", and a diff --git a/internal/config/settings.go b/internal/config/settings.go index 6ad56c26ff..77826d3e3f 100644 --- a/internal/config/settings.go +++ b/internal/config/settings.go @@ -994,8 +994,9 @@ const ( var ModelTiers = []string{ModelTierReflex, ModelTierLow, ModelTierWorker, ModelTierHigh, ModelTierMastermind} // THE SHIPPED CREW. All five tiers arrive pointed at a model, and the five -// together are exactly the `balanced` preset (crew.go) — which is what makes the -// crew row read "balanced" on a profile nobody has touched instead of reading +// together are exactly the `balanced` row of the DEFAULT FAMILY (crew.go's +// [crewAllModels], named by [DefaultCrewSource]) — which is what makes the crew +// row read "balanced" on a profile nobody has touched instead of reading // "custom" about its own defaults. // // Each is a bare OpenRouter id, spelled ONCE here and read by every caller @@ -1007,29 +1008,30 @@ var ModelTiers = []string{ModelTierReflex, ModelTierLow, ModelTierWorker, ModelT // to, which is [roles.Resolve]'s floor. UNSET and CLEARED are different answers // here, and that distinction is the whole mechanism ([TierModelAt] says how). // -// The ids are OPEN-WEIGHT MODELS, chosen off the catalog's own published -// scores (OpenRouter republishes Artificial Analysis's coding and agentic -// indexes on every row) against the blended price (crew.go's crewModels comment -// owns the date). The low row is pinned to a DATED build on purpose: the bare `deepseek/deepseek-v4-flash` id -// resolves to the April build, and the July build at the same price scores -// thirteen coding points higher. +// The ids are read off the catalog's own published rows — its intelligence, +// coding and agentic indexes against its prompt, completion and cache-read +// prices, priced under each seat's own call shape (crew.go's [crewAllModels] +// comment owns the method and the date). The low row is pinned to a DATED build +// on purpose: the bare `deepseek/deepseek-v4-flash` id resolves to the April +// build, and the July build costs the same. const ( - DefaultReflexModel = "mistralai/mistral-nemo" + DefaultReflexModel = "google/gemini-2.5-flash" DefaultLowModel = "deepseek/deepseek-v4-flash-0731" - // The worker is the seat that pays most of a task's bill, so the balanced - // crew puts the best agentic score per dollar on it rather than the best - // score: glm-5.3-flash sits one point under glm-5.3 on the agentic index at - // a twentieth of the price, and it can see images, which the parent can hand - // it without a vision detour. + // The worker is the seat that pays most of a task's bill, so it is the last + // seat a preset spends on: a step here multiplies through every token a task + // runs up, where a step on the two low-volume seats is paid a handful of + // times. glm-5.3-flash is the point on the long-cached-loop front that + // balanced runs at, and it can see images, which the parent can hand it + // without a vision detour. DefaultWorkerModel = "z-ai/glm-5.3-flash" // The careful tier is ALWAYS A DIFFERENT VENDOR FROM THE WORKER, in every // preset, and always a model that sees images: a check from a second vendor // catches what the first vendor's blind spots let through, and the vision // role rides this row. - DefaultHighModel = "moonshotai/kimi-k3" + DefaultHighModel = "anthropic/claude-fable-5.1" // The mastermind names a capable planning model. Its generation behavior is // left to the provider unless an operator adds a level to the model id. - DefaultMastermindModel = "moonshotai/kimi-k3" + DefaultMastermindModel = "anthropic/claude-fable-5.1" ) // DocumentEngines are the four rungs CODEAF_DOC_ENGINE accepts. @@ -2278,16 +2280,16 @@ func (s *Settings) build() []Setting { // THE FAMILY THE THREE WORDS DRAW FROM, one row under the crew. It sits // beside the crew row because it is the same decision read one level up: // the crew row says which five models, and this says which shelf those - // five come off. Open is the default and the law the shipped crew rests - // on; `all` is the opt-in that spends what the frontier costs. + // five come off. `all` is the default and what the shipped five are the + // balanced row of; `open` narrows the same three words to open weights. Setting{ Key: KeyCrewSource, Category: CategoryModels, Kind: SettingChoice, Label: "model family", Choices: CrewSources, - Hint: "which family the crew words draw from. `open` is the default: every " + - "seat an open-weight model, so a crew nobody chose is never a bet on one " + - "vendor's pricing. `all` reads the same three words, frugal, balanced and " + - "max, off the whole catalog, closed and frontier models included, and " + - "costs what those models cost. Seats nobody pinned move with the family at " + + Hint: "which family the crew words draw from. `all` is the default: frugal, " + + "balanced and max read off the whole catalog, closed and frontier models " + + "included, and cost what those models cost. `open` reads the same three " + + "words off the open-weight rows only, so no seat is a bet on one vendor's " + + "pricing. Seats nobody pinned move with the family at " + "once, because an unwritten seat is the default crew; rows already written " + "keep their ids until you pick the crew again.", read: func() string { return CrewSourceAt(dir) }, @@ -3786,10 +3788,10 @@ func tierKeyFor(tier string) string { // defaultTierModel is what a tier answers on a profile that has never held its // key: the DEFAULT CREW, resolved in the family the profile chose. It is not the // build's constant alone, because a profile that answered the family row and no -// tier row would otherwise run the open-weight crew under a frontier label: the -// family saying one thing and the ladder another. Under the shipped family the +// tier row would otherwise run one family's crew under the other's label: the +// family saying one thing and the ladder another. Under the default family the // answer is the constant, which is what [DefaultWorkerModel] and its kin name, -// because the open table's default row and the builtin five name the same set by +// because that family's default row and the builtin five name the same set by // construction (crew_test.go pins it). func defaultTierModel(family, tier string) string { if table, ok := CrewModelsForSource(family, DefaultCrew); ok { diff --git a/internal/manual/chat/commands.md b/internal/manual/chat/commands.md index 1683a21d82..eb4ac685b2 100644 --- a/internal/manual/chat/commands.md +++ b/internal/manual/chat/commands.md @@ -671,7 +671,7 @@ The `crew` line sits directly under `model` and reads the preset word — or `cu the three classes: ``` -crew max · brain kimi-k3 · hands glm-5.3 · checks kimi-k3 +crew max · brain claude-fable-5.1 · hands claude-fable-5.1 · checks gpt-6-astra ``` The crew is **not on the status line**. It was one short segment there — `crew max`, or @@ -1423,12 +1423,12 @@ opens the six-seat reading, bottom-anchored like the model picker. From the top: the five models codeaf uses on its own behalf — not the one you chat with you talk to · deepseek-v4-flash family ‹ open models · all models › - frugal — deepseek works, glm-5.3 thinks · pennies a day - reflex mistralai/mistral-nemo · small work deepseek/deepseek-v4-flash-0731 · worker deepseek/deepseek-v4-flash-0731 · careful work z-ai/glm-5.3-flash · mastermind z-ai/glm-5.3 -› balanced — glm-flash works, kimi-k3 checks and thinks - reflex mistralai/mistral-nemo · small work deepseek/deepseek-v4-flash-0731 · worker z-ai/glm-5.3-flash · careful work moonshotai/kimi-k3 · mastermind moonshotai/kimi-k3 - max — glm-5.3 works, kimi-k3 thinks and checks - reflex mistralai/mistral-nemo · small work deepseek/deepseek-v4-flash-0731 · worker z-ai/glm-5.3 · careful work moonshotai/kimi-k3 · mastermind moonshotai/kimi-k3 + frugal — glm-flash works and thinks, gemini-flash checks + reflex google/gemini-2.5-flash · small work deepseek/deepseek-v4-flash-0731 · worker z-ai/glm-5.3-flash · careful work google/gemini-3.8-flash · mastermind z-ai/glm-5.3-flash +› balanced — glm-flash works, fable checks and thinks + reflex google/gemini-2.5-flash · small work deepseek/deepseek-v4-flash-0731 · worker z-ai/glm-5.3-flash · careful work anthropic/claude-fable-5.1 · mastermind anthropic/claude-fable-5.1 + max — fable works and thinks, astra checks + reflex google/gemini-2.5-flash · small work deepseek/deepseek-v4-flash-0731 · worker anthropic/claude-fable-5.1 · careful work openai/gpt-6-astra · mastermind anthropic/claude-fable-5.1 each of the five can be pinned on its own in /settings → Providers ``` @@ -1455,7 +1455,7 @@ page. `/crew frugal`, `/crew balanced` or `/crew max` sets the five and confirms in one line: ``` -crew → max · brain kimi-k3 · hands glm-5.3 · checks kimi-k3 · you are still talking to deepseek-v4-flash — /model changes that +crew → max · brain claude-fable-5.1 · hands claude-fable-5.1 · checks gpt-6-astra · you are still talking to deepseek-v4-flash — /model changes that ``` **The model ids are drawn brighter than the words around them.** `crew →`, the preset diff --git a/internal/manual/chat/models-and-cost.md b/internal/manual/chat/models-and-cost.md index e51f418e42..375418b665 100644 --- a/internal/manual/chat/models-and-cost.md +++ b/internal/manual/chat/models-and-cost.md @@ -331,60 +331,67 @@ preset: | class | as shipped | | --- | --- | -| reflex | `mistralai/mistral-nemo` | +| reflex | `google/gemini-2.5-flash` | | small work | `deepseek/deepseek-v4-flash-0731` | | worker | `z-ai/glm-5.3-flash` | -| careful work | `moonshotai/kimi-k3` | -| mastermind | `moonshotai/kimi-k3` | - -They are all open-weight models, and none of them is the model you are talking to. A crew -that followed your conversation would put the most expensive model in the build on the -cheapest questions in it — a call made twice every turn on a frontier model is a bill nobody -agreed to. Closed models that are cheaper on their own vendor's platform than through the -router are not in any open-family preset; the `all` family is where they are, and you -can still pin one on any row. -You can also connect that vendor yourself as a direct service; the +| careful work | `anthropic/claude-fable-5.1` | +| mastermind | `anthropic/claude-fable-5.1` | + +None of them is the model you are talking to. A crew that followed your conversation would +put the most expensive model in the build on the cheapest questions in it — a call made +twice every turn on a frontier model is a bill nobody agreed to. That is also why the two +seats that read every turn stay on near-free models while the frontier ids sit on the seats +that answer a handful of times. You can pin any vendor's model on any row yourself, and you +can connect that vendor as a direct service; the [services page](services.md) explains its names, limits and missing Phase 1 cost record. **The `model family` row** (`models.crew.source`, just under the crew row) decides which family -the three preset words draw from. `open` is the default and is the table above: open weights only, -so a crew nobody chose is never a bet on one vendor's pricing. `all` reads the same three words, -`frugal`, `balanced` and `max`, off the whole catalog instead, closed and frontier models included, -and costs what those models cost. Flip the row and the seats nobody pinned move with it at once, +the three preset words draw from. `all` is the default and is the table above: the whole +catalog, closed and frontier models included, costing what those models cost. `open` reads the +same three words, `frugal`, `balanced` and `max`, off the open-weight rows only, so no seat is a +bet on one vendor's pricing. Flip the row and the seats nobody pinned move with it at once, because an unwritten seat is the default crew resolved in the family you are on; the rows already on disk keep their ids until you pick the crew again, and the crew word reads `custom` while they match no preset in the family you flipped to. -Under `all`, the same three words resolve to these: +Under `open`, the same three words resolve to these: | class | frugal | balanced | max | | --- | --- | --- | --- | -| reflex | `google/gemini-2.5-flash` | `google/gemini-2.5-flash` | `google/gemini-2.5-flash` | +| reflex | `mistralai/mistral-nemo` | `mistralai/mistral-nemo` | `mistralai/mistral-nemo` | | small work | `deepseek/deepseek-v4-flash-0731` | `deepseek/deepseek-v4-flash-0731` | `deepseek/deepseek-v4-flash-0731` | -| worker | `openai/gpt-5.6-sol` | `openai/gpt-5.6-sol` | `anthropic/claude-fable-5.1` | -| careful work | `google/gemini-3.8-flash` | `anthropic/claude-opus-5` | `openai/gpt-6-astra` | -| mastermind | `anthropic/claude-opus-5` | `anthropic/claude-fable-5.1` | `anthropic/claude-fable-5.1` | - -Two of the columns spend differently here. The worker is the same model in `frugal` and -`balanced`, and only `max` buys the frontier coder, because on the catalog the coding ceiling is -the one expensive step. What `frugal` to `balanced` buys instead is the careful seat and a -smarter mastermind. The `max` mastermind sits on the worker's own model on purpose: it is the -catalog's ceiling, so there is no bigger planner to buy above it. - -**Why the open ids.** They were picked off the catalog's own published scores (OpenRouter -republishes Artificial Analysis's coding and agentic indexes on every model row) against -blended price, open weights only, and the `frugal` mastermind and the `balanced` careful and mastermind seats were -picked on 2026-09-16, the rest on 2026-09-01. The worker seat is the dial: `glm-5.3-flash` -scores 58 on the agentic index and 72 on coding at about $0.12 per million tokens blended, -one point under `glm-5.3` at a twentieth of its price, and it can see images. The -mastermind is the one seat whose price may be the model rather than the rung: it answers a -few times a task, so `frugal` pays for `glm-5.3` while the work below it stays on -`glm-5.3-flash` and DeepSeek. -The careful class is always a different vendor from the worker and always sees images: -`moonshotai/kimi-k3` scores 76 on coding and is the open field's strongest checker, so -`balanced` and `max` put it on both the careful and the mastermind seat. The small-work row is pinned to the July build of DeepSeek V4 Flash on purpose: -the bare `deepseek/deepseek-v4-flash` id resolves to the April build, and the July build at -the same price scores thirteen coding points higher. +| worker | `deepseek/deepseek-v4-flash-0731` | `z-ai/glm-5.3-flash` | `z-ai/glm-5.3` | +| careful work | `z-ai/glm-5.3-flash` | `moonshotai/kimi-k3` | `moonshotai/kimi-k3` | +| mastermind | `z-ai/glm-5.3-flash` | `z-ai/glm-5.3` | `z-ai/glm-5.3` | + +The worker column is the dial in that family — it climbs one step per preset, because it is +the seat that pays most of a task's bill and a preset that moved every other seat would +change everything about a task except its cost. + +**Why these ids.** Every row of the catalog was placed on two axes on 2026-09-16: the bill a +seat's own call shape runs up, built from the catalog's published prompt, completion and +cache-read prices, against that seat's quality, taken from its published intelligence, coding +and agentic indexes. The call shape is part of the price, so the seats were priced apart — the +worker and the careful seats as long cached loops, where a large prompt is read back turn +after turn and the cache-read price carries most of the weight, and the mastermind as one-shot +calls, where the prompt is paid in full each time and there are few of them. Each preset then +takes, for each seat, a point on the pareto front of that plot at the bill it is willing to +run: nothing on the front costs less at the same quality, and nothing at the same bill scores +higher. The two families are the same plot over two sets of rows, which is why the columns do +not climb together in either of them. + +In the `all` family the worker stays on `glm-5.3-flash` through `balanced`, because the worker +seat carries most of a task's tokens: a step there multiplies through the whole bill, where a +step on the careful or the mastermind seat is paid a handful of times. So `frugal` to +`balanced` spends on those two low-volume seats, and only `max` moves the worker itself. The +`max` mastermind then sits on the worker's own model, because at that bill the one-shot front +has no separate planner above it. + +The careful class is always a different vendor from the worker and always sees images, in +every preset of both families: a check from a second vendor catches what the first vendor's +blind spots let through, and the vision role rides that row. The small-work row is pinned to +the July build of DeepSeek V4 Flash on purpose: the bare `deepseek/deepseek-v4-flash` id +resolves to the April build, and the July build costs the same. **Clearing a row is still an answer.** A class you empty on purpose reads `follows the conversation`, and every role on it runs on the model you are talking to. That @@ -393,20 +400,22 @@ say rather than the default. ### The three presets +These are the three in the family you are on unless you changed the `model family` row — the +default one, `all`: + | | frugal | balanced | max | | --- | --- | --- | --- | -| reflex | `mistral-nemo` | `mistral-nemo` | `mistral-nemo` | +| reflex | `gemini-2.5-flash` | `gemini-2.5-flash` | `gemini-2.5-flash` | | small work | `deepseek-v4-flash-0731` | `deepseek-v4-flash-0731` | `deepseek-v4-flash-0731` | -| worker | `deepseek-v4-flash-0731` | `glm-5.3-flash` | `glm-5.3` | -| careful work | `glm-5.3-flash` | `kimi-k3` | `kimi-k3` | -| mastermind | `glm-5.3` | `kimi-k3` | `kimi-k3` | +| worker | `glm-5.3-flash` | `glm-5.3-flash` | `claude-fable-5.1` | +| careful work | `gemini-3.8-flash` | `claude-fable-5.1` | `gpt-6-astra` | +| mastermind | `glm-5.3-flash` | `claude-fable-5.1` | `claude-fable-5.1` | -- **frugal**: deepseek works, glm-5.3 thinks · pennies a day -- **balanced**: glm-flash works, kimi-k3 checks and thinks -- **max**: glm-5.3 works, kimi-k3 thinks and checks +- **frugal**: glm-flash works and thinks, gemini-flash checks +- **balanced**: glm-flash works, fable checks and thinks +- **max**: fable works and thinks, astra checks -The worker column climbs the open-weight front one step per preset, because it is the seat -that pays most of a task's bill. The reflex and small-work columns never vary — they are the +The reflex and small-work columns never vary — they are the same near-free models in all three — so `/crew` never names them: the confirmation and `/status` say **brain**, **hands** and **checks**, which are the mastermind, the worker and the careful class. @@ -510,7 +519,7 @@ running keeps the model it was admitted on. **Where to read the crew back:** - `/status` prints a `crew` line directly under `model`: - `crew max · brain kimi-k3 · hands glm-5.3 · checks kimi-k3`. The word is + `crew max · brain claude-fable-5.1 · hands claude-fable-5.1 · checks gpt-6-astra`. The word is the preset, or `custom` when the five classes are your own arrangement. **brain** is the mastermind, **hands** is the worker, **checks** is the careful class. - `/settings` → Providers has the **crew** row above the five class rows. @@ -539,7 +548,7 @@ the crew — or `/model` — half way through does not move work already going. 4. the model you are talking to, only when the worker row is blank. So on the shipped `balanced` crew a task runs on `z-ai/glm-5.3-flash` whatever you are -chatting on, and `/crew max` moves the next task onto `z-ai/glm-5.3`. The task's row on the +chatting on, and `/crew max` moves the next task onto `anthropic/claude-fable-5.1`. The task's row on the roster, its room's status line and its finished card all name the model it actually ran on. The worker of an adaptive run's nodes is the same seat, and so is the work model of `codeaf do` — one row, every door. @@ -574,7 +583,7 @@ was silently lost the moment the same brain ran from a script. Each of those runs opens by saying which voice answered, so nothing has to be guessed at: ``` -models: work deepseek/deepseek-v4-flash-0731 (crew frugal) · plan z-ai/glm-5.3-flash (crew frugal) +models: work z-ai/glm-5.3-flash (crew frugal) · plan z-ai/glm-5.3-flash (crew frugal) ``` Two details worth knowing. A crew answers only once you have actually set one — a profile @@ -678,7 +687,7 @@ No. `/crew max` moves the five crew seats and leaves the model you talk to exact was. The confirmation names it: ``` -crew → max · brain kimi-k3 · hands glm-5.3 · checks kimi-k3 · you are still talking to deepseek-v4-flash — /model changes that +crew → max · brain claude-fable-5.1 · hands claude-fable-5.1 · checks gpt-6-astra · you are still talking to deepseek-v4-flash — /model changes that ``` `/model`, `/model ` or the model row in `/settings` are the only ways to change the @@ -2246,7 +2255,7 @@ The `crew` line sits directly under `model` and reads the preset word — or `cu the three classes after it: ``` -crew max · brain kimi-k3 · hands glm-5.3 · checks kimi-k3 +crew max · brain claude-fable-5.1 · hands claude-fable-5.1 · checks gpt-6-astra ``` On the live status line the crew is one short segment — `crew max`, or `crew custom` — at diff --git a/internal/tui3/crew.go b/internal/tui3/crew.go index aefbeca475..528536b62b 100644 --- a/internal/tui3/crew.go +++ b/internal/tui3/crew.go @@ -445,7 +445,7 @@ type crewPicker struct { func (p *crewPicker) start(current, source, inherited string) { // source is already a word this build knows: every caller hands it // [config.CrewSourceAt]'s answer, which folds blank, unknown and retired words - // back to open. The fold lives there and nowhere else. + // back to the default family. The fold lives there and nowhere else. *p = crewPicker{open: true, current: current, persistedSource: source, inherited: inherited, source: source} for i, preset := range config.CrewPresets { if preset == current { @@ -697,9 +697,9 @@ func (a *app) crewListing() string { source := config.CrewSourceAt(a.profileDir) var out strings.Builder // THE WORDS ARE THE SAME IN EITHER FAMILY, so the listing names the pool - // whenever it is not the shipped one: an all listing that looked exactly like - // an open one is the ambiguity this whole feature is about. - if source != config.CrewSourceOpen { + // whenever it is not the default one: two listings that looked exactly alike + // over two different sets of ids is the ambiguity this whole feature is about. + if source != config.DefaultCrewSource { out.WriteString(crewSourceLead + " · " + source + " models\n") } for at, preset := range config.CrewPresets { diff --git a/internal/tui3/crew_test.go b/internal/tui3/crew_test.go index 7112fda523..a65f8c470c 100644 --- a/internal/tui3/crew_test.go +++ b/internal/tui3/crew_test.go @@ -147,7 +147,15 @@ func TestCrewAppliesAPresetAndConfirmsInOneLine(t *testing.T) { if strings.Count(text, "\n") != 0 { t.Fatalf("the confirmation is more than one line:\n%s", text) } - for _, part := range []string{"crew → max", "brain kimi-k3", "hands glm-5.3", "checks kimi-k3"} { + // The three class names come from the table rather than being spelled here: + // a preset that moves onto better models must not leave this test asserting + // the ids it used to name. + for _, part := range []string{ + "crew → max", + "brain " + modelBase(want[config.ModelTierMastermind]), + "hands " + modelBase(want[config.ModelTierWorker]), + "checks " + modelBase(want[config.ModelTierHigh]), + } { if !strings.Contains(text, part) { t.Errorf("the confirmation is missing %q: %q", part, text) } @@ -328,32 +336,32 @@ func TestTheCrewChooserNamesTheTwoFamiliesAboveThePresets(t *testing.T) { // THE SHIPPED FAMILY IS THE WORD LIFTED, and it is the only one: the word // the next enter writes is the fact the row answers, and a second lifted // word would be a second answer on a row with one question. - if !strings.Contains(rows[2], a.pal.accent("open models")) { - t.Fatalf("the open family is not the word in force:\n%q", rows[2]) + if !strings.Contains(rows[2], a.pal.accent("all models")) { + t.Fatalf("the all family is not the word in force:\n%q", rows[2]) } - if strings.Contains(rows[2], a.pal.accent("all models")) { + if strings.Contains(rows[2], a.pal.accent("open models")) { t.Fatalf("both families are lifted at once:\n%q", rows[2]) } // AND ←→ MOVES THE WORD while the preset cursor keeps its place and its // mark: the family is a second axis, not a row the ↑↓ list gained. a.crewPickerKey(key("left")) rows = a.overlayRows(a.width, a.overlayHeight()) - if !strings.Contains(rows[2], a.pal.accent("all models")) { - t.Fatalf("← did not walk the family to all:\n%q", rows[2]) + if !strings.Contains(rows[2], a.pal.accent("open models")) { + t.Fatalf("← did not walk the family to open:\n%q", rows[2]) } - // AND THE PRESETS BELOW IT ANSWER IN THAT FAMILY: a header saying all above - // rows still naming open models is the contradiction this chooser exists to - // prevent, and it is the one regression a header-only check would pass. + // AND THE PRESETS BELOW IT ANSWER IN THAT FAMILY: a header saying open above + // rows still naming frontier models is the contradiction this chooser exists + // to prevent, and it is the one regression a header-only check would pass. for i, preset := range config.CrewPresets { - if row := plain(rows[3+2*i]); !strings.Contains(row, config.CrewLineFor(config.CrewSourceAll, preset)) { - t.Errorf("row %d under the all family is not %s's all line:\n%q", 3+2*i, preset, row) + if row := plain(rows[3+2*i]); !strings.Contains(row, config.CrewLineFor(config.CrewSourceOpen, preset)) { + t.Errorf("row %d under the open family is not %s's open line:\n%q", 3+2*i, preset, row) } } // AND THE IN-FORCE MARK DOES NOT MOVE WITH THE FAMILY. The row that wears the // selected ground is the preset the profile holds IN THE FAMILY IT HOLDS, so // once ←→ lifts the family no row on screen is in force: one that kept the // mark would name models the profile does not run. - if strings.Contains(rows[3+2*1], a.pal.accent("balanced — "+config.CrewLineFor(config.CrewSourceAll, config.CrewBalanced))) { + if strings.Contains(rows[3+2*1], a.pal.accent("balanced — "+config.CrewLineFor(config.CrewSourceOpen, config.CrewBalanced))) { t.Errorf("the balanced row is still marked in force after ←:\n%q", rows[3+2*1]) } if a.crewPick.cursor != 1 { @@ -420,12 +428,21 @@ func TestChoosingAFamilyAndAPresetWritesTheSourceRow(t *testing.T) { if a.crewPick.open { t.Fatal("enter left the chooser open") } - if got := config.CrewSourceAt(dir); got != config.CrewSourceAll { - t.Fatalf("enter wrote the family %q, want all", got) + // ← walked the family OFF the default, so the row is written: this is the + // case the family row exists for, and the one a write that skipped an + // unchanged family would still have to land. + if got := config.CrewSourceAt(dir); got != config.CrewSourceOpen { + t.Fatalf("enter wrote the family %q, want open", got) } if got := config.CrewAt(dir); got != config.CrewBalanced { t.Fatalf("enter applied %q, want balanced", got) } + open, _ := config.CrewModelsForSource(config.CrewSourceOpen, config.CrewBalanced) + for _, tier := range config.ModelTiers { + if got := config.TierModelAt(dir, tier); got != open[tier] { + t.Errorf("enter wrote %s as %q, want the open family's %q", tier, got, open[tier]) + } + } // The neighbour survived the one write. data, err := os.ReadFile(config.BudgetConfigPath(dir)) if err != nil { @@ -448,9 +465,9 @@ func TestChoosingAFamilyAndAPresetWritesTheSourceRow(t *testing.T) { // A ROW NOBODY WROTE READS AS THE SHIPPED FAMILY, and so does a word this build // does not know: the chooser offers two options and a stray string off the disk // is not a third. -func TestTheCrewSourceRowDefaultsToOpen(t *testing.T) { +func TestTheCrewSourceRowDefaultsToAll(t *testing.T) { dir := t.TempDir() - if got := config.CrewSourceAt(dir); got != config.CrewSourceOpen { + if got := config.CrewSourceAt(dir); got != config.CrewSourceAll { t.Fatalf("a profile with no row read %q", got) } for _, row := range []string{ @@ -461,15 +478,15 @@ func TestTheCrewSourceRowDefaultsToOpen(t *testing.T) { if err := os.WriteFile(config.BudgetConfigPath(dir), []byte(row), 0o600); err != nil { t.Fatal(err) } - if got := config.CrewSourceAt(dir); got != config.CrewSourceOpen { - t.Fatalf("the row %s read %q, want open", row, got) + if got := config.CrewSourceAt(dir); got != config.CrewSourceAll { + t.Fatalf("the row %s read %q, want all", row, got) } } - if err := os.WriteFile(config.BudgetConfigPath(dir), []byte(`{"models.crew.source": "all"}`), 0o600); err != nil { + if err := os.WriteFile(config.BudgetConfigPath(dir), []byte(`{"models.crew.source": "open"}`), 0o600); err != nil { t.Fatal(err) } - if got := config.CrewSourceAt(dir); got != config.CrewSourceAll { - t.Fatalf("the all row read %q", got) + if got := config.CrewSourceAt(dir); got != config.CrewSourceOpen { + t.Fatalf("the open row read %q", got) } } @@ -500,9 +517,14 @@ func TestStatusNamesTheCrewUnderTheModel(t *testing.T) { t.Fatalf("/status says nothing about the crew:\n%s", text) } // The preset word first, then the same three class names the confirmation - // prints — base names, in [config.CrewClasses]'s own order. + // prints — base names, in [config.CrewClasses]'s own order, read off the + // table rather than spelled here. + max, _ := config.CrewModels(config.CrewMax) for _, want := range []string{ - config.CrewMax, "brain kimi-k3", "hands glm-5.3", "checks kimi-k3", + config.CrewMax, + "brain " + modelBase(max[config.ModelTierMastermind]), + "hands " + modelBase(max[config.ModelTierWorker]), + "checks " + modelBase(max[config.ModelTierHigh]), } { if !strings.Contains(lines[at], want) { t.Errorf("the crew line lost %q: %q", want, lines[at]) diff --git a/internal/tui3/onboarding.go b/internal/tui3/onboarding.go index 20f8df326c..d48cbaa4c5 100644 --- a/internal/tui3/onboarding.go +++ b/internal/tui3/onboarding.go @@ -1468,8 +1468,8 @@ const setupNoMatchWord = "nothing matches · backspace widens it" // is deliberately NOT [config.CrewLine]. // // The registry's line is written for somebody who already has the vocabulary: -// `deepseek works, glm-flash thinks · pennies a day` names four models and makes -// a claim about money. Both halves are wrong here. Nobody meeting codeaf for the +// `deepseek works, glm-flash checks and thinks · pennies a day` names four +// models and makes a claim about money. Both halves are wrong here. Nobody meeting codeaf for the // first time can rank `glm-flash` against `kimi-k3`, and — the harder rule — // THIS SCREEN MAKES NO CLAIM ABOUT WHAT ANYTHING COSTS. "Pennies a day" is a // forecast about somebody else's usage on somebody else's pricing, said on the @@ -1495,7 +1495,7 @@ func crewChoiceWord(preset string) string { return word } // The fallback is for a preset this screen has not been taught, and it is the - // open family's line: setupCrewRows runs inside the draw, so reading the profile + // default family's line: setupCrewRows runs inside the draw, so reading the profile // here would put a profile read on the frame clock, and the seats this screen // shows already take the family from a value read at the door. return config.CrewLine(preset) diff --git a/internal/tui3/settings.go b/internal/tui3/settings.go index 017bfa832e..6ea57f29c2 100644 --- a/internal/tui3/settings.go +++ b/internal/tui3/settings.go @@ -378,7 +378,7 @@ var settingUI = map[string]settingMeta{ config.KeyCrewSource: { tab: tabProviders, label: "model family", widget: widgetCycle, about: "which models the crew word draws from: open weights, or the whole " + - "catalog with closed and frontier models in it. Open is the default.", + "catalog with closed and frontier models in it. All is the default.", }, config.KeyTierLowModel: { tab: tabProviders, label: "small work", widget: widgetSelect, @@ -737,7 +737,7 @@ func init() { // same row, the same write, the same live seam onto [app.switchModel] — only // the word above the Models section changed. crew := settingUI[config.KeyCrew] - crew.about = crewAbout(config.CrewSourceOpen) + crew.about = crewAbout(config.DefaultCrewSource) settingUI[config.KeyCrew] = crew talk := settingUI[config.ModelSettingKey(talkSlot)] @@ -751,8 +751,8 @@ func init() { // with its own sentence, then what makes the row read custom. The sentences are // [config.CrewLineFor]'s IN THE FAMILY ON SCREEN, so the panel and /crew say the // same words about the same thing and neither names an open model above frontier -// ids. The init-time value is the open family; [sheet.metaFor] re-says it from the -// profile, which is where a family that is not the default comes from. +// ids. The init-time value is the default family; [sheet.metaFor] re-says it from +// the profile, which is where a family that is not the default comes from. func crewAbout(source string) string { said := make([]string, 0, len(config.CrewPresets)) for _, preset := range config.CrewPresets { From 8fd6cdaa8df9f6ca65155e69dcbb489e6796e8f4 Mon Sep 17 00:00:00 2001 From: santoshkumarradha Date: Thu, 17 Sep 2026 01:04:11 -0400 Subject: [PATCH 003/173] docs/changes: the crew default family note carries its pull request number Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01Sb7knukoGMPXo8kK876MsU --- ...ault-family-is-all.md => 1093-crew-default-family-is-all.md} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename docs/changes/unreleased/{1090-crew-default-family-is-all.md => 1093-crew-default-family-is-all.md} (99%) diff --git a/docs/changes/unreleased/1090-crew-default-family-is-all.md b/docs/changes/unreleased/1093-crew-default-family-is-all.md similarity index 99% rename from docs/changes/unreleased/1090-crew-default-family-is-all.md rename to docs/changes/unreleased/1093-crew-default-family-is-all.md index 73d7b1f980..92094fa422 100644 --- a/docs/changes/unreleased/1090-crew-default-family-is-all.md +++ b/docs/changes/unreleased/1093-crew-default-family-is-all.md @@ -1,7 +1,7 @@ --- kind: changed title: The crew's default family is `all`, and both families' worker, careful and mastermind seats moved -pr: 1090 +pr: 1093 surface: [chat, engine] invalidates: - "`models.crew.source` defaulted to `open`, so a profile that never answered the row resolved the open-weight table. The default is now `all`: `DefaultCrewSource = CrewSourceAll`, and a profile that never chose a family resolves the all family's balanced row — `google/gemini-2.5-flash`, `deepseek/deepseek-v4-flash-0731`, `z-ai/glm-5.3-flash`, `anthropic/claude-fable-5.1`, `anthropic/claude-fable-5.1`." From 457f3ebd0d22fda5bdc34a966907c17bb44319a8 Mon Sep 17 00:00:00 2001 From: santoshkumarradha Date: Thu, 17 Sep 2026 01:04:04 -0400 Subject: [PATCH 004/173] crewpick: pick one model per seat off the pareto front of bill against quality MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A new pure package — standard library only, no disk, no network — that scores every (worker, high, mastermind) combination a candidate list can field by the bill its seat volumes run up and the mean of its seat qualities. Per seat the running is narrowed by family, the seat's needs and a floor at 80% of the seat's best in the family, a crew is refused while its high seat shares the worker's vendor, and the non-dominated crews survive as the front, sorted by bill. Frugal, balanced and max read picks off it, and a knob walks the budget geometrically between its ends. Ties break in id order, so the same candidates give the same front whatever order they arrive in. Co-Authored-By: codeaf --- internal/crewpick/crewpick.go | 375 +++++++++++++++++++++ internal/crewpick/crewpick_test.go | 336 ++++++++++++++++++ internal/crewpick/testdata/candidates.json | 1 + 3 files changed, 712 insertions(+) create mode 100644 internal/crewpick/crewpick.go create mode 100644 internal/crewpick/crewpick_test.go create mode 100644 internal/crewpick/testdata/candidates.json diff --git a/internal/crewpick/crewpick.go b/internal/crewpick/crewpick.go new file mode 100644 index 0000000000..f32a5030f2 --- /dev/null +++ b/internal/crewpick/crewpick.go @@ -0,0 +1,375 @@ +// Package crewpick picks one model per seat for a three-seat crew by reading +// the pareto front of expected task bill against crew quality. +// +// It deliberately imports nothing from the rest of the tree and touches no +// disk and no network: the caller owns the candidate list and the seat +// shapes, and the same candidates give the same front however often they are +// asked about and in whatever order they arrive. +// +// The law it implements: every (worker, high, mastermind) combination the +// candidates can field is scored by the bill its seats' token volumes run up +// and by the mean of its three seat qualities. A candidate is in the running +// for a seat only when it meets the family and the seat's needs and its +// quality reaches Floor of that seat's best in the family, and a crew is +// refused outright when its high seat comes from the worker's own vendor. +// The crews that no other crew beats on both bill and quality survive as the +// front, sorted by bill; Presets and AtKnob read picks off that front. +package crewpick + +import ( + "math" + "sort" + "strings" +) + +// A Candidate is one model up for a seat. Prices are dollars per 1M tokens. +// +// A candidate missing any of the three indexes — the zero value, which is how +// an unpublished index reads — or carrying a zero prompt price is not a +// candidate at all: it never occupies a seat, never clears a floor, and +// raises no ceiling. A model that publishes no cache-read price says so with +// HasCacheRead false and pays the prompt price on the cache share too. +type Candidate struct { + // ID names the candidate; the text before its first slash is the vendor. + ID string + // Open marks a candidate the open family may draw from. + Open bool + // Intelligence, Coding and Agentic are the three capability indexes + // quality is read from. + Intelligence float64 + Coding float64 + Agentic float64 + // PromptPrice, CompletionPrice and CacheReadPrice are the dollars per + // 1M tokens of each kind. + PromptPrice float64 + CompletionPrice float64 + CacheReadPrice float64 + // HasCacheRead reports whether CacheReadPrice is published at all. + HasCacheRead bool + // Context is the model's context window, in tokens. + Context int + // Images and Tools are what the model can take and call. + Images bool + Tools bool +} + +// A Seat is one of a crew's three roles: the worker carries the volume of +// the work, the high seat judges and repairs what the worker writes, and the +// mastermind spends a little context steering the whole task. +type Seat int + +const ( + Worker Seat = iota + High + Mastermind +) + +// A SeatShape is how one seat uses whatever model sits it: the weights +// quality is read from, the token mix cost is computed on, what the model must +// be able to take and call, the window it must have, and the share of a +// task's tokens the seat burns. +type SeatShape struct { + // Weights reads quality from the three indexes: intelligence, coding, + // agentic. + Weights [3]float64 + // InOut is the seat's input tokens per output token; the completion + // price is spread over it. + InOut float64 + // CacheShare is the share of the seat's input tokens served from cache. + CacheShare float64 + // NeedTools and NeedImages are what a model must take and call to sit + // the seat. + NeedTools bool + NeedImages bool + // MinContext is the window a model must have to sit the seat. + MinContext int + // Volume is the seat's share of a task's tokens, and the weight its + // cost carries in a crew's bill. + Volume float64 +} + +// DefaultShapes returns the three seat shapes crews are picked for. The +// worker takes half its quality from the agentic index and reads barely +// anything but cached input; the high seat judges the three indexes evenly, +// needs tools and images both, and sees the same token mix on a sliver of the +// volume; the mastermind reads intelligence and agentic only, on a terse +// ten-in-one mix with no cache and a fraction of the volume. +func DefaultShapes() map[Seat]SeatShape { + return map[Seat]SeatShape{ + Worker: { + Weights: [3]float64{0.2, 0.3, 0.5}, + InOut: 100, + CacheShare: 0.75, + NeedTools: true, + MinContext: 200_000, + Volume: 0.90, + }, + High: { + Weights: [3]float64{1.0 / 3.0, 1.0 / 3.0, 1.0 / 3.0}, + InOut: 100, + CacheShare: 0.75, + NeedTools: true, + NeedImages: true, + MinContext: 200_000, + Volume: 0.08, + }, + Mastermind: { + Weights: [3]float64{0.6, 0, 0.4}, + InOut: 10, + MinContext: 200_000, + Volume: 0.02, + }, + } +} + +// A Family is which candidates a crew may be drawn from. +type Family int + +const ( + // Open draws only from candidates marked Open. + Open Family = iota + // All draws from every candidate. + All +) + +// A Crew is one (worker, high, mastermind) pick. Bill is the expected dollars +// per 1M task tokens once each seat's share of the task is paid for at its +// seat cost; Quality is the mean of the three seat qualities on a 0-100 +// scale. +type Crew struct { + Worker string + High string + Mastermind string + Bill float64 + Quality float64 +} + +// Floor is the share of a seat's best quality in the family that a candidate +// must reach to stay in the running for that seat. A model that cannot do +// the job is not cheap, whatever it costs. +const Floor = 0.80 + +// SeatCost is the seat's expected cost of a candidate, in dollars per 1M +// input-equivalent tokens: the prompt price blended with the cache-read +// price by the seat's cache share, plus the completion price spread over the +// seat's input-to-output ratio. A candidate that publishes no cache-read +// price pays the prompt price on the cache share too. +func SeatCost(c Candidate, shape SeatShape) float64 { + cache := c.PromptPrice + if c.HasCacheRead { + cache = c.CacheReadPrice + } + return (1-shape.CacheShare)*c.PromptPrice + shape.CacheShare*cache + c.CompletionPrice/shape.InOut +} + +// SeatQuality is the candidate's quality for the seat on a 0-100 scale: each +// index counted against the pool's best for that index and weighted by the +// seat's weights. The pool sets the scale, so the same candidate scores +// differently in a stronger pool. A row that is not a candidate at all, and +// any row against a pool that has no candidates to raise a scale, score +// zero. +func SeatQuality(c Candidate, shape SeatShape, pool []Candidate) float64 { + if !isCandidate(c) { + return 0 + } + maxI, maxC, maxA := maxima(pool) + return quality(c, shape, maxI, maxC, maxA) +} + +// Front returns the pareto front of (bill, quality) over every crew the +// candidates can field in the family, sorted by bill: a crew on it is beaten +// by no other crew on both, and every crew off it is beaten by one on it. +// +// shapes must carry an entry for every seat. Per seat the running is +// narrowed twice before crews are enumerated — family and the seat's needs +// first, then Floor against that seat's best in the family — and a crew is +// refused while its high seat comes from the worker's own vendor, so the two +// seats that see the same work never share one shop. Ties in (bill, quality) +// break by id order — worker, then high, then mastermind — which is what +// makes the front a property of the candidates rather than of their order. +func Front(candidates []Candidate, shapes map[Seat]SeatShape, fam Family) []Crew { + maxI, maxC, maxA := maxima(candidates) + workers := shortlist(candidates, shapes[Worker], fam, maxI, maxC, maxA) + highs := shortlist(candidates, shapes[High], fam, maxI, maxC, maxA) + minds := shortlist(candidates, shapes[Mastermind], fam, maxI, maxC, maxA) + vw := shapes[Worker].Volume + vh := shapes[High].Volume + vm := shapes[Mastermind].Volume + + crews := make([]Crew, 0, len(workers)*len(highs)*len(minds)) + for _, w := range workers { + for _, h := range highs { + if vendor(w.c.ID) == vendor(h.c.ID) { + continue + } + for _, m := range minds { + crews = append(crews, Crew{ + Worker: w.c.ID, + High: h.c.ID, + Mastermind: m.c.ID, + Bill: vw*w.cost + vh*h.cost + vm*m.cost, + Quality: (w.quality + h.quality + m.quality) / 3, + }) + } + } + } + + // The front: cheapest bill first, best quality first within a bill, and + // a crew survives when its quality beats everything cheaper so far. + sort.SliceStable(crews, func(i, j int) bool { + if crews[i].Bill != crews[j].Bill { + return crews[i].Bill < crews[j].Bill + } + return crews[i].Quality > crews[j].Quality + }) + var front []Crew + best := math.Inf(-1) + for _, crew := range crews { + if crew.Quality > best+1e-9 { + front = append(front, crew) + best = crew.Quality + } + } + return front +} + +// Presets reads the three named picks off a front: frugal is the cheapest +// crew, max the dearest, and balanced the knee between them — the crew +// farthest above the straight line from one end of the front to the other in +// (ln bill, quality), which is where money stops buying quality in earnest. +// A front of one crew is all three; an empty front is none. +func Presets(front []Crew) (frugal, balanced, max Crew) { + if len(front) == 0 { + return Crew{}, Crew{}, Crew{} + } + frugal, balanced, max = front[0], knee(front), front[len(front)-1] + return frugal, balanced, max +} + +// AtKnob reads one pick off the front at knob k in [0, 1]: the budget runs +// geometrically from the frugal bill to the max bill — lo*(hi/lo)^k — and +// the pick is the best-quality crew whose bill fits it, so k of 0 is frugal +// and k of 1 is max. An empty front picks nothing. +func AtKnob(front []Crew, k float64) Crew { + if len(front) == 0 { + return Crew{} + } + lo, hi := front[0].Bill, front[len(front)-1].Bill + budget := lo * math.Pow(hi/lo, k) + var pick Crew + for _, crew := range front { + if crew.Bill <= budget*(1+1e-9) { + pick = crew + } + } + return pick +} + +// knee returns the front's knee: the crew farthest above the straight line +// from the first crew to the last in (ln bill, quality). Ties go to the +// earlier crew, and a front whose two ends share a bill has no line to be +// above — its best crew carries the name. +func knee(front []Crew) Crew { + x0, y0 := math.Log(front[0].Bill), front[0].Quality + x1, y1 := math.Log(front[len(front)-1].Bill), front[len(front)-1].Quality + if x1 == x0 { + return front[len(front)-1] + } + pick := front[0] + best := math.Inf(-1) + for _, crew := range front { + d := (crew.Quality - y0) - (y1-y0)*(math.Log(crew.Bill)-x0)/(x1-x0) + if d > best { + pick, best = crew, d + } + } + return pick +} + +// A seatPick is a candidate already priced and scored for one seat. +type seatPick struct { + c Candidate + cost float64 + quality float64 +} + +// shortlist returns a seat's running: the candidates that pass family and the +// seat's needs, kept when their quality reaches Floor of the family's best +// for the seat, in id order. +func shortlist(candidates []Candidate, shape SeatShape, fam Family, maxI, maxC, maxA float64) []seatPick { + var running []seatPick + top := 0.0 + for _, c := range candidates { + if !eligible(c, shape, fam) { + continue + } + pick := seatPick{c: c, cost: SeatCost(c, shape), quality: quality(c, shape, maxI, maxC, maxA)} + running = append(running, pick) + top = math.Max(top, pick.quality) + } + var kept []seatPick + for _, pick := range running { + if pick.quality >= Floor*top { + kept = append(kept, pick) + } + } + sort.Slice(kept, func(i, j int) bool { return kept[i].c.ID < kept[j].c.ID }) + return kept +} + +// quality is SeatQuality against pre-read maxima. +func quality(c Candidate, shape SeatShape, maxI, maxC, maxA float64) float64 { + if maxI <= 0 || maxC <= 0 || maxA <= 0 { + return 0 + } + w := shape.Weights + return 100 * (w[0]*c.Intelligence/maxI + w[1]*c.Coding/maxC + w[2]*c.Agentic/maxA) +} + +// maxima reads the pool's best value of each index — the scale quality is +// measured against. Only real candidates raise a ceiling. +func maxima(pool []Candidate) (maxI, maxC, maxA float64) { + for _, c := range pool { + if !isCandidate(c) { + continue + } + maxI = math.Max(maxI, c.Intelligence) + maxC = math.Max(maxC, c.Coding) + maxA = math.Max(maxA, c.Agentic) + } + return maxI, maxC, maxA +} + +// isCandidate reports whether a row is a candidate at all: all three indexes +// present and a prompt price above zero. Everything downstream — +// eligibility, the floor, the maxima — starts from this. +func isCandidate(c Candidate) bool { + return c.Intelligence > 0 && c.Coding > 0 && c.Agentic > 0 && c.PromptPrice > 0 +} + +// eligible reports whether a candidate may sit the seat: family first, then +// what the seat needs the model to take and call, then the window it needs. +func eligible(c Candidate, shape SeatShape, fam Family) bool { + if !isCandidate(c) { + return false + } + if fam == Open && !c.Open { + return false + } + if shape.NeedTools && !c.Tools { + return false + } + if shape.NeedImages && !c.Images { + return false + } + return c.Context >= shape.MinContext +} + +// vendor is the id text before the first slash, or the whole id when there is +// no slash. +func vendor(id string) string { + if i := strings.Index(id, "/"); i >= 0 { + return id[:i] + } + return id +} diff --git a/internal/crewpick/crewpick_test.go b/internal/crewpick/crewpick_test.go new file mode 100644 index 0000000000..9d19c43c39 --- /dev/null +++ b/internal/crewpick/crewpick_test.go @@ -0,0 +1,336 @@ +package crewpick + +import ( + "encoding/json" + "math" + "os" + "slices" + "testing" +) + +// A fixture row in its own field names: a null index reads as zero — +// missing, the way the package reads it — and a null cache-read price reads +// as unpublished. The rows with unpublished indexes and unpublished cache +// prices stay in the fixture, so the exclusion rules have something to bite +// on. +type row struct { + ID string `json:"id"` + Open bool `json:"open"` + I float64 `json:"I"` + C float64 `json:"C"` + A float64 `json:"A"` + Pi float64 `json:"pi"` + Po float64 `json:"po"` + Cr *float64 `json:"cr"` + Ctx int `json:"ctx"` + Img bool `json:"img"` + Tools bool `json:"tools"` +} + +// loadCandidates reads the fixture into candidates. +func loadCandidates(t *testing.T) []Candidate { + t.Helper() + raw, err := os.ReadFile("testdata/candidates.json") + if err != nil { + t.Fatal(err) + } + var rows []row + if err := json.Unmarshal(raw, &rows); err != nil { + t.Fatal(err) + } + cands := make([]Candidate, 0, len(rows)) + for _, r := range rows { + c := Candidate{ + ID: r.ID, + Open: r.Open, + Intelligence: r.I, + Coding: r.C, + Agentic: r.A, + PromptPrice: r.Pi, + CompletionPrice: r.Po, + Context: r.Ctx, + Images: r.Img, + Tools: r.Tools, + } + if r.Cr != nil { + c.CacheReadPrice = *r.Cr + c.HasCacheRead = true + } + cands = append(cands, c) + } + return cands +} + +// model builds a candidate that can sit any seat: every index at 100, tools +// and images, a 200k window, and a published cache price. +func model(id string) Candidate { + return Candidate{ + ID: id, + Open: true, + Intelligence: 100, + Coding: 100, + Agentic: 100, + PromptPrice: 1, + CompletionPrice: 4, + CacheReadPrice: 0.1, + HasCacheRead: true, + Context: 200_000, + Images: true, + Tools: true, + } +} + +// wantCrew checks a pick against its worker|high|mastermind line. +func wantCrew(t *testing.T, name string, got Crew, want string) { + t.Helper() + line := got.Worker + "|" + got.High + "|" + got.Mastermind + if line != want { + t.Fatalf("%s = %s, want %s", name, line, want) + } +} + +// The fixture's front is a fixed property of its rows: seven crews in the +// open family, eighteen in all, and these are the picks each front reads. +// The all-family balanced crew is where money stops buying quality — a bill +// of 0.590 at quality 95.4. +func TestPresetsReadTheFixtureFront(t *testing.T) { + cands := loadCandidates(t) + shapes := DefaultShapes() + + open := Front(cands, shapes, Open) + if len(open) != 7 { + t.Fatalf("open front holds %d crews, want 7", len(open)) + } + frugal, balanced, max := Presets(open) + wantCrew(t, "open frugal", frugal, "deepseek/deepseek-v4-flash-0731|z-ai/glm-5.3-flash|z-ai/glm-5.3-flash") + wantCrew(t, "open balanced", balanced, "z-ai/glm-5.3-flash|moonshotai/kimi-k3|z-ai/glm-5.3") + wantCrew(t, "open max", max, "z-ai/glm-5.3|moonshotai/kimi-k3|z-ai/glm-5.3") + + all := Front(cands, shapes, All) + if len(all) != 18 { + t.Fatalf("all front holds %d crews, want 18", len(all)) + } + frugal, balanced, max = Presets(all) + wantCrew(t, "all frugal", frugal, "z-ai/glm-5.3-flash|google/gemini-3.8-flash|z-ai/glm-5.3-flash") + wantCrew(t, "all balanced", balanced, "z-ai/glm-5.3-flash|anthropic/claude-fable-5.1|anthropic/claude-fable-5.1") + wantCrew(t, "all max", max, "anthropic/claude-fable-5.1|openai/gpt-6-astra|anthropic/claude-fable-5.1") + if math.Abs(balanced.Bill-0.590) > 0.005 { + t.Fatalf("all balanced bill = %f, want about 0.590", balanced.Bill) + } + if math.Abs(balanced.Quality-95.4) > 0.05 { + t.Fatalf("all balanced quality = %f, want about 95.4", balanced.Quality) + } +} + +// The high seat sees the same work the worker does, so a crew never draws it +// from the worker's own vendor. Every crew on both fixture fronts holds +// that line, and a pool that cannot field two vendors fields no crew at all. +func TestTheHighSeatNeverSharesAVendorWithTheWorker(t *testing.T) { + cands := loadCandidates(t) + for _, fam := range []Family{Open, All} { + for _, crew := range Front(cands, DefaultShapes(), fam) { + if vendor(crew.Worker) == vendor(crew.High) { + t.Fatalf("worker %s and high %s share a vendor", crew.Worker, crew.High) + } + } + } + + oneVendor := []Candidate{model("one/first"), model("one/second")} + if front := Front(oneVendor, DefaultShapes(), All); len(front) != 0 { + t.Fatalf("a one-vendor pool fielded %d crews, want none", len(front)) + } +} + +// The floor keeps a seat's running to candidates that can do the job: a +// model whose quality falls below Floor of the seat's best in the family is +// not cheap and never reaches a crew. The boundary is inclusive — a +// candidate sitting exactly on the floor runs, and at a tenth of the price +// of anything above it, it runs first. +func TestTheFloorDropsAModelThatCannotDoTheJob(t *testing.T) { + pool := []Candidate{model("a/m"), model("c/h")} + + // Cheap enough to be every crew's frugal worker, and short one index: + // 70 of the worker seat's 100, 76 of the mastermind's, and ineligible + // for the high seat, which needs images. + cheap := model("b/w") + cheap.Agentic = 40 + cheap.Images = false + cheap.PromptPrice = 0.01 + cheap.CompletionPrice = 0.02 + cheap.HasCacheRead = false + pool = append(pool, cheap) + + // Exactly on the worker seat's floor: 80 of 100. + edge := model("e/b") + edge.Agentic = 60 + edge.PromptPrice = 0.02 + edge.CompletionPrice = 0.04 + edge.HasCacheRead = false + pool = append(pool, edge) + + front := Front(pool, DefaultShapes(), All) + if len(front) == 0 { + t.Fatal("the pool must field crews") + } + frugal, _, _ := Presets(front) + if frugal.Worker != "e/b" { + t.Fatalf("frugal worker = %s, want the on-floor candidate e/b", frugal.Worker) + } + for _, crew := range front { + for _, id := range []string{crew.Worker, crew.High, crew.Mastermind} { + if id == "b/w" { + t.Fatalf("the below-floor model b/w reached a crew: %+v", crew) + } + } + } +} + +// A row missing an index or priced at zero is not a candidate at all: it +// never reaches a seat and it raises no ceiling. Both rules bite where the +// floor cannot — the missing coding index leaves the mastermind seat its +// full quality, and a free model clears any floor. +func TestARowMissingAnIndexOrPricedAtZeroIsNoCandidate(t *testing.T) { + good := []Candidate{model("a/m"), model("c/h")} + good[1].Intelligence = 90 + good[1].PromptPrice = 2 + good[1].CompletionPrice = 8 + good[1].CacheReadPrice = 0.2 + + broken := model("d/x") + broken.Coding = 0 + broken.PromptPrice = 0.01 + broken.CompletionPrice = 0.02 + broken.HasCacheRead = false + + free := model("e/y") + free.PromptPrice = 0 + + pool := append(slices.Clone(good), broken, free) + front := Front(pool, DefaultShapes(), All) + for _, crew := range front { + for _, id := range []string{crew.Worker, crew.High, crew.Mastermind} { + if id == "d/x" || id == "e/y" { + t.Fatalf("a non-candidate row reached a crew: %+v", crew) + } + } + } + // The non-candidate rows leave the real candidates' front exactly what + // it was without them. + if want := Front(good, DefaultShapes(), All); !slices.Equal(front, want) { + t.Fatalf("the non-candidate rows moved the front:\n got %+v\nwant %+v", front, want) + } +} + +// The knob runs from one end of the front to the other: at 0 the budget is +// the cheapest bill and the pick is frugal, at 1 it is the dearest and the +// pick is max. +func TestAtKnobAtTheEndsReadsFrugalAndMax(t *testing.T) { + cands := loadCandidates(t) + for _, fam := range []Family{Open, All} { + front := Front(cands, DefaultShapes(), fam) + frugal, _, max := Presets(front) + if got := AtKnob(front, 0); got != frugal { + t.Fatalf("AtKnob at 0 = %+v, want the frugal %+v", got, frugal) + } + if got := AtKnob(front, 1); got != max { + t.Fatalf("AtKnob at 1 = %+v, want the max %+v", got, max) + } + } +} + +// The front is a property of the candidates, not of their order: the running +// for each seat is read in id order, so the same list gives the same front +// however it arrives. +func TestTheSameCandidatesGiveTheSameFrontInAnyOrder(t *testing.T) { + cands := loadCandidates(t) + shapes := DefaultShapes() + one := Front(cands, shapes, All) + + reversed := slices.Clone(cands) + slices.Reverse(reversed) + if two := Front(reversed, shapes, All); !slices.Equal(one, two) { + t.Fatal("the front changed when the candidates arrived reversed") + } +} + +// Two crews that tie in (bill, quality) are one point on the front, and the +// point keeps the crew whose ids read first — worker, then high, then +// mastermind — whichever way round the pool arrived. +func TestTiesInBillAndQualityBreakInIdOrder(t *testing.T) { + // Two candidates identical in every way but the id, and able to sit + // only the mastermind seat: every crew one finishes ties with the crew + // the other finishes. + aaa := model("aaa/x") + aaa.Tools = false + aaa.Images = false + aaa.PromptPrice = 0.1 + aaa.CompletionPrice = 0.2 + aaa.HasCacheRead = false + bbb := aaa + bbb.ID = "bbb/x" + + pool := []Candidate{model("a/m"), model("c/h"), aaa, bbb} + one := Front(pool, DefaultShapes(), All) + if len(one) == 0 { + t.Fatal("the pool must field crews") + } + for _, crew := range one { + if crew.Mastermind != "aaa/x" { + t.Fatalf("a tie went to the wrong twin: %+v", crew) + } + } + + reversed := slices.Clone(pool) + slices.Reverse(reversed) + if two := Front(reversed, DefaultShapes(), All); !slices.Equal(one, two) { + t.Fatal("the tie broke differently when the pool arrived reversed") + } +} + +// The seat's cost is its token mix priced: the prompt price blended with the +// cache-read price by the cache share, and the completion price spread over +// the input-to-output ratio. A model that publishes no cache-read price pays +// the prompt price on the cache share too. +func TestSeatCostPricesTheTokenMix(t *testing.T) { + shape := SeatShape{InOut: 2, CacheShare: 0.5} + published := Candidate{PromptPrice: 2, CompletionPrice: 8, CacheReadPrice: 0.5, HasCacheRead: true} + if got := SeatCost(published, shape); got != 5.25 { + t.Fatalf("SeatCost with a published cache price = %f, want 5.25", got) + } + unpublished := Candidate{PromptPrice: 2, CompletionPrice: 8} + if got := SeatCost(unpublished, shape); got != 6 { + t.Fatalf("SeatCost with no published cache price = %f, want 6", got) + } +} + +// Quality reads each index against the pool's best for that index, so the +// same candidate scores lower in a stronger pool. A row that is not a +// candidate scores nothing, and so does any row against a pool with no +// candidates to raise a scale. +func TestSeatQualityReadsAgainstThePoolsBest(t *testing.T) { + shape := SeatShape{Weights: [3]float64{0.25, 0.25, 0.5}} + pool := []Candidate{ + {ID: "best", Intelligence: 100, Coding: 100, Agentic: 100, PromptPrice: 1}, + {ID: "half", Intelligence: 50, Coding: 100, Agentic: 100, PromptPrice: 1}, + } + if got := SeatQuality(pool[0], shape, pool); got != 100 { + t.Fatalf("the pool's best scored %f, want 100", got) + } + if got := SeatQuality(pool[1], shape, pool); got != 87.5 { + t.Fatalf("the half scored %f, want 87.5", got) + } + + stronger := append(slices.Clone(pool), + Candidate{ID: "top", Intelligence: 200, Coding: 100, Agentic: 100, PromptPrice: 1}) + if got := SeatQuality(pool[1], shape, stronger); got != 81.25 { + t.Fatalf("the half scored %f against a stronger pool, want 81.25", got) + } + + blank := Candidate{ID: "blank", Coding: 0, PromptPrice: 1} + if got := SeatQuality(blank, shape, pool); got != 0 { + t.Fatalf("a row missing an index scored %f, want 0", got) + } + if got := SeatQuality(pool[0], shape, []Candidate{blank}); got != 0 { + t.Fatalf("a pool with no candidates scored %f, want 0", got) + } +} diff --git a/internal/crewpick/testdata/candidates.json b/internal/crewpick/testdata/candidates.json new file mode 100644 index 0000000000..493a31a519 --- /dev/null +++ b/internal/crewpick/testdata/candidates.json @@ -0,0 +1 @@ +[{"id": "inclusionai/ling-3.0-flash-vl", "open": true, "I": 25, "C": 57, "A": 30, "pi": 0.06, "po": 0.18, "cr": 0.012, "ctx": 131072, "img": true, "tools": true, "so": true, "created": 1789056114, "exp": null, "alias": null}, {"id": "deepseek/deepseek-v4.1-flash", "open": true, "I": 39.5, "C": null, "A": null, "pi": 0.3, "po": 1.2, "cr": 0.006, "ctx": 1048576, "img": true, "tools": true, "so": true, "created": 1789021285, "exp": null, "alias": null}, {"id": "openai/gpt-6-astra", "open": false, "I": 52.8, "C": 76.9, "A": 51.5, "pi": 10.0, "po": 50.0, "cr": 1.0, "ctx": 1050000, "img": true, "tools": true, "so": true, "created": 1788552838, "exp": null, "alias": null}, {"id": "qwen/qwen3.8-max-0902", "open": false, "I": 45.4, "C": 76.2, "A": 56.1, "pi": 2.0, "po": 6.0, "cr": 0.25, "ctx": 1000000, "img": true, "tools": true, "so": true, "created": 1788469704, "exp": null, "alias": null}, {"id": "google/gemini-3.8-flash", "open": false, "I": 41.2, "C": 76.3, "A": 41.1, "pi": 0.75, "po": 3.75, "cr": 0.075, "ctx": 1048576, "img": true, "tools": true, "so": true, "created": 1788362056, "exp": null, "alias": null}, {"id": "anthropic/claude-fable-5.1", "open": false, "I": 53.4, "C": 81.6, "A": 58, "pi": 10.0, "po": 50.0, "cr": 0.25, "ctx": 1000000, "img": true, "tools": true, "so": true, "created": 1788285838, "exp": null, "alias": null}, {"id": "ibm-granite/granite-4.2-8b", "open": true, "I": 11.8, "C": 22.4, "A": 3.7, "pi": 0.06, "po": 0.25, "cr": 0.015, "ctx": 131072, "img": false, "tools": true, "so": true, "created": 1788206780, "exp": null, "alias": null}, {"id": "inclusionai/ling-3.0-flash-fin", "open": false, "I": 23, "C": 55.6, "A": 29.3, "pi": 0.06, "po": 0.18, "cr": 0.012, "ctx": 262144, "img": false, "tools": true, "so": true, "created": 1787846290, "exp": null, "alias": null}, {"id": "z-ai/glm-5.3-flash", "open": true, "I": 41.9, "C": 71.5, "A": 51.2, "pi": 0.09, "po": 0.3, "cr": 0.018, "ctx": 1310720, "img": true, "tools": true, "so": true, "created": 1787752741, "exp": null, "alias": null}, {"id": "z-ai/glm-5.3", "open": true, "I": 44.9, "C": 74.8, "A": 53.4, "pi": 1.4, "po": 4.4, "cr": 0.26, "ctx": 1310720, "img": false, "tools": true, "so": true, "created": 1787086655, "exp": null, "alias": null}, {"id": "qwen/qwen3.8-27b", "open": true, "I": 33.9, "C": 68.1, "A": 46.5, "pi": 0.21400000000000002, "po": 2.5500000000000003, "cr": 0.15, "ctx": 1000000, "img": true, "tools": true, "so": true, "created": 1786722910, "exp": null, "alias": null}, {"id": "google/gemini-3.7-flash", "open": false, "I": 39.4, "C": 76.1, "A": 36.4, "pi": 0.75, "po": 3.75, "cr": 0.075, "ctx": 1048576, "img": true, "tools": true, "so": true, "created": 1786640581, "exp": null, "alias": null}, {"id": "qwen/qwen3.8-2.4t-a95b", "open": true, "I": 40, "C": 71.9, "A": 50.4, "pi": 2.0, "po": 6.0, "cr": 0.25, "ctx": 1048576, "img": false, "tools": true, "so": true, "created": 1786551702, "exp": null, "alias": null}, {"id": "deepseek/deepseek-v4-pro-0813", "open": true, "I": 36.3, "C": 68.8, "A": 42.3, "pi": 1.32, "po": 3.9600000000000004, "cr": 0.044, "ctx": 1048576, "img": false, "tools": true, "so": true, "created": 1786549364, "exp": null, "alias": null}, {"id": "x-ai/grok-4.6", "open": false, "I": 44.4, "C": 76.8, "A": 53.4, "pi": 2.0, "po": 6.0, "cr": 0.5, "ctx": 500000, "img": true, "tools": true, "so": true, "created": 1786548957, "exp": null, "alias": null}, {"id": "nvidia/nemotron-3.5-lightning", "open": true, "I": 13.6, "C": 26.8, "A": 6.1, "pi": 0.08, "po": 0.19999999999999998, "cr": 0.04, "ctx": 262144, "img": false, "tools": true, "so": true, "created": 1786452751, "exp": null, "alias": null}, {"id": "meta/muse-spark-1.2", "open": false, "I": 39.8, "C": 72.2, "A": 44, "pi": 1.25, "po": 4.25, "cr": 0.15, "ctx": 1048576, "img": true, "tools": true, "so": true, "created": 1785959287, "exp": null, "alias": null}, {"id": "deepseek/deepseek-v4-flash-0731", "open": true, "I": 34.5, "C": 69.1, "A": 41.7, "pi": 0.06, "po": 0.12, "cr": 0.012, "ctx": 1310720, "img": false, "tools": true, "so": true, "created": 1785478908, "exp": null, "alias": null}, {"id": "thinkingmachines/inkling-small", "open": true, "I": 26.1, "C": 52.9, "A": 25, "pi": 0.44999999999999996, "po": 1.2, "cr": 0.09999999999999999, "ctx": 1048576, "img": true, "tools": true, "so": false, "created": 1785443117, "exp": null, "alias": null}, {"id": "anthropic/claude-opus-5", "open": false, "I": 50.7, "C": 78, "A": 56.2, "pi": 5.0, "po": 25.0, "cr": 0.5, "ctx": 1000000, "img": true, "tools": true, "so": true, "created": 1784912544, "exp": null, "alias": null}, {"id": "inclusionai/ling-3.0-flash", "open": true, "I": 20.6, "C": 50.6, "A": 21, "pi": 0.020999999999999998, "po": 0.063, "cr": 0.004200000000000001, "ctx": 262144, "img": false, "tools": true, "so": false, "created": 1784818580, "exp": null, "alias": null}, {"id": "google/gemini-3.6-flash", "open": false, "I": 34.3, "C": 69.2, "A": 30.2, "pi": 0.75, "po": 3.75, "cr": 0.075, "ctx": 1048576, "img": true, "tools": true, "so": true, "created": 1784646733, "exp": null, "alias": null}, {"id": "google/gemini-3.5-flash-lite", "open": false, "I": 22.7, "C": 49.3, "A": 15.9, "pi": 0.3, "po": 2.5, "cr": 0.03, "ctx": 1048576, "img": true, "tools": true, "so": true, "created": 1784646726, "exp": null, "alias": null}, {"id": "meituan/longcat-2.0", "open": true, "I": 19.7, "C": 45.3, "A": 15.9, "pi": 0.3, "po": 1.2, "cr": 0.006, "ctx": 1048756, "img": false, "tools": true, "so": false, "created": 1784554658, "exp": null, "alias": null}, {"id": "thinkingmachines/inkling", "open": true, "I": 25.5, "C": 52.1, "A": 24.3, "pi": 1.0, "po": 4.05, "cr": 0.16999999999999998, "ctx": 1048576, "img": true, "tools": true, "so": false, "created": 1784325956, "exp": null, "alias": null}, {"id": "moonshotai/kimi-k3", "open": true, "I": 43.8, "C": 76.2, "A": 50.6, "pi": 3.0, "po": 15.0, "cr": 0.3, "ctx": 1048576, "img": true, "tools": true, "so": true, "created": 1784215858, "exp": null, "alias": null}, {"id": "meta/muse-spark-1.1", "open": false, "I": 34.3, "C": 71.3, "A": 27.5, "pi": 1.25, "po": 4.25, "cr": 0.15, "ctx": 1048576, "img": true, "tools": true, "so": true, "created": 1784215741, "exp": null, "alias": null}, {"id": "openai/gpt-5.6-luna", "open": false, "I": 37.5, "C": 71.4, "A": 42.7, "pi": 0.19999999999999998, "po": 1.2, "cr": 0.02, "ctx": 1050000, "img": true, "tools": true, "so": true, "created": 1783590864, "exp": null, "alias": null}, {"id": "openai/gpt-5.6-terra", "open": false, "I": 42.3, "C": 76.7, "A": 43.7, "pi": 2.0, "po": 12.0, "cr": 0.19999999999999998, "ctx": 1050000, "img": true, "tools": true, "so": true, "created": 1783590857, "exp": null, "alias": null}, {"id": "openai/gpt-5.6-sol", "open": false, "I": 47.1, "C": 77.4, "A": 50.5, "pi": 2.0, "po": 10.0, "cr": 0.19999999999999998, "ctx": 1050000, "img": true, "tools": true, "so": true, "created": 1783590850, "exp": null, "alias": null}, {"id": "x-ai/grok-4.5", "open": false, "I": 39.1, "C": 72.4, "A": 42.1, "pi": 2.0, "po": 6.0, "cr": 0.3, "ctx": 500000, "img": true, "tools": true, "so": true, "created": 1783523154, "exp": null, "alias": null}, {"id": "anthropic/claude-sonnet-5", "open": false, "I": 38.4, "C": 71.5, "A": 44.3, "pi": 2.0, "po": 10.0, "cr": 0.19999999999999998, "ctx": 1000000, "img": true, "tools": true, "so": true, "created": 1782843083, "exp": null, "alias": null}, {"id": "z-ai/glm-5.2", "open": true, "I": 34, "C": 68.8, "A": 39.4, "pi": 1.4, "po": 4.4, "cr": 0.14, "ctx": 1048576, "img": false, "tools": true, "so": true, "created": 1781631930, "exp": null, "alias": null}, {"id": "moonshotai/kimi-k2.7-code", "open": true, "I": 26.3, "C": 60.8, "A": 22.5, "pi": 0.7062, "po": 3.21, "cr": 0.18, "ctx": 262144, "img": true, "tools": true, "so": true, "created": 1781266361, "exp": null, "alias": null}, {"id": "anthropic/claude-fable-5", "open": false, "I": 49.7, "C": 76.5, "A": 51, "pi": 10.0, "po": 50.0, "cr": 1.0, "ctx": 1000000, "img": true, "tools": true, "so": true, "created": 1781007515, "exp": null, "alias": null}, {"id": "nvidia/nemotron-3-ultra-550b-a55b", "open": true, "I": 23.4, "C": 49.3, "A": 21.7, "pi": 0.625, "po": 3.125, "cr": 0.1875, "ctx": 262144, "img": false, "tools": true, "so": true, "created": 1780551208, "exp": null, "alias": null}, {"id": "qwen/qwen3.7-plus", "open": false, "I": 25.8, "C": 55.9, "A": 19.7, "pi": 0.32, "po": 1.28, "cr": 0.064, "ctx": 1000000, "img": true, "tools": true, "so": true, "created": 1780491783, "exp": null, "alias": null}, {"id": "minimax/minimax-m3", "open": true, "I": 29.6, "C": 58.6, "A": 30.8, "pi": 0.3, "po": 1.2, "cr": 0.06, "ctx": 1048576, "img": true, "tools": true, "so": true, "created": 1780245374, "exp": null, "alias": null}, {"id": "anthropic/claude-opus-4.8", "open": false, "I": 42, "C": 74.3, "A": 42.6, "pi": 5.0, "po": 25.0, "cr": 0.5, "ctx": 1000000, "img": true, "tools": true, "so": true, "created": 1779905091, "exp": null, "alias": null}, {"id": "qwen/qwen3.7-max", "open": false, "I": 29.9, "C": 66, "A": 23.9, "pi": 1.475, "po": 4.425, "cr": 0.295, "ctx": 1000000, "img": false, "tools": true, "so": true, "created": 1779376861, "exp": null, "alias": null}, {"id": "google/gemini-3.5-flash", "open": false, "I": 33, "C": 70.1, "A": 27.3, "pi": 1.5, "po": 9.0, "cr": 0.15, "ctx": 1048576, "img": true, "tools": true, "so": true, "created": 1779193800, "exp": null, "alias": null}, {"id": "x-ai/grok-4.3", "open": false, "I": 25.4, "C": 42.2, "A": 17.2, "pi": 1.25, "po": 2.5, "cr": 0.19999999999999998, "ctx": 1000000, "img": true, "tools": true, "so": true, "created": 1777591821, "exp": null, "alias": null}, {"id": "mistralai/mistral-medium-3-5", "open": false, "I": 14.9, "C": 46.9, "A": 9.4, "pi": 1.5, "po": 7.5, "cr": null, "ctx": 262144, "img": true, "tools": true, "so": true, "created": 1777570439, "exp": null, "alias": null}, {"id": "qwen/qwen3.6-35b-a3b", "open": true, "I": 18.8, "C": 41.9, "A": 15, "pi": 0.09999999999999999, "po": 0.8999999999999999, "cr": 0.049999999999999996, "ctx": 262144, "img": true, "tools": true, "so": true, "created": 1777260255, "exp": null, "alias": null}, {"id": "qwen/qwen3.6-27b", "open": true, "I": 21.9, "C": 53.7, "A": 20.1, "pi": 0.3, "po": 2.0, "cr": 0.03, "ctx": 262144, "img": true, "tools": true, "so": true, "created": 1777255064, "exp": null, "alias": null}, {"id": "openai/gpt-5.5", "open": false, "I": 38.6, "C": 74.9, "A": 37.3, "pi": 5.0, "po": 30.0, "cr": 0.5, "ctx": 1050000, "img": true, "tools": true, "so": true, "created": 1777051893, "exp": null, "alias": null}, {"id": "deepseek/deepseek-v4-pro", "open": true, "I": 30.9, "C": 59.4, "A": 27.7, "pi": 1.5999999999999999, "po": 3.1999999999999997, "cr": 0.135, "ctx": 1048576, "img": false, "tools": true, "so": true, "created": 1777000679, "exp": null, "alias": null}, {"id": "deepseek/deepseek-v4-flash", "open": true, "I": 24.8, "C": 52, "A": 27.9, "pi": 0.088606, "po": 0.177212, "cr": 0.017721200000000003, "ctx": 1048576, "img": false, "tools": true, "so": true, "created": 1777000666, "exp": null, "alias": null}, {"id": "tencent/hy3-preview", "open": true, "I": 25.8, "C": 58.8, "A": 25.6, "pi": 0.18, "po": 0.6, "cr": 0.06, "ctx": 262144, "img": false, "tools": true, "so": false, "created": 1776878150, "exp": null, "alias": null}, {"id": "xiaomi/mimo-v2.5-pro", "open": true, "I": 26.4, "C": 60.2, "A": 22.7, "pi": 0.435, "po": 0.87, "cr": 0.0036, "ctx": 1050000, "img": false, "tools": true, "so": true, "created": 1776874273, "exp": null, "alias": null}, {"id": "xiaomi/mimo-v2.5", "open": true, "I": 22.3, "C": 56.8, "A": 17.4, "pi": 0.14, "po": 0.28, "cr": 0.0028, "ctx": 1050000, "img": true, "tools": true, "so": true, "created": 1776874269, "exp": null, "alias": null}, {"id": "moonshotai/kimi-k2.6", "open": true, "I": 27.5, "C": 61.8, "A": 22.1, "pi": 0.95, "po": 4.0, "cr": 0.16, "ctx": 262144, "img": true, "tools": true, "so": true, "created": 1776699402, "exp": null, "alias": null}, {"id": "z-ai/glm-5.1", "open": true, "I": 26.4, "C": 55.8, "A": 25.2, "pi": 0.966, "po": 3.036, "cr": 0.1794, "ctx": 204800, "img": false, "tools": true, "so": true, "created": 1775578025, "exp": null, "alias": null}, {"id": "google/gemma-4-31b-it", "open": true, "I": 15.4, "C": 43.4, "A": 6.7, "pi": 0.09, "po": 0.33999999999999997, "cr": 0.049999999999999996, "ctx": 262144, "img": true, "tools": true, "so": true, "created": 1775148486, "exp": null, "alias": null}, {"id": "arcee-ai/trinity-large-thinking", "open": true, "I": 10.9, "C": 25.8, "A": 1.2, "pi": 0.25, "po": 0.7999999999999999, "cr": 0.06, "ctx": 262144, "img": false, "tools": true, "so": false, "created": 1775058318, "exp": null, "alias": null}, {"id": "minimax/minimax-m2.7", "open": true, "I": 23.2, "C": 52.6, "A": 16.8, "pi": 0.3, "po": 1.2, "cr": 0.06, "ctx": 204800, "img": false, "tools": true, "so": true, "created": 1773836697, "exp": null, "alias": null}, {"id": "openai/gpt-5.4-nano", "open": false, "I": 21.2, "C": 56.1, "A": 17.7, "pi": 0.19999999999999998, "po": 1.25, "cr": 0.02, "ctx": 400000, "img": true, "tools": true, "so": true, "created": 1773748187, "exp": null, "alias": null}, {"id": "openai/gpt-5.4-mini", "open": false, "I": 24.6, "C": 56.1, "A": 19.7, "pi": 0.75, "po": 4.5, "cr": 0.075, "ctx": 400000, "img": true, "tools": true, "so": true, "created": 1773748178, "exp": null, "alias": null}, {"id": "mistralai/mistral-small-2603", "open": true, "I": 11.5, "C": 26.6, "A": 1.4, "pi": 0.15, "po": 0.6, "cr": 0.015, "ctx": 262144, "img": true, "tools": true, "so": true, "created": 1773695685, "exp": null, "alias": null}, {"id": "nvidia/nemotron-3-super-120b-a12b", "open": true, "I": 13.6, "C": 37.7, "A": 4.1, "pi": 0.08, "po": 0.44999999999999996, "cr": null, "ctx": 262144, "img": false, "tools": true, "so": true, "created": 1773245239, "exp": null, "alias": null}, {"id": "inception/mercury-2", "open": false, "I": 11.5, "C": 31.1, "A": 4, "pi": 0.25, "po": 0.75, "cr": 0.024999999999999998, "ctx": 128000, "img": false, "tools": true, "so": true, "created": 1772636275, "exp": null, "alias": null}, {"id": "google/gemini-3.1-flash-lite-preview", "open": false, "I": 16, "C": 34.7, "A": 3.2, "pi": 0.25, "po": 1.5, "cr": 0.024999999999999998, "ctx": 1048576, "img": true, "tools": true, "so": true, "created": 1772512673, "exp": null, "alias": null}, {"id": "qwen/qwen3.5-122b-a10b", "open": true, "I": 16.2, "C": 45.7, "A": 9.6, "pi": 0.26, "po": 2.08, "cr": null, "ctx": 262144, "img": true, "tools": true, "so": true, "created": 1772053789, "exp": null, "alias": null}, {"id": "google/gemini-3.1-pro-preview", "open": false, "I": 30.4, "C": 68.8, "A": 10.3, "pi": 2.0, "po": 12.0, "cr": 0.19999999999999998, "ctx": 1048576, "img": true, "tools": true, "so": true, "created": 1771509627, "exp": null, "alias": null}, {"id": "anthropic/claude-sonnet-4.6", "open": false, "I": 30.5, "C": 63, "A": 33.1, "pi": 3.0, "po": 15.0, "cr": 0.3, "ctx": 1000000, "img": true, "tools": true, "so": true, "created": 1771342990, "exp": null, "alias": null}, {"id": "qwen/qwen3.5-397b-a17b", "open": true, "I": 19.1, "C": 48.2, "A": 10.6, "pi": 0.55, "po": 3.5, "cr": 0.22499999999999998, "ctx": 262144, "img": true, "tools": true, "so": true, "created": 1771223018, "exp": null, "alias": null}, {"id": "qwen/qwen3-coder-next", "open": true, "I": 10.1, "C": 36.2, "A": 3.6, "pi": 0.12, "po": 0.7999999999999999, "cr": 0.07, "ctx": 262144, "img": false, "tools": true, "so": true, "created": 1770164101, "exp": null, "alias": null}, {"id": "upstage/solar-pro-3", "open": false, "I": 7.8, "C": 16.2, "A": 1.4, "pi": 0.15, "po": 0.6, "cr": 0.015, "ctx": 131072, "img": false, "tools": true, "so": true, "created": 1769481200, "exp": null, "alias": null}, {"id": "nvidia/nemotron-3-nano-30b-a3b", "open": true, "I": 8.9, "C": 14.4, "A": 1, "pi": 0.06, "po": 0.24, "cr": null, "ctx": 262144, "img": false, "tools": true, "so": true, "created": 1765731275, "exp": null, "alias": null}, {"id": "mistralai/devstral-2512", "open": true, "I": 9.4, "C": 31.3, "A": 4.9, "pi": 0.39999999999999997, "po": 2.0, "cr": 0.04, "ctx": 262144, "img": false, "tools": true, "so": true, "created": 1765285419, "exp": null, "alias": null}, {"id": "mistralai/ministral-14b-2512", "open": true, "I": 6, "C": 14.4, "A": 1.1, "pi": 0.19999999999999998, "po": 0.19999999999999998, "cr": 0.02, "ctx": 262144, "img": true, "tools": true, "so": true, "created": 1764681735, "exp": null, "alias": null}, {"id": "mistralai/ministral-8b-2512", "open": true, "I": 5.5, "C": 9.7, "A": 0.6, "pi": 0.15, "po": 0.15, "cr": 0.015, "ctx": 262144, "img": true, "tools": true, "so": true, "created": 1764681654, "exp": null, "alias": null}, {"id": "mistralai/ministral-3b-2512", "open": true, "I": 4.8, "C": 4.8, "A": 0.8, "pi": 0.09999999999999999, "po": 0.09999999999999999, "cr": 0.01, "ctx": 131072, "img": true, "tools": true, "so": true, "created": 1764681560, "exp": null, "alias": null}, {"id": "mistralai/mistral-large-2512", "open": false, "I": 9.7, "C": 20.1, "A": 2.4, "pi": 0.5, "po": 1.5, "cr": 0.049999999999999996, "ctx": 262144, "img": true, "tools": true, "so": true, "created": 1764624472, "exp": null, "alias": null}, {"id": "anthropic/claude-haiku-4.5", "open": false, "I": 17.6, "C": 43.9, "A": 10.3, "pi": 1.0, "po": 5.0, "cr": 0.09999999999999999, "ctx": 200000, "img": true, "tools": true, "so": true, "created": 1760547638, "exp": null, "alias": null}, {"id": "anthropic/claude-sonnet-4.5", "open": false, "I": 21.2, "C": 52.1, "A": 17.5, "pi": 3.0, "po": 15.0, "cr": 0.3, "ctx": 1000000, "img": true, "tools": true, "so": true, "created": 1759161676, "exp": null, "alias": null}, {"id": "deepseek/deepseek-v3.1-terminus", "open": true, "I": 15.4, "C": 43.5, "A": 8.9, "pi": 0.27, "po": 1.0, "cr": 0.135, "ctx": 163840, "img": false, "tools": true, "so": true, "created": 1758548275, "exp": null, "alias": null}, {"id": "qwen/qwen3-30b-a3b-thinking-2507", "open": true, "I": 9.8, "C": 12.1, "A": 0.9, "pi": 0.19999999999999998, "po": 2.4, "cr": null, "ctx": 81920, "img": false, "tools": true, "so": false, "created": 1756399192, "exp": null, "alias": null}, {"id": "mistralai/mistral-medium-3.1", "open": false, "I": 9.5, "C": 20.5, "A": 3.1, "pi": 0.39999999999999997, "po": 2.0, "cr": 0.04, "ctx": 131072, "img": true, "tools": true, "so": true, "created": 1755095639, "exp": null, "alias": null}, {"id": "openai/gpt-5-mini", "open": false, "I": 17.4, "C": 15.6, "A": 8.9, "pi": 0.25, "po": 2.0, "cr": 0.024999999999999998, "ctx": 400000, "img": true, "tools": true, "so": true, "created": 1754587407, "exp": null, "alias": null}, {"id": "openai/gpt-oss-120b", "open": true, "I": 12.3, "C": 30.4, "A": 6.2, "pi": 0.037, "po": 0.16999999999999998, "cr": null, "ctx": 131072, "img": false, "tools": true, "so": true, "created": 1754414231, "exp": null, "alias": null}, {"id": "openai/gpt-oss-20b", "open": true, "I": 9, "C": 20.7, "A": 1.4, "pi": 0.03, "po": 0.13, "cr": 0.03, "ctx": 131072, "img": false, "tools": true, "so": true, "created": 1754414229, "exp": null, "alias": null}, {"id": "qwen/qwen3-235b-a22b-thinking-2507", "open": true, "I": 12.7, "C": 22.1, "A": 1.3, "pi": 0.22999999999999998, "po": 2.3, "cr": null, "ctx": 131072, "img": false, "tools": true, "so": false, "created": 1753449557, "exp": null, "alias": null}, {"id": "google/gemini-2.5-pro", "open": false, "I": 16.7, "C": 33.3, "A": 3.5, "pi": 1.25, "po": 10.0, "cr": 0.125, "ctx": 1048576, "img": true, "tools": true, "so": true, "created": 1750169544, "exp": null, "alias": null}, {"id": "qwen/qwen3-8b", "open": true, "I": 5.2, "C": 9, "A": 0.8, "pi": 0.117, "po": 0.45499999999999996, "cr": null, "ctx": 131072, "img": false, "tools": true, "so": false, "created": 1745876632, "exp": null, "alias": null}, {"id": "qwen/qwen3-14b", "open": true, "I": 6.4, "C": 13.8, "A": 0.9, "pi": 0.12, "po": 0.24, "cr": null, "ctx": 131072, "img": false, "tools": true, "so": true, "created": 1745876478, "exp": null, "alias": null}, {"id": "qwen/qwen3-32b", "open": true, "I": 7.2, "C": 15.3, "A": 0.9, "pi": 0.08, "po": 0.28, "cr": null, "ctx": 131072, "img": false, "tools": true, "so": true, "created": 1745875945, "exp": null, "alias": null}, {"id": "meta-llama/llama-4-maverick", "open": true, "I": 9.3, "C": 16.3, "A": 0.6, "pi": 0.1875, "po": 0.6525, "cr": null, "ctx": 1048576, "img": true, "tools": true, "so": true, "created": 1743881822, "exp": null, "alias": null}, {"id": "meta-llama/llama-4-scout", "open": true, "I": 6.5, "C": 8.2, "A": 0.5, "pi": 0.09999999999999999, "po": 0.3, "cr": null, "ctx": 1310720, "img": true, "tools": true, "so": true, "created": 1743881519, "exp": null, "alias": null}, {"id": "deepseek/deepseek-chat-v3-0324", "open": true, "I": 9.7, "C": 21.2, "A": 0.8, "pi": 0.25, "po": 1.0, "cr": null, "ctx": 163840, "img": false, "tools": true, "so": true, "created": 1742824755, "exp": null, "alias": null}, {"id": "google/gemma-3-12b-it", "open": true, "I": 3.8, "C": 5.8, "A": 0.1, "pi": 0.049999999999999996, "po": 0.15, "cr": null, "ctx": 131072, "img": true, "tools": true, "so": true, "created": 1741902625, "exp": null, "alias": null}, {"id": "cohere/command-a", "open": true, "I": 13.9, "C": 27.8, "A": 3.6, "pi": 2.5, "po": 10.0, "cr": null, "ctx": 256000, "img": false, "tools": false, "so": true, "created": 1741894342, "exp": null, "alias": null}, {"id": "google/gemma-3-27b-it", "open": true, "I": 4.9, "C": 10.1, "A": 0.1, "pi": 0.08, "po": 0.44999999999999996, "cr": 0.04, "ctx": 131072, "img": true, "tools": true, "so": true, "created": 1741756359, "exp": null, "alias": null}, {"id": "openai/o3-mini-high", "open": false, "I": 11, "C": 16.3, "A": 0.9, "pi": 1.1, "po": 4.4, "cr": 0.55, "ctx": 200000, "img": false, "tools": true, "so": true, "created": 1739372611, "exp": null, "alias": null}, {"id": "deepseek/deepseek-r1", "open": true, "I": 11.4, "C": 24.6, "A": 1.1, "pi": 0.7, "po": 2.5, "cr": null, "ctx": 64000, "img": false, "tools": true, "so": false, "created": 1737381095, "exp": null, "alias": null}] \ No newline at end of file From 5dd78e6afeb50532b048c09b871b35d3c0e5dfb3 Mon Sep 17 00:00:00 2001 From: santoshkumarradha Date: Thu, 17 Sep 2026 01:14:28 -0400 Subject: [PATCH 005/173] pool/tally: a cell keeps n, sum and sum of squares and reads back mean and sample variance Co-Authored-By: codeaf --- internal/pool/tally/tally.go | 38 +++++++++++++++++++++++++++++++ internal/pool/tally/tally_test.go | 29 +++++++++++++++++++++++ 2 files changed, 67 insertions(+) create mode 100644 internal/pool/tally/tally.go create mode 100644 internal/pool/tally/tally_test.go diff --git a/internal/pool/tally/tally.go b/internal/pool/tally/tally.go new file mode 100644 index 0000000000..d4b9730004 --- /dev/null +++ b/internal/pool/tally/tally.go @@ -0,0 +1,38 @@ +// Package tally keeps additive sufficient statistics for observations so +// that two sheets recorded on different machines can be merged by addition, +// in any order, and give the same answer as one sheet that saw everything. +// It is standard library only and touches no network, no disk and no clock. +// The only strings a sheet holds are the metric, role, model and dim labels +// an observation carries. +package tally + +// Cell is the sufficient statistic recorded for one address: how many +// observations were seen, their sum, and the sum of their squares. Two +// cells for the same address add field by field. +type Cell struct { + N int64 + Sum float64 + SumSq float64 +} + +// Mean returns the mean of the observations, or 0 when the cell is empty. +func (c Cell) Mean() float64 { + if c.N == 0 { + return 0 + } + return c.Sum / float64(c.N) +} + +// Var returns the sample variance of the observations. It is 0 when the +// cell holds fewer than two observations, and it is never negative: +// rounding in the sum-of-squares formula is clamped to zero. +func (c Cell) Var() float64 { + if c.N < 2 { + return 0 + } + v := (c.SumSq - c.Sum*c.Sum/float64(c.N)) / float64(c.N-1) + if v < 0 { + return 0 + } + return v +} diff --git a/internal/pool/tally/tally_test.go b/internal/pool/tally/tally_test.go new file mode 100644 index 0000000000..80ec55ad93 --- /dev/null +++ b/internal/pool/tally/tally_test.go @@ -0,0 +1,29 @@ +package tally + +import "testing" + +func TestCellMeanAndVar(t *testing.T) { + if got := (Cell{}).Mean(); got != 0 { + t.Fatalf("empty Mean = %v, want 0", got) + } + if got := (Cell{}).Var(); got != 0 { + t.Fatalf("empty Var = %v, want 0", got) + } + if got := (Cell{N: 1, Sum: 5, SumSq: 25}).Var(); got != 0 { + t.Fatalf("single-observation Var = %v, want 0", got) + } + c := Cell{N: 3, Sum: 6, SumSq: 14} // observations 1, 2, 3 + if got := c.Mean(); got != 2 { + t.Fatalf("Mean = %v, want 2", got) + } + if got := c.Var(); got != 1 { + t.Fatalf("Var = %v, want 1", got) + } +} + +func TestCellVarNeverNegative(t *testing.T) { + // (4 - 3*3/2) / 1 rounds negative; the clamp must hold it at zero. + if got := (Cell{N: 2, Sum: 3, SumSq: 4}).Var(); got != 0 { + t.Fatalf("Var = %v, want 0", got) + } +} From 9ceb82af62233a4f51f08da9ddb0907ea823a13d Mon Sep 17 00:00:00 2001 From: santoshkumarradha Date: Thu, 17 Sep 2026 01:16:48 -0400 Subject: [PATCH 006/173] pool/tally: sheets record observations and paired wins under addressed labels Co-Authored-By: codeaf --- internal/pool/tally/tally.go | 181 ++++++++++++++++++++++++++++++ internal/pool/tally/tally_test.go | 159 +++++++++++++++++++++++++- 2 files changed, 339 insertions(+), 1 deletion(-) diff --git a/internal/pool/tally/tally.go b/internal/pool/tally/tally.go index d4b9730004..cb637e774c 100644 --- a/internal/pool/tally/tally.go +++ b/internal/pool/tally/tally.go @@ -6,6 +6,187 @@ // an observation carries. package tally +import ( + "maps" + "math" + "slices" + "strconv" + "strings" + "sync" +) + +// maxNameLen bounds every label a sheet stores. +const maxNameLen = 128 + +// validName reports whether s may be stored: at most maxNameLen bytes, no +// newline, and no path separator — except that a model id may carry the +// single "/" between vendor and name. +func validName(s string, model bool) bool { + if len(s) > maxNameLen { + return false + } + slashes := 0 + for i := 0; i < len(s); i++ { + switch s[i] { + case '\n', '\\': + return false + case '/': + if !model { + return false + } + slashes++ + if slashes > 1 { + return false + } + } + } + return true +} + +// lp prefixes s with its byte length, so an address key built by +// concatenating labels is unambiguous whatever bytes the labels hold. +func lp(s string) string { + return strconv.Itoa(len(s)) + ":" + s +} + +// dimsKey canonicalises the dim labels of an address: keys sorted, every +// key and value length-prefixed. Nil and an empty map give the same key, +// and the order the keys were inserted in never matters. +func dimsKey(dims map[string]string) string { + keys := slices.Sorted(maps.Keys(dims)) + var b strings.Builder + for _, k := range keys { + b.WriteString(lp(k)) + b.WriteString(lp(dims[k])) + } + return b.String() +} + +// cellAddr is the full address of a cell, kept so the sheet can name it +// again in a document. +type cellAddr struct { + metric, role, model string + dims map[string]string +} + +// key is the unambiguous form of the address. +func (a cellAddr) key() string { + return lp(a.metric) + lp(a.role) + lp(a.model) + dimsKey(a.dims) +} + +type cellEntry struct { + addr cellAddr + cell Cell +} + +// winKey addresses one direction of a paired comparison. +type winKey struct { + role, winner, loser string +} + +// Sheet keeps additive sufficient statistics: one cell per address of +// metric, role, model and dims, and paired-comparison tallies per role. +// A Sheet is safe for concurrent use by multiple goroutines, and the zero +// value is an empty sheet ready to use. +type Sheet struct { + mu sync.Mutex + cells map[string]cellEntry + wins map[winKey]int64 +} + +// New returns an empty sheet. +func New() *Sheet { + return &Sheet{} +} + +// copyDims returns a private copy of dims, nil for nil or empty. +func copyDims(dims map[string]string) map[string]string { + if len(dims) == 0 { + return nil + } + out := make(map[string]string, len(dims)) + for k, v := range dims { + out[k] = v + } + return out +} + +// Observe records one observation x of metric for role and model, under +// the optional dim labels (for example quant=fp8). Nil dims and an empty +// map are the same address, and the order the labels were inserted in +// never matters. An observation that is NaN or infinite is ignored, and so +// is the whole call when any label is longer than 128 bytes or contains a +// newline or a path separator other than the single "/" a model id carries +// between vendor and name. +func (s *Sheet) Observe(metric, role, model string, dims map[string]string, x float64) { + if math.IsNaN(x) || math.IsInf(x, 0) { + return + } + if !validName(metric, false) || !validName(role, false) || !validName(model, true) { + return + } + for k, v := range dims { + if !validName(k, false) || !validName(v, false) { + return + } + } + a := cellAddr{metric: metric, role: role, model: model, dims: copyDims(dims)} + k := a.key() + s.mu.Lock() + defer s.mu.Unlock() + if s.cells == nil { + s.cells = make(map[string]cellEntry) + } + e, ok := s.cells[k] + if !ok { + e.addr = a + } + e.cell.N++ + e.cell.Sum += x + e.cell.SumSq += x * x + s.cells[k] = e +} + +// Cell returns the cell recorded for the address, and whether any +// observation was recorded for it. Nil dims and an empty map are the same +// address, and the order the labels were inserted in never matters. +func (s *Sheet) Cell(metric, role, model string, dims map[string]string) (Cell, bool) { + k := cellAddr{metric: metric, role: role, model: model, dims: dims}.key() + s.mu.Lock() + defer s.mu.Unlock() + e, ok := s.cells[k] + return e.cell, ok +} + +// Win records one paired comparison in which winner beat loser for role. +// The call is ignored when a name is empty, when winner and loser are +// equal, or when a name is longer than 128 bytes or contains a newline or +// a path separator other than the single "/" a model id carries between +// vendor and name. +func (s *Sheet) Win(role, winner, loser string) { + if role == "" || winner == "" || loser == "" || winner == loser { + return + } + if !validName(role, false) || !validName(winner, true) || !validName(loser, true) { + return + } + s.mu.Lock() + defer s.mu.Unlock() + if s.wins == nil { + s.wins = make(map[winKey]int64) + } + s.wins[winKey{role: role, winner: winner, loser: loser}]++ +} + +// Wins returns the paired comparisons recorded for role in both directions +// between a and b. +func (s *Sheet) Wins(role, a, b string) (aOverB, bOverA int64) { + s.mu.Lock() + defer s.mu.Unlock() + return s.wins[winKey{role: role, winner: a, loser: b}], + s.wins[winKey{role: role, winner: b, loser: a}] +} + // Cell is the sufficient statistic recorded for one address: how many // observations were seen, their sum, and the sum of their squares. Two // cells for the same address add field by field. diff --git a/internal/pool/tally/tally_test.go b/internal/pool/tally/tally_test.go index 80ec55ad93..e667c1ce34 100644 --- a/internal/pool/tally/tally_test.go +++ b/internal/pool/tally/tally_test.go @@ -1,6 +1,10 @@ package tally -import "testing" +import ( + "math" + "strings" + "testing" +) func TestCellMeanAndVar(t *testing.T) { if got := (Cell{}).Mean(); got != 0 { @@ -27,3 +31,156 @@ func TestCellVarNeverNegative(t *testing.T) { t.Fatalf("Var = %v, want 0", got) } } + +func checkCell(t *testing.T, s *Sheet, metric, role, model string, dims map[string]string, want Cell) { + t.Helper() + got, ok := s.Cell(metric, role, model, dims) + if !ok { + t.Fatalf("Cell(%q, %q, %q, %v) not recorded", metric, role, model, dims) + } + if got != want { + t.Fatalf("Cell(%q, %q, %q, %v) = %+v, want %+v", metric, role, model, dims, got, want) + } +} + +func checkNoCell(t *testing.T, s *Sheet, metric, role, model string, dims map[string]string) { + t.Helper() + if got, ok := s.Cell(metric, role, model, dims); ok { + t.Fatalf("Cell(%q, %q, %q, %v) = %+v, want nothing recorded", metric, role, model, dims, got) + } +} + +func TestObserveAccumulates(t *testing.T) { + s := New() + s.Observe("tok", "coder", "vendor/model", nil, 2) + s.Observe("tok", "coder", "vendor/model", nil, 4) + checkCell(t, s, "tok", "coder", "vendor/model", nil, Cell{N: 2, Sum: 6, SumSq: 20}) + c, _ := s.Cell("tok", "coder", "vendor/model", nil) + if c.Mean() != 3 || c.Var() != 2 { + t.Fatalf("Mean = %v, Var = %v, want 3 and 2", c.Mean(), c.Var()) + } + s.Observe("tok", "coder", "vendor/model", nil, -1.5) + checkCell(t, s, "tok", "coder", "vendor/model", nil, Cell{N: 3, Sum: 4.5, SumSq: 22.25}) +} + +func TestDimsAddressing(t *testing.T) { + s := New() + s.Observe("m", "r", "model", nil, 1) + s.Observe("m", "r", "model", map[string]string{}, 1) + s.Observe("m", "r", "model", map[string]string{"quant": "fp8"}, 1) + s.Observe("m", "r", "model", map[string]string{"quant": "fp8"}, 1) // same address again + s.Observe("m", "r", "model", map[string]string{"a": "1", "b": "2"}, 1) + s.Observe("m", "r", "model", map[string]string{"b": "2", "a": "1"}, 1) // order never matters + checkCell(t, s, "m", "r", "model", nil, Cell{N: 2, Sum: 2, SumSq: 2}) + checkCell(t, s, "m", "r", "model", map[string]string{"quant": "fp8"}, Cell{N: 2, Sum: 2, SumSq: 2}) + checkCell(t, s, "m", "r", "model", map[string]string{"b": "2", "a": "1"}, Cell{N: 2, Sum: 2, SumSq: 2}) + checkNoCell(t, s, "m", "r", "model", map[string]string{"quant": "fp4"}) + checkNoCell(t, s, "m", "r", "model", map[string]string{"quant": "fp8", "extra": "x"}) + s.Observe("m", "r2", "model", nil, 1) + s.Observe("m2", "r", "model", nil, 1) + s.Observe("m", "r", "model2", nil, 1) + checkCell(t, s, "m", "r2", "model", nil, Cell{N: 1, Sum: 1, SumSq: 1}) + checkCell(t, s, "m2", "r", "model", nil, Cell{N: 1, Sum: 1, SumSq: 1}) + checkCell(t, s, "m", "r", "model2", nil, Cell{N: 1, Sum: 1, SumSq: 1}) +} + +func TestAddressingIsUnambiguous(t *testing.T) { + // Labels that would collide in a plain concatenation stay distinct. + s := New() + s.Observe("a:b", "c", "m", nil, 1) + s.Observe("a", "b:c", "m", nil, 1) + checkCell(t, s, "a:b", "c", "m", nil, Cell{N: 1, Sum: 1, SumSq: 1}) + checkCell(t, s, "a", "b:c", "m", nil, Cell{N: 1, Sum: 1, SumSq: 1}) +} + +func TestObserveIgnoresNonFinite(t *testing.T) { + s := New() + s.Observe("m", "r", "model", nil, math.NaN()) + s.Observe("m", "r", "model", nil, math.Inf(1)) + s.Observe("m", "r", "model", nil, math.Inf(-1)) + checkNoCell(t, s, "m", "r", "model", nil) + s.Observe("m", "r", "model", nil, 1) // the sheet still records after refusals + checkCell(t, s, "m", "r", "model", nil, Cell{N: 1, Sum: 1, SumSq: 1}) +} + +func TestObserveRefusesBadNames(t *testing.T) { + long := strings.Repeat("x", maxNameLen+1) + cases := []struct { + name string + metric, role, mod string + dims map[string]string + }{ + {"metric with newline", "to\nk", "r", "m", nil}, + {"long metric", long, "r", "m", nil}, + {"metric with path separator", "to/k", "r", "m", nil}, + {"role with path separator", "m", "ro/le", "m", nil}, + {"long role", "m", long, "m", nil}, + {"long model", "m", "r", long, nil}, + {"model with backslash", "m", "r", `ven\dor`, nil}, + {"model with two separators", "m", "r", "ven/dor/name", nil}, + {"dim key with separator", "m", "r", "m", map[string]string{"qu/ant": "fp8"}}, + {"long dim key", "m", "r", "m", map[string]string{long: "fp8"}}, + {"long dim value", "m", "r", "m", map[string]string{"quant": long}}, + {"dim value with newline", "m", "r", "m", map[string]string{"quant": "fp\n8"}}, + } + for _, tc := range cases { + s := New() + s.Observe(tc.metric, tc.role, tc.mod, tc.dims, 1) + s.mu.Lock() + got := len(s.cells) + s.mu.Unlock() + if got != 0 { + t.Fatalf("%s: recorded a cell", tc.name) + } + } + // At the bound a label is fine, and the single separator a model id + // carries between vendor and name is fine. + s := New() + s.Observe(strings.Repeat("x", maxNameLen), "r", "vendor/name", nil, 1) + s.Observe("plain", "r", "bare", nil, 1) + checkCell(t, s, strings.Repeat("x", maxNameLen), "r", "vendor/name", nil, Cell{N: 1, Sum: 1, SumSq: 1}) + checkCell(t, s, "plain", "r", "bare", nil, Cell{N: 1, Sum: 1, SumSq: 1}) +} + +func TestWinAndWins(t *testing.T) { + s := New() + s.Win("coder", "a", "b") + s.Win("coder", "a", "b") + s.Win("coder", "b", "a") + aOverB, bOverA := s.Wins("coder", "a", "b") + if aOverB != 2 || bOverA != 1 { + t.Fatalf("Wins = %d, %d, want 2, 1", aOverB, bOverA) + } + aOverB, bOverA = s.Wins("coder", "b", "a") // the query is symmetric + if aOverB != 1 || bOverA != 2 { + t.Fatalf("Wins = %d, %d, want 1, 2", aOverB, bOverA) + } + if x, y := s.Wins("reviewer", "a", "b"); x != 0 || y != 0 { + t.Fatalf("wins leak across roles: %d, %d", x, y) + } + if x, y := s.Wins("coder", "a", "c"); x != 0 || y != 0 { + t.Fatalf("unrecorded pair: %d, %d", x, y) + } + // A model id may carry its separator here too. + s.Win("coder", "vendor/a", "vendor/b") + if x, y := s.Wins("coder", "vendor/a", "vendor/b"); x != 1 || y != 0 { + t.Fatalf("Wins = %d, %d, want 1, 0", x, y) + } +} + +func TestWinRefusesBadCalls(t *testing.T) { + s := New() + s.Win("", "a", "b") + s.Win("r", "", "b") + s.Win("r", "a", "") + s.Win("r", "a", "a") // winner == loser + s.Win("r", "a\n", "b") + s.Win("r", "a", "b\n") + s.Win("r", "a/b/c", "b") // more than the one separator a model id carries + s.Win("r", "a", `b\c`) // a backslash is a path separator + s.Win("ro/le", "a", "b") // a role carries none + s.Win("r", "a", strings.Repeat("b", maxNameLen+1)) + if x, y := s.Wins("r", "a", "b"); x != 0 || y != 0 { + t.Fatalf("refused Win recorded: %d, %d", x, y) + } +} From c64b200c9f0844cae273fe0f2294434e753a6915 Mon Sep 17 00:00:00 2001 From: santoshkumarradha Date: Thu, 17 Sep 2026 01:17:43 -0400 Subject: [PATCH 007/173] pool/tally: sheets merge by adding cells and wins, order-free Co-Authored-By: codeaf --- internal/pool/tally/tally.go | 71 +++++++++++++++ internal/pool/tally/tally_test.go | 146 ++++++++++++++++++++++++++++++ 2 files changed, 217 insertions(+) diff --git a/internal/pool/tally/tally.go b/internal/pool/tally/tally.go index cb637e774c..e1674f5ae0 100644 --- a/internal/pool/tally/tally.go +++ b/internal/pool/tally/tally.go @@ -187,6 +187,77 @@ func (s *Sheet) Wins(role, a, b string) (aOverB, bOverA int64) { s.wins[winKey{role: role, winner: b, loser: a}] } +// winCount is one snapshot entry of the paired-comparison tallies. +type winCount struct { + key winKey + n int64 +} + +// Merge adds other into s: afterwards s holds what it held plus what +// other holds, and other is left unmodified. Merging a sheet into itself +// doubles it. A nil sheet on either side is a no-op. +func (s *Sheet) Merge(other *Sheet) { + if s == nil || other == nil { + return + } + if other == s { + s.mu.Lock() + cells, wins := s.snapshotLocked() + s.applyLocked(cells, wins) + s.mu.Unlock() + return + } + other.mu.Lock() + cells, wins := other.snapshotLocked() + other.mu.Unlock() + + s.mu.Lock() + s.applyLocked(cells, wins) + s.mu.Unlock() +} + +// snapshotLocked copies the sheet's cells and wins. +func (s *Sheet) snapshotLocked() ([]cellEntry, []winCount) { + cells := make([]cellEntry, 0, len(s.cells)) + for _, e := range s.cells { + cells = append(cells, e) + } + wins := make([]winCount, 0, len(s.wins)) + for k, n := range s.wins { + wins = append(wins, winCount{key: k, n: n}) + } + return cells, wins +} + +// applyLocked adds snapshot cells and wins to the sheet. +func (s *Sheet) applyLocked(cells []cellEntry, wins []winCount) { + if s.cells == nil { + s.cells = make(map[string]cellEntry, len(cells)) + } + for _, e := range cells { + k := e.addr.key() + cur, ok := s.cells[k] + if !ok { + cur = cellEntry{addr: cellAddr{ + metric: e.addr.metric, + role: e.addr.role, + model: e.addr.model, + dims: copyDims(e.addr.dims), + }} + } + cur.cell.N += e.cell.N + cur.cell.Sum += e.cell.Sum + cur.cell.SumSq += e.cell.SumSq + s.cells[k] = cur + } + if s.wins == nil { + s.wins = make(map[winKey]int64, len(wins)) + } + for _, w := range wins { + s.wins[w.key] += w.n + } +} + // Cell is the sufficient statistic recorded for one address: how many // observations were seen, their sum, and the sum of their squares. Two // cells for the same address add field by field. diff --git a/internal/pool/tally/tally_test.go b/internal/pool/tally/tally_test.go index e667c1ce34..7087a04d0e 100644 --- a/internal/pool/tally/tally_test.go +++ b/internal/pool/tally/tally_test.go @@ -1,6 +1,8 @@ package tally import ( + "bytes" + "encoding/json" "math" "strings" "testing" @@ -184,3 +186,147 @@ func TestWinRefusesBadCalls(t *testing.T) { t.Fatalf("refused Win recorded: %d, %d", x, y) } } + +func sameJSON(t *testing.T, a, b *Sheet) bool { + t.Helper() + ja, err := json.Marshal(a) + if err != nil { + t.Fatalf("marshal: %v", err) + } + jb, err := json.Marshal(b) + if err != nil { + t.Fatalf("marshal: %v", err) + } + return bytes.Equal(ja, jb) +} + +func TestMergeCommutative(t *testing.T) { + a := New() + a.Observe("m", "r", "model", map[string]string{"quant": "fp8"}, 3) + a.Win("r", "x", "y") + b := New() + b.Observe("m", "r", "model", nil, 4) + b.Observe("m", "r", "model", nil, 5) + b.Win("r", "y", "x") + + ab := New() + ab.Merge(a) + ab.Merge(b) + ba := New() + ba.Merge(b) + ba.Merge(a) + if !sameJSON(t, ab, ba) { + t.Fatal("merge is not commutative") + } +} + +func TestMergeAssociative(t *testing.T) { + a := New() + a.Observe("m", "r", "model", nil, 1) + a.Win("r", "x", "y") + b := New() + b.Observe("m", "r", "model", map[string]string{"q": "1"}, 2) + c := New() + c.Observe("m", "r", "model", nil, 3) + c.Observe("m", "r", "model", nil, 4) + c.Win("r", "y", "x") + + ab := New() + ab.Merge(a) + ab.Merge(b) + abC := New() + abC.Merge(ab) + abC.Merge(c) + + bc := New() + bc.Merge(b) + bc.Merge(c) + aBC := New() + aBC.Merge(a) + aBC.Merge(bc) + + if !sameJSON(t, abC, aBC) { + t.Fatal("merge is not associative") + } +} + +func TestMergeSelfDoubles(t *testing.T) { + s := New() + s.Observe("m", "r", "model", nil, 2) + s.Observe("m", "r", "model", map[string]string{"q": "1"}, 5) + s.Win("r", "a", "b") + s.Win("r", "b", "a") + s.Merge(s) + checkCell(t, s, "m", "r", "model", nil, Cell{N: 2, Sum: 4, SumSq: 8}) + checkCell(t, s, "m", "r", "model", map[string]string{"q": "1"}, Cell{N: 2, Sum: 10, SumSq: 50}) + if x, y := s.Wins("r", "a", "b"); x != 2 || y != 2 { + t.Fatalf("Wins = %d, %d, want 2, 2", x, y) + } +} + +func TestMergeNilIsNoOp(t *testing.T) { + s := New() + s.Observe("m", "r", "model", nil, 1) + before, err := json.Marshal(s) + if err != nil { + t.Fatal(err) + } + s.Merge(nil) + if j, _ := json.Marshal(s); !bytes.Equal(j, before) { + t.Fatal("Merge(nil) changed the sheet") + } + var nilSheet *Sheet + nilSheet.Merge(s) // must not panic + nilSheet.Merge(nil) // must not panic either +} + +func TestMergeLeavesOtherAlone(t *testing.T) { + other := New() + other.Observe("m", "r", "model", nil, 7) + other.Observe("m", "r", "model", map[string]string{"q": "1"}, 1) + other.Win("r", "p", "q") + before, err := json.Marshal(other) + if err != nil { + t.Fatal(err) + } + s := New() + s.Merge(other) + if j, _ := json.Marshal(other); !bytes.Equal(j, before) { + t.Fatal("Merge modified the merged-in sheet") + } + checkCell(t, s, "m", "r", "model", nil, Cell{N: 1, Sum: 7, SumSq: 49}) + checkCell(t, s, "m", "r", "model", map[string]string{"q": "1"}, Cell{N: 1, Sum: 1, SumSq: 1}) + if x, y := s.Wins("r", "p", "q"); x != 1 || y != 0 { + t.Fatalf("Wins = %d, %d, want 1, 0", x, y) + } +} + +func TestMergedSheetsEqualSingleSheet(t *testing.T) { + one := New() + one.Observe("tok", "coder", "vendor/m", nil, 1) + one.Observe("tok", "coder", "vendor/m", nil, 2) + one.Observe("tok", "coder", "vendor/m", map[string]string{"quant": "fp8"}, 7) + one.Observe("latency", "reviewer", "vendor/m", nil, 0.5) + one.Win("coder", "a", "b") + one.Win("coder", "b", "a") + one.Win("reviewer", "a", "b") + + // The same observations and wins, split over three sheets and recorded + // in a different order. + parts := []*Sheet{New(), New(), New()} + parts[2].Observe("latency", "reviewer", "vendor/m", nil, 0.5) + parts[0].Observe("tok", "coder", "vendor/m", nil, 2) + parts[1].Observe("tok", "coder", "vendor/m", map[string]string{"quant": "fp8"}, 7) + parts[0].Win("coder", "b", "a") + parts[1].Win("reviewer", "a", "b") + parts[0].Observe("tok", "coder", "vendor/m", nil, 1) + parts[1].Win("coder", "a", "b") + + merged := New() + merged.Merge(parts[2]) + merged.Merge(parts[0]) + merged.Merge(parts[1]) + if !sameJSON(t, merged, one) { + t.Fatal("merged sheets disagree with one sheet that saw everything") + } +} From 58fb841cebc0202a3d1689ea935974319afcc07e Mon Sep 17 00:00:00 2001 From: santoshkumarradha Date: Thu, 17 Sep 2026 01:19:45 -0400 Subject: [PATCH 008/173] pool/tally: a deterministic json round trip behind a schema gate Co-Authored-By: codeaf --- internal/pool/tally/tally.go | 119 +++++++++++++++++++ internal/pool/tally/tally_test.go | 187 ++++++++++++++++++++++++++++++ 2 files changed, 306 insertions(+) diff --git a/internal/pool/tally/tally.go b/internal/pool/tally/tally.go index e1674f5ae0..553985424b 100644 --- a/internal/pool/tally/tally.go +++ b/internal/pool/tally/tally.go @@ -7,6 +7,8 @@ package tally import ( + "encoding/json" + "fmt" "maps" "math" "slices" @@ -288,3 +290,120 @@ func (c Cell) Var() float64 { } return v } + +// cellDoc and winDoc are the JSON shapes of one cell and one win; doc is +// the whole document. Fields unknown to this package, here and inside an +// entry, are ignored when reading. +type cellDoc struct { + Metric string `json:"metric"` + Role string `json:"role"` + Model string `json:"model"` + Dims map[string]string `json:"dims"` + N int64 `json:"n"` + Sum float64 `json:"sum"` + SumSq float64 `json:"sumsq"` +} + +type winDoc struct { + Role string `json:"role"` + Winner string `json:"winner"` + Loser string `json:"loser"` + Wins int64 `json:"wins"` +} + +type doc struct { + Schema int `json:"schema"` + Cells []cellDoc `json:"cells"` + Wins []winDoc `json:"wins"` +} + +// schemaVersion is the only document schema this package writes and reads. +const schemaVersion = 1 + +// MarshalJSON encodes the sheet deterministically: sheets holding the same +// statistics give byte-identical documents, whatever order anything was +// inserted in. Cells are sorted by metric, role, model and dim labels, +// wins by role, winner and loser, dim labels by key. The document carries +// a top-level "schema": 1. +func (s *Sheet) MarshalJSON() ([]byte, error) { + s.mu.Lock() + cells, wins := s.snapshotLocked() + s.mu.Unlock() + + docs := make([]cellDoc, 0, len(cells)) + for _, e := range cells { + dims := e.addr.dims + if dims == nil { + dims = map[string]string{} + } + docs = append(docs, cellDoc{ + Metric: e.addr.metric, + Role: e.addr.role, + Model: e.addr.model, + Dims: dims, + N: e.cell.N, + Sum: e.cell.Sum, + SumSq: e.cell.SumSq, + }) + } + slices.SortFunc(docs, func(a, b cellDoc) int { + if c := strings.Compare(a.Metric, b.Metric); c != 0 { + return c + } + if c := strings.Compare(a.Role, b.Role); c != 0 { + return c + } + if c := strings.Compare(a.Model, b.Model); c != 0 { + return c + } + return strings.Compare(dimsKey(a.Dims), dimsKey(b.Dims)) + }) + wdocs := make([]winDoc, 0, len(wins)) + for _, w := range wins { + wdocs = append(wdocs, winDoc{Role: w.key.role, Winner: w.key.winner, Loser: w.key.loser, Wins: w.n}) + } + slices.SortFunc(wdocs, func(a, b winDoc) int { + if c := strings.Compare(a.Role, b.Role); c != 0 { + return c + } + if c := strings.Compare(a.Winner, b.Winner); c != 0 { + return c + } + return strings.Compare(a.Loser, b.Loser) + }) + return json.Marshal(doc{Schema: schemaVersion, Cells: docs, Wins: wdocs}) +} + +// UnmarshalJSON replaces the sheet's contents with the document. Fields +// unknown to this package, at the top level and inside an entry, are +// ignored. A document whose schema is greater than 1 is refused with an +// error. +func (s *Sheet) UnmarshalJSON(data []byte) error { + var d doc + if err := json.Unmarshal(data, &d); err != nil { + return err + } + if d.Schema > schemaVersion { + return fmt.Errorf("tally: document schema %d is newer than this build reads", d.Schema) + } + cells := make(map[string]cellEntry, len(d.Cells)) + for _, c := range d.Cells { + a := cellAddr{metric: c.Metric, role: c.Role, model: c.Model, dims: copyDims(c.Dims)} + k := a.key() + e := cells[k] + e.addr = a + e.cell.N += c.N + e.cell.Sum += c.Sum + e.cell.SumSq += c.SumSq + cells[k] = e + } + wins := make(map[winKey]int64, len(d.Wins)) + for _, w := range d.Wins { + wins[winKey{role: w.Role, winner: w.Winner, loser: w.Loser}] += w.Wins + } + s.mu.Lock() + s.cells = cells + s.wins = wins + s.mu.Unlock() + return nil +} diff --git a/internal/pool/tally/tally_test.go b/internal/pool/tally/tally_test.go index 7087a04d0e..a42fcf2b7c 100644 --- a/internal/pool/tally/tally_test.go +++ b/internal/pool/tally/tally_test.go @@ -4,7 +4,9 @@ import ( "bytes" "encoding/json" "math" + "strconv" "strings" + "sync" "testing" ) @@ -330,3 +332,188 @@ func TestMergedSheetsEqualSingleSheet(t *testing.T) { t.Fatal("merged sheets disagree with one sheet that saw everything") } } + +func TestMarshalEmpty(t *testing.T) { + j, err := json.Marshal(New()) + if err != nil { + t.Fatal(err) + } + if want := `{"schema":1,"cells":[],"wins":[]}`; string(j) != want { + t.Fatalf("empty sheet = %s, want %s", j, want) + } +} + +func TestMarshalDeterministic(t *testing.T) { + s1 := New() + s1.Observe("b", "r", "m", nil, 1) + s1.Observe("a", "r", "m", nil, 1) + s1.Observe("m", "r", "m1", map[string]string{"b": "2"}, 1) + s1.Observe("m", "r", "m1", map[string]string{"a": "1"}, 2) + s1.Win("r", "y", "x") + s1.Win("r", "x", "y") + + s2 := New() + s2.Observe("a", "r", "m", nil, 1) + s2.Observe("b", "r", "m", nil, 1) + s2.Observe("m", "r", "m1", map[string]string{"a": "1"}, 2) + s2.Observe("m", "r", "m1", map[string]string{"b": "2"}, 1) + s2.Win("r", "x", "y") + s2.Win("r", "y", "x") + + j1, err := json.Marshal(s1) + if err != nil { + t.Fatal(err) + } + j2, err := json.Marshal(s2) + if err != nil { + t.Fatal(err) + } + if !bytes.Equal(j1, j2) { + t.Fatalf("marshal depends on insertion order:\n%s\n%s", j1, j2) + } + if i, k := strings.Index(string(j1), `"metric":"a"`), strings.Index(string(j1), `"metric":"b"`); i > k { + t.Fatalf("cells not sorted: %s", j1) + } + if i, k := strings.Index(string(j1), `"winner":"x"`), strings.Index(string(j1), `"winner":"y"`); i > k { + t.Fatalf("wins not sorted: %s", j1) + } +} + +func TestRoundTrip(t *testing.T) { + s := New() + s.Observe("tok", "coder", "vendor/m", map[string]string{"quant": "fp8"}, 1.5) + s.Observe("tok", "coder", "vendor/m", nil, 2) + s.Win("coder", "a", "b") + j, err := json.Marshal(s) + if err != nil { + t.Fatal(err) + } + back := New() + if err := json.Unmarshal(j, back); err != nil { + t.Fatal(err) + } + if !sameJSON(t, back, s) { + t.Fatalf("round trip changed the sheet: %s", j) + } + checkCell(t, back, "tok", "coder", "vendor/m", map[string]string{"quant": "fp8"}, Cell{N: 1, Sum: 1.5, SumSq: 2.25}) + checkCell(t, back, "tok", "coder", "vendor/m", nil, Cell{N: 1, Sum: 2, SumSq: 4}) + if x, y := back.Wins("coder", "a", "b"); x != 1 || y != 0 { + t.Fatalf("Wins = %d, %d, want 1, 0", x, y) + } +} + +func TestUnmarshalReplacesContents(t *testing.T) { + src := New() + src.Observe("m", "r", "model", nil, 1) + j, err := json.Marshal(src) + if err != nil { + t.Fatal(err) + } + dirty := New() + dirty.Observe("old", "r", "model", nil, 9) + dirty.Win("r", "x", "y") + if err := json.Unmarshal(j, dirty); err != nil { + t.Fatal(err) + } + if !sameJSON(t, dirty, src) { + t.Fatal("UnmarshalJSON did not replace existing contents") + } + if _, ok := dirty.Cell("old", "r", "model", nil); ok { + t.Fatal("old contents survived UnmarshalJSON") + } +} + +func TestUnmarshalIgnoresUnknownFields(t *testing.T) { + doc := `{"schema":1,"note":"hello","cells":[` + + `{"metric":"m","role":"r","model":"vendor/m","dims":{"quant":"fp8"},"n":2,"sum":3,"sumsq":5,"taken_by":"x"},` + + `{"metric":"m2","role":"r","model":"vendor/m","n":1,"sum":2,"sumsq":4}` + + `],"wins":[{"role":"r","winner":"a","loser":"b","wins":3,"when":12}],"extra":[1]}` + s := New() + if err := json.Unmarshal([]byte(doc), s); err != nil { + t.Fatal(err) + } + checkCell(t, s, "m", "r", "vendor/m", map[string]string{"quant": "fp8"}, Cell{N: 2, Sum: 3, SumSq: 5}) + checkCell(t, s, "m2", "r", "vendor/m", nil, Cell{N: 1, Sum: 2, SumSq: 4}) + if x, y := s.Wins("r", "a", "b"); x != 3 || y != 0 { + t.Fatalf("Wins = %d, %d, want 3, 0", x, y) + } +} + +func TestUnmarshalRefusesNewerSchema(t *testing.T) { + if err := json.Unmarshal([]byte(`{"schema":2}`), New()); err == nil { + t.Fatal("schema 2 accepted") + } + if err := json.Unmarshal([]byte(`{"schema":99,"cells":[]}`), New()); err == nil { + t.Fatal("schema 99 accepted") + } + if err := json.Unmarshal([]byte(`{"schema":1,"cells":[],"wins":[]}`), New()); err != nil { + t.Fatalf("schema 1 refused: %v", err) + } + if err := json.Unmarshal([]byte(`{}`), New()); err != nil { + t.Fatalf("document without a schema refused: %v", err) + } +} + +func TestUnmarshalInvalidJSON(t *testing.T) { + for _, bad := range []string{``, `{`, `{"schema":1,"cells":"no"}`, `{"schema":"1"}`} { + if err := json.Unmarshal([]byte(bad), New()); err == nil { + t.Fatalf("invalid document %q accepted", bad) + } + } +} + +func TestConcurrentUse(t *testing.T) { + const goroutines = 8 + const each = 100 + s := New() + + var wg sync.WaitGroup + for g := 0; g < goroutines; g++ { + wg.Add(1) + go func(g int) { + defer wg.Done() + for i := 0; i < each; i++ { + s.Observe("m", "r", "model", nil, 1) + s.Observe("m", "r", "model", map[string]string{"g": strconv.Itoa(g)}, 2) + s.Win("r", "a", "b") + } + }(g) + } + // A writer merging while the others observe, and a reader walking the + // sheet the whole time. + part := New() + part.Observe("m", "r", "model", map[string]string{"part": "1"}, 3) + wg.Add(1) + go func() { + defer wg.Done() + for i := 0; i < 50; i++ { + s.Merge(part) + } + }() + wg.Add(1) + go func() { + defer wg.Done() + for i := 0; i < 200; i++ { + s.Wins("r", "a", "b") + s.Cell("m", "r", "model", nil) + if j, err := json.Marshal(s); err != nil { + t.Error(err) + return + } else if len(j) == 0 { + t.Error("empty marshal") + return + } + } + }() + wg.Wait() + + checkCell(t, s, "m", "r", "model", nil, Cell{N: goroutines * each, Sum: goroutines * each, SumSq: goroutines * each}) + for g := 0; g < goroutines; g++ { + checkCell(t, s, "m", "r", "model", map[string]string{"g": strconv.Itoa(g)}, + Cell{N: each, Sum: 2 * each, SumSq: 4 * each}) + } + checkCell(t, s, "m", "r", "model", map[string]string{"part": "1"}, Cell{N: 50, Sum: 150, SumSq: 450}) + if x, y := s.Wins("r", "a", "b"); x != goroutines*each || y != 0 { + t.Fatalf("Wins = %d, %d, want %d, 0", x, y, goroutines*each) + } +} From f4f3fa1877fd8f60fadd92cd33ea69c4669528f5 Mon Sep 17 00:00:00 2001 From: codeaf Date: Thu, 17 Sep 2026 01:26:01 -0400 Subject: [PATCH 009/173] pool/index: read a schema-versioned measurement document into a shared read-only index Co-Authored-By: codeaf --- internal/pool/index/index.go | 451 +++++++++++++++++++++++++++++++++++ 1 file changed, 451 insertions(+) create mode 100644 internal/pool/index/index.go diff --git a/internal/pool/index/index.go b/internal/pool/index/index.go new file mode 100644 index 0000000000..de844fc4dc --- /dev/null +++ b/internal/pool/index/index.go @@ -0,0 +1,451 @@ +// Package index reads a schema-versioned JSON document of per-model +// measurements into a value every reader in a process can share. +// +// THE INDEX IS READ-ONLY AND IMMUTABLE, which is the whole design: nothing +// merges into it and nothing mutates it after Parse, so one document may be +// handed to every goroutine without a lock. Whoever read the file stays outside +// the package — Parse takes bytes, Read takes a reader, and Fallback chooses +// between two documents that both arrive as bytes. +// +// FORWARD COMPATIBILITY IS THE POINT. An unknown top-level field, an unknown +// metric kind, an undeclared dim on a cell and an unknown cell field are all +// ignored, because a reader that refused them would make every future field a +// flag day. The one thing refused outright is a schema this reader does not +// understand, which is what the schema number exists to say. +package index + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "io" + "sort" + "strings" + "time" +) + +// ErrSchema is wrapped by the error a document whose schema is newer than +// this reader returns. A reader that is behind the document cannot guess what +// the newer fields mean, so it refuses rather than half-reads. +var ErrSchema = errors.New("document schema is newer than this reader") + +// Cell is one measurement from the document. Dims carries the declared dims +// beyond role and model that the cell spelled, and is nil for a cell that +// carried none. +type Cell struct { + Metric string + Role string + Model string + Mean float64 + SD float64 + N int + Dims map[string]string +} + +// Want is one row of the document's wanted list, with the role normalised and +// the model resolved to its canonical id. +type Want struct { + Role string + Model string + Weight float64 +} + +// Index is a parsed measurement document. Nothing on it mutates after it is +// built, and every accessor hands back copies, so many goroutines may read one +// Index at once. +type Index struct { + schema int + generated time.Time + minInstalls int + judges []string + rubrics map[string]int + // metrics holds the declared names as spelled; kinds and dims are keyed by + // the folded name every lookup arrives under. + metrics map[string]string + kinds map[string]string + dims map[string]map[string]bool + // aliases maps a folded, ~-stripped id to the canonical id as the document + // spells it. The canonical id is in the map under its own folded form. + aliases map[string]string + // cells is keyed by folded metric, then by an address that folds the role, + // the canonical model and the normalised dims into one exact key. + cells map[string]map[string]Cell + wanted []Want +} + +// Parse reads a measurement document from bytes. +func Parse(data []byte) (*Index, error) { + return parse(data) +} + +// Read reads a measurement document from a reader. The reader is drained and +// closed by nobody but the caller; the document is all this package takes. +func Read(r io.Reader) (*Index, error) { + data, err := io.ReadAll(r) + if err != nil { + return nil, fmt.Errorf("read the index document: %w", err) + } + return parse(data) +} + +// Fallback answers with the document that should be trusted: primary, unless +// it is missing, empty, unparsable, or older by its generated date, in which +// case the embedded one. Equal dates keep primary. When neither parses it +// returns the error from the embedded document. +func Fallback(primary, embedded []byte) (*Index, error) { + p, perr := parse(primary) + e, eerr := parse(embedded) + if perr == nil { + if eerr != nil || !p.generated.Before(e.generated) { + return p, nil + } + return e, nil + } + if eerr != nil { + return nil, eerr + } + return e, nil +} + +// document is the wire shape. Fields this build does not know are dropped by +// the decoder, which is the forward-compatibility rule above. +type document struct { + Schema *int `json:"schema"` + Generated string `json:"generated"` + MinInstalls int `json:"min_installs"` + Judges []string `json:"judges"` + Rubrics map[string]int `json:"rubrics"` + Aliases map[string][]string `json:"aliases"` + Metrics map[string]metricDecl `json:"metrics"` + Cells []map[string]any `json:"cells"` + Wanted []wantEntry `json:"wanted"` +} + +type metricDecl struct { + Kind string `json:"kind"` + Dims []string `json:"dims"` +} + +type wantEntry struct { + Role string `json:"role"` + Model string `json:"model"` + Weight float64 `json:"weight"` +} + +// reservedCellFields are the fixed fields of a cell. A declared dim named one +// of these could never be read off a cell, so the name is reserved rather than +// resolved twice. +var reservedCellFields = map[string]bool{ + "metric": true, "role": true, "model": true, "mean": true, "sd": true, "n": true, +} + +// The separators are unprintable so no dim key or value can contain one and +// forge a different address. +const ( + pairSep = "\x1f" + keySep = "\x1e" +) + +func parse(data []byte) (*Index, error) { + if len(bytes.TrimSpace(data)) == 0 { + return nil, errors.New("index: empty document") + } + var d document + if err := json.Unmarshal(data, &d); err != nil { + return nil, fmt.Errorf("index: decode: %w", err) + } + if d.Schema != nil && *d.Schema > 1 { + return nil, fmt.Errorf("index: schema %d is newer than this reader, which understands 1: %w", *d.Schema, ErrSchema) + } + + x := &Index{ + schema: 0, + minInstalls: d.MinInstalls, + judges: d.Judges, + rubrics: d.Rubrics, + metrics: map[string]string{}, + kinds: map[string]string{}, + dims: map[string]map[string]bool{}, + aliases: map[string]string{}, + cells: map[string]map[string]Cell{}, + } + if d.Schema != nil { + x.schema = *d.Schema + } + if stamp, err := time.Parse("2006-01-02", d.Generated); err == nil { + x.generated = stamp.UTC() + } + + // The declared metrics are keyed by their folded name for lookup, and + // their kinds are folded once here rather than on every Kind call. + for name, decl := range d.Metrics { + folded := fold(name) + if _, seen := x.metrics[folded]; !seen { + x.metrics[folded] = name + } + if _, seen := x.kinds[folded]; !seen { + x.kinds[folded] = fold(decl.Kind) + } + dimset := map[string]bool{} + for _, dim := range decl.Dims { + fd := fold(dim) + if fd == "role" || fd == "model" { + continue + } + dimset[fd] = true + } + if _, seen := x.dims[folded]; !seen { + x.dims[folded] = dimset + } + } + + // Aliases are resolved in sorted canonical order so a name claimed by two + // canonical ids always resolves the same way. + ids := make([]string, 0, len(d.Aliases)) + for id := range d.Aliases { + ids = append(ids, id) + } + sort.Strings(ids) + for _, id := range ids { + x.addAlias(id, id) + for _, alt := range d.Aliases[id] { + x.addAlias(alt, id) + } + } + + for _, raw := range d.Cells { + x.addCell(raw) + } + + for _, w := range d.Wanted { + x.wanted = append(x.wanted, Want{Role: fold(w.Role), Model: x.resolve(w.Model), Weight: w.Weight}) + } + sort.SliceStable(x.wanted, func(i, j int) bool { + if x.wanted[i].Weight != x.wanted[j].Weight { + return x.wanted[i].Weight > x.wanted[j].Weight + } + return x.wanted[i].Model < x.wanted[j].Model + }) + return x, nil +} + +// addAlias records that id is an alternate spelling of canonical, unless the +// id already resolves — the first claim in sorted order wins. +func (x *Index) addAlias(id, canonical string) { + key := normModel(id) + if _, seen := x.aliases[key]; !seen { + x.aliases[key] = canonical + } +} + +// addCell reads one raw cell into the index, or drops it. A cell whose metric +// is not declared is skipped — a measurement with no metric behind it has no +// units, and guessing is worse than dropping it. A cell whose N is below the +// document's min_installs is skipped too: too few observations is not a +// measurement. +func (x *Index) addCell(raw map[string]any) { + metricName, _ := raw["metric"].(string) + mk := fold(metricName) + if _, declared := x.metrics[mk]; !declared { + return + } + role, _ := raw["role"].(string) + model, _ := raw["model"].(string) + n := int(number(raw["n"])) + if n < x.minInstalls { + return + } + dims := map[string]string{} + for k, v := range raw { + if reservedCellFields[fold(k)] { + continue + } + if !x.dims[mk][fold(k)] { + continue + } + if s, ok := v.(string); ok { + dims[k] = s + } + } + canon := x.resolve(model) + c := Cell{ + Metric: metricName, + Role: role, + Model: canon, + Mean: number(raw["mean"]), + SD: number(raw["sd"]), + N: n, + } + if len(dims) > 0 { + c.Dims = dims + } + bucket := x.cells[mk] + if bucket == nil { + bucket = map[string]Cell{} + x.cells[mk] = bucket + } + bucket[address(role, canon, c.Dims)] = c +} + +// address renders the exact place a measurement lives: folded role, folded +// canonical model, and the normalised dims as sorted key-value pairs. A nil +// dims map and an empty one render the same, which is the rule — they are the +// same address. +func address(role, canonicalModel string, dims map[string]string) string { + parts := []string{fold(role), fold(canonicalModel)} + if len(dims) > 0 { + pairs := make([]string, 0, len(dims)) + for k, v := range dims { + pairs = append(pairs, fold(k)+keySep+fold(v)) + } + sort.Strings(pairs) + parts = append(parts, strings.Join(pairs, pairSep)) + } + return strings.Join(parts, pairSep) +} + +// resolve answers with the canonical id a model id is an alternate of, or the +// normalised id itself when the document never names it. +func (x *Index) resolve(model string) string { + key := normModel(model) + if canon, ok := x.aliases[key]; ok { + return canon + } + return key +} + +// fold is how metric names, roles, dim keys and dim values are matched. +func fold(s string) string { + return strings.ToLower(strings.TrimSpace(s)) +} + +// normModel folds a model id and then strips one leading "~". +func normModel(s string) string { + return strings.TrimPrefix(fold(s), "~") +} + +// number reads a JSON number, and zero for anything else on the cell — a +// missing field is not a malformed document. +func number(v any) float64 { + f, _ := v.(float64) + return f +} + +// Schema is the document's schema version. A missing schema reads as 0 and is +// accepted. +func (x *Index) Schema() int { return x.schema } + +// Generated is the document's "generated" day in UTC. A missing or unreadable +// date is the zero time, which sorts as the oldest thing there is. +func (x *Index) Generated() time.Time { return x.generated } + +// MinInstalls is the document's floor on observations per measurement. +func (x *Index) MinInstalls() int { return x.minInstalls } + +// Judges answers with a copy of the document's judges. +func (x *Index) Judges() []string { + if x.judges == nil { + return nil + } + return append([]string(nil), x.judges...) +} + +// Rubrics answers with a copy of the document's rubrics. +func (x *Index) Rubrics() map[string]int { + out := make(map[string]int, len(x.rubrics)) + for k, v := range x.rubrics { + out[k] = v + } + return out +} + +// Metrics lists the declared metric names, sorted. +func (x *Index) Metrics() []string { + out := make([]string, 0, len(x.metrics)) + for _, name := range x.metrics { + out = append(out, name) + } + sort.Strings(out) + return out +} + +// Kind answers with the kind word the document spells for the metric, +// lowercased, whether or not this build knows it. +func (x *Index) Kind(metric string) (string, bool) { + kind, ok := x.kinds[fold(metric)] + return kind, ok +} + +// Canonical answers with the canonical id the document spells for a model id, +// for the id itself and for any of its alternates, and with the normalised +// input for an id the document never names. +func (x *Index) Canonical(model string) string { + return x.resolve(model) +} + +// Cell addresses a measurement exactly: metric, role, model and dims all have +// to match, and a nil dims map and an empty one are the same address. A lookup +// with no dims finds the cell that carries none, and never a cell that carries +// some. +func (x *Index) Cell(metric, role, model string, dims map[string]string) (Cell, bool) { + bucket := x.cells[fold(metric)] + if bucket == nil { + return Cell{}, false + } + c, ok := bucket[address(role, x.resolve(model), dims)] + if !ok { + return Cell{}, false + } + return copyCell(c), true +} + +// Cells answers with every returnable cell of one metric, sorted by role, then +// model, then by its dims rendered as sorted "key=value" pairs. +func (x *Index) Cells(metric string) []Cell { + bucket := x.cells[fold(metric)] + out := make([]Cell, 0, len(bucket)) + for _, c := range bucket { + out = append(out, copyCell(c)) + } + sort.Slice(out, func(i, j int) bool { + if out[i].Role != out[j].Role { + return out[i].Role < out[j].Role + } + if out[i].Model != out[j].Model { + return out[i].Model < out[j].Model + } + return dimsKey(out[i].Dims) < dimsKey(out[j].Dims) + }) + return out +} + +// Wanted answers with a copy of the wanted list, sorted by weight descending, +// then by model ascending. +func (x *Index) Wanted() []Want { + return append([]Want(nil), x.wanted...) +} + +// dimsKey renders a cell's dims for sorting: sorted "key=value" pairs. +func dimsKey(dims map[string]string) string { + pairs := make([]string, 0, len(dims)) + for k, v := range dims { + pairs = append(pairs, k+"="+v) + } + sort.Strings(pairs) + return strings.Join(pairs, ",") +} + +// copyCell hands back a cell whose Dims map is its own, so a caller cannot +// reach into the shared document. +func copyCell(c Cell) Cell { + if c.Dims == nil { + return c + } + dims := make(map[string]string, len(c.Dims)) + for k, v := range c.Dims { + dims[k] = v + } + c.Dims = dims + return c +} From f76050a56b863069eca503fe70d519797c238081 Mon Sep 17 00:00:00 2001 From: codeaf Date: Thu, 17 Sep 2026 01:26:52 -0400 Subject: [PATCH 010/173] pool/index: test the reader against the example document, its forward-compatibility rules and its sharing Co-Authored-By: codeaf --- internal/pool/index/index_test.go | 519 ++++++++++++++++++++++++++++++ 1 file changed, 519 insertions(+) create mode 100644 internal/pool/index/index_test.go diff --git a/internal/pool/index/index_test.go b/internal/pool/index/index_test.go new file mode 100644 index 0000000000..8ebe165f4e --- /dev/null +++ b/internal/pool/index/index_test.go @@ -0,0 +1,519 @@ +package index + +import ( + "bytes" + "errors" + "strings" + "sync" + "testing" + "time" +) + +// example is the shape the whole reader holds in mind: one metric with dims, +// one alias pair, one cell, one wanted row. +const example = `{ + "schema": 1, + "generated": "2026-09-17", + "min_installs": 5, + "judges": ["z-ai/glm-5.3"], + "rubrics": {"planner": 3}, + "aliases": {"z-ai/glm-5.3": ["glm-5.3", "zai-org/GLM-5.3"]}, + "metrics": {"role_rating": {"kind": "gaussian", "unit": "elo", "dims": ["role", "model"]}}, + "cells": [ + {"metric": "role_rating", "role": "planner", "model": "z-ai/glm-5.3", "mean": 1312, "sd": 18, "n": 410} + ], + "wanted": [{"role": "planner", "model": "qwen/qwen3.8-max-0902", "weight": 0.4}] +}` + +func mustParse(t *testing.T, data string) *Index { + t.Helper() + x, err := Parse([]byte(data)) + if err != nil { + t.Fatalf("parse: %v", err) + } + return x +} + +// ── THE DOCUMENT, READ BACK ───────────────────────────────────────────────── + +func TestTheDocumentReadsBack(t *testing.T) { + x := mustParse(t, example) + if x.Schema() != 1 { + t.Errorf("schema is %d, want 1", x.Schema()) + } + if got, want := x.Generated(), time.Date(2026, 9, 17, 0, 0, 0, 0, time.UTC); !got.Equal(want) { + t.Errorf("generated is %v, want %v", got, want) + } + if x.MinInstalls() != 5 { + t.Errorf("min_installs is %d, want 5", x.MinInstalls()) + } + if judges := x.Judges(); len(judges) != 1 || judges[0] != "z-ai/glm-5.3" { + t.Errorf("judges are %v", judges) + } + if rubrics := x.Rubrics(); rubrics["planner"] != 3 || len(rubrics) != 1 { + t.Errorf("rubrics are %v", rubrics) + } + if metrics := x.Metrics(); len(metrics) != 1 || metrics[0] != "role_rating" { + t.Errorf("metrics are %v", metrics) + } + if kind, ok := x.Kind("role_rating"); !ok || kind != "gaussian" { + t.Errorf("kind is %q, %v", kind, ok) + } + c, ok := x.Cell("role_rating", "planner", "z-ai/glm-5.3", nil) + if !ok { + t.Fatal("the example cell was not found") + } + if c.Mean != 1312 || c.SD != 18 || c.N != 410 { + t.Errorf("cell reads %v", c) + } + if len(x.Wanted()) != 1 || x.Wanted()[0].Weight != 0.4 { + t.Errorf("wanted reads %v", x.Wanted()) + } +} + +func TestReadTakesAReader(t *testing.T) { + x, err := Read(bytes.NewBufferString(example)) + if err != nil { + t.Fatalf("read: %v", err) + } + if len(x.Cells("role_rating")) != 1 { + t.Errorf("read through a reader found %d cells", len(x.Cells("role_rating"))) + } +} + +// ── FORWARD COMPATIBILITY ─────────────────────────────────────────────────── + +// EVERY UNKNOWN THING IS IGNORED, because a reader that refused them would +// make every future field a flag day. +func TestUnknownThingsAreIgnoredNotErrors(t *testing.T) { + x := mustParse(t, `{ + "schema": 1, + "tomorrow_field": {"anything": true}, + "metrics": {"role_rating": {"kind": "not-a-kind-we-know", "dims": []}}, + "cells": [ + {"metric": "role_rating", "role": "planner", "model": "m", "mean": 1, "sd": 1, "n": 9, "unknown_cell_field": "x"} + ] + }`) + if kind, ok := x.Kind("role_rating"); !ok || kind != "not-a-kind-we-know" { + t.Errorf("an unknown kind was refused or rewritten: %q %v", kind, ok) + } + if _, ok := x.Cell("role_rating", "planner", "m", nil); !ok { + t.Error("a cell carrying an unknown field was dropped") + } +} + +func TestSchemaRefusesOnlyWhatIsTooNew(t *testing.T) { + for _, row := range []struct { + name, schema string + want int + accepted bool + }{ + {"a missing schema", "", 0, true}, // missing reads as 0 and is accepted + {"schema 0", `"schema": 0,`, 0, true}, + {"schema 1", `"schema": 1,`, 1, true}, + {"a schema below 1", `"schema": -1,`, -1, true}, + {"schema 2", `"schema": 2,`, 0, false}, + {"schema 99", `"schema": 99,`, 0, false}, + } { + x, err := Parse([]byte("{" + row.schema + `"metrics": {}}`)) + if (err == nil) != row.accepted { + t.Errorf("%s: err=%v, want accepted=%v", row.name, err, row.accepted) + continue + } + if row.accepted && x.Schema() != row.want { + t.Errorf("%s reads back as %d, want %d", row.name, x.Schema(), row.want) + } + } + + _, err := Parse([]byte(`{"schema": 2}`)) + if !errors.Is(err, ErrSchema) { + t.Errorf("a too-new schema answered %v, want it wrapped in ErrSchema", err) + } +} + +func TestMalformedAndEmptyDocumentsAreErrors(t *testing.T) { + for _, row := range []struct { + name string + data string + }{ + {"malformed json", `{"schema": 1,`}, + {"an empty document", ``}, + {"a whitespace document", " \n\t "}, + {"a document that is not an object", `[1, 2, 3]`}, + } { + if _, err := Parse([]byte(row.data)); err == nil { + t.Errorf("%s was accepted", row.name) + } + } +} + +func TestAGeneratedDateThatWillNotReadIsTheZeroTime(t *testing.T) { + for _, row := range []struct { + name, generated string + wantZero bool + }{ + {"missing", "", true}, + {"unreadable", "17/09/2026", true}, + {"readable", "2026-09-17", false}, + } { + data := `{"metrics": {}}` + if row.generated != "" { + data = `{"generated": "` + row.generated + `", "metrics": {}}` + } + got := mustParse(t, data).Generated() + if row.wantZero != got.IsZero() { + t.Errorf("%s generated date gave %v, want zero=%v", row.name, got, row.wantZero) + } + } +} + +// ── METRICS AND DIMS ──────────────────────────────────────────────────────── + +func TestMetricsAreSortedAndCellsOfUndeclaredMetricsAreSkipped(t *testing.T) { + x := mustParse(t, `{ + "metrics": {"zebra": {"kind": "a"}, "alpha": {"kind": "b"}, "mid": {"kind": "c"}}, + "cells": [ + {"metric": "zebra", "role": "r", "model": "m", "mean": 1, "sd": 1, "n": 9}, + {"metric": "undeclared", "role": "r", "model": "m", "mean": 1, "sd": 1, "n": 9} + ] + }`) + if got, want := strings.Join(x.Metrics(), ","), "alpha,mid,zebra"; got != want { + t.Errorf("metrics are %q, want %q", got, want) + } + if len(x.Cells("zebra")) != 1 { + t.Errorf("a declared metric's cell count is %d, want 1", len(x.Cells("zebra"))) + } + if len(x.Cells("undeclared")) != 0 { + t.Error("a cell whose metric is not declared was kept") + } + if _, ok := x.Kind("undeclared"); ok { + t.Error("an undeclared metric answered a kind") + } +} + +func TestDeclaredDimsBecomeCellDims(t *testing.T) { + x := mustParse(t, `{ + "metrics": {"m": {"kind": "a", "dims": ["role", "model", "region", "variant"]}}, + "cells": [ + {"metric": "m", "role": "r", "model": "mo", "mean": 1, "sd": 1, "n": 1, + "region": "EU", "variant": "lite", "mean_x": "not a dim", "n_other": 7} + ] + }`) + c, ok := x.Cell("m", "r", "mo", map[string]string{"region": "eu", "variant": "LITE"}) + if !ok { + t.Fatal("the dims cell was not found") + } + if len(c.Dims) != 2 || c.Dims["region"] != "EU" || c.Dims["variant"] != "lite" { + t.Errorf("dims read %v, want region=EU and variant=lite as spelled", c.Dims) + } + // role and model are declared dims but never cell dims — they are the + // address itself. + for _, banned := range []string{"role", "model"} { + if _, is := c.Dims[banned]; is { + t.Errorf("%s became a cell dim", banned) + } + } +} + +func TestADimWhoseValueIsNotAStringIsIgnored(t *testing.T) { + x := mustParse(t, `{ + "metrics": {"m": {"kind": "a", "dims": ["region"]}}, + "cells": [ + {"metric": "m", "role": "r", "model": "mo", "mean": 1, "sd": 1, "n": 1, "region": 4} + ] + }`) + c, ok := x.Cell("m", "r", "mo", nil) + if !ok { + t.Fatal("the cell was dropped over a non-string dim") + } + if len(c.Dims) != 0 { + t.Errorf("a non-string dim became %v", c.Dims) + } +} + +// ── NORMALISATION AND ALIASES ─────────────────────────────────────────────── + +// THE SAME ID CAN ARRIVE SPELLED FOUR WAYS, and a lookup written with one of +// them has to find a cell written with another: that is the only way an +// alias table earns its place in the document. +func TestAliasesMeetInBothDirections(t *testing.T) { + x := mustParse(t, `{ + "aliases": {"Z-AI/GLM-5.3": ["glm-5.3"]}, + "metrics": {"m": {"kind": "a"}}, + "cells": [{"metric": "m", "role": "planner", "model": "glm-5.3", "mean": 2, "sd": 1, "n": 5}], + "wanted": [{"role": " Planner ", "model": " glm-5.3 ", "weight": 1}] + }`) + if got, want := x.Canonical("~z-ai/glm-5.3"), "Z-AI/GLM-5.3"; got != want { + t.Errorf("canonical of the folded id is %q, want %q", got, want) + } + if got, want := x.Canonical("~glm-5.3"), "Z-AI/GLM-5.3"; got != want { + t.Errorf("canonical of the ~-stripped alias is %q, want %q", got, want) + } + if got, want := x.Canonical("never/named"), "never/named"; got != want { + t.Errorf("canonical of an unnamed id is %q, want the normalised input %q", got, want) + } + // The cell was written under an alias; a lookup under a different alias + // finds it because cells are stored under their canonical model. + c, ok := x.Cell(" M ", "PLANNER", "~Z-AI/GLM-5.3", nil) + if !ok { + t.Fatal("a lookup under one alias did not find a cell written under another") + } + if c.Model != "Z-AI/GLM-5.3" { + t.Errorf("the cell's model is %q, want the canonical id", c.Model) + } + if w := x.Wanted(); len(w) != 1 || w[0].Role != "planner" || w[0].Model != "Z-AI/GLM-5.3" { + t.Errorf("wanted reads %v, want the role folded and the model canonical", w) + } +} + +func TestMetricNamesRolesAndDimsMatchFolded(t *testing.T) { + x := mustParse(t, `{ + "metrics": {"Role_Rating": {"kind": "a", "dims": [" Region "]}}, + "cells": [{"metric": "role_rating", "role": " Planner ", "model": "m", "mean": 1, "sd": 1, "n": 1, "region": "eu"}] + }`) + if _, ok := x.Kind(" ROLE_RATING "); !ok { + t.Error("a kind lookup did not fold the metric name") + } + if _, ok := x.Cell("ROLE_rating", "planner", "M", map[string]string{"REGION": "EU"}); !ok { + t.Error("an exact address did not fold metric, role and dims") + } +} + +// ── THE EXACT ADDRESS ─────────────────────────────────────────────────────── + +// NIL DIMS AND EMPTY DIMS ARE THE SAME ADDRESS, and a lookup that carries +// some dims never finds a cell that carries none, nor the other way round. +func TestTheAddressIsExact(t *testing.T) { + x := mustParse(t, `{ + "metrics": {"m": {"kind": "a", "dims": ["region", "tier"]}}, + "cells": [ + {"metric": "m", "role": "r", "model": "a", "mean": 1, "sd": 1, "n": 1}, + {"metric": "m", "role": "r", "model": "b", "mean": 2, "sd": 1, "n": 1, "region": "eu"}, + {"metric": "m", "role": "r", "model": "c", "mean": 3, "sd": 1, "n": 1, "region": "eu", "tier": "lite"} + ] + }`) + if c, ok := x.Cell("m", "r", "a", map[string]string{}); !ok || c.Mean != 1 { + t.Error("an empty dims map did not find the cell that carries none") + } + if _, ok := x.Cell("m", "r", "b", nil); ok { + t.Error("a no-dims lookup found a cell that carries dims") + } + if c, ok := x.Cell("m", "r", "b", map[string]string{"region": "eu"}); !ok || c.Mean != 2 { + t.Error("the one-dim cell was not found by its one dim") + } + if _, ok := x.Cell("m", "r", "c", map[string]string{"region": "eu"}); ok { + t.Error("a partial dims lookup found a cell with more dims") + } + if _, ok := x.Cell("m", "r", "b", map[string]string{"region": "us"}); ok { + t.Error("a dims lookup with the wrong value found the cell") + } +} + +func TestMinInstallsHoldsBackThinCells(t *testing.T) { + x := mustParse(t, `{ + "min_installs": 5, + "metrics": {"m": {"kind": "a"}}, + "cells": [ + {"metric": "m", "role": "r", "model": "thin", "mean": 1, "sd": 1, "n": 4}, + {"metric": "m", "role": "r", "model": "held", "mean": 2, "sd": 1, "n": 5} + ] + }`) + if _, ok := x.Cell("m", "r", "thin", nil); ok { + t.Error("a cell below min_installs was returned by Cell") + } + names := "" + for _, c := range x.Cells("m") { + names += c.Model + } + if names != "held" { + t.Errorf("Cells answered %q, want only the cell that met min_installs", names) + } +} + +func TestCellsAreSortedByRoleThenModelThenDims(t *testing.T) { + x := mustParse(t, `{ + "metrics": {"m": {"kind": "a", "dims": ["region", "tier"]}}, + "cells": [ + {"metric": "m", "role": "zebra", "model": "a", "mean": 0, "sd": 0, "n": 1}, + {"metric": "m", "role": "alpha", "model": "z", "mean": 0, "sd": 0, "n": 1}, + {"metric": "m", "role": "alpha", "model": "a", "mean": 0, "sd": 0, "n": 1}, + {"metric": "m", "role": "alpha", "model": "a", "mean": 0, "sd": 0, "n": 1, "region": "eu", "tier": "lite"}, + {"metric": "m", "role": "alpha", "model": "a", "mean": 0, "sd": 0, "n": 1, "region": "eu"}, + {"metric": "m", "role": "alpha", "model": "a", "mean": 0, "sd": 0, "n": 1, "tier": "lite"} + ] + }`) + got := x.Cells("m") + order := "" + for _, c := range got { + order += c.Role + ":" + c.Model + ":" + dimsKey(c.Dims) + ";" + } + want := "alpha:a:;alpha:a:region=eu;alpha:a:region=eu,tier=lite;alpha:a:tier=lite;alpha:z:;zebra:a:;" + if order != want { + t.Errorf("cells came out as\n%s\nwant\n%s", order, want) + } +} + +func TestWantedIsSortedByWeightThenModel(t *testing.T) { + x := mustParse(t, `{ + "aliases": {"c/one": []}, + "wanted": [ + {"role": "r", "model": "c/one", "weight": 0.5}, + {"role": "r", "model": "a/two", "weight": 0.5}, + {"role": "r", "model": "b/three", "weight": 0.9} + ] + }`) + got := x.Wanted() + order := "" + for _, w := range got { + order += w.Model + "," + } + if order != "b/three,a/two,c/one," { + t.Errorf("wanted came out as %q", order) + } +} + +// ── COPIES AND SHARING ────────────────────────────────────────────────────── + +// NOTHING A CALLER HOLDS REACHES INTO THE SHARED DOCUMENT: every accessor +// hands back copies, and a mutation through one must not leak into another. +func TestReturnedValuesAreCopies(t *testing.T) { + x := mustParse(t, example) + + judges := x.Judges() + judges[0] = "mutated" + if x.Judges()[0] == "mutated" { + t.Error("Judges handed back the shared slice") + } + + rubrics := x.Rubrics() + rubrics["planner"] = 99 + if x.Rubrics()["planner"] == 99 { + t.Error("Rubrics handed back the shared map") + } + + metrics := x.Metrics() + metrics[0] = "mutated" + if x.Metrics()[0] == "mutated" { + t.Error("Metrics handed back the shared slice") + } + + c, _ := x.Cell("role_rating", "planner", "z-ai/glm-5.3", nil) + if c.Dims != nil { + c.Dims["region"] = "mutated" + } + cells := x.Cells("role_rating") + if len(cells) == 1 && cells[0].Dims != nil { + cells[0].Dims["region"] = "mutated" + if again, _ := x.Cell("role_rating", "planner", "z-ai/glm-5.3", nil); again.Dims != nil && again.Dims["region"] == "mutated" { + t.Error("Cells handed back the shared dims map") + } + } + + wanted := x.Wanted() + wanted[0].Weight = -1 + if x.Wanted()[0].Weight == -1 { + t.Error("Wanted handed back the shared slice") + } +} + +func TestDimsMapsAreCopiesOnEveryCell(t *testing.T) { + x := mustParse(t, `{ + "metrics": {"m": {"kind": "a", "dims": ["region"]}}, + "cells": [{"metric": "m", "role": "r", "model": "a", "mean": 1, "sd": 1, "n": 1, "region": "eu"}] + }`) + first, _ := x.Cell("m", "r", "a", map[string]string{"region": "eu"}) + first.Dims["region"] = "mutated" + second, _ := x.Cell("m", "r", "a", map[string]string{"region": "eu"}) + if second.Dims["region"] == "mutated" { + t.Error("a dims map handed back by Cell was the shared one") + } +} + +// ONE INDEX, MANY GOROUTINES, NO LOCK: the immutability is the design, and +// this is the cheapest way to catch anything that quietly broke it. +func TestOneIndexServesManyReadersAtOnce(t *testing.T) { + x := mustParse(t, `{ + "min_installs": 2, + "metrics": {"m": {"kind": "a", "dims": ["region"]}}, + "cells": [ + {"metric": "m", "role": "r", "model": "a", "mean": 1, "sd": 1, "n": 9, "region": "eu"} + ], + "wanted": [{"role": "r", "model": "a", "weight": 1}] + }`) + var wg sync.WaitGroup + for g := 0; g < 8; g++ { + wg.Add(1) + go func() { + defer wg.Done() + for i := 0; i < 200; i++ { + x.Metrics() + x.Kind("m") + x.Cells("m") + x.Cell("m", "r", "a", map[string]string{"region": "eu"}) + x.Wanted() + x.Judges() + x.Rubrics() + x.Canonical("a") + } + }() + } + wg.Wait() +} + +// ── FALLBACK ──────────────────────────────────────────────────────────────── + +const fallbackEmbedded = `{"schema": 1, "generated": "2026-09-01", "metrics": {"old": {"kind": "a"}}}` + +func TestFallbackPrefersPrimaryAndFallsBackOnItsFailures(t *testing.T) { + fresh := `{"schema": 1, "generated": "2026-09-17", "metrics": {"fresh": {"kind": "a"}}}` + for _, row := range []struct { + name, primary string + wantMetric string + }{ + {"a fresh primary", fresh, "fresh"}, + {"a missing primary", "", "old"}, + {"an empty primary", " ", "old"}, + {"an unparsable primary", `{"schema": `, "old"}, + {"a too-new primary", `{"schema": 2}`, "old"}, + {"an older primary", `{"schema": 1, "generated": "2026-08-31", "metrics": {"stale": {"kind": "a"}}}`, "old"}, + } { + x, err := Fallback([]byte(row.primary), []byte(fallbackEmbedded)) + if err != nil { + t.Errorf("%s: %v", row.name, err) + continue + } + if got := x.Metrics(); len(got) != 1 || got[0] != row.wantMetric { + t.Errorf("%s answered with %v, want the %s document", row.name, got, row.wantMetric) + } + } +} + +func TestFallbackKeepsPrimaryOnEqualDates(t *testing.T) { + x, err := Fallback( + []byte(`{"schema": 1, "generated": "2026-09-17", "metrics": {"primary": {"kind": "a"}}}`), + []byte(fallbackEmbedded), + ) + if err != nil { + t.Fatal(err) + } + if got := x.Metrics(); len(got) != 1 || got[0] != "primary" { + t.Errorf("equal dates answered with %v, want primary kept", got) + } +} + +func TestFallbackKeepsPrimaryWhenEmbeddedWillNotParse(t *testing.T) { + x, err := Fallback([]byte(`{"metrics": {"primary": {"kind": "a"}}}`), []byte("not json")) + if err != nil { + t.Fatal(err) + } + if got := x.Metrics(); len(got) != 1 || got[0] != "primary" { + t.Errorf("a good primary lost to a broken embedded: %v", got) + } +} + +func TestFallbackReturnsTheEmbeddedErrorWhenNeitherParses(t *testing.T) { + _, err := Fallback([]byte(""), []byte(`{"schema": `)) + if err == nil || !strings.Contains(err.Error(), "decode") { + t.Errorf("neither parsing answered %v, want the embedded document's error", err) + } +} From 0d60843306ea3d80f0839448d49b380c44cf0e5c Mon Sep 17 00:00:00 2001 From: santoshkumarradha Date: Thu, 17 Sep 2026 01:15:21 -0400 Subject: [PATCH 011/173] orchestrate: ask an installed tariff before the fallback price table Co-Authored-By: codeaf --- internal/orchestrate/fuel.go | 63 ++++++++++++-- internal/orchestrate/fuel_test.go | 133 +++++++++++++++++++++++++++++- 2 files changed, 187 insertions(+), 9 deletions(-) diff --git a/internal/orchestrate/fuel.go b/internal/orchestrate/fuel.go index 2a3a035cbe..5b48fec175 100644 --- a/internal/orchestrate/fuel.go +++ b/internal/orchestrate/fuel.go @@ -20,6 +20,7 @@ import ( "fmt" "strconv" "strings" + "sync/atomic" ) // The three answers to the gate. They are the whole vocabulary, and a surface @@ -43,6 +44,46 @@ type Price struct { Out float64 } +// PriceSource is a reader this package does not own, asked before the table +// falls back. It answers with the same pair PriceOf itself returns — one +// model's row in dollars per MILLION tokens — and false means "ask somebody +// else", never "free". +type PriceSource func(model string) (Price, bool) + +// priceTariff is the installed source, held behind an atomic because the +// package's meters read it on every call and the installer may swap it while a +// run is in flight. A nil tariff is the unset state: the table alone. +var priceTariff atomic.Value // PriceSource + +// UsePrices installs (or, with nil, removes) the source asked before the +// table. Replacing one source with another is the whole point — a tariff can +// move without this package being rebuilt — and a swap is safe to make while +// other goroutines are metering: a call in flight sees either the old source +// or the new one, never half of either. +func UsePrices(src PriceSource) { + priceTariff.Store(src) // a nil PriceSource is a stored value, not a nil store +} + +// CatalogPrices adapts a reader that answers per TOKEN — the shape a model +// catalog publishes — into the per-MILLION PriceSource this package meters +// with, so the unit conversion exists in exactly one place. A reader that +// knows a model carries the answer straight through, a per-token zero +// included; a reader that publishes nothing for it answers false, and the +// table gets its turn. A nil reader adapts to a nil source: nobody published +// anything. +func CatalogPrices(priceNow func(model string) (prompt, completion float64, known bool)) PriceSource { + if priceNow == nil { + return nil + } + return func(model string) (Price, bool) { + in, out, known := priceNow(model) + if !known { + return Price{}, false + } + return Price{In: in * 1e6, Out: out * 1e6}, true + } +} + // prices is the table this build meters against — the models an adaptive run // actually rides, at their OpenRouter list price. // @@ -53,11 +94,12 @@ type Price struct { // all, which is common enough that a run metering only reported costs would // have a tank that never empties. var prices = map[string]Price{ - "deepseek/deepseek-v4-flash": {In: 0.14, Out: 0.28}, - "z-ai/glm-5.2": {In: 0.60, Out: 2.20}, - "moonshotai/kimi-k3": {In: 0.60, Out: 2.50}, - "anthropic/claude-opus": {In: 15.00, Out: 75.00}, - "openai/gpt-5": {In: 1.25, Out: 10.00}, + "deepseek/deepseek-v4-flash-0731": {In: 0.14, Out: 0.28}, + "z-ai/glm-5.3": {In: 0.60, Out: 2.20}, + "z-ai/glm-5.3-flash": {In: 0.10, Out: 0.40}, + "moonshotai/kimi-k3": {In: 0.60, Out: 2.50}, + "anthropic/claude-opus": {In: 15.00, Out: 75.00}, + "openai/gpt-5": {In: 1.25, Out: 10.00}, } // unpriced is what a model nobody has a row for costs. @@ -70,9 +112,11 @@ var prices = map[string]Price{ // governor people turn off. var unpriced = Price{In: 1.25, Out: 10.00} -// PriceOf is one model's row, and whether the table actually holds one. The id +// PriceOf is one model's row, and whether anybody actually holds one. The id // is matched the way the wire spells it, with suffixes an endpoint adds -// (":free", "@2026-01") cut before the lookup. +// (":free", "@2026-01") cut before the lookup — and the cut, lowercased name +// is what the installed source is asked too, so a source never has to repeat +// this package's normalisation to agree with its own table. // // A BARE NAME IS NOT A VENDOR'S ROW. A vendor serving a model on its own base // does not charge the router's price for it, and a wrong price is worse than no @@ -85,6 +129,11 @@ func PriceOf(model string) (Price, bool) { if name == "" { return unpriced, false } + if tariff, _ := priceTariff.Load().(PriceSource); tariff != nil { + if price, known := tariff(name); known { + return price, true + } + } if price, known := prices[name]; known { return price, true } diff --git a/internal/orchestrate/fuel_test.go b/internal/orchestrate/fuel_test.go index bce5a36113..fdcbade220 100644 --- a/internal/orchestrate/fuel_test.go +++ b/internal/orchestrate/fuel_test.go @@ -17,10 +17,10 @@ func TestMeter(t *testing.T) { t.Fatalf("output tokens: got %v want %v", got, want) } // Meter has one number and charges it at the dear rate on purpose. - if Meter(1_000, "z-ai/glm-5.2") != MeterCall(0, 1_000, "z-ai/glm-5.2") { + if Meter(1_000, "z-ai/glm-5.3") != MeterCall(0, 1_000, "z-ai/glm-5.3") { t.Fatalf("Meter is the output rate") } - if _, known := PriceOf("z-ai/glm-5.2:free"); !known { + if _, known := PriceOf("z-ai/glm-5.3:free"); !known { t.Fatalf("an endpoint suffix is not a different model") } if _, known := PriceOf("kimi-k3"); known { @@ -285,3 +285,132 @@ func waitForEnd(t *testing.T, ended chan Snapshot) Snapshot { return Snapshot{} } } + +// TestInstalledSourceAnswersBeforeTheTable: the source is asked first, with +// the name this package normalised, and a source that knows the model is the +// answer even where the table holds a row of its own. +func TestInstalledSourceAnswersBeforeTheTable(t *testing.T) { + var asked []string + UsePrices(func(model string) (Price, bool) { + asked = append(asked, model) + if model == "z-ai/glm-5.3" { + return Price{In: 3, Out: 9}, true + } + return Price{}, false + }) + defer UsePrices(nil) + + price, known := PriceOf(" Z-Ai/GLM-5.3:free ") + if !known || price != (Price{In: 3, Out: 9}) { + t.Fatalf("the installed source is the answer: %+v, %v", price, known) + } + // THE SOURCE SEES THE NORMALISED NAME, not the raw one the caller passed: + // a source would otherwise have to repeat this package's trimming and + // suffix-cutting to agree with the table it sits above. + if len(asked) != 1 || asked[0] != "z-ai/glm-5.3" { + t.Fatalf("the source was asked %q", asked) + } +} + +// TestASourceThatDoesNotKnowFallsThroughToTheTable: false from the source is +// "ask somebody else", and the table's row for that model is what meters. +func TestASourceThatDoesNotKnowFallsThroughToTheTable(t *testing.T) { + UsePrices(func(string) (Price, bool) { return Price{}, false }) + defer UsePrices(nil) + + price, known := PriceOf("openai/gpt-5") + if !known || price != (Price{In: 1.25, Out: 10.00}) { + t.Fatalf("a refusing source leaves the table in charge: %+v, %v", price, known) + } + if _, known := PriceOf("somebody/new-model"); known { + t.Fatalf("a refusing source does not invent a row") + } +} + +// TestUsePricesNilLeavesTheTableAlone: removing the source is the state the +// package started in. +func TestUsePricesNilLeavesTheTableAlone(t *testing.T) { + UsePrices(func(string) (Price, bool) { return Price{In: 99, Out: 99}, true }) + UsePrices(nil) + if price, known := PriceOf("moonshotai/kimi-k3"); !known || price != (Price{In: 0.60, Out: 2.50}) { + t.Fatalf("a removed source unmasks the table: %+v, %v", price, known) + } +} + +// TestCatalogPricesConvertsPerTokenToPerMillion: the catalog publishes per +// token, this package meters per million, and the doubling of that conversion +// stops here. +func TestCatalogPricesConvertsPerTokenToPerMillion(t *testing.T) { + if CatalogPrices(nil) != nil { + t.Fatalf("no reader is no source, not a source that never answers") + } + src := CatalogPrices(func(model string) (float64, float64, bool) { + if model == "vendored/big-model" { + return 0.0000035, 0.0000150, true + } + if model == "vendored/free-model" { + return 0, 0, true + } + return 0, 0, false + }) + price, known := src("vendored/big-model") + if !known || price != (Price{In: 3.5, Out: 15.0}) { + t.Fatalf("per-token became per-million: %+v, %v", price, known) + } + if price, known := src("vendored/free-model"); !known || (price != Price{}) { + t.Fatalf("a published zero is a published price: %+v, %v", price, known) + } + if _, known := src("vendored/unpublished"); known { + t.Fatalf("an unpublished model is not a free model") + } +} + +// TestTheMeterRidesTheInstalledSource: MeterCall prices through whatever is +// installed, so the whole tank — not just PriceOf — follows the tariff. +func TestTheMeterRidesTheInstalledSource(t *testing.T) { + UsePrices(CatalogPrices(func(model string) (float64, float64, bool) { + return 0.0000010, 0.0000030, true + })) + defer UsePrices(nil) + + if got, want := MeterCall(1_000_000, 1_000_000, "any/model"), 4.0; math.Abs(got-want) > 1e-9 { + t.Fatalf("the meter followed the source: got %v want %v", got, want) + } +} + +// TestRetiredIdsAreStrangers: an id the table no longer holds meters at the +// unpriced rate, exactly as one it never had. +func TestRetiredIdsAreStrangers(t *testing.T) { + for _, id := range []string{"z-ai/glm-5.2", "deepseek/deepseek-v4-flash"} { + price, known := PriceOf(id) + if known || price != unpriced { + t.Fatalf("PriceOf(%q) = %+v, %v; want %+v, false", id, price, known, unpriced) + } + } +} + +// TestMeteringSurvivesASwap: sources are swapped while runs meter, and every +// call sees one whole source. +func TestMeteringSurvivesASwap(t *testing.T) { + odd := func(string) (Price, bool) { return Price{In: 1, Out: 2}, true } + even := func(string) (Price, bool) { return Price{In: 2, Out: 4}, true } + UsePrices(odd) + defer UsePrices(nil) + done := make(chan struct{}) + go func() { + defer close(done) + for i := 0; i < 1000; i++ { + if i%2 == 0 { + UsePrices(odd) + } else { + UsePrices(even) + } + } + }() + for i := 0; i < 1000; i++ { + if _, known := PriceOf("any/model"); !known { + t.Fatalf("a swap lost the source mid-flight") + } + } + <-done +} From 0769cb46a4bd8fb376f27218ef167da9c8a10311 Mon Sep 17 00:00:00 2001 From: santoshkumarradha Date: Thu, 17 Sep 2026 01:16:02 -0400 Subject: [PATCH 012/173] session: an orchestrated run meters against the catalog's published tariff Co-Authored-By: codeaf --- internal/session/orchestrate.go | 13 +++++++++++++ internal/session/orchestrate_cost_test.go | 4 ++-- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/internal/session/orchestrate.go b/internal/session/orchestrate.go index 1c459dd36f..63de34095e 100644 --- a/internal/session/orchestrate.go +++ b/internal/session/orchestrate.go @@ -142,6 +142,10 @@ func (a *Agent) RunOrchestrate(ctx context.Context, goal, model string, capDolla seq := a.orchestrateSeq named := strings.TrimSpace(model) source, session := a.config.RolesSource, a.model + // The published tariff rides with the rest of the config: read under the + // same lock every other config field on this path is, installed below + // beside the run that meters by it. + priceNow := a.config.ModelPrice // AND THE TANK IS HELD TO THIS SESSION'S OWN CAP. A session given a spend // rail may not hand out a run larger than the rail (rail.go's [Agent.railCap]); // it is read here, under the same lock every other config field on this path @@ -197,6 +201,15 @@ func (a *Agent) RunOrchestrate(ctx context.Context, goal, model string, capDolla // says which model is doing the work, and the answer must not be able to move // under a `/model` switch half way through the run. family.worker = worker.model() + // THE TANK ASKS A PUBLISHED TARIFF FIRST, and only falls back to the + // meter package's own table when nobody published one for the model — the + // same order of authority the reported cost on a call's usage already has + // over both. The catalog lives behind [Config.ModelPrice] because a meter + // must not import it: this function value is the whole seam, installed here + // because this is the one place an orchestrated run is built and a reader + // is already in hand. A nil reader installs nothing, and the table stands + // alone exactly as before. + orchestrate.UsePrices(orchestrate.CatalogPrices(priceNow)) run := orchestrate.New(goal, planner, worker, orchestrate.Options{ Cap: capDollars, Lanes: orchestrateLanes, diff --git a/internal/session/orchestrate_cost_test.go b/internal/session/orchestrate_cost_test.go index 163889fee5..d4f7141627 100644 --- a/internal/session/orchestrate_cost_test.go +++ b/internal/session/orchestrate_cost_test.go @@ -13,7 +13,7 @@ func TestOrchestrateCostKeepsABareNameOutOfAVendorsRow(t *testing.T) { want float64 }{ {model: "deepseek-v4-flash", want: 1.25}, - {model: "deepseek/deepseek-v4-flash", want: 0.14}, + {model: "deepseek/deepseek-v4-flash-0731", want: 0.14}, } { if got := orchestrateCost(withoutReportedCost, test.model); got != test.want { t.Errorf("orchestrateCost without a reported cost for %q = %v, want %v", test.model, got, test.want) @@ -25,7 +25,7 @@ func TestOrchestrateCostKeepsABareNameOutOfAVendorsRow(t *testing.T) { PromptTokens: 1_000_000, Cost: &reportedCost, }} - for _, model := range []string{"deepseek-v4-flash", "deepseek/deepseek-v4-flash"} { + for _, model := range []string{"deepseek-v4-flash", "deepseek/deepseek-v4-flash-0731"} { if got := orchestrateCost(withReportedCost, model); got != reportedCost { t.Errorf("orchestrateCost with a reported cost for %q = %v, want %v", model, got, reportedCost) } From 521e1e0566af58214d118c796320611eb918ba7c Mon Sep 17 00:00:00 2001 From: santoshkumarradha Date: Thu, 17 Sep 2026 01:51:17 -0400 Subject: [PATCH 013/173] pool/index: the declared metrics are read in sorted order Two spellings of one metric name fold to one key and the first claim wins, as the aliases below already do. The loop walked the decoded map, so map order chose the winner and a document that spells a metric twice read differently on different runs. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01Sb7knukoGMPXo8kK876MsU --- internal/pool/index/index.go | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/internal/pool/index/index.go b/internal/pool/index/index.go index de844fc4dc..89c77b99c1 100644 --- a/internal/pool/index/index.go +++ b/internal/pool/index/index.go @@ -178,8 +178,17 @@ func parse(data []byte) (*Index, error) { } // The declared metrics are keyed by their folded name for lookup, and - // their kinds are folded once here rather than on every Kind call. - for name, decl := range d.Metrics { + // their kinds are folded once here rather than on every Kind call. The + // names are walked in sorted order for the same reason the aliases below + // are: two spellings that fold to one name are a first-claim-wins race, + // and map order would settle it differently on every run. + metricNames := make([]string, 0, len(d.Metrics)) + for name := range d.Metrics { + metricNames = append(metricNames, name) + } + sort.Strings(metricNames) + for _, name := range metricNames { + decl := d.Metrics[name] folded := fold(name) if _, seen := x.metrics[folded]; !seen { x.metrics[folded] = name From e957086047ba54eeac87d84c7ff862825f0a3d39 Mon Sep 17 00:00:00 2001 From: santoshkumarradha Date: Thu, 17 Sep 2026 01:51:17 -0400 Subject: [PATCH 014/173] session: a run with no published tariff leaves the installed one alone The tariff seam is one package-level value shared by every run in the process. Installing the adaptation of a nil reader stored a nil source, which does not leave the table alone: it removes whatever another session installed while that session's run is still metering by it. The install is skipped when there is no reader instead. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01Sb7knukoGMPXo8kK876MsU --- internal/session/orchestrate.go | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/internal/session/orchestrate.go b/internal/session/orchestrate.go index 63de34095e..7265854809 100644 --- a/internal/session/orchestrate.go +++ b/internal/session/orchestrate.go @@ -209,7 +209,14 @@ func (a *Agent) RunOrchestrate(ctx context.Context, goal, model string, capDolla // because this is the one place an orchestrated run is built and a reader // is already in hand. A nil reader installs nothing, and the table stands // alone exactly as before. - orchestrate.UsePrices(orchestrate.CatalogPrices(priceNow)) + // + // THE NIL CASE IS A SKIP, NOT AN INSTALL OF NOTHING. The seam is one + // package-level value shared by every run in the process, so passing a nil + // source through would not leave the table alone — it would pull whatever + // another session installed out from under a run still metering by it. + if priceNow != nil { + orchestrate.UsePrices(orchestrate.CatalogPrices(priceNow)) + } run := orchestrate.New(goal, planner, worker, orchestrate.Options{ Cap: capDollars, Lanes: orchestrateLanes, From e341d3a7052d060f743a66da3ccea31ad988d67a Mon Sep 17 00:00:00 2001 From: santoshkumarradha Date: Thu, 17 Sep 2026 01:52:02 -0400 Subject: [PATCH 015/173] docs/changes: notes for the three new packages and the fuel tariff seam Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01Sb7knukoGMPXo8kK876MsU --- ...-crew-picking-and-pool-reading-packages.md | 21 +++++++++++++++++++ ...093-fuel-asks-an-installed-tariff-first.md | 15 +++++++++++++ 2 files changed, 36 insertions(+) create mode 100644 docs/changes/unreleased/1093-crew-picking-and-pool-reading-packages.md create mode 100644 docs/changes/unreleased/1093-fuel-asks-an-installed-tariff-first.md diff --git a/docs/changes/unreleased/1093-crew-picking-and-pool-reading-packages.md b/docs/changes/unreleased/1093-crew-picking-and-pool-reading-packages.md new file mode 100644 index 0000000000..93f0dc5e06 --- /dev/null +++ b/docs/changes/unreleased/1093-crew-picking-and-pool-reading-packages.md @@ -0,0 +1,21 @@ +--- +kind: added +title: Three pure packages — a crew picker, an additive tally and a measurement-index reader +pr: 1093 +surface: [engine] +invalidates: [] +--- + +Standard library only, no disk, no network, and nothing on a surface wired to +them yet. + +- `internal/crewpick` scores every (worker, high, mastermind) combination a + candidate list can field by the bill its seat volumes run up against the mean + of its seat qualities, and reads the frugal, balanced and max picks — and a + knob between them — off the non-dominated front. +- `internal/pool/tally` keeps per-address sufficient statistics (n, sum, sum of + squares) and paired-comparison counts, so two sheets merge by addition in any + order and marshal to a byte-identical schema-1 document. +- `internal/pool/index` parses a schema-versioned JSON document of per-model + measurements into an immutable value every goroutine can share, resolving + model aliases and ignoring fields, metric kinds and dims it does not know. diff --git a/docs/changes/unreleased/1093-fuel-asks-an-installed-tariff-first.md b/docs/changes/unreleased/1093-fuel-asks-an-installed-tariff-first.md new file mode 100644 index 0000000000..652e1e6470 --- /dev/null +++ b/docs/changes/unreleased/1093-fuel-asks-an-installed-tariff-first.md @@ -0,0 +1,15 @@ +--- +kind: changed +title: The fuel meter asks an installed tariff first, and its table holds the ids this build ships +pr: 1093 +surface: [engine] +invalidates: + - "`orchestrate.PriceOf` answered from one package-level table and nothing else. An installed `PriceSource` is asked first and the table is the fallback behind it; `UsePrices` installs or removes one, and `CatalogPrices` adapts a per-token catalog reader into the per-million shape the meter uses." + - "The fuel table's rows were `deepseek/deepseek-v4-flash`, `z-ai/glm-5.2`, `moonshotai/kimi-k3`, `anthropic/claude-opus` and `openai/gpt-5`. The first two are gone and `deepseek/deepseek-v4-flash-0731`, `z-ai/glm-5.3` and `z-ai/glm-5.3-flash` are there instead, so the two retired ids now meter at the unpriced rate rather than at a row of their own." + - "An orchestrated run metered against the fuel table whenever a call reported no cost of its own. It meters against the catalog's published price when the session has one, and falls back to the table only for a model the catalog publishes nothing for." +--- + +The seam is a function value, so the meter never imports a catalog: the one +place an orchestrated run is built already holds a reader and installs it +there. A session with no reader installs nothing and leaves whatever is +installed alone, because the seam is shared by every run in the process. From 120bbc3029df06aa63bee36e85f62458c9f2c211 Mon Sep 17 00:00:00 2001 From: santoshkumarradha Date: Thu, 17 Sep 2026 02:37:52 -0400 Subject: [PATCH 016/173] config: a tier row that says auto resolves through the catalog's own front, on two new rungs --- internal/config/auto.go | 244 +++++++++++++++++++++++++++++++++++++++ internal/config/seats.go | 43 ++++++- 2 files changed, 285 insertions(+), 2 deletions(-) create mode 100644 internal/config/auto.go diff --git a/internal/config/auto.go b/internal/config/auto.go new file mode 100644 index 0000000000..b94211a2c0 --- /dev/null +++ b/internal/config/auto.go @@ -0,0 +1,244 @@ +package config + +import ( + "strings" + + "github.com/Agent-Field/codeaf/internal/catalog" + "github.com/Agent-Field/codeaf/internal/crewpick" +) + +// THE WORD THAT ASKS THE CATALOG TO ANSWER A TIER ROW. +// +// A tier row names a model id, and a model id is a bet the person made one day +// against a catalog that keeps moving under it. `auto` is the row that +// declines to bet: the seat's model is computed from the catalog's own +// published figures — the three capability indexes against the three prices, +// under each seat's call shape (internal/crewpick) — every time the row is +// read, through [autoRow], the one seam both ladders pass through. +// +// NOTHING IS REWRITTEN. The row stays `auto` on disk and the id is derived on +// every read, so a catalog that moves moves the seat with it, and a profile +// never holds a model id this build chose for somebody. + +// AutoValue is the bare word a tier row holds to have its seat computed from +// the catalog. +const AutoValue = "auto" + +// IsAuto reports whether a tier value is the bare word, case folded with the +// surrounding space ignored. `auto:high` is not it: the rows own the +// `:` notation, and a suffixed word is a model id with a level, handed +// on whole the way every other one is. +func IsAuto(value string) bool { + return strings.ToLower(strings.TrimSpace(value)) == AutoValue +} + +// AutoModels is how the catalog reaches seat resolution: the binary holding +// the catalog sets it ONCE AT START-UP, from its non-blocking read, and never +// a fetch — a tier row that says auto resolves from whatever the catalog +// already holds, the same posture every other catalog reader here keeps. Nil, +// and a func answering with no rows, are ordinary states rather than errors: +// a row that says auto then reads the family's table row ([autoRow]), which +// is the answer it had before this word existed. +var AutoModels func() []catalog.Model + +// AutoPick is the word's own answer for one tier, computed from the rows. +// +// It is PURE: no disk, no network, the rows are only read, and the same rows +// give the same answer however often it is asked and in whatever order they +// arrive (crewpick breaks its ties by id, not by order). Each row is read as +// a crewpick candidate — the three indexes, the three prices AS PUBLISHED (a +// uniform scale changes no pick on a front sorted by bill), whether the +// provider published a cache-read price at all, the window, the modalities +// and the parameters — and a row whose price the provider did not publish is +// left out, because a model that may cost anything has no place in a pick +// that is about cost. +// +// tier names the seat the pick is read from: worker, high and mastermind have +// answers, reflex and low and any other word do not. family narrows the +// shelf: `open` picks off the open-weight rows, every other word off the +// whole catalog. preset is one of the three crew words, and any other word +// has no answer. No rows, an empty front or an empty id are no answer too; +// no answer is ok false and an empty id, which is the caller's cue to read +// the family's table row instead ([autoRow]). +func AutoPick(tier, family, preset string, models []catalog.Model) (modelID string, ok bool) { + seat, ok := autoSeat(tier) + if !ok || len(models) == 0 { + return "", false + } + fam := crewpick.All + if normalCrewSource(family) == CrewSourceOpen { + fam = crewpick.Open + } + candidates := autoCandidates(models) + if len(candidates) == 0 { + return "", false + } + frugal, balanced, max := crewpick.Presets(crewpick.Front(candidates, crewpick.DefaultShapes(), fam)) + var pick crewpick.Crew + switch strings.ToLower(strings.TrimSpace(preset)) { + case CrewFrugal: + pick = frugal + case CrewBalanced: + pick = balanced + case CrewMax: + pick = max + default: + return "", false + } + var id string + switch seat { + case crewpick.Worker: + id = pick.Worker + case crewpick.High: + id = pick.High + case crewpick.Mastermind: + id = pick.Mastermind + } + return id, id != "" +} + +// autoSeat is the tier word that names which of crewpick's three seats the +// pick is read from. The two tiers that never vary — reflex and small work — +// have no answer here, the same law their columns in the shipped tables keep. +func autoSeat(tier string) (crewpick.Seat, bool) { + switch strings.ToLower(strings.TrimSpace(tier)) { + case ModelTierWorker: + return crewpick.Worker, true + case ModelTierHigh: + return crewpick.High, true + case ModelTierMastermind: + return crewpick.Mastermind, true + } + return 0, false +} + +// autoCandidates reads every published row as a candidate. Rows with no +// published price are dropped here, where the reason is nameable, rather than +// being carried into the picker to fall out of its own candidacy law. +func autoCandidates(models []catalog.Model) []crewpick.Candidate { + candidates := make([]crewpick.Candidate, 0, len(models)) + for _, model := range models { + if model.PriceUnknown { + continue + } + candidates = append(candidates, crewpick.Candidate{ + ID: model.ID, + Open: model.OpenWeights, + Intelligence: model.IntelligenceIndex, + Coding: model.CodingIndex, + Agentic: model.AgenticIndex, + PromptPrice: model.PromptPrice, + CompletionPrice: model.CompletionPrice, + CacheReadPrice: model.CacheReadPrice, + HasCacheRead: model.CacheReadPrice > 0, + Context: model.ContextLength, + Images: listHolds(model.InputModalities, "image"), + Tools: listHolds(model.Parameters, "tools"), + }) + } + return candidates +} + +// listHolds says whether a row's own list carries the word, case folded — the +// catalog cleans its lists, but a reader that folds is one fewer assumption. +func listHolds(words []string, word string) bool { + for _, held := range words { + if strings.EqualFold(strings.TrimSpace(held), word) { + return true + } + } + return false +} + +// ── the seam ──────────────────────────────────────────────────────────────── + +// autoRow is THE ONE SEAM both ladders resolve a tier row that says auto +// through — [tierSeatUnder], which the conversation, the settings sheet and +// the crew word read, and [resolveSeat], which every headless door climbs. A +// second copy in each would be a seat that means one thing in chat and +// another headless, which is the defect this file exists to prevent one +// ladder at a time. +// +// The answer is the computed id, on its own rung, when the catalog can +// compute one; otherwise the family's table row FOR THE PRESET, on the table +// rung — the same id a preset write would have landed, and never `auto` and +// never empty. +func autoRow(profileDir, family, tier string) (model string, source SeatSource, preset string) { + preset = crewPresetUnder(profileDir, family) + if id, ok := AutoPick(tier, family, preset, autoCatalogRows()); ok { + return id, SeatComputed, preset + } + if row, ok := CrewModelsForSource(family, preset); ok { + if id := strings.TrimSpace(row[tier]); id != "" { + return id, SeatTable, preset + } + } + // Unreachable for a tier this build knows — every preset row answers all + // five — but the never-empty law is carried rather than assumed: the + // family's default row is the last word. + return defaultTierModel(family, tier), SeatTable, preset +} + +// autoCatalogRows is [AutoModels] read with its two ordinary absences folded +// into one answer. +func autoCatalogRows() []catalog.Model { + if AutoModels == nil { + return nil + } + return AutoModels() +} + +// crewPresetUnder is the preset the profile's five STORED rows make, in the +// family given — read from the rows and never through seat resolution, so +// the seam cannot ask the ladder that is asking it. +// +// Each stored tier value is read through the ladder's own row reader +// ([crewRow]), which is what keeps a row reached through the lineage saying +// it the same way. A row the reader cannot answer — cleared, or never held — +// reads the family's default-preset id, which is what that tier runs until +// somebody writes it. A row that says auto matches WHICHEVER preset is being +// compared, because auto is the one row with no opinion of its own: it is +// asking to be computed at the budget the other four rows name. +// +// The answer is the first preset whose every row matches, the default preset +// tried first and then the crew's own order; when none matches, the default — +// the budget an undecided profile runs at is the budget an auto seat runs at. +func crewPresetUnder(profileDir, family string) string { + defaults := crewTableFor(family)[DefaultCrew] + read := make(map[string]string, len(ModelTiers)) + for _, tier := range ModelTiers { + value, _, source, cleared := crewRow(profileDir, tier) + switch { + case cleared || source == "": + read[tier] = strings.ToLower(strings.TrimSpace(defaults[tier])) + case IsAuto(value): + read[tier] = AutoValue + default: + read[tier] = strings.ToLower(strings.TrimSpace(value)) + } + } + matches := func(preset string) bool { + row, ok := CrewModelsForSource(family, preset) + if !ok { + return false + } + for _, tier := range ModelTiers { + if read[tier] == AutoValue { + continue + } + if read[tier] != strings.ToLower(strings.TrimSpace(row[tier])) { + return false + } + } + return true + } + if matches(DefaultCrew) { + return DefaultCrew + } + for _, preset := range CrewPresets { + if preset != DefaultCrew && matches(preset) { + return preset + } + } + return DefaultCrew +} diff --git a/internal/config/seats.go b/internal/config/seats.go index 4066a73c1b..c0ffd457ad 100644 --- a/internal/config/seats.go +++ b/internal/config/seats.go @@ -65,6 +65,18 @@ const ( // difference between "the row you wrote" and "the row your row was split out // of". SeatInherited SeatSource = "inherited" + // SeatComputed is a tier row that says `auto` answering from the catalog: + // the seat's model computed off the published figures at read time + // ([AutoPick]). It is a rung of the crew's own, not the build's default — + // the person wrote the row — and it says so because a receipt that called it + // `crew` would hide the one fact a reader of the run is checking: the id was + // derived, not named. + SeatComputed SeatSource = "computed" + // SeatTable is a tier row that says `auto` answering from the family's + // table row ([autoRow]) because nothing could be computed — no catalog, or + // no pick off it. The id is the preset's own, and the rung says which kind + // of answer it was. + SeatTable SeatSource = "table" // SeatDefault is this build's choice, for a profile that has never said // anything about models at all. SeatDefault SeatSource = "default" @@ -244,6 +256,15 @@ func tierSeatUnder(profileDir, family, tier string) Seat { model = defaultTierModel(family, tier) } } + // A row that says auto resolves through the one seam both ladders share + // ([autoRow]). Only a row somebody WROTE — directly, or through the lineage + // — can say it: the default rung names a model id, and a cleared row names + // nothing at all. + if IsAuto(model) { + var preset string + model, source, preset = autoRow(profileDir, family, tier) + return Seat{Role: tierSeatRole(tier), Model: model, Source: source, Crew: preset} + } return Seat{Role: tierSeatRole(tier), Model: model, Source: source, From: from} } @@ -272,8 +293,12 @@ type Seat struct { From string // Crew is the preset word the profile's five tier rows make — `frugal`, // `balanced`, `max` or `custom` ([CrewAt]) — and is empty unless Source is - // [SeatCrew]. It is read rather than stored, exactly as the settings sheet - // reads it, so a receipt and the sheet cannot disagree about which crew ran. + // [SeatCrew], [SeatInherited], [SeatComputed] or [SeatTable]. It is read + // rather than stored, exactly as the settings sheet reads it, so a receipt + // and the sheet cannot disagree about which crew ran. On the computed and + // table rungs it is the preset the auto row is computed at, read from the + // stored rows ([crewPresetUnder]) rather than derived through the resolver, + // which would be the seam asking itself. Crew string } @@ -313,6 +338,10 @@ func (s Seat) Rung() string { // because a receipt that said only `crew custom` would hide exactly the // substitution this rung exists to report. return s.crewRung() + ", inherited" + case SeatComputed: + return s.crewRung() + ", computed" + case SeatTable: + return s.crewRung() + ", table" } return "default" } @@ -575,6 +604,16 @@ func resolveSeat(role SeatRole, profileDir, flag, tier, fallback string) Seat { seat.Model, seat.Source = fallback, SeatDefault return seat } + // A row that says auto resolves through the one seam both ladders share + // ([autoRow]) — on the rung it earned, with the preset it was computed at + // carried for the receipt — and never to `auto` and never to empty, which + // is what a headless run could do nothing with. + if IsAuto(model) { + var preset string + seat.Model, seat.Source, preset = autoRow(profileDir, CrewSourceAt(profileDir), tier) + seat.Crew = preset + return seat + } seat.Model, seat.Source, seat.From, seat.Crew = model, source, from, CrewAt(profileDir) return seat } From 0295be5771dcdc3910b97a8a339f98e5e2ccdb72 Mon Sep 17 00:00:00 2001 From: santoshkumarradha Date: Thu, 17 Sep 2026 02:41:04 -0400 Subject: [PATCH 017/173] config: test the auto word, its pick and both ladders' resolution of it --- internal/config/auto_test.go | 309 +++++++++++++++++++++++++++++++++++ 1 file changed, 309 insertions(+) create mode 100644 internal/config/auto_test.go diff --git a/internal/config/auto_test.go b/internal/config/auto_test.go new file mode 100644 index 0000000000..d2ae892e66 --- /dev/null +++ b/internal/config/auto_test.go @@ -0,0 +1,309 @@ +package config + +import ( + "encoding/json" + "os" + "strings" + "testing" + + "github.com/Agent-Field/codeaf/internal/catalog" + "github.com/Agent-Field/codeaf/internal/crewpick" +) + +// THE WORD, AND WHAT IS NOT THE WORD. `auto` is a bare word a tier row holds, +// and everything that only resembles one is a model id: a suffixed word is the +// rows' own `:` notation, a slashed one is a vendor's namespace, and +// `automatic` is somebody's model that happens to begin with it. +func TestIsAutoReadsTheBareWordAlone(t *testing.T) { + for word, want := range map[string]bool{ + "auto": true, " AUTO ": true, "Auto": true, "\tauto\n": true, + "": false, "auto:high": false, "vendor/auto": false, "automatic": false, + } { + if got := IsAuto(word); got != want { + t.Errorf("IsAuto(%q) = %t, want %t", word, got, want) + } + } +} + +// autoTestRows is a small catalog with every fact a pick reads, and one row +// the provider priced "-1" on — which is not a candidate, for the reason a +// pick that is about cost cannot count a model whose cost nobody published. +func autoTestRows() []catalog.Model { + return []catalog.Model{ + {ID: "a/cheap", OpenWeights: true, IntelligenceIndex: 30, CodingIndex: 35, AgenticIndex: 40, + PromptPrice: 0.0000002, CompletionPrice: 0.0000009, CacheReadPrice: 0.00000002, + ContextLength: 200_000, InputModalities: []string{"text", "image"}, Parameters: []string{"tools"}}, + {ID: "a/mid", OpenWeights: true, IntelligenceIndex: 45, CodingIndex: 50, AgenticIndex: 48, + PromptPrice: 0.000001, CompletionPrice: 0.000003, CacheReadPrice: 0.0000001, + ContextLength: 200_000, InputModalities: []string{"text"}, Parameters: []string{"tools"}}, + {ID: "b/care", IntelligenceIndex: 60, CodingIndex: 55, AgenticIndex: 50, + PromptPrice: 0.000004, CompletionPrice: 0.00002, CacheReadPrice: 0.0000004, + ContextLength: 400_000, InputModalities: []string{"text", "image"}, Parameters: []string{"tools"}}, + {ID: "c/mind", IntelligenceIndex: 75, CodingIndex: 70, AgenticIndex: 65, + PromptPrice: 0.00001, CompletionPrice: 0.00005, ContextLength: 200_000, + InputModalities: []string{"text"}, Parameters: []string{"tools"}}, + {ID: "r/router", PriceUnknown: true, IntelligenceIndex: 80, CodingIndex: 80, AgenticIndex: 80, + PromptPrice: -1, CompletionPrice: -1, ContextLength: 200_000}, + } +} + +// THE PICK IS PURE AND ITS ANSWER IS THE FRONT'S. The same rows give the same +// id however often it is asked, the rows are never touched, and a row whose +// price nobody published is in no seat. The front is read the way the shipped +// tables were (crew.go owns the method), so the pick answers exactly what +// crewpick answers for these rows — asserted here against crewpick itself +// rather than against a figure copied out of one. +func TestAutoPickAnswersTheFront(t *testing.T) { + rows := autoTestRows() + workers := make([]string, 0, 3) + for i := 0; i < 3; i++ { + id, ok := AutoPick(ModelTierWorker, CrewSourceAll, CrewFrugal, rows) + if !ok || id == "" { + t.Fatalf("AutoPick answered ok=%t id=%q", ok, id) + } + workers = append(workers, id) + } + for i := 1; i < len(workers); i++ { + if workers[i] != workers[0] { + t.Fatalf("the same rows answered %q then %q — the pick is not pure", workers[0], workers[i]) + } + } + _ = workers + if rows[4].ID != "r/router" || !rows[4].PriceUnknown { + t.Fatal("AutoPick modified the rows it was handed") + } + + frugal, _, maxCrew := crewpick.Presets(crewpick.Front(autoCandidates(rows), crewpick.DefaultShapes(), crewpick.All)) + if len(frugal.Worker) == 0 { + t.Fatal("the test rows field no crew — the rows are broken") + } + if got, _ := AutoPick(ModelTierWorker, CrewSourceAll, CrewFrugal, rows); got != frugal.Worker { + t.Fatalf("worker frugal = %q, want the front's own %q", got, frugal.Worker) + } + if got, _ := AutoPick(ModelTierMastermind, CrewSourceAll, CrewMax, rows); got != maxCrew.Mastermind { + t.Fatalf("mastermind max = %q, want the front's own %q", got, maxCrew.Mastermind) + } +} + +// THE TWO TIERS THAT NEVER VARY HAVE NO ANSWER HERE, the same law their +// columns in the shipped tables keep — reflex and small work are the same +// near-free models in all three presets, and a word that computes for them +// would be a second opinion about a row that has none. +func TestAutoPickRefusesTheTiersWithoutOpinions(t *testing.T) { + for _, tier := range []string{ModelTierReflex, ModelTierLow, "", "banana"} { + if id, ok := AutoPick(tier, CrewSourceAll, CrewFrugal, autoTestRows()); ok || id != "" { + t.Errorf("AutoPick(%q) answered %q, want no answer", tier, id) + } + } +} + +// THE THREE WORDS ARE THE ONLY PRESETS, `open` IS THE ONLY OTHER FAMILY, AND +// NOTHING IS AN ERROR: no answer is ok false and an empty id, on no rows, +// on an empty front, on a word that is not a preset, and on a tier the +// catalog cannot field. +func TestAutoPickAnswersNothingWhenNothingCanBePicked(t *testing.T) { + if id, ok := AutoPick(ModelTierWorker, CrewSourceAll, "banana", autoTestRows()); ok || id != "" { + t.Errorf("an unknown preset answered %q", id) + } + if id, ok := AutoPick(ModelTierWorker, CrewSourceAll, CrewFrugal, nil); ok || id != "" { + t.Errorf("no rows answered %q", id) + } + if id, ok := AutoPick(ModelTierHigh, CrewSourceAll, CrewFrugal, []catalog.Model{{ID: "a/cheap", PromptPrice: 1}}); ok || id != "" { + t.Errorf("rows that field no crew answered %q", id) + } + // A family word this build does not know reads as the default family, + // the way [CrewSourceAt] reads one — and that family answers here. + if _, ok := AutoPick(ModelTierWorker, "misplaced", CrewFrugal, autoTestRows()); !ok { + t.Error("an unknown family word read as no family at all") + } +} + +// ── the ladder ────────────────────────────────────────────────────────────── + +// A ROW THAT SAYS AUTO RESOLVES THROUGH THE ONE SEAM, ON BOTH LADDERS, to the +// computed id with the rung that says so — and with the catalog absent, to +// the family's table row for the preset the other four rows name, on the +// table rung. It never resolves to `auto` and never to empty. +func TestAnAutoRowResolvesOnBothLadders(t *testing.T) { + t.Setenv(ModelEnv, "") + t.Setenv(PlanModelEnv, "") + restore := AutoModels + AutoModels = func() []catalog.Model { return autoTestRows() } + defer func() { AutoModels = restore }() + + rows := writeProfileRows(t, map[string]string{ + KeyTierWorkerModel: " auto ", + KeyTierMastermindModel: "AUTO", + }) + for _, seat := range []Seat{ + TierSeatAt(rows, ModelTierWorker), + ResolveSeats(rows, "", "").Work, + } { + if seat.Source != SeatComputed { + t.Errorf("conversation ladder: the seat reads %s, want computed", seat.Rung()) + } + if strings.TrimSpace(seat.Model) == "" || IsAuto(seat.Model) { + t.Errorf("conversation ladder: the seat reads %q, want a computed id", seat.Model) + } + } + plan := ResolveSeats(rows, "", "").Plan + if plan.Source != SeatComputed { + t.Errorf("headless ladder: the plan seat reads %s, want computed", plan.Rung()) + } + + // The catalog reaches nothing: the family's table row answers, on the + // table rung, at the preset the stored rows make. These rows pin nothing + // else, so they are the default crew — and the worker column of that + // preset in the DEFAULT family is the answer the table owes. + AutoModels = nil + table, _ := CrewModelsForSource(DefaultCrewSource, DefaultCrew) + want := table[ModelTierWorker] + for name, seat := range map[string]Seat{ + "conversation": TierSeatAt(rows, ModelTierWorker), + "headless": ResolveSeats(rows, "", "").Work, + } { + if seat.Model != want || seat.Source != SeatTable { + t.Errorf("%s ladder with no catalog: %q (%s), want %q (table)", name, seat.Model, seat.Rung(), want) + } + } +} + +// A REACHED-THROUGH-THE-LINEAGE ROW SAYS AUTO THE SAME WAY. The worker row +// landed after profiles held only the small-work row, and a profile of that +// vintage whose small-work row says auto hands its work seat the same answer +// the conversation hands it — computed when the catalog can, and never the +// bare word. +func TestAnInheritedAutoRowResolvesTheSameWay(t *testing.T) { + t.Setenv(ModelEnv, "") + t.Setenv(PlanModelEnv, "") + restore := AutoModels + AutoModels = func() []catalog.Model { return autoTestRows() } + defer func() { AutoModels = restore }() + + dir := writeProfileRows(t, map[string]string{KeyTierLowModel: AutoValue}) + for name, seat := range map[string]Seat{ + "conversation": TierSeatAt(dir, ModelTierWorker), + "headless": ResolveSeats(dir, "", "").Work, + } { + if seat.Model == AutoValue || seat.Model == "" { + t.Errorf("%s ladder: the seat reads %q, want what the row was reached through", name, seat.Model) + } + } +} + +// THE FLAG AND THE VARIABLE STILL OUTRANK A ROW THAT SAYS AUTO, and a flag +// whose text is the word is handed on whole — a person who named a model +// called auto got a model called auto, exactly as they did before this rung +// existed. +func TestTheInvocationRungsStillOutrankAnAutoRow(t *testing.T) { + t.Setenv(ModelEnv, "vendor/from-the-environment") + t.Setenv(PlanModelEnv, "vendor/plans-from-the-environment") + dir := writeProfileRows(t, map[string]string{KeyTierWorkerModel: AutoValue}) + seats := ResolveSeats(dir, "", "") + if seats.Work.Model != "vendor/from-the-environment" || seats.Work.Source != SeatEnv { + t.Errorf("the environment rung read %q (%s)", seats.Work.Model, seats.Work.Rung()) + } + if seats.Plan.Model != "vendor/plans-from-the-environment" || seats.Plan.Source != SeatEnv { + t.Errorf("the plan seat read %q (%s)", seats.Plan.Model, seats.Plan.Rung()) + } + seats = ResolveSeats(dir, AutoValue, "") + if seats.Work.Model != AutoValue || seats.Work.Source != SeatFlag { + t.Errorf("a flag that says auto read %q (%s), want the word handed on whole", seats.Work.Model, seats.Work.Rung()) + } +} + +// THE PRESET AN AUTO SEAT RUNS AT IS READ FROM THE STORED ROWS, never through +// seat resolution — the seam cannot ask the ladder that is asking it — and a +// row that says auto matches whichever preset is being compared. Four pinned +// rows of one preset plus an auto row are that preset. +func TestThePresetAnAutoSeatRunsAtIsReadFromTheStoredRows(t *testing.T) { + t.Setenv(ModelEnv, "") + t.Setenv(PlanModelEnv, "") + for _, preset := range CrewPresets { + models, _ := CrewModelsForSource(DefaultCrewSource, preset) + rows := map[string]string{KeyTierWorkerModel: AutoValue} + for _, tier := range ModelTiers { + if tier != ModelTierWorker { + rows[tierKeyFor(tier)] = models[tier] + } + } + dir := writeProfileRows(t, rows) + restore := AutoModels + AutoModels = func() []catalog.Model { return autoTestRows() } + seat := TierSeatAt(dir, ModelTierWorker) + AutoModels = restore + if seat.Crew != preset { + t.Errorf("four %s rows and an auto row read as %q", preset, seat.Crew) + } + if seat.Rung() != "crew "+preset+", computed" { + t.Errorf("the rung reads %q, want crew %s, computed", seat.Rung(), preset) + } + } + // A profile with no auto row anywhere reads as it always read — the + // default rung, no crew word — which is the unchanged-behaviour law: the + // seam fires only on a row that says the word. + seat := TierSeatAt(t.TempDir(), ModelTierWorker) + if seat.Rung() != "default" || seat.Crew != "" { + t.Errorf("an untouched profile reads %q (%s), want the default rung as before", seat.Rung(), seat.Crew) + } +} + +// A SETTINGS ROW SHOWS THE MODEL RUNNING, because it reads through the +// resolver — and a profile that says auto nowhere is untouched by any of +// this: the same rows, the same ids, the same rungs. +func TestAutoNowhereChangesNothing(t *testing.T) { + t.Setenv(ModelEnv, "") + t.Setenv(PlanModelEnv, "") + restore := AutoModels + AutoModels = func() []catalog.Model { return autoTestRows() } + defer func() { AutoModels = restore }() + + dir := t.TempDir() + if err := ApplyCrew(dir, CrewBalanced); err != nil { + t.Fatal(err) + } + before := map[string]string{} + for _, tier := range ModelTiers { + before[tier] = TierModelAt(dir, tier) + } + raw, err := os.ReadFile(BudgetConfigPath(dir)) + if err != nil { + t.Fatal(err) + } + var stored map[string]any + if err := json.Unmarshal(raw, &stored); err != nil { + t.Fatal(err) + } + if len(stored) == 0 { + t.Fatal("the write stored nothing") + } + for tier, want := range before { + if got := TierModelAt(dir, tier); got != want { + t.Errorf("%s read %q then %q — a read rewrote a row", tier, want, got) + } + } + if got := CrewAt(dir); got != CrewBalanced { + t.Errorf("the crew word reads %q, want balanced", got) + } +} + +// THE SEAM'S TABLE RUNG IS THE PRESET'S OWN ID, not merely a non-empty +// answer: with the catalog gone, an auto row under a pinned preset resolves +// to that preset's id for the tier, not to the default preset's. +func TestTheTableRungAnswersThePresetTheRowsName(t *testing.T) { + t.Setenv(ModelEnv, "") + t.Setenv(PlanModelEnv, "") + models, _ := CrewModelsForSource(DefaultCrewSource, CrewMax) + rows := map[string]string{KeyTierWorkerModel: AutoValue} + for _, tier := range ModelTiers { + if tier != ModelTierWorker { + rows[tierKeyFor(tier)] = models[tier] + } + } + dir := writeProfileRows(t, rows) + seat := TierSeatAt(dir, ModelTierWorker) + if seat.Model != models[ModelTierWorker] || seat.Source != SeatTable { + t.Errorf("with no catalog the seat reads %q (%s), want max's own worker id on the table rung", + seat.Model, seat.Rung()) + } +} From 8c50bc0133492e591ddb6bd5db53be8a2e219af8 Mon Sep 17 00:00:00 2001 From: santoshkumarradha Date: Thu, 17 Sep 2026 02:46:03 -0400 Subject: [PATCH 018/173] config: wire the catalog's own front into every door's auto word Co-Authored-By: codeaf --- cmd/codeaf/chat.go | 3 +++ cmd/codeaf/chatv3_host.go | 3 +++ cmd/codeaf/chatv3_process.go | 3 +++ cmd/codeaf/subharness.go | 4 ++++ 4 files changed, 13 insertions(+) diff --git a/cmd/codeaf/chat.go b/cmd/codeaf/chat.go index ec201ac6e2..e31d015a1a 100644 --- a/cmd/codeaf/chat.go +++ b/cmd/codeaf/chat.go @@ -183,6 +183,9 @@ func buildBrain(w *chatWindow, session string, opts brainOptions) (*chatBrain, e modelCatalog := catalog.LoadLazy(context.Background(), catalog.Options{ BaseURL: settings.BaseURL, APIKey: settings.APIKey, Dir: settings.ProfileDir, }) + // A tier row that says auto is answered from this catalog (config.AutoModels): + // the same non-blocking read, never a fetch, and set once at start-up. + config.AutoModels = modelCatalog.ModelsNow // Every client built below shapes its requests against these rows: which // knobs a model accepts is the catalog's answer, not a guess. settings.Models = modelCatalog diff --git a/cmd/codeaf/chatv3_host.go b/cmd/codeaf/chatv3_host.go index 6d3fe33c4b..c2b1e48635 100644 --- a/cmd/codeaf/chatv3_host.go +++ b/cmd/codeaf/chatv3_host.go @@ -609,6 +609,9 @@ func hostOptions(fleet *engineFleet, welcome remote.Welcome, pick bool) (tui3.Op } discovery := catalog.Options{BaseURL: settings.BaseURL, APIKey: settings.APIKey, Dir: profileDir} models := catalog.LoadLazy(context.Background(), discovery) + // A tier row that says auto is answered from this catalog (config.AutoModels): + // the same non-blocking read, never a fetch, and set once at start-up. + config.AutoModels = models.ModelsNow // The refresh key in /model asks the same router THIS machine's list came // from, and refills the same shelf — the list is this laptop's list of // names on both doors (chatv3_modelshelf.go). diff --git a/cmd/codeaf/chatv3_process.go b/cmd/codeaf/chatv3_process.go index 880b46cf99..2ed2a357df 100644 --- a/cmd/codeaf/chatv3_process.go +++ b/cmd/codeaf/chatv3_process.go @@ -177,6 +177,9 @@ func openV3ProcessWith(door string, askKey bool) (*v3Process, error) { BaseURL: settings.BaseURL, APIKey: settings.APIKey, Dir: settings.ProfileDir, } models := catalog.LoadLazy(context.Background(), discovery) + // A tier row that says auto is answered from this catalog (config.AutoModels): + // the same non-blocking read, never a fetch, and set once at start-up. + config.AutoModels = models.ModelsNow shelf := newV3ModelShelf(models, discovery) shelf.setSources(settings.Sources) return &v3Process{ diff --git a/cmd/codeaf/subharness.go b/cmd/codeaf/subharness.go index 170529bed5..5a247ecc9e 100644 --- a/cmd/codeaf/subharness.go +++ b/cmd/codeaf/subharness.go @@ -376,6 +376,10 @@ var sharedCatalog = func() func(config.Config) *catalog.Catalog { resolved = catalog.LoadLazy(context.Background(), catalog.Options{ BaseURL: settings.BaseURL, APIKey: settings.APIKey, Dir: settings.ProfileDir, }) + // A tier row that says auto is answered from this catalog (config.AutoModels): + // the same non-blocking read, never a fetch, and set once at start-up so every + // headless door resolves the word against the list it already holds. + config.AutoModels = resolved.ModelsNow }) return resolved } From 8f69b8c8fa74be20a07ac2f1a336fc83a7b6a2ba Mon Sep 17 00:00:00 2001 From: santoshkumarradha Date: Thu, 17 Sep 2026 02:54:38 -0400 Subject: [PATCH 019/173] codeaf: the do door wires the auto word before the seats handed in are applied --- cmd/codeaf/chat.go | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/cmd/codeaf/chat.go b/cmd/codeaf/chat.go index e31d015a1a..0a24e53827 100644 --- a/cmd/codeaf/chat.go +++ b/cmd/codeaf/chat.go @@ -166,6 +166,14 @@ func buildBrain(w *chatWindow, session string, opts brainOptions) (*chatBrain, e // then let the machine form print underneath, which said one fact twice. return nil, err } + // A tier row that says auto is answered from this catalog (config.AutoModels), + // so the word is wired BEFORE the seats handed in are applied — a door whose + // ladder answered the word before this line resolved it from nothing. The + // read is the same non-blocking one, never a fetch. + modelCatalog := catalog.LoadLazy(context.Background(), catalog.Options{ + BaseURL: settings.BaseURL, APIKey: settings.APIKey, Dir: settings.ProfileDir, + }) + config.AutoModels = modelCatalog.ModelsNow if opts.seats != nil { applySeats(&settings, *opts.seats) } else { @@ -180,12 +188,6 @@ func buildBrain(w *chatWindow, session string, opts brainOptions) (*chatBrain, e // to a tool the user has not been able to reach yet — the surface has not // been drawn. Asking it in front of the first frame buys nothing and can // cost fifteen seconds of dead terminal. - modelCatalog := catalog.LoadLazy(context.Background(), catalog.Options{ - BaseURL: settings.BaseURL, APIKey: settings.APIKey, Dir: settings.ProfileDir, - }) - // A tier row that says auto is answered from this catalog (config.AutoModels): - // the same non-blocking read, never a fetch, and set once at start-up. - config.AutoModels = modelCatalog.ModelsNow // Every client built below shapes its requests against these rows: which // knobs a model accepts is the catalog's answer, not a guess. settings.Models = modelCatalog From 6fa70e74be9b56332f48c98c807d935ef310192b Mon Sep 17 00:00:00 2001 From: santoshkumarradha Date: Thu, 17 Sep 2026 02:55:12 -0400 Subject: [PATCH 020/173] docs/changes: the tier row that says auto, and the two rungs it resolves on --- .../1-tier-row-auto-computes-from-catalog.md | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 docs/changes/unreleased/1-tier-row-auto-computes-from-catalog.md diff --git a/docs/changes/unreleased/1-tier-row-auto-computes-from-catalog.md b/docs/changes/unreleased/1-tier-row-auto-computes-from-catalog.md new file mode 100644 index 0000000000..6860fbd962 --- /dev/null +++ b/docs/changes/unreleased/1-tier-row-auto-computes-from-catalog.md @@ -0,0 +1,29 @@ +--- +kind: added +title: A tier row that says `auto` computes its model from the catalog +pr: 1 +surface: [chat, engine] +invalidates: [] +--- + +Any of the five tier rows (`models.tiers.*`) may hold the bare word `auto`, +case folded: the seat's model is computed from the catalog's own published +figures through `internal/crewpick` — the three indexes against the three +prices, under each seat's call shape — every time the row is read. The word +stays on disk; the id is derived on every read. + +- `config.AutoValue`, `config.IsAuto` and `config.AutoPick` are the word and + its pure pick; `config.AutoModels` is how the catalog reaches seat + resolution, set once at start-up from the binary's non-blocking read, + never a fetch. Nil is an ordinary state, not an error. +- Two new rungs on the seat ladder: `config.SeatComputed` and + `config.SeatTable`. An auto row resolves on both ladders + (`config.TierSeatAt` and `config.ResolveSeats`) through one seam — to the + computed id, or, when nothing can be computed, to the family's table row + for the preset the stored rows name. It never resolves to `auto` and never + to empty. +- A seat on either new rung names the preset it ran at: `crew frugal, + computed` on a run's receipt. +- Rows that don't say auto are unchanged, and so is everything above the + crew: a flag, `--plan-model` and the environment still outrank an auto + row, and a flag whose text is `auto` is handed on whole. From b4e152217d0801a701a808de19055558dec0fab1 Mon Sep 17 00:00:00 2001 From: santoshkumarradha Date: Thu, 17 Sep 2026 03:05:56 -0400 Subject: [PATCH 021/173] codeaf: the headless doors seat the catalog before they climb the seat ladder A tier row that says `auto` is answered from the rows the process already holds (config.AutoModels), and the catalog that fills them was asked for AFTER the ladder was climbed on every headless door: `do`, `run`, `exec`, `plan new`, `plan revise` and the subharness run all resolved their seats against a nil reader and landed on the family's table row, with the catalog sitting in the cache file beside them. The one memoised catalog is seated first now, through useAutoSeats, which waits for nothing. The change note carries the pull request it lands under. Co-Authored-By: Claude Fable 5.1 --- cmd/codeaf/do.go | 9 +++++++++ cmd/codeaf/exec.go | 1 + cmd/codeaf/main.go | 2 ++ cmd/codeaf/run.go | 1 + cmd/codeaf/subharness.go | 12 ++++++++++++ cmd/codeaf/subharness_run.go | 1 + ...d => 1093-tier-row-auto-computes-from-catalog.md} | 2 +- 7 files changed, 27 insertions(+), 1 deletion(-) rename docs/changes/unreleased/{1-tier-row-auto-computes-from-catalog.md => 1093-tier-row-auto-computes-from-catalog.md} (99%) diff --git a/cmd/codeaf/do.go b/cmd/codeaf/do.go index 9ce388f972..673877fd54 100644 --- a/cmd/codeaf/do.go +++ b/cmd/codeaf/do.go @@ -390,6 +390,15 @@ func doErrand(request doRequest) error { // The two seats, resolved before anything is opened or built, so the run // says which models it is about to use and on whose authority — and says it // even on a run that dies before it reaches a provider. + // + // The catalog is seated under the ladder first, because a tier row may say + // `auto` and that word is answered from the rows this process already holds + // (useAutoSeats). It is read keyless: a run with no key fails further down + // with a sentence about the key, and a seat that fell to the table row on + // the way there would report a model this run never meant to use. + if settings, err := config.LoadKeyless(); err == nil { + useAutoSeats(settings) + } seats := config.ResolveSeats(config.ProfileDir(), request.model, request.planModel) fmt.Fprintln(request.stderr, seats.Report()) outcome, err := errandRun(request, seats, started) diff --git a/cmd/codeaf/exec.go b/cmd/codeaf/exec.go index f6b92cb6a2..57e0765a1a 100644 --- a/cmd/codeaf/exec.go +++ b/cmd/codeaf/exec.go @@ -113,6 +113,7 @@ func runExec(args []string) error { // that took the flag and then resolved it differently from every other door // would be the parity it claims in name only. Only the work seat is printed, // because only the work seat runs anything. + useAutoSeats(settings) seats := config.ResolveSeats(settings.ProfileDir, *model, *planModel) applySeats(&settings, seats) fmt.Fprintln(os.Stderr, seats.Work.Report()) diff --git a/cmd/codeaf/main.go b/cmd/codeaf/main.go index 9e03dd5229..b6e4ccd7d3 100644 --- a/cmd/codeaf/main.go +++ b/cmd/codeaf/main.go @@ -620,6 +620,7 @@ func runPlanNew(name string, args []string) error { if err != nil { return err } + useAutoSeats(settings) seats := config.ResolveSeats(settings.ProfileDir, *model, *planModel) applySeats(&settings, seats) workClient, err := settings.Client() @@ -799,6 +800,7 @@ func runRevise(name string, args []string) error { if err != nil { return err } + useAutoSeats(settings) seats := config.ResolveSeats(settings.ProfileDir, *model, *planModel) applySeats(&settings, seats) workClient, err := settings.Client() diff --git a/cmd/codeaf/run.go b/cmd/codeaf/run.go index e56d7644d8..c119220d4d 100644 --- a/cmd/codeaf/run.go +++ b/cmd/codeaf/run.go @@ -190,6 +190,7 @@ func runGraph(name string, args []string) error { if err != nil { return err } + useAutoSeats(settings) seats := config.ResolveSeats(settings.ProfileDir, *model, *planModel) applySeats(&settings, seats) // A graph may be loaded from disk and expanded again after an overrun, so diff --git a/cmd/codeaf/subharness.go b/cmd/codeaf/subharness.go index 5a247ecc9e..10a8750899 100644 --- a/cmd/codeaf/subharness.go +++ b/cmd/codeaf/subharness.go @@ -385,6 +385,18 @@ var sharedCatalog = func() func(config.Config) *catalog.Catalog { } }() +// useAutoSeats seats this process's catalog under the seat ladder, and is what +// a headless door calls BEFORE it resolves its seats. +// +// The order is the whole of it. A tier row that says `auto` is answered from +// the rows already in hand ([config.AutoModels]), and every headless door +// climbed the ladder before it asked for a catalog at all — so the word read +// against nothing and landed on the family's table row on every run, on a +// machine whose catalog was sitting in its own cache file. It is the same lazy, +// memoised catalog every one of those doors goes on to use; asking for it a few +// lines earlier waits for nothing. +func useAutoSeats(settings config.Config) { sharedCatalog(settings) } + // promisedWorker is the node's own answer to "who runs this", read in the order // admission settled it: the row's worker where there is one, the subtree's // otherwise. It is a pure read — every surface that only wants to *know* asks diff --git a/cmd/codeaf/subharness_run.go b/cmd/codeaf/subharness_run.go index acd4f568f1..201e0a49e6 100644 --- a/cmd/codeaf/subharness_run.go +++ b/cmd/codeaf/subharness_run.go @@ -110,6 +110,7 @@ func runSubharnessCommand(args []string) error { // One seat here — a saved program executes and never plans — climbed on the // same ladder every other headless door climbs, so a profile's crew reaches // this one too (config.ResolveSeats). + useAutoSeats(settings) seats := config.ResolveSeats(settings.ProfileDir, *model, "") applySeats(&settings, seats) fmt.Fprintln(os.Stderr, seats.Work.Report()) diff --git a/docs/changes/unreleased/1-tier-row-auto-computes-from-catalog.md b/docs/changes/unreleased/1093-tier-row-auto-computes-from-catalog.md similarity index 99% rename from docs/changes/unreleased/1-tier-row-auto-computes-from-catalog.md rename to docs/changes/unreleased/1093-tier-row-auto-computes-from-catalog.md index 6860fbd962..9f2d6b87dc 100644 --- a/docs/changes/unreleased/1-tier-row-auto-computes-from-catalog.md +++ b/docs/changes/unreleased/1093-tier-row-auto-computes-from-catalog.md @@ -1,7 +1,7 @@ --- kind: added title: A tier row that says `auto` computes its model from the catalog -pr: 1 +pr: 1093 surface: [chat, engine] invalidates: [] --- From 576d03cc4c2fa7d7e1b2ec28e3a591e19c5862ab Mon Sep 17 00:00:00 2001 From: santoshkumarradha Date: Thu, 17 Sep 2026 02:37:00 -0400 Subject: [PATCH 022/173] session: every usage row names the seat the call ran under A row now carries the tier's word beside the registry's role name, derived at the bank door and never typed at a call site: a turn bills to its agent's own kind, an errand to its role's tier, and a call the registry never seated stays wordless. Co-Authored-By: codeaf --- internal/session/checkpoint.go | 4 +- internal/session/guardian.go | 5 +- internal/session/harness_build.go | 5 +- internal/session/image.go | 2 +- internal/session/loop.go | 2 +- internal/session/orchestrate.go | 2 +- internal/session/route_judge.go | 7 +- internal/session/session.go | 14 +++ internal/session/spellout.go | 5 +- internal/session/task_divide.go | 7 +- internal/session/task_person.go | 2 +- internal/session/task_run.go | 7 ++ internal/session/task_shape.go | 5 +- internal/session/usage_ledger.go | 75 ++++++++++---- internal/session/usage_seat.go | 166 ++++++++++++++++++++++++++++++ 15 files changed, 268 insertions(+), 40 deletions(-) create mode 100644 internal/session/usage_seat.go diff --git a/internal/session/checkpoint.go b/internal/session/checkpoint.go index 88061efc3a..9416c079dd 100644 --- a/internal/session/checkpoint.go +++ b/internal/session/checkpoint.go @@ -1934,7 +1934,7 @@ func (a *Agent) readMark(ctx context.Context) checkpointRead { read.ownerGone = owner.Err() != nil return read } - a.addAuxiliaryUsage(response, reader, 1) + a.addAuxiliaryUsageAs(response, reader, 1, string(roles.RoleMarkReader)) if response.Usage != nil { read.costUSD = costOf(response.Usage) } @@ -3810,7 +3810,7 @@ func (a *Agent) readRemains(ctx context.Context) readerLine { if err != nil || response == nil { return readerLine{unreachable: true} } - a.addAuxiliaryUsage(response, reader, 1) + a.addAuxiliaryUsageAs(response, reader, 1, string(roles.RoleMarkReader)) said := strings.TrimSpace(response.Text()) if declaresNothingLeft(said) { // THE READER SAYING NOTHING IS LEFT IS A FACT, NOT A SILENCE. It used to diff --git a/internal/session/guardian.go b/internal/session/guardian.go index d19619607b..dcc933e357 100644 --- a/internal/session/guardian.go +++ b/internal/session/guardian.go @@ -187,8 +187,9 @@ func (a *Agent) guardianAllows(ctx context.Context, hub *eventHub, call ai.ToolC } // The person pays for it, so it is folded into the session's auxiliary usage // — the same pocket the title and the summary come out of, and for the same - // reason it is not charged to the turn: no turn asked for it. - a.addAuxiliaryUsage(response, judge, 1) + // reason it is not charged to the turn: no turn asked for it. It names + // itself as a gate, so the row is seated by the gate's own tier. + a.addAuxiliaryUsageAs(response, judge, 1, string(roles.RoleGuardian)) if !guardianSaysAllow(response.Text()) { return false diff --git a/internal/session/harness_build.go b/internal/session/harness_build.go index 5339d79d6c..753524ee3b 100644 --- a/internal/session/harness_build.go +++ b/internal/session/harness_build.go @@ -1047,8 +1047,9 @@ func (a *Agent) harnessComplete(ctx context.Context, messages []ai.Message, mode return "", false, empty } // The design is spent on the person's account like every other auxiliary - // call (title.go, guardian.go): it is not a turn, and it is not free. - a.addAuxiliaryUsage(response, model, 1) + // call (title.go, guardian.go): it is not a turn, and it is not free. And + // it names itself a designer, so the row is seated by the designer's tier. + a.addAuxiliaryUsageAs(response, model, 1, string(roles.RoleDesigner)) // THE REPLY IS NOT THE ROOM'S HISTORY, and this is where it stopped being one. // It used to be journaled here verbatim, which meant a design reopened tomorrow // replayed the JSON envelope the designer had written — the same wall of braces diff --git a/internal/session/image.go b/internal/session/image.go index c7f36be81d..67248bc96c 100644 --- a/internal/session/image.go +++ b/internal/session/image.go @@ -493,7 +493,7 @@ func (a *Agent) runVision(ctx context.Context, hub *eventHub, live ai.Message, s provider.WithRole(ctx, lane.RoleTalk), callPurpose(roles.RoleVision), []ai.Message{live}, seer) if err == nil && response != nil { - a.addAuxiliaryUsage(response, seer, 1) + a.addAuxiliaryUsageAs(response, seer, 1, string(roles.RoleVision)) } answer := "" if response != nil { diff --git a/internal/session/loop.go b/internal/session/loop.go index 3a1690bee0..c52876cc84 100644 --- a/internal/session/loop.go +++ b/internal/session/loop.go @@ -3855,7 +3855,7 @@ func (a *Agent) bank(call bankedCall) { } a.mu.Unlock() if call.ledger { - a.recordUsageLine(call.used, call.model, call.role, call.lane, call.reconciled) + a.recordUsageLine(call) } } diff --git a/internal/session/orchestrate.go b/internal/session/orchestrate.go index 7265854809..94c49d9b38 100644 --- a/internal/session/orchestrate.go +++ b/internal/session/orchestrate.go @@ -739,7 +739,7 @@ func (p *orchestratePlanner) ask(ctx context.Context, messages []ai.Message) (st if response == nil { return "", errors.New("the planner answered with nothing") } - p.agent.addAuxiliaryUsage(response, p.call.model, 1) + p.agent.addAuxiliaryUsageAs(response, p.call.model, 1, string(roles.RolePlanner)) p.orch.Charge(orchestrateCost(response, p.call.model)) return response.Text(), nil } diff --git a/internal/session/route_judge.go b/internal/session/route_judge.go index c94e87b599..0732b42b98 100644 --- a/internal/session/route_judge.go +++ b/internal/session/route_judge.go @@ -696,9 +696,10 @@ func (a *Agent) putRouteQuestion(ctx context.Context, role roles.Role, model, br if err != nil || response == nil { return routeVerdict{}, false } - // The person pays for it, out of the pocket every auxiliary call comes from, - // against the model that answered. - a.addAuxiliaryUsage(response, judge, 1) + // The person pays for it, out of the pocket every auxiliary call comes + // from, against the model that answered — and the call names itself on the + // row, so the ledger can seat it by the role's own tier. + a.addAuxiliaryUsageAs(response, judge, 1, string(role)) // The salvage ladder is internal/subharness's, shared rather than reimplemented // so that a fenced or smart-quoted reply is read here exactly as it is read diff --git a/internal/session/session.go b/internal/session/session.go index ca5b86efbb..ec17fff1a4 100644 --- a/internal/session/session.go +++ b/internal/session/session.go @@ -1810,6 +1810,20 @@ type Config struct { // (#941). A worker leaves it empty; the zero value is the node it always was. crewRole roles.Role + // repairRound marks this agent as ONE REPAIR ROUND'S FRESH WORKER + // (task_audit.go's [Agent.repairNode]) rather than the node's own. It + // exists for the usage ledger's seat ([Agent.agentKind]): a round is the + // node's escalation onto the careful tier, and a row that read it as the + // worker's seat would hide the cascade's whole bill inside the ordinary + // work's. It is set by the one builder that takes a model outright + // ([Agent.newTaskAgentOn]) and nowhere else. + // + // IT DOES NOT TOUCH [Config.crewRole] ON PURPOSE. A crew role is what this + // agent's calls are FOR in the router's vocabulary ([Agent.laneRole]), and + // a repair round is a leaf's turns of work in a worktree, not a one-answer + // gate — erranding its lane role would re-price every call it makes. + repairRound bool + // Errand marks this agent as the short exchange behind home's `ask here` // (cmd/codeaf's chatv3_exchange.go) rather than a conversation somebody // sits in. It is a conversation in every other way — a real model, a real diff --git a/internal/session/spellout.go b/internal/session/spellout.go index 02b14c18f3..ba44b978a3 100644 --- a/internal/session/spellout.go +++ b/internal/session/spellout.go @@ -178,8 +178,9 @@ func (a *Agent) SpellOut(ctx context.Context, draft string) string { return "" } // The person pays for it out of the same pocket the session's title, the - // namer and the shaper come out of, and no turn asked for it. - a.addAuxiliaryUsage(response, call.Model, 1) + // namer and the shaper come out of, and no turn asked for it. It names + // itself the spell-out, so the row is seated by the spell-out's tier. + a.addAuxiliaryUsageAs(response, call.Model, 1, string(roles.RoleSpellOut)) return cleanSpellOut(response.Text()) } diff --git a/internal/session/task_divide.go b/internal/session/task_divide.go index 68be2dbec0..5633a350b2 100644 --- a/internal/session/task_divide.go +++ b/internal/session/task_divide.go @@ -1607,9 +1607,10 @@ func (a *Agent) reviewDivision(ctx context.Context, parent *TaskNode, parsed div } return unanswered(why) } - // The person pays for it, out of the pocket every auxiliary call comes from, - // against the model that actually answered. - a.addAuxiliaryUsage(response, reviewer, 1) + // The person pays for it, out of the pocket every auxiliary call comes + // from, against the model that actually answered — and names itself on the + // row for the seat's sake. + a.addAuxiliaryUsageAs(response, reviewer, 1, string(roles.RoleDivision)) raw, err := subharness.Salvage(response.Text()) if err != nil { diff --git a/internal/session/task_person.go b/internal/session/task_person.go index 4799ee8855..43b24e941f 100644 --- a/internal/session/task_person.go +++ b/internal/session/task_person.go @@ -175,7 +175,7 @@ func (a *Agent) judgeDecomposable(ctx context.Context, brief string) (bool, []st if callErr != nil || response == nil { return false, nil, "" } - a.addAuxiliaryUsage(response, judge, 1) + a.addAuxiliaryUsageAs(response, judge, 1, string(roles.RolePlanner)) if verdict, ok := parseTaskJudge(response.Text()); ok { return verdict.Parallel, verdict.Parts, verdict.Why } diff --git a/internal/session/task_run.go b/internal/session/task_run.go index 0e71f27d15..64e5392445 100644 --- a/internal/session/task_run.go +++ b/internal/session/task_run.go @@ -7389,6 +7389,13 @@ func (a *Agent) newTaskAgentOn(ctx context.Context, dir string, node *TaskNode, // node hears about it while it happens: a card that would otherwise show // a task working says it is waiting instead. pacing: node.pacing, + // AND A NAMED MODEL IS THE REPAIR ROUND, which is the one thing this + // builder's only caller with a model says (task_audit.go's + // [Agent.repairNode]). The marker is what seats the round's rows high + // ([Agent.agentKind]); the crew role is deliberately left unset, because + // a round is a leaf's turns of work and erranding its lane role would + // re-price every call in it. + repairRound: on != "", // AND THE NODE'S PULSE TRAVELS THE SAME WAY, for the same reason: the // checker and each repair round are the same NODE working, so a reader // outside the process must see one heartbeat across all of them rather diff --git a/internal/session/task_shape.go b/internal/session/task_shape.go index 8239f6fbe9..ca67e68cd4 100644 --- a/internal/session/task_shape.go +++ b/internal/session/task_shape.go @@ -184,8 +184,9 @@ func (a *Agent) shapeBrief(ctx context.Context, request string) (shapedBrief, bo return shapedBrief{}, false } // The person pays for it out of the same pocket the title and the - // guardian come out of, and no turn asked for it. - a.addAuxiliaryUsage(response, call.Model, 1) + // guardian come out of, and no turn asked for it. The call names itself + // a shaper, so the row is seated by the shaper's own tier. + a.addAuxiliaryUsageAs(response, call.Model, 1, string(roles.RoleShaper)) if shaped, ok := parseShapedBrief(response.Text()); ok { return shaped, true } diff --git a/internal/session/usage_ledger.go b/internal/session/usage_ledger.go index b3a2127f56..ec05ac8234 100644 --- a/internal/session/usage_ledger.go +++ b/internal/session/usage_ledger.go @@ -73,14 +73,17 @@ package session // turn forever. [UsageCache] keeps what it has parsed and reads only what // was appended since ([UsageCache.Read]). // -// WHAT IT DOES NOT HOLD, said plainly, because a page must not imply otherwise: -// the four-way token split (input and output are kept, the cache share is not — -// the journal this line's session id names has it), which of the five router -// slots a call ran under (nothing records that anywhere; `Role` here is -// internal/roles' auxiliary name, and only three auxiliary calls in the whole -// program name themselves at all), and any title for a task or a standing item — -// only their ids, which a page joins against the task index and the standing -// store it is already reading. +// WHAT IT HOLDS AND WHAT IT DOES NOT, said plainly, because a page must not +// imply otherwise. It now holds WHICH SEAT a call ran under — the tier's word +// ([Seat], usage_seat.go), with the registry's own name beside it where the +// call named one ([UsageLine.Role]) — so "did the money go on the seat that +// does the work or the seat that thinks" is answerable from this file alone. It +// still does not hold: the four-way token split (input and output are kept, the +// cache share is not — the journal this line's session id names has it), which +// of the five router slots a call ran under (nothing records that anywhere; the +// seat is the tier's chair and NOT the router's slot), and any title for a task +// or a standing item — only their ids, which a page joins against the task +// index and the standing store it is already reading. import ( "bufio" @@ -99,6 +102,7 @@ import ( "github.com/Agent-Field/agentfield/sdk/go/ai" "github.com/Agent-Field/codeaf/internal/home" "github.com/Agent-Field/codeaf/internal/provider" + "github.com/Agent-Field/codeaf/internal/roles" ) // UsageLedgerName is the file, under the v3 home directory @@ -139,14 +143,30 @@ type UsageLine struct { // provider knows, not a pretty name: a page that wants "opus 4.1" makes that // word itself, from one place, the way every other surface does. Model string `json:"model,omitempty"` - // Role is WHAT the call was for — "title", "taskname", "intake" — on the - // three auxiliary calls in the program that name themselves, and empty on - // every other line including every call a turn makes for itself. It is internal/roles' - // vocabulary and NOT the five router slots (execution, conversation, - // verification, naming, planning): nothing in the program records which slot - // a call ran under, and a page that labelled this column with those words - // would be inventing the join. + // Role is WHAT the call was for — "title", "guardian", "planner" — on + // every call that went through the role registry and named itself at the + // billing door, and empty on the rest, including every call a turn makes + // for itself. It is internal/roles' vocabulary and NOT the five router + // slots (execution, conversation, verification, naming, planning): nothing + // in the program records which slot a call ran under, and a page that + // labelled this column with those words would be inventing the join. Role string `json:"role,omitempty"` + // Seat is WHICH TIER'S MODEL ANSWERED, one word of the closed vocabulary + // [Seat] spells — reflex, low, worker, high, mastermind, talk — derived at + // the bank door ([TagUsage], [SeatOfRole], [SeatOfAgent]) and never typed + // at a call site. It is the fact a spend page wants when it asks whether + // the money went on the seat that does the work or the seat that thinks, + // and it rides beside Role: a call the registry seated carries both, and + // the tier's word stays true of a row whose model id is a fallback, a pin + // or a rescue, because the seat names the chair the call ran in, not the + // id that answered. + // + // IT IS omitempty LIKE EVERY ADDITIVE FIELD BEFORE IT, and a row without + // one is a row nobody could seat: every row written before this field + // existed, an errand that resolved its model outside the registry (a media + // pin, a document reader, a tool ask), a receipt that arrived late. Empty + // reads as "nobody said", which is the truth about it. + Seat Seat `json:"seat,omitempty"` // Calls is how many provider requests this line covers, and it is ONE. The // field is kept rather than dropped because rows written before issue #269 // carry a whole turn's worth on one line — an observed `calls: 41` — and a @@ -800,7 +820,8 @@ func scanUsage(reader io.Reader, since time.Time) ([]UsageLine, int64, error) { // now happens on a writer goroutine ([RecordUsage]), the lock is still released // before the hand-off so no reader of the session's totals ever queues behind // the ledger at all. -func (a *Agent) recordUsageLine(used Usage, model, role string, lane laneFacts, reconciled bool) { +func (a *Agent) recordUsageLine(call bankedCall) { + used := call.used if used.Input == 0 && used.Output == 0 && used.CostUSD == 0 { return } @@ -815,14 +836,13 @@ func (a *Agent) recordUsageLine(used Usage, model, role string, lane laneFacts, line := UsageLine{ At: now, Day: now.Local().Format(usageDayLayout), - Model: strings.TrimSpace(model), - Role: strings.TrimSpace(role), + Model: strings.TrimSpace(call.model), Calls: used.Calls, Input: used.Input, Output: used.Output, USD: used.CostUSD, Empty: used.EmptyReflex > 0, - Reconciled: reconciled, + Reconciled: call.reconciled, Session: session, // The node this agent IS, and nothing for a conversation — the same @@ -837,6 +857,21 @@ func (a *Agent) recordUsageLine(used Usage, model, role string, lane laneFacts, Workspace: strings.TrimSpace(a.config.Workspace), } + // THE ROW'S TWO NAMES GO ON THROUGH ONE DOOR ([TagUsage]), and the seat + // is argued in only where it is known outright: an agent's own turns bill + // to the seat [SeatOfAgent] answers for the kind this agent is — talk on a + // conversation, worker on a node, high on the checker and a repair round — + // and every other row falls back to the role's own tier. That fallback is + // the errand's word when the call went through the registry and nobody's + // word at all when it resolved its model elsewhere (a media pin, a tool + // ask), which is exactly the row the emptiness law wants: as wide as it + // was, and saying "nobody said" where nobody did. + seat := Seat("") + if call.turn { + seat, _ = SeatOfAgent(a.agentKind()) + } + line = TagUsage(line, roles.Role(call.role), seat) + // THE LANE HALF OF THE LINE GOES ON THROUGH ONE DOOR, and it comes in as an // argument rather than being read from anywhere here — because the only // honest source for it is the request this row is about, and by the time a @@ -849,7 +884,7 @@ func (a *Agent) recordUsageLine(used Usage, model, role string, lane laneFacts, // Under the emptiness law an unwatched call therefore writes all five // absent, which is the true sentence "nobody said" rather than a zero // somebody reads as a figure. - RecordUsage(path, usageFromResponse(line, lane.Lane, lane.TTFT, lane.Gen, lane.Output, lane.Hedged, lane.Waste)) + RecordUsage(path, usageFromResponse(line, call.lane.Lane, call.lane.TTFT, call.lane.Gen, call.lane.Output, call.lane.Hedged, call.lane.Waste)) } // usageTaskID spells a node's id the way the task index spells it, and answers diff --git a/internal/session/usage_seat.go b/internal/session/usage_seat.go new file mode 100644 index 0000000000..e5180019d5 --- /dev/null +++ b/internal/session/usage_seat.go @@ -0,0 +1,166 @@ +package session + +// THE SEAT: WHICH TIER'S MODEL ANSWERED, IN ONE WORD. +// +// A model id is an address (usage_ledger.go). The SEAT is the chair the call +// ran in — which class of model the person's settings put there. A spend page +// can already say what a day cost and which model took the money; without a +// seat it cannot say whether the money went on the seat that does the work or +// the seat that thinks. The vocabulary is internal/roles' five tiers — the +// classes the person actually configures — plus `talk`, the one kind of call +// no tier governs: a conversation's own turns. +// +// THE WORDS ARE DERIVED, NEVER TYPED AT A CALL SITE. The registry already +// knows which tier a role sits on ([roles.TierOf]), so a call that went +// through it is seated by one hop, and the only hand-written table here is +// the five tiers to the five words ([SeatOfTier]). A seat is a fact about +// which tier's model answered, not a judgement about the call, and the six +// words are the whole vocabulary: no free text ever reaches the field +// ([TagUsage] drops anything else). + +import ( + "strings" + + "github.com/Agent-Field/codeaf/internal/roles" +) + +// Seat is one word of the closed vocabulary a usage row's seat field carries. +type Seat string + +const ( + // SeatReflex is the cheapest of all: the two calls made every single turn. + SeatReflex Seat = "reflex" + // SeatLow is the cheap, fast one: a title, a caption, the guardian. + SeatLow Seat = "low" + // SeatWorker is the tier that DOES THE WORK: a task node's own turns. + SeatWorker Seat = "worker" + // SeatHigh is the capable, expensive one: the auditor, a repair round. + SeatHigh Seat = "high" + // SeatMastermind is the tier whose one answer shapes all the others: the + // planner, the designer, a division review. + SeatMastermind Seat = "mastermind" + // SeatTalk is a conversation's own turns — the one kind of call no tier + // governs, because the person is sitting in it and its model is theirs to + // change mid-sentence. + SeatTalk Seat = "talk" +) + +// Seats is the whole vocabulary, cheapest first, talk last. Talk closes the +// list rather than opening it because it is not in the tier economy at all: it +// is the word for the turns a conversation makes for itself, and a settings +// surface that drew it as a tier would be drawing a dial that answers nothing. +var Seats = []Seat{SeatReflex, SeatLow, SeatWorker, SeatHigh, SeatMastermind, SeatTalk} + +// SeatValid reports whether a word is one of the six. It compares EXACTLY: no +// trimming, no case folding. A word from outside the set is not a seat and is +// never written to a row — [TagUsage] drops it rather than writing a guess. +func SeatValid(word string) bool { + for _, seat := range Seats { + if string(seat) == word { + return true + } + } + return false +} + +// SeatOfTier maps each of [roles.Tiers] to its own word, and reports false for +// anything else. No tier maps to talk: talk is what a conversation's own turns +// answer under, and no tier governs those. +func SeatOfTier(tier roles.Tier) (Seat, bool) { + switch tier { + case roles.TierReflex: + return SeatReflex, true + case roles.TierLow: + return SeatLow, true + case roles.TierWorker: + return SeatWorker, true + case roles.TierHigh: + return SeatHigh, true + case roles.TierMastermind: + return SeatMastermind, true + } + return "", false +} + +// SeatOfRole is the seat a registered role's calls bill to: [roles.TierOf] +// followed by [SeatOfTier], one hop through the registry rather than a second +// hand-written table. A role the registry never had is nobody's seat — false, +// and the row carries no word rather than a guess. +func SeatOfRole(role roles.Role) (Seat, bool) { + tier, ok := roles.TierOf(role) + if !ok { + return "", false + } + return SeatOfTier(tier) +} + +// AgentKind is what an agent IS, in the one question a seat needs answered: +// whose turns are these? +type AgentKind string + +const ( + // AgentChat is a conversation somebody sits in — and every agent that is + // neither a node, a checker nor a repair round, because that is what they + // all are: a session answering for itself. + AgentChat AgentKind = "chat" + // AgentTask is one task node's runner (task_run.go). + AgentTask AgentKind = "task" + // AgentAudit is the read-only judge a node's landing waits on + // (task_audit.go's [Agent.newAuditAgent]). + AgentAudit AgentKind = "audit" + // AgentRepair is one repair round's fresh worker + // (task_audit.go's [Agent.repairNode]). + AgentRepair AgentKind = "repair" +) + +// SeatOfAgent is the seat an agent's OWN turns bill to: chat is talk, task is +// worker, audit and repair are high. An unknown kind is false, and the row it +// would have carried stays wordless rather than guessed. +func SeatOfAgent(kind AgentKind) (Seat, bool) { + switch kind { + case AgentChat: + return SeatTalk, true + case AgentTask: + return SeatWorker, true + case AgentAudit, AgentRepair: + return SeatHigh, true + } + return "", false +} + +// agentKind is what THIS agent is, from the three facts its config carries: +// the crew role it answers for (the checker, [Config.crewRole]), the repair +// marker its builder set ([Config.repairRound]), the node flag — and, the +// default, none of those: a conversation, or an agent standing in for one. +func (a *Agent) agentKind() AgentKind { + switch { + case a.config.crewRole != "": + return AgentAudit + case a.config.repairRound: + return AgentRepair + case a.config.InTask: + return AgentTask + } + return AgentChat +} + +// TagUsage writes a row's two name fields, and is the ONE door they go +// through. Nothing else on the line is touched. +// +// A non-empty role is written to UsageLine.Role; an empty role leaves whatever +// was there. The seat argument wins when it is one of the six words; a word +// outside the set is dropped rather than written, and the seat then falls back +// to the role's own tier ([SeatOfRole]); if that fails too, whatever seat the +// line already carried stays — which is what keeps a caller that knows nothing +// about a row from overwriting a word somebody else wrote onto it. +func TagUsage(line UsageLine, role roles.Role, seat Seat) UsageLine { + if name := strings.TrimSpace(string(role)); name != "" { + line.Role = name + } + if SeatValid(string(seat)) { + line.Seat = seat + } else if derived, ok := SeatOfRole(role); ok { + line.Seat = derived + } + return line +} From 99811b09754b911fee2dfe3a6a033a942e908a67 Mon Sep 17 00:00:00 2001 From: santoshkumarradha Date: Thu, 17 Sep 2026 02:37:05 -0400 Subject: [PATCH 023/173] session: the seat words and their tagged rows are tested Co-Authored-By: codeaf --- internal/session/usage_ledger_test.go | 2 +- internal/session/usage_seat_test.go | 374 ++++++++++++++++++++++++++ 2 files changed, 375 insertions(+), 1 deletion(-) create mode 100644 internal/session/usage_seat_test.go diff --git a/internal/session/usage_ledger_test.go b/internal/session/usage_ledger_test.go index 918fc3ff85..b7ceaeb8ee 100644 --- a/internal/session/usage_ledger_test.go +++ b/internal/session/usage_ledger_test.go @@ -468,7 +468,7 @@ func TestAConversationLineSpellsNoEmptyIds(t *testing.T) { if err != nil { t.Fatalf("marshal: %v", err) } - for _, absent := range []string{`"task"`, `"standing"`, `"workspace"`, `"role"`} { + for _, absent := range []string{`"task"`, `"standing"`, `"workspace"`, `"role", "seat"`} { if strings.Contains(string(line), absent) { t.Fatalf("an empty %s reached the file: %s", absent, line) } diff --git a/internal/session/usage_seat_test.go b/internal/session/usage_seat_test.go new file mode 100644 index 0000000000..996025e04c --- /dev/null +++ b/internal/session/usage_seat_test.go @@ -0,0 +1,374 @@ +package session + +import ( + "encoding/json" + "os" + "path/filepath" + "strings" + "testing" + "time" + + "github.com/Agent-Field/codeaf/internal/roles" +) + +// ── the vocabulary ────────────────────────────────────────────────────────── + +func TestTheSeatVocabularyIsSixWordsCheapestFirstTalkLast(t *testing.T) { + want := []Seat{SeatReflex, SeatLow, SeatWorker, SeatHigh, SeatMastermind, SeatTalk} + if len(Seats) != len(want) { + t.Fatalf("the vocabulary holds %d words, want the six: %v", len(Seats), Seats) + } + for i, seat := range want { + if Seats[i] != seat { + t.Fatalf("Seats[%d] is %q, want %q — cheapest first, talk last", i, Seats[i], seat) + } + } +} + +func TestSeatValidComparesExactly(t *testing.T) { + for _, seat := range Seats { + if !SeatValid(string(seat)) { + t.Fatalf("SeatValid(%q) is false, want true", seat) + } + } + for _, word := range []string{"", "Talk", "TALK", " talk", "talk ", "worker\n", "work", "leaf", "mastermind "} { + if SeatValid(word) { + t.Fatalf("SeatValid(%q) is true, want false — no trimming, no folding", word) + } + } +} + +func TestSeatOfTierMapsEveryTierAndNothingElse(t *testing.T) { + for _, tier := range roles.Tiers { + seat, ok := SeatOfTier(tier) + if !ok { + t.Fatalf("SeatOfTier(%q) is false — a tier of the registry has no seat word", tier) + } + if !SeatValid(string(seat)) { + t.Fatalf("SeatOfTier(%q) wrote %q, which is not one of the six", tier, seat) + } + } + // Talk is the word for a conversation's own turns, and no tier governs + // those, so no tier answers talk. + for _, tier := range []roles.Tier{"", "talk", "speech", "execution"} { + if _, ok := SeatOfTier(tier); ok { + t.Fatalf("SeatOfTier(%q) answered, want false", tier) + } + } +} + +func TestSeatOfRoleIsOneHopThroughTheRegistry(t *testing.T) { + want := map[roles.Role]Seat{ + roles.RoleWorker: SeatWorker, + roles.RoleAuditor: SeatHigh, + roles.RoleRepair: SeatHigh, + roles.RolePlanner: SeatMastermind, + roles.RoleTitle: SeatLow, + roles.RoleReflex: SeatReflex, + roles.RoleHandoff: SeatMastermind, + roles.RoleGuardian: SeatLow, + roles.RoleCaption: SeatLow, + } + for role, seat := range want { + got, ok := SeatOfRole(role) + if !ok { + t.Fatalf("SeatOfRole(%q) is false — a registered role has no seat", role) + } + if got != seat { + t.Fatalf("SeatOfRole(%q) is %q, want %q", role, got, seat) + } + } + // The media roles are declared and deliberately never registered, so they + // seat nobody: false, and the row carries no word rather than a guess. + for _, role := range []roles.Role{roles.RoleSpeech, roles.RoleVideo, "hand", "nowhere"} { + if seat, ok := SeatOfRole(role); ok { + t.Fatalf("SeatOfRole(%q) answered %q, want false — a role the registry never had", role, seat) + } + } +} + +func TestSeatOfAgentIsTheTurnsOwnSeat(t *testing.T) { + want := map[AgentKind]Seat{ + AgentChat: SeatTalk, + AgentTask: SeatWorker, + AgentAudit: SeatHigh, + AgentRepair: SeatHigh, + } + for kind, seat := range want { + got, ok := SeatOfAgent(kind) + if !ok { + t.Fatalf("SeatOfAgent(%q) is false, want %q", kind, seat) + } + if got != seat { + t.Fatalf("SeatOfAgent(%q) is %q, want %q", kind, got, seat) + } + } + for _, kind := range []AgentKind{"", "crew", "Talk", "subharness"} { + if seat, ok := SeatOfAgent(kind); ok { + t.Fatalf("SeatOfAgent(%q) answered %q, want false — an unknown kind seats nobody", kind, seat) + } + } +} + +func TestAgentKindReadsTheThreeConfigFacts(t *testing.T) { + want := []struct { + name string + more []func(*Config) + kind AgentKind + }{ + {"a conversation", nil, AgentChat}, + {"an errand beside one", []func(*Config){func(config *Config) { config.Errand = true }}, AgentChat}, + {"a task node", []func(*Config){func(config *Config) { config.InTask = true }}, AgentTask}, + {"the checker", []func(*Config){func(config *Config) { config.InTask = true; config.crewRole = roles.RoleAuditor }}, AgentAudit}, + {"a repair round", []func(*Config){func(config *Config) { config.InTask = true; config.repairRound = true }}, AgentRepair}, + } + for _, want := range want { + agent, _ := ledgerAgent(t, filepath.Join(t.TempDir(), UsageLedgerName), want.more...) + if got := agent.agentKind(); got != want.kind { + t.Fatalf("%s: agentKind() is %q, want %q", want.name, got, want.kind) + } + } +} + +// ── TagUsage ──────────────────────────────────────────────────────────────── + +func TestTagUsageIsTheOneDoorForARowsNames(t *testing.T) { + lines := []struct { + name string + line UsageLine + role roles.Role + seat Seat + want func(t *testing.T, got UsageLine) + }{ + { + name: "a clean role and the seat that goes with it", + line: UsageLine{Model: "opus"}, + role: roles.RoleTitle, seat: SeatLow, + want: func(t *testing.T, got UsageLine) { + if got.Role != "title" || got.Seat != SeatLow { + t.Fatalf("the row names %q seated %q, want title/low", got.Role, got.Seat) + } + }, + }, + { + name: "the seat argument wins even against the role's own tier", + line: UsageLine{Model: "opus"}, + role: roles.RolePlanner, seat: SeatWorker, + want: func(t *testing.T, got UsageLine) { + if got.Seat != SeatWorker { + t.Fatalf("the seat is %q, want worker — the argument wins", got.Seat) + } + }, + }, + { + name: "a word outside the set falls back to the role's own tier", + line: UsageLine{Model: "opus"}, + role: roles.RolePlanner, seat: Seat("leaf"), + want: func(t *testing.T, got UsageLine) { + if got.Seat != SeatMastermind { + t.Fatalf("the seat is %q, want mastermind — the role's own tier", got.Seat) + } + }, + }, + { + name: "an empty role leaves whatever was there", + line: UsageLine{Role: "caption", Seat: SeatLow}, + role: "", seat: Seat(""), + want: func(t *testing.T, got UsageLine) { + if got.Role != "caption" || got.Seat != SeatLow { + t.Fatalf("the row names %q seated %q, want caption/low untouched", got.Role, got.Seat) + } + }, + }, + { + name: "an unseatable call keeps the word it already carried", + line: UsageLine{Role: "hand", Seat: SeatHigh}, + role: "hand", seat: Seat("broad"), + want: func(t *testing.T, got UsageLine) { + if got.Role != "hand" || got.Seat != SeatHigh { + t.Fatalf("the row names %q seated %q, want hand/high kept", got.Role, got.Seat) + } + }, + }, + { + name: "nothing else on the line is touched", + line: UsageLine{Model: "opus", USD: 0.02, Input: 900, Output: 40, Lane: "Fast", TTFTms: 640}, + role: roles.RoleGuardian, seat: SeatLow, + want: func(t *testing.T, got UsageLine) { + if got.Model != "opus" || got.USD != 0.02 || got.Input != 900 || got.Output != 40 || got.Lane != "Fast" || got.TTFTms != 640 { + t.Fatalf("the row moved: %+v", got) + } + }, + }, + } + for _, want := range lines { + got := TagUsage(want.line, want.role, want.seat) + want.want(t, got) + } +} + +// ── the rows through the bank door ────────────────────────────────────────── + +// A conversation's own turns answer under talk, the one seat no tier governs. +func TestAConversationTurnsRowNamesItsSeatTalk(t *testing.T) { + ledger := filepath.Join(t.TempDir(), UsageLedgerName) + agent, _ := ledgerAgent(t, ledger) + + var turn Usage + bankCall(agent, &turn, "deepseek/deepseek-v4-flash", 900, 232, 0.31, laneFacts{}) + + row := sealRow(t, ledger) + wantRowFields(t, row, map[string]any{"seat": "talk"}) +} + +// A node's turns bill to the seat that does the work, and an errand the node +// runs beside that work bills to the errand's OWN tier — the seat names who +// answered, not who asked. +func TestATaskNodeSeatsItsTurnsWorkerAndItsErrandsByTheirRole(t *testing.T) { + ledger := filepath.Join(t.TempDir(), UsageLedgerName) + agent, _ := ledgerAgent(t, ledger, func(config *Config) { config.InTask = true }) + + var turn Usage + bankCall(agent, &turn, "deepseek/deepseek-v4-flash", 900, 232, 0.31, laneFacts{}) + agent.addAuxiliaryUsageAs(billedCall("haiku-4.5", 300, 12, 0.002), "haiku-4.5", 1, auxRoleHandoff) + + rows := rawUsageRows(t, ledger) + if len(rows) != 2 { + t.Fatalf("wrote %d rows, want the node's turn and the handoff errand: %v", len(rows), rows) + } + for _, row := range rows { + if row["role"] == auxRoleHandoff { + wantRowFields(t, row, map[string]any{"role": "handoff", "seat": "mastermind"}) + } else { + wantRowFields(t, row, map[string]any{"seat": "worker"}) + } + } +} + +// The checker is the node as far as everybody watching is concerned, and its +// bill is the gate's: high. +func TestTheAuditorsTurnsRowSeatsHigh(t *testing.T) { + ledger := filepath.Join(t.TempDir(), UsageLedgerName) + agent, _ := ledgerAgent(t, ledger, func(config *Config) { + config.InTask = true + config.crewRole = roles.RoleAuditor + }) + + var turn Usage + bankCall(agent, &turn, "deepseek/deepseek-v4-flash", 900, 232, 0.31, laneFacts{}) + + wantRowFields(t, sealRow(t, ledger), map[string]any{"seat": "high"}) +} + +// A repair round's fresh worker escalates onto the careful tier, and its rows +// say so rather than hiding the cascade's bill inside the ordinary work's. +func TestARepairRoundsRowSeatsHigh(t *testing.T) { + ledger := filepath.Join(t.TempDir(), UsageLedgerName) + agent, _ := ledgerAgent(t, ledger, func(config *Config) { + config.InTask = true + config.repairRound = true + }) + + var turn Usage + bankCall(agent, &turn, "deepseek/deepseek-v4-flash", 900, 232, 0.31, laneFacts{}) + + wantRowFields(t, sealRow(t, ledger), map[string]any{"seat": "high"}) +} + +// A named errand carries its registry name AND the tier's word that name sits +// on, whatever agent ran it. +func TestANamedErrandRowCarriesItsRoleAndItsTierSeat(t *testing.T) { + ledger := filepath.Join(t.TempDir(), UsageLedgerName) + agent, _ := ledgerAgent(t, ledger) + + agent.addAuxiliaryUsageAs(billedCall("haiku-4.5", 300, 12, 0.002), "haiku-4.5", 1, auxRoleTitle) + + rows := rawUsageRows(t, ledger) + if len(rows) != 1 { + t.Fatalf("wrote %d rows, want the one named errand: %v", len(rows), rows) + } + wantRowFields(t, rows[0], map[string]any{"role": "title", "seat": "low"}) +} + +// An errand whose word the registry never had carries the word and no seat: +// the word is still true, the tier is not. +func TestAnUnregisteredErrandWordCarriesItsRoleButNoSeat(t *testing.T) { + ledger := filepath.Join(t.TempDir(), UsageLedgerName) + agent, _ := ledgerAgent(t, ledger) + + agent.addAuxiliaryUsageAs(billedCall("haiku-4.5", 300, 12, 0.002), "haiku-4.5", 1, auxRoleHand) + + rows := rawUsageRows(t, ledger) + if len(rows) != 1 { + t.Fatalf("wrote %d rows, want the one worded errand: %v", len(rows), rows) + } + row := rows[0] + wantRowFields(t, row, map[string]any{"role": "hand"}) + wantRowSilentAbout(t, row, "seat") +} + +// An errand that resolved its model outside the registry — a media pin, a +// document reader, a tool ask — names nothing and seats nobody: the row is +// exactly as wide as it was. +func TestAnUnnamedErrandRowStaysSeatless(t *testing.T) { + ledger := filepath.Join(t.TempDir(), UsageLedgerName) + agent, _ := ledgerAgent(t, ledger) + + agent.addAuxiliaryUsage(billedCall("vendor/pin", 10, 900, 0.04), "vendor/pin", 1) + + row := sealRow(t, ledger) + wantRowSilentAbout(t, row, "role", "seat") +} + +// A receipt that arrived after the call names no role of its own, so it seats +// nobody either: the figures are true, the word was never in them. +func TestAReconciledReceiptStaysSeatless(t *testing.T) { + ledger := filepath.Join(t.TempDir(), UsageLedgerName) + agent, _ := ledgerAgent(t, ledger) + + agent.bank(bankedCall{ + used: Usage{Calls: 1, Input: 100, Output: 10, CostUSD: 0.02}, + model: "deepseek/deepseek-v4-flash", lane: laneFacts{}, + ledger: true, late: true, reconciled: true, + }) + + row := sealRow(t, ledger) + wantRowFields(t, row, map[string]any{"reconciled": true}) + wantRowSilentAbout(t, row, "role", "seat") +} + +// EVERY ROW ALREADY IN THE FILE STILL DECODES: an old row reads back with an +// empty seat and an empty role, which is the truth about it. +func TestAnOldRowStillDecodesWithoutASeat(t *testing.T) { + ledger := filepath.Join(t.TempDir(), UsageLedgerName) + old := `{"at":"2026-01-05T09:00:00Z","day":"2026-01-05","model":"opus","calls":1,"in":10,"out":2,"usd":0.01}` + if err := os.WriteFile(ledger, []byte(old+"\n"), 0o600); err != nil { + t.Fatalf("write the old ledger: %v", err) + } + lines, err := ReadUsage(ledger, time.Time{}) + if err != nil || len(lines) != 1 { + t.Fatalf("read %d lines, %v", len(lines), err) + } + if lines[0].Seat != "" || lines[0].Role != "" || lines[0].Model != "opus" { + t.Fatalf("the old row read back as %+v, want seatless and roleless", lines[0]) + } +} + +// The field is spelled `seat`, and a row nobody could seat is exactly as wide +// as it was. +func TestTheSeatIsSpelledSeatAndOmittedWhenUnset(t *testing.T) { + seated, err := json.Marshal(UsageLine{At: time.Now(), Day: "2026-09-17", Model: "m", Calls: 1, Input: 5, USD: 0.01, Session: "abc", Seat: SeatWorker}) + if err != nil { + t.Fatalf("marshal: %v", err) + } + if !strings.Contains(string(seated), `"seat":"worker"`) { + t.Fatalf("a seated row carries no seat word: %s", seated) + } + wide, err := json.Marshal(UsageLine{At: time.Now(), Day: "2026-09-17", Model: "m", Calls: 1, Input: 5, USD: 0.01, Session: "abc"}) + if err != nil { + t.Fatalf("marshal: %v", err) + } + if strings.Contains(string(wide), `"seat"`) { + t.Fatalf("an unseated row grew a seat key: %s", wide) + } +} From f25f12317bdbb173ed2075b240aa8a88336d7791 Mon Sep 17 00:00:00 2001 From: codeaf Date: Thu, 17 Sep 2026 02:14:59 -0400 Subject: [PATCH 024/173] outbox: keep measurement rows in an append-only file and deliver them in batches Co-Authored-By: codeaf --- internal/pool/outbox/outbox.go | 515 +++++++++++++++++++++++++++++++++ 1 file changed, 515 insertions(+) create mode 100644 internal/pool/outbox/outbox.go diff --git a/internal/pool/outbox/outbox.go b/internal/pool/outbox/outbox.go new file mode 100644 index 0000000000..38f9ad19c1 --- /dev/null +++ b/internal/pool/outbox/outbox.go @@ -0,0 +1,515 @@ +// Package outbox keeps small measurement rows in a local append-only file and +// hands them to a destination: an HTTP endpoint that takes newline-delimited +// JSON, or another file. One line is one row, so the rows survive a restart +// and Pending() is answered by reading the file back. Nothing here gets in the +// caller's way — a full disk, a dead server, a destination that is nonsense +// all surface as error values — and a row whose batch arrived is marked in the +// file, so it is never transmitted twice. +// +// The file holds rows and, interleaved with them, one-line sent and dropped +// markers that retire rows; a line that is not a row is skipped on read. That +// is what keeps the file append-only while a cap on pending rows still drops +// from the old end and a mark outlives closing the outbox and opening the same +// path again. +package outbox + +import ( + "bytes" + "context" + cryptorand "crypto/rand" + "encoding/hex" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "os" + "path/filepath" + "strings" + "sync" + "time" +) + +// DefaultMaxPending is the cap on pending rows when Outbox.MaxPending is zero +// or below. +const DefaultMaxPending = 5000 + +const ( + rowSchema = 1 + nonceLen = 16 + maxPayload = 64 << 10 // 64 KiB, measured on the compacted payload + maxBatch = 200 // rows per POST + // defaultBudget bounds one Send when Outbox.Budget is zero. + defaultBudget = 2 * time.Second + dirMode = 0700 + fileMode = 0600 +) + +// Row is one measurement as it is stored in the outbox file, one row per line. +type Row struct { + Schema int `json:"schema"` // always 1 + Day string `json:"day"` // "YYYY-MM-DD", UTC + Nonce string `json:"nonce"` // 16 random bytes, lowercase hex + Payload json.RawMessage `json:"payload"` +} + +// mark retires rows: a retired row is no longer pending, either because its +// batch arrived or because the pending cap dropped it. A marker is a line of +// its own in the same file, which is what keeps the outbox append-only. +type mark struct { + Sent string `json:"sent,omitempty"` + Dropped string `json:"dropped,omitempty"` +} + +// Outbox keeps rows in one append-only file until they are sent. The six +// exported fields are read every time they are used, so a caller may set them +// after Open and before the first Append or Send. +type Outbox struct { + // Now answers the time a row was appended; nil means time.Now. + Now func() time.Time + // Rand is read for nonce bytes; nil means crypto/rand.Reader. + Rand io.Reader + // Keep decides, at Send time, which payloads are transmitted; nil keeps + // every row. A payload Keep declines leaves the outbox without reaching + // the destination. + Keep func(payload json.RawMessage) bool + // Budget bounds one Send call as a whole; zero means 2s. + Budget time.Duration + // Client sends the batches; nil means a client the package makes. + Client *http.Client + // MaxPending caps the pending rows, dropping from the old end; zero or + // below means DefaultMaxPending. + MaxPending int + + path string + sendMu sync.Mutex // one Send at a time, so two never carry the same rows + mu sync.Mutex // guards f and pend + f *os.File + pend []string // nonces of the pending rows, oldest first +} + +// defaultClient is shared by every outbox that does not name its own. Its +// requests are bounded by the Send budget, so no client-level timeout. +var defaultClient = &http.Client{} + +// Open prepares the outbox file at path, creating the parent directory with +// mode 0700 if it is missing and the file itself with mode 0600. A path that +// already holds rows keeps them, markers included. An empty path is an error. +func Open(path string) (*Outbox, error) { + if path == "" { + return nil, errors.New("outbox: empty path") + } + if err := os.MkdirAll(filepath.Dir(path), dirMode); err != nil { + return nil, err + } + f, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_APPEND, fileMode) + if err != nil { + return nil, err + } + o := &Outbox{path: path, f: f} + o.mu.Lock() + defer o.mu.Unlock() + rows := o.load() + o.pend = make([]string, 0, len(rows)) + for _, r := range rows { + o.pend = append(o.pend, r.Nonce) + } + return o, nil +} + +// Append writes one row holding payload. The row is refused, and nothing is +// written, when the payload is not valid JSON, when it is longer than 64 KiB +// once compacted, or when it still holds a newline once compacted. The payload +// is stored compacted, so one row is always one line. +func (o *Outbox) Append(payload json.RawMessage) error { + compacted, err := compact(payload) + if err != nil { + return err + } + if len(compacted) > maxPayload { + return errors.New("outbox: payload is longer than 64 KiB once compacted") + } + if bytes.IndexByte(compacted, '\n') >= 0 { + return errors.New("outbox: payload holds a newline once compacted") + } + nonce := make([]byte, nonceLen) + rand := o.Rand + if rand == nil { + rand = cryptorand.Reader + } + if _, err := io.ReadFull(rand, nonce); err != nil { + return fmt.Errorf("outbox: read nonce: %w", err) + } + now := o.Now + var t time.Time + if now == nil { + t = time.Now() + } else { + t = now() + } + row := Row{ + Schema: rowSchema, + Day: t.UTC().Format("2006-01-02"), + Nonce: hex.EncodeToString(nonce), + Payload: json.RawMessage(compacted), + } + line, err := rowLine(row) + if err != nil { + return err + } + + o.mu.Lock() + defer o.mu.Unlock() + if o.f == nil { + return errors.New("outbox: outbox is closed") + } + if _, err := o.f.Write(line); err != nil { + return err + } + o.pend = append(o.pend, row.Nonce) + o.trimLocked() + return nil +} + +// Pending answers the rows that have not been sent, in the order they were +// appended. It never fails: a line that does not parse, or that is not a row, +// is skipped, and a file that cannot be read answers as no rows at all. +func (o *Outbox) Pending() []Row { + o.mu.Lock() + defer o.mu.Unlock() + return o.load() +} + +// Send hands the pending rows to dest and answers with how many rows it +// transmitted. A dest beginning https:// or http:// receives one POST per +// batch of at most 200 rows, content-type application/x-ndjson, the body one +// row per line in pending order, each line the same JSON object the file +// holds; a 2xx answer means that batch arrived. Any other dest is a +// filesystem path, plain or written file://, that the rows are appended to, +// created with mode 0600 and a parent directory of mode 0700 if either is +// missing. An empty dest, or a Send with nothing pending, is a no-op: 0 and a +// nil error, the rows staying pending. +// +// The whole call lives inside Budget, which is 2s when it is zero, and inside +// ctx as well, whichever ends first. Rows whose batch arrived are marked sent +// and leave the outbox; a row Keep declines is marked sent without being +// transmitted and is not counted. Rows whose batch did not arrive, and every +// row after it, stay pending, and Send returns an error that says what +// failed. +func (o *Outbox) Send(ctx context.Context, dest string) (int, error) { + if dest == "" { + return 0, nil + } + o.sendMu.Lock() + defer o.sendMu.Unlock() + + o.mu.Lock() + if o.f == nil { + o.mu.Unlock() + return 0, errors.New("outbox: outbox is closed") + } + rows := o.load() + pend := make([]string, len(rows)) + for i, r := range rows { + pend[i] = r.Nonce + } + o.pend = pend + o.mu.Unlock() + + if len(rows) == 0 { + return 0, nil + } + + budget := o.Budget + if budget <= 0 { + budget = defaultBudget + } + ctx, cancel := context.WithDeadline(ctx, time.Now().Add(budget)) + defer cancel() + + // Rows the caller declines leave the outbox at once, whether or not any + // later batch arrives. + keep := o.Keep + var deliver []Row + if keep == nil { + deliver = rows + } else { + deliver = make([]Row, 0, len(rows)) + var declined []string + for _, r := range rows { + if keep(r.Payload) { + deliver = append(deliver, r) + } else { + declined = append(declined, r.Nonce) + } + } + if len(declined) > 0 { + o.retire(sentMarks(declined)) + } + } + if len(deliver) == 0 { + return 0, nil + } + + if strings.HasPrefix(dest, "https://") || strings.HasPrefix(dest, "http://") { + return o.sendHTTP(ctx, dest, deliver) + } + return o.sendFile(ctx, strings.TrimPrefix(dest, "file://"), deliver) +} + +// Close releases whatever the outbox holds open. Calling it more than once is +// not an error. A closed outbox refuses Append and Send; Pending still +// answers by reading the file. +func (o *Outbox) Close() error { + o.mu.Lock() + defer o.mu.Unlock() + if o.f == nil { + return nil + } + err := o.f.Close() + o.f = nil + return err +} + +// trimLocked drops the oldest pending rows until the cap holds, writing a +// dropped marker for each so they stay gone across a reopen. The row just +// appended is the last one, so it always survives. The caller holds mu. +func (o *Outbox) trimLocked() { + limit := o.MaxPending + if limit <= 0 { + limit = DefaultMaxPending + } + drop := len(o.pend) - limit + if drop <= 0 { + return + } + dropped := make([]string, drop) + copy(dropped, o.pend[:drop]) + o.writeMarksLocked(droppedMarks(dropped)) + o.pend = append(o.pend[:0], o.pend[drop:]...) +} + +// retire appends one marker per nonce and takes those rows out of the pending +// set. A marker write is best effort: a failed write leaves the rows pending, +// where the next Send tries them again rather than losing them. +func (o *Outbox) retire(marks []mark) { + gone := make(map[string]bool, len(marks)) + for _, m := range marks { + if m.Sent != "" { + gone[m.Sent] = true + } + if m.Dropped != "" { + gone[m.Dropped] = true + } + } + o.mu.Lock() + defer o.mu.Unlock() + o.writeMarksLocked(marks) + o.dropPendingLocked(gone) +} + +// writeMarksLocked appends one marker line per mark. The caller holds mu. +func (o *Outbox) writeMarksLocked(marks []mark) { + if o.f == nil || len(marks) == 0 { + return + } + buf := make([]byte, 0, 24*len(marks)) + for _, m := range marks { + line, err := json.Marshal(m) + if err != nil { + continue // a struct of strings never fails to marshal + } + buf = append(buf, line...) + buf = append(buf, '\n') + } + o.f.Write(buf) // best effort; see retire +} + +// dropPendingLocked removes the named nonces from the pending list. The +// caller holds mu. +func (o *Outbox) dropPendingLocked(gone map[string]bool) { + kept := o.pend[:0] + for _, n := range o.pend { + if !gone[n] { + kept = append(kept, n) + } + } + o.pend = kept +} + +// load reads the outbox file and answers the rows still pending, in file +// order. A line that does not parse, or that is not a row, is skipped, and a +// file that cannot be read answers as no rows at all. A row whose nonce a +// sent or dropped marker retires is left out, wherever the marker sits. The +// caller holds mu. +func (o *Outbox) load() []Row { + b, err := os.ReadFile(o.path) + if err != nil { + return nil + } + var rows []Row + retired := make(map[string]bool) + for len(b) > 0 { + line := b + if i := bytes.IndexByte(b, '\n'); i >= 0 { + line, b = b[:i], b[i+1:] + } else { + b = nil + } + if len(line) == 0 { + continue + } + var m mark + if json.Unmarshal(line, &m) == nil { + if m.Sent != "" { + retired[m.Sent] = true + continue + } + if m.Dropped != "" { + retired[m.Dropped] = true + continue + } + } + var r Row + if json.Unmarshal(line, &r) != nil || r.Nonce == "" { + continue + } + rows = append(rows, r) + } + kept := rows[:0] + for _, r := range rows { + if !retired[r.Nonce] { + kept = append(kept, r) + } + } + return kept +} + +// sendHTTP posts the rows to dest, at most maxBatch per POST, and marks each +// batch that came back 2xx. The first batch that fails ends the call; its +// rows and every row after it stay pending. +func (o *Outbox) sendHTTP(ctx context.Context, dest string, rows []Row) (int, error) { + client := o.Client + if client == nil { + client = defaultClient + } + n := 0 + for start := 0; start < len(rows); start += maxBatch { + end := start + maxBatch + if end > len(rows) { + end = len(rows) + } + if err := ctx.Err(); err != nil { + return n, err + } + body := make([]byte, 0, (end-start)*128) + for _, r := range rows[start:end] { + line, err := rowLine(r) + if err != nil { + return n, err + } + body = append(body, line...) + } + req, err := http.NewRequestWithContext(ctx, http.MethodPost, dest, bytes.NewReader(body)) + if err != nil { + return n, err + } + req.Header.Set("Content-Type", "application/x-ndjson") + resp, err := client.Do(req) + if err != nil { + return n, err + } + ok := resp.StatusCode >= 200 && resp.StatusCode < 300 + io.CopyN(io.Discard, resp.Body, 1<<16) + resp.Body.Close() + if !ok { + return n, fmt.Errorf("outbox: %s replied %s", dest, resp.Status) + } + o.retire(sentMarks(rowNonces(rows[start:end]))) + n += end - start + } + return n, nil +} + +// sendFile appends the rows to the file at path, creating it with mode 0600 +// and its parent directory with mode 0700 if either is missing. +func (o *Outbox) sendFile(ctx context.Context, path string, rows []Row) (int, error) { + if err := ctx.Err(); err != nil { + return 0, err + } + if err := os.MkdirAll(filepath.Dir(path), dirMode); err != nil { + return 0, err + } + f, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_APPEND, fileMode) + if err != nil { + return 0, err + } + defer f.Close() + body := make([]byte, 0, len(rows)*128) + for _, r := range rows { + line, err := rowLine(r) + if err != nil { + return 0, err + } + body = append(body, line...) + } + if _, err := f.Write(body); err != nil { + return 0, err + } + o.retire(sentMarks(rowNonces(rows))) + return len(rows), nil +} + +// compact checks that payload is valid JSON and answers it compacted, so a +// row is always one line. +func compact(payload []byte) ([]byte, error) { + if len(payload) == 0 { + return nil, errors.New("outbox: payload is not valid JSON") + } + var buf bytes.Buffer + if err := json.Compact(&buf, payload); err != nil { + return nil, fmt.Errorf("outbox: payload is not valid JSON: %w", err) + } + return buf.Bytes(), nil +} + +// rowLine answers the JSON object for one row as it is stored in the file, +// newline included. HTML escaping is off so a payload keeps the bytes compaction +// gave it, and every later read of the same row answers the same bytes. +func rowLine(row Row) ([]byte, error) { + var buf bytes.Buffer + enc := json.NewEncoder(&buf) + enc.SetEscapeHTML(false) + if err := enc.Encode(row); err != nil { + return nil, err + } + return buf.Bytes(), nil +} + +func rowNonces(rows []Row) []string { + nonces := make([]string, len(rows)) + for i, r := range rows { + nonces[i] = r.Nonce + } + return nonces +} + +func sentMarks(nonces []string) []mark { + return nonceMarks(nonces, true) +} + +func droppedMarks(nonces []string) []mark { + return nonceMarks(nonces, false) +} + +func nonceMarks(nonces []string, sent bool) []mark { + marks := make([]mark, len(nonces)) + for i, n := range nonces { + if sent { + marks[i] = mark{Sent: n} + } else { + marks[i] = mark{Dropped: n} + } + } + return marks +} From 3faea5cd14eda95759c8be2efa4a334334dedbe7 Mon Sep 17 00:00:00 2001 From: codeaf Date: Thu, 17 Sep 2026 02:35:11 -0400 Subject: [PATCH 025/173] outbox: test the file format, the cap, the batch sends and the delivery guarantees Co-Authored-By: codeaf --- internal/pool/outbox/outbox_test.go | 1015 +++++++++++++++++++++++++++ 1 file changed, 1015 insertions(+) create mode 100644 internal/pool/outbox/outbox_test.go diff --git a/internal/pool/outbox/outbox_test.go b/internal/pool/outbox/outbox_test.go new file mode 100644 index 0000000000..e77a63913e --- /dev/null +++ b/internal/pool/outbox/outbox_test.go @@ -0,0 +1,1015 @@ +package outbox + +import ( + "bytes" + "context" + "encoding/binary" + "encoding/hex" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "net/http/httptest" + "os" + "path/filepath" + "strings" + "sync" + "testing" + "time" +) + +// fixedClock answers a clock standing on the named UTC day. +func fixedClock(day string) func() time.Time { + t, err := time.Parse("2006-01-02", day) + if err != nil { + panic(err) + } + return func() time.Time { return t } +} + +// stepRand answers 16 fresh bytes per read, the first byte carrying step and +// the rest a counter, so every nonce a test appends is its own. +type stepRand struct { + step byte + n uint64 +} + +func (r *stepRand) Read(p []byte) (int, error) { + if len(p) < 16 { + return 0, io.ErrShortBuffer + } + p[0] = r.step + binary.LittleEndian.PutUint64(p[1:9], r.n) + copy(p[9:16], "outbox+") + r.n++ + return 16, nil +} + +// shortRand answers fewer bytes than a nonce needs. +type shortRand struct{ n int } + +func (r *shortRand) Read(p []byte) (int, error) { + if r.n == 0 { + return 0, io.EOF + } + n := len(p) + if n > r.n { + n = r.n + } + for i := 0; i < n; i++ { + p[i] = 0xaa + } + r.n -= n + return n, nil +} + +type errReader struct{} + +func (errReader) Read([]byte) (int, error) { return 0, errors.New("no bytes today") } + +func openOutbox(t *testing.T) (*Outbox, string) { + t.Helper() + path := filepath.Join(t.TempDir(), "outbox.jsonl") + o, err := Open(path) + if err != nil { + t.Fatalf("Open: %v", err) + } + t.Cleanup(func() { o.Close() }) + return o, path +} + +func appendRow(t *testing.T, o *Outbox, payload string) { + t.Helper() + if err := o.Append(json.RawMessage(payload)); err != nil { + t.Fatalf("Append(%s): %v", payload, err) + } +} + +func readAll(t *testing.T, path string) []byte { + t.Helper() + b, err := os.ReadFile(path) + if err != nil { + t.Fatalf("read %s: %v", path, err) + } + return b +} + +func fileLines(t *testing.T, path string) []string { + t.Helper() + text := strings.TrimSuffix(string(readAll(t, path)), "\n") + if text == "" { + return nil + } + return strings.Split(text, "\n") +} + +func countMarks(t *testing.T, path, kind string) int { + t.Helper() + return strings.Count(string(readAll(t, path)), `{"`+kind+`":"`) +} + +func pendingNonces(t *testing.T, o *Outbox) []string { + t.Helper() + rows := o.Pending() + nonces := make([]string, len(rows)) + for i, r := range rows { + nonces[i] = r.Nonce + } + return nonces +} + +func mustEqual(t *testing.T, what string, got, want []byte) { + t.Helper() + if !bytes.Equal(got, want) { + t.Fatalf("%s: got %q, want %q", what, got, want) + } +} + +// postRecord answers every POST with 200 and records the body and the +// content-type under its own lock. A seconds field sleeps first; a status +// list answers its entries in request order before falling back to 200. +type postRecord struct { + mu sync.Mutex + bodies []string + types []string + seconds int + status []int +} + +func (p *postRecord) handle(w http.ResponseWriter, r *http.Request) { + b, _ := io.ReadAll(r.Body) + p.mu.Lock() + p.bodies = append(p.bodies, string(b)) + p.types = append(p.types, r.Header.Get("Content-Type")) + p.mu.Unlock() + if p.seconds > 0 { + time.Sleep(time.Duration(p.seconds) * time.Millisecond) + } + if len(p.status) > 0 { + s := p.status[0] + p.status = p.status[1:] + w.WriteHeader(s) + return + } + w.WriteHeader(200) +} + +func TestOpenRefusesAnEmptyPath(t *testing.T) { + if _, err := Open(""); err == nil { + t.Fatal(`Open("") answered no error`) + } +} + +func TestAFreshRunLeavesMode0600OnTheFileAnd0700OnTheDirectory(t *testing.T) { + path := filepath.Join(t.TempDir(), "missing", "nested", "outbox.jsonl") + if _, err := Open(path); err != nil { + t.Fatalf("Open: %v", err) + } + fi, err := os.Stat(path) + if err != nil { + t.Fatalf("stat file: %v", err) + } + if fi.Mode().Perm() != 0600 { + t.Fatalf("file mode is %v, want 0600", fi.Mode().Perm()) + } + di, err := os.Stat(filepath.Dir(path)) + if err != nil { + t.Fatalf("stat dir: %v", err) + } + if di.Mode().Perm() != 0700 { + t.Fatalf("directory mode is %v, want 0700", di.Mode().Perm()) + } +} + +func TestOpenKeepsExistingRowsAndSkipsLinesThatAreNotRows(t *testing.T) { + path := filepath.Join(t.TempDir(), "outbox.jsonl") + nonce := func(n int) string { return fmt.Sprintf("%032x", n) } + row1 := `{"schema":1,"day":"2026-09-16","nonce":"` + nonce(1) + `","payload":{"k":1}}` + row2 := `{"schema":1,"day":"2026-09-16","nonce":"` + nonce(2) + `","payload":{"k":2}}` + row3 := `{"schema":1,"day":"2026-09-17","nonce":"` + nonce(3) + `","payload":{"k":3}}` + row4 := `{"schema":1,"day":"2026-09-17","nonce":"` + nonce(4) + `","payload":{"k":4}}` + file := strings.Join([]string{ + row1, + "not json at all", + "null", + "[]", + `{"x":1}`, + row2, + `{"sent":"` + nonce(1) + `"}`, + `{"dropped":"` + nonce(3) + `"}`, + row3, // a marker may sit before the row it retires + row4, // unterminated last line + }, "\n") + if err := os.WriteFile(path, []byte(file), 0600); err != nil { + t.Fatal(err) + } + o, err := Open(path) + if err != nil { + t.Fatalf("Open: %v", err) + } + defer o.Close() + want := []string{nonce(2), nonce(4)} + got := pendingNonces(t, o) + if len(got) != len(want) { + t.Fatalf("Pending answered %d rows, want %d", len(got), len(want)) + } + for i := range want { + if got[i] != want[i] { + t.Fatalf("Pending[%d] nonce is %s, want %s", i, got[i], want[i]) + } + } +} + +func TestReopenKeepsPendingRowsInTheOrderTheyWereAppended(t *testing.T) { + path := filepath.Join(t.TempDir(), "outbox.jsonl") + o, err := Open(path) + if err != nil { + t.Fatalf("Open: %v", err) + } + o.Rand = &stepRand{step: 1} + appendRow(t, o, `{"k":1}`) + appendRow(t, o, `{"k":2}`) + if err := o.Close(); err != nil { + t.Fatalf("Close: %v", err) + } + o2, err := Open(path) + if err != nil { + t.Fatalf("reopen: %v", err) + } + defer o2.Close() + got := pendingNonces(t, o2) + if len(got) != 2 || got[0] == got[1] { + t.Fatalf("reopen answered %v, want the two rows in order", got) + } + first := got[0] + appendRow(t, o2, `{"k":3}`) + got = pendingNonces(t, o2) + if len(got) != 3 || got[0] != first { + t.Fatalf("the third append disturbed the order: %v", got) + } +} + +func TestAppendWritesExactlyTheExpectedLine(t *testing.T) { + o, path := openOutbox(t) + o.Now = fixedClock("2026-09-17") + o.Rand = bytes.NewReader([]byte{0x0f, 0x1e, 0x2d, 0x3c, 0x4b, 0x5a, 0x69, 0x78, 0x87, 0x96, 0xa5, 0xb4, 0xc3, 0xd2, 0xe1, 0xf0}) + appendRow(t, o, `{"k":1}`) + want := `{"schema":1,"day":"2026-09-17","nonce":"0f1e2d3c4b5a69788796a5b4c3d2e1f0","payload":{"k":1}}` + "\n" + mustEqual(t, "file", readAll(t, path), []byte(want)) +} + +func TestAppendWritesTheUTCDayWhenTheClockSitsInAnotherZone(t *testing.T) { + o, path := openOutbox(t) + zone := time.FixedZone("test+0530", 5*3600+1800) + local := time.Date(2026, 9, 18, 2, 0, 0, 0, zone) // UTC: 2026-09-17 20:30 + o.Now = func() time.Time { return local } + o.Rand = &stepRand{step: 1} + appendRow(t, o, `{"k":1}`) + lines := fileLines(t, path) + if !strings.Contains(lines[0], `"day":"2026-09-17"`) { + t.Fatalf("row day is not the UTC day: %s", lines[0]) + } +} + +func TestAppendStoresThePayloadCompactedOnOneLine(t *testing.T) { + o, path := openOutbox(t) + o.Rand = &stepRand{step: 2} + appendRow(t, o, "{\n \"k\": 1,\n \"j\": [ 1 , 2 ]\n}") + lines := fileLines(t, path) + if len(lines) != 1 { + t.Fatalf("file holds %d lines, want 1", len(lines)) + } + if !strings.Contains(lines[0], `"payload":{"k":1,"j":[1,2]}`) { + t.Fatalf("payload was not stored compacted: %s", lines[0]) + } + rows := o.Pending() + if len(rows) != 1 || string(rows[0].Payload) != `{"k":1,"j":[1,2]}` { + t.Fatalf("Pending answered payload %v", rows) + } +} + +func TestAppendsBelowTheCapWriteOneLineEach(t *testing.T) { + o, path := openOutbox(t) + o.Rand = &stepRand{step: 3} + for i := 0; i < 10; i++ { + appendRow(t, o, fmt.Sprintf(`{"k":%d}`, i)) + } + if lines := fileLines(t, path); len(lines) != 10 { + t.Fatalf("file holds %d lines, want 10 and no marker lines", len(lines)) + } +} + +func TestAppendRefusesPayloadsItCannotStoreAndWritesNothing(t *testing.T) { + cases := []struct { + name string + payload string + }{ + {"empty", ``}, + {"open brace", `{`}, + {"bare word", `truthy`}, + {"two values", `{"a":1} {"b":2}`}, + {"truncated array", `[1,2`}, + {"newline inside the value", "[\"a\n\"]"}, + } + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + o, path := openOutbox(t) + o.Rand = &stepRand{step: 4} + appendRow(t, o, `{"k":1}`) + before := readAll(t, path) + err := o.Append(json.RawMessage(c.payload)) + if err == nil { + t.Fatalf("Append accepted %q", c.payload) + } + mustEqual(t, "file", readAll(t, path), before) + if got := len(o.Pending()); got != 1 { + t.Fatalf("Pending answers %d rows after a refused append, want 1", got) + } + }) + } +} + +func TestAppendAppliesThePayloadCapOnceCompacted(t *testing.T) { + o, path := openOutbox(t) + o.Rand = &stepRand{step: 5} + // {"k":"aaa…"} compacts to a payload of len 8 + n. + filler := strings.Repeat("a", 65528) + if err := o.Append(json.RawMessage(`{"k":"` + filler + `"}`)); err != nil { + t.Fatalf("a payload of exactly 64 KiB once compacted was refused: %v", err) + } + if err := o.Append(json.RawMessage(`{"k":"` + filler + `a"}`)); err == nil { + t.Fatal("a payload one byte over 64 KiB once compacted was kept") + } + if lines := fileLines(t, path); len(lines) != 1 { + t.Fatalf("a refused append left %d lines, want the file untouched at 1", len(lines)) + } +} + +func TestAppendRefusesWhenTheNonceCannotBeRead(t *testing.T) { + cases := []struct { + name string + rand io.Reader + }{ + {"short read", &shortRand{n: 15}}, + {"failing reader", errReader{}}, + } + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + o, path := openOutbox(t) + appendRow(t, o, `{"k":1}`) // crypto/rand answers this one + before := readAll(t, path) + o.Rand = c.rand + if err := o.Append(json.RawMessage(`{"k":2}`)); err == nil { + t.Fatal("Append answered no error for an unreadable nonce") + } + mustEqual(t, "file", readAll(t, path), before) + }) + } +} + +func TestAppendSurvivesManyGoroutinesWithoutLosingOrMixingRows(t *testing.T) { + o, path := openOutbox(t) + const goroutines, per = 32, 25 + var wg sync.WaitGroup + for g := 0; g < goroutines; g++ { + wg.Add(1) + go func(g int) { + defer wg.Done() + for i := 0; i < per; i++ { + if err := o.Append(json.RawMessage(fmt.Sprintf(`{"g":%d,"i":%d}`, g, i))); err != nil { + t.Errorf("Append: %v", err) + return + } + } + }(g) + } + wg.Wait() + lines := fileLines(t, path) + if len(lines) != goroutines*per { + t.Fatalf("file holds %d lines, want %d", len(lines), goroutines*per) + } + seen := make(map[string]bool, len(lines)) + for _, line := range lines { + var r Row + if err := json.Unmarshal([]byte(line), &r); err != nil || r.Nonce == "" { + t.Fatalf("line does not parse on its own: %q (%v)", line, err) + } + seen[r.Nonce] = true + } + if len(seen) != len(lines) { + t.Fatalf("file holds %d distinct nonces across %d rows", len(seen), len(lines)) + } + if got := len(o.Pending()); got != len(lines) { + t.Fatalf("Pending answers %d rows, want %d", got, len(lines)) + } +} + +func TestPendingAnswersNothingForAFileItCannotRead(t *testing.T) { + path := filepath.Join(t.TempDir(), "outbox.jsonl") + o, err := Open(path) + if err != nil { + t.Fatalf("Open: %v", err) + } + defer o.Close() + o.Rand = &stepRand{step: 6} + appendRow(t, o, `{"k":1}`) + if got := len(o.Pending()); got != 1 { + t.Fatalf("Pending answers %d rows while the file is readable, want 1", got) + } + // A file that is gone, and one that turned into a directory, both read as + // no rows, and neither fails. + if err := os.Remove(path); err != nil { + t.Fatalf("remove: %v", err) + } + if got := len(o.Pending()); got != 0 { + t.Fatalf("Pending answered %d rows for a missing file, want 0", got) + } + if err := os.Mkdir(path, 0700); err != nil { + t.Fatalf("mkdir: %v", err) + } + if got := len(o.Pending()); got != 0 { + t.Fatalf("Pending answered %d rows for an unreadable file, want 0", got) + } +} + +func TestTheCapDropsPendingRowsFromTheOldEnd(t *testing.T) { + o, path := openOutbox(t) + o.MaxPending = 3 + o.Rand = &stepRand{step: 7} + nonces := make([]string, 0, 5) + for i := 0; i < 5; i++ { + appendRow(t, o, fmt.Sprintf(`{"k":%d}`, i)) + rows := o.Pending() + nonces = append(nonces, rows[len(rows)-1].Nonce) + } + got := pendingNonces(t, o) + if len(got) != 3 || got[0] != nonces[2] || got[2] != nonces[4] { + t.Fatalf("Pending answers the wrong end: got %v, want %v..%v", got, nonces[2], nonces[4]) + } + if n := countMarks(t, path, "dropped"); n != 2 { + t.Fatalf("file holds %d dropped markers, want 2", n) + } + // The dropped rows stay gone across a reopen. + if err := o.Close(); err != nil { + t.Fatalf("Close: %v", err) + } + o2, err := Open(path) + if err != nil { + t.Fatalf("reopen: %v", err) + } + defer o2.Close() + after := pendingNonces(t, o2) + if len(after) != 3 || after[0] != got[0] { + t.Fatalf("a reopen resurrected dropped rows: %v", after) + } +} + +func TestTheCapDefaultsTo5000AndAppliesToNegativeValues(t *testing.T) { + if DefaultMaxPending != 5000 { + t.Fatalf("DefaultMaxPending is %d, want 5000", DefaultMaxPending) + } + for _, max := range []int{0, -1} { + t.Run(fmt.Sprintf("MaxPending=%d", max), func(t *testing.T) { + o, path := openOutbox(t) + o.Rand = &stepRand{step: 7} + o.MaxPending = max + for i := 0; i < DefaultMaxPending+1; i++ { + if err := o.Append(json.RawMessage(fmt.Sprintf(`{"k":%d}`, i))); err != nil { + t.Fatalf("Append: %v", err) + } + } + if got := len(o.Pending()); got != DefaultMaxPending { + t.Fatalf("Pending answers %d rows, want %d", got, DefaultMaxPending) + } + if n := countMarks(t, path, "dropped"); n != 1 { + t.Fatalf("file holds %d dropped markers, want 1", n) + } + got := pendingNonces(t, o) + if got[0] == got[len(got)-1] { + t.Fatal("the cap did not drop from the old end") + } + }) + } +} + +func TestTheRowJustAppendedSurvivesACapOfOne(t *testing.T) { + o, _ := openOutbox(t) + o.MaxPending = 1 + o.Rand = &stepRand{step: 7} + appendRow(t, o, `{"k":1}`) + appendRow(t, o, `{"k":2}`) + got := o.Pending() + if len(got) != 1 || string(got[0].Payload) != `{"k":2}` { + t.Fatalf("Pending answers %v, want just the last row", got) + } +} + +func TestSendWithAnEmptyDestIsANoop(t *testing.T) { + o, _ := openOutbox(t) + o.Rand = &stepRand{step: 8} + appendRow(t, o, `{"k":1}`) + appendRow(t, o, `{"k":2}`) + n, err := o.Send(context.Background(), "") + if n != 0 || err != nil { + t.Fatalf(`Send("") answered (%d, %v), want (0, nil)`, n, err) + } + if got := len(o.Pending()); got != 2 { + t.Fatalf("Pending answers %d rows after an empty dest, want 2", got) + } +} + +func TestSendWithNothingPendingMakesNoRequest(t *testing.T) { + rec := &postRecord{} + srv := httptest.NewServer(http.HandlerFunc(rec.handle)) + defer srv.Close() + o, _ := openOutbox(t) + n, err := o.Send(context.Background(), srv.URL) + if n != 0 || err != nil { + t.Fatalf("Send answered (%d, %v), want (0, nil)", n, err) + } + if len(rec.bodies) != 0 { + t.Fatalf("a Send with nothing pending made %d requests", len(rec.bodies)) + } +} + +func TestSendPostsBatchesOfAtMost200Rows(t *testing.T) { + rec := &postRecord{} + srv := httptest.NewServer(http.HandlerFunc(rec.handle)) + defer srv.Close() + o, path := openOutbox(t) + o.Rand = &stepRand{step: 9} + for i := 0; i < 450; i++ { + appendRow(t, o, fmt.Sprintf(`{"k":%d}`, i)) + } + rows := fileLines(t, path)[:450] // the rows, before any marker lines + n, err := o.Send(context.Background(), srv.URL) + if n != 450 || err != nil { + t.Fatalf("Send answered (%d, %v), want (450, nil)", n, err) + } + if len(rec.bodies) != 3 { + t.Fatalf("Send made %d posts, want 3", len(rec.bodies)) + } + wantLines := []int{200, 200, 50} + for i, want := range wantLines { + body := rec.bodies[i] + if got := strings.Count(body, "\n"); got != want { + t.Fatalf("post %d carries %d lines, want %d", i, got, want) + } + if !strings.HasSuffix(body, "\n") { + t.Fatalf("post %d does not end in a newline", i) + } + if rec.types[i] != "application/x-ndjson" { + t.Fatalf("post %d content-type is %q", i, rec.types[i]) + } + lo := i * 200 + hi := min(lo+200, 450) + mustEqual(t, fmt.Sprintf("post %d body", i), []byte(body), []byte(strings.Join(rows[lo:hi], "\n")+"\n")) + } + if got := o.Pending(); len(got) != 0 { + t.Fatalf("Pending answers %d rows after a full send, want 0", len(got)) + } + if err := o.Close(); err != nil { + t.Fatalf("Close: %v", err) + } + o2, err := Open(path) + if err != nil { + t.Fatalf("reopen: %v", err) + } + defer o2.Close() + if got := pendingNonces(t, o2); len(got) != 0 { + t.Fatalf("the sent marks did not survive a reopen: %d rows pending", len(got)) + } +} + +func TestSendReachesAnHTTPSDestination(t *testing.T) { + rec := &postRecord{} + srv := httptest.NewTLSServer(http.HandlerFunc(rec.handle)) + defer srv.Close() + o, _ := openOutbox(t) + o.Client = srv.Client() + o.Rand = &stepRand{step: 10} + appendRow(t, o, `{"k":1}`) + dest := strings.Replace(srv.URL, "http://", "https://", 1) + n, err := o.Send(context.Background(), dest) + if n != 1 || err != nil { + t.Fatalf("Send(%s) answered (%d, %v), want (1, nil)", dest, n, err) + } + if len(rec.bodies) != 1 { + t.Fatal("the https post never arrived") + } +} + +func TestSendKeepsUnarrivedRowsAndNeverSendsAnArrivedRowTwice(t *testing.T) { + seen := make(map[string]int) + var mu sync.Mutex + note := func(body string) { + mu.Lock() + defer mu.Unlock() + for _, line := range strings.Split(strings.TrimSuffix(body, "\n"), "\n") { + var r Row + if json.Unmarshal([]byte(line), &r) == nil && r.Nonce != "" { + seen[r.Nonce]++ + } + } + } + fail := &postRecord{status: []int{200, 500}} + srv1 := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + b, _ := io.ReadAll(r.Body) + note(string(b)) + fail.handle(w, r) + })) + defer srv1.Close() + o, path := openOutbox(t) + o.Rand = &stepRand{step: 11} + for i := 0; i < 250; i++ { + appendRow(t, o, fmt.Sprintf(`{"k":%d}`, i)) + } + n, err := o.Send(context.Background(), srv1.URL) + if n != 200 || err == nil { + t.Fatalf("Send answered (%d, %v), want (200, an error)", n, err) + } + if cn := countMarks(t, path, "sent"); cn != 200 { + t.Fatalf("file holds %d sent markers after the first batch arrived, want 200", cn) + } + remaining := pendingNonces(t, o) + if len(remaining) != 50 { + t.Fatalf("%d rows stayed pending, want 50", len(remaining)) + } + ok := &postRecord{} + srv2 := httptest.NewServer(http.HandlerFunc(ok.handle)) + defer srv2.Close() + n, err = o.Send(context.Background(), srv2.URL) + if n != 50 || err != nil { + t.Fatalf("the resume answered (%d, %v), want (50, nil)", n, err) + } + for _, body := range ok.bodies { + note(body) + } + mu.Lock() + defer mu.Unlock() + if len(seen) != 250 { + t.Fatalf("%d distinct rows were sent, want 250", len(seen)) + } + for nonce, count := range seen { + // The 200 rows whose first batch answered 2xx were transmitted once + // and never again; the 50 rows the 500 refused were transmitted + // twice — once refused, once accepted — and never a third time. + want := 1 + for _, n2 := range remaining { + if n2 == nonce { + want = 2 + break + } + } + if count != want { + t.Fatalf("nonce %s… was sent %d times, want %d", nonce[:8], count, want) + } + } + if len(remaining) != 50 { + t.Fatal("the pending list was consumed by the check") + } +} + +func TestSendSaysWhatFailedAndLeavesTheRowsPending(t *testing.T) { + rec := &postRecord{status: []int{404}} + srv := httptest.NewServer(http.HandlerFunc(rec.handle)) + defer srv.Close() + o, _ := openOutbox(t) + o.Rand = &stepRand{step: 12} + appendRow(t, o, `{"k":1}`) + n, err := o.Send(context.Background(), srv.URL) + if n != 0 || err == nil { + t.Fatalf("Send answered (%d, %v), want (0, an error)", n, err) + } + if !strings.Contains(err.Error(), "404") { + t.Fatalf("the error does not say what failed: %v", err) + } + if got := len(o.Pending()); got != 1 { + t.Fatalf("%d rows stayed pending after the refusal, want 1", got) + } +} + +func TestSendAnswersARefusedConnection(t *testing.T) { + o, _ := openOutbox(t) + o.Rand = &stepRand{step: 12} + appendRow(t, o, `{"k":1}`) + n, err := o.Send(context.Background(), "http://127.0.0.1:1/") + if n != 0 || err == nil { + t.Fatalf("Send answered (%d, %v), want (0, an error)", n, err) + } + if got := len(o.Pending()); got != 1 { + t.Fatalf("%d rows stayed pending, want 1", got) + } +} + +func TestSendLivesInsideTheBudget(t *testing.T) { + rec := &postRecord{seconds: 900} + srv := httptest.NewServer(http.HandlerFunc(rec.handle)) + defer srv.Close() + o, _ := openOutbox(t) + o.Rand = &stepRand{step: 13} + o.Budget = 150 * time.Millisecond + for i := 0; i < 5; i++ { + appendRow(t, o, fmt.Sprintf(`{"k":%d}`, i)) + } + n, err := o.Send(context.Background(), srv.URL) + if n != 0 || err == nil { + t.Fatalf("Send answered (%d, %v), want (0, an error)", n, err) + } + if got := len(o.Pending()); got != 5 { + t.Fatalf("%d rows stayed pending, want 5", got) + } +} + +func TestSendLivesInsideTheContextWhenItEndsFirst(t *testing.T) { + rec := &postRecord{seconds: 900} + srv := httptest.NewServer(http.HandlerFunc(rec.handle)) + defer srv.Close() + o, _ := openOutbox(t) + o.Rand = &stepRand{step: 14} + o.Budget = time.Minute + o.Rand = &stepRand{step: 14} + appendRow(t, o, `{"k":1}`) + ctx, cancel := context.WithTimeout(context.Background(), 150*time.Millisecond) + defer cancel() + if _, err := o.Send(ctx, srv.URL); err == nil { + t.Fatal("Send outlived its context") + } + if got := len(o.Pending()); got != 1 { + t.Fatalf("%d rows stayed pending, want 1", got) + } +} + +func TestSendWithACanceledContextMakesNoRequest(t *testing.T) { + rec := &postRecord{} + srv := httptest.NewServer(http.HandlerFunc(rec.handle)) + defer srv.Close() + o, _ := openOutbox(t) + o.Rand = &stepRand{step: 15} + appendRow(t, o, `{"k":1}`) + ctx, cancel := context.WithCancel(context.Background()) + cancel() + n, err := o.Send(ctx, srv.URL) + if n != 0 || err == nil { + t.Fatalf("Send answered (%d, %v), want (0, an error)", n, err) + } + if len(rec.bodies) != 0 { + t.Fatal("a canceled context still made a request") + } +} + +func TestSendAppendsTheRowsToFileDestination(t *testing.T) { + dest := filepath.Join(t.TempDir(), "missing", "sub", "out.ndjson") + o, path := openOutbox(t) + o.Rand = &stepRand{step: 16} + for i := 0; i < 3; i++ { + appendRow(t, o, fmt.Sprintf(`{"k":%d}`, i)) + } + rows := fileLines(t, path) + n, err := o.Send(context.Background(), dest) + if n != 3 || err != nil { + t.Fatalf("Send answered (%d, %v), want (3, nil)", n, err) + } + sent := fileLines(t, dest) + if len(sent) != 3 { + t.Fatalf("destination holds %d lines, want 3", len(sent)) + } + for i := range sent { + if sent[i] != rows[i] { + t.Fatalf("destination line %d is %q, want the row %q", i, sent[i], rows[i]) + } + } + fi, err := os.Stat(dest) + if err != nil { + t.Fatalf("stat dest: %v", err) + } + if fi.Mode().Perm() != 0600 { + t.Fatalf("destination mode is %v, want 0600", fi.Mode().Perm()) + } + di, err := os.Stat(filepath.Dir(dest)) + if err != nil { + t.Fatalf("stat dest dir: %v", err) + } + if di.Mode().Perm() != 0700 { + t.Fatalf("destination directory mode is %v, want 0700", di.Mode().Perm()) + } + if got := len(o.Pending()); got != 0 { + t.Fatalf("%d rows stayed pending after a file send, want 0", got) + } + if err := o.Close(); err != nil { + t.Fatalf("Close: %v", err) + } + o2, err := Open(path) + if err != nil { + t.Fatalf("reopen: %v", err) + } + defer o2.Close() + if got := pendingNonces(t, o2); len(got) != 0 { + t.Fatalf("a reopen still answers %d rows, want none", len(got)) + } +} + +func TestSendAcceptsTheFileScheme(t *testing.T) { + dest := filepath.Join(t.TempDir(), "out.ndjson") + o, _ := openOutbox(t) + o.Rand = &stepRand{step: 17} + appendRow(t, o, `{"k":1}`) + n, err := o.Send(context.Background(), "file://"+dest) + if n != 1 || err != nil { + t.Fatalf("Send answered (%d, %v), want (1, nil)", n, err) + } + if lines := fileLines(t, dest); len(lines) != 1 { + t.Fatalf("destination holds %d lines, want 1", len(lines)) + } +} + +func TestSendAppendsToFileAcrossSends(t *testing.T) { + dest := filepath.Join(t.TempDir(), "out.ndjson") + o, _ := openOutbox(t) + o.Rand = &stepRand{step: 17} + appendRow(t, o, `{"k":1}`) + appendRow(t, o, `{"k":2}`) + if n, err := o.Send(context.Background(), dest); n != 2 || err != nil { + t.Fatalf("first send answered (%d, %v)", n, err) + } + appendRow(t, o, `{"k":3}`) + if n, err := o.Send(context.Background(), dest); n != 1 || err != nil { + t.Fatalf("second send answered (%d, %v)", n, err) + } + if lines := fileLines(t, dest); len(lines) != 3 || !strings.Contains(lines[2], `"k":3`) { + t.Fatalf("destination holds %v", lines) + } +} + +func TestKeepLeavesDeclinedRowsUnsentAndGone(t *testing.T) { + rec := &postRecord{} + srv := httptest.NewServer(http.HandlerFunc(rec.handle)) + defer srv.Close() + o, path := openOutbox(t) + o.Rand = &stepRand{step: 18} + asked := 0 + o.Keep = func(payload json.RawMessage) bool { + asked++ + s := string(payload) + return !strings.Contains(s, `"k":2`) && !strings.Contains(s, `"k":4`) + } + for i := 1; i <= 4; i++ { + appendRow(t, o, fmt.Sprintf(`{"k":%d}`, i)) + } + n, err := o.Send(context.Background(), srv.URL) + if n != 2 || err != nil { + t.Fatalf("Send answered (%d, %v), want (2, nil)", n, err) + } + if asked != 4 { + t.Fatalf("Keep was asked %d times, want 4", asked) + } + body := strings.Join(rec.bodies, "") + if strings.Contains(body, `"k":2`) || strings.Contains(body, `"k":4`) { + t.Fatal("a declined payload reached the destination") + } + if got := len(o.Pending()); got != 0 { + t.Fatalf("%d rows stayed pending, want 0 — a declined row leaves the outbox", got) + } + if err := o.Close(); err != nil { + t.Fatalf("Close: %v", err) + } + o2, err := Open(path) + if err != nil { + t.Fatalf("reopen: %v", err) + } + defer o2.Close() + if got := pendingNonces(t, o2); len(got) != 0 { + t.Fatalf("a declined row came back after a reopen: %d rows", len(got)) + } +} + +func TestKeepDecliningEverythingSendsNothing(t *testing.T) { + rec := &postRecord{} + srv := httptest.NewServer(http.HandlerFunc(rec.handle)) + defer srv.Close() + o, _ := openOutbox(t) + o.Rand = &stepRand{step: 19} + o.Keep = func(json.RawMessage) bool { return false } + appendRow(t, o, `{"k":1}`) + n, err := o.Send(context.Background(), srv.URL) + if n != 0 || err != nil { + t.Fatalf("Send answered (%d, %v), want (0, nil)", n, err) + } + if len(rec.bodies) != 0 { + t.Fatal("a Send that declines everything still made a request") + } + if got := len(o.Pending()); got != 0 { + t.Fatalf("%d rows stayed pending, want 0", got) + } +} + +func TestRowsAppendedAfterASendArePendingLikeAnyOther(t *testing.T) { + dest := filepath.Join(t.TempDir(), "out.ndjson") + o, _ := openOutbox(t) + o.Rand = &stepRand{step: 19} + appendRow(t, o, `{"k":1}`) + if n, err := o.Send(context.Background(), dest); n != 1 || err != nil { + t.Fatalf("first send answered (%d, %v)", n, err) + } + appendRow(t, o, `{"k":2}`) + got := o.Pending() + if len(got) != 1 || string(got[0].Payload) != `{"k":2}` { + t.Fatalf("Pending answers %v, want the fresh row", got) + } +} + +func TestSendRefusesNonsenseDestinationsWithoutPanicking(t *testing.T) { + cases := []string{ + "http://", + "https://", + "http://[::1", + "file://", + "\x00", + "file://\x00", + } + for _, dest := range cases { + t.Run(fmt.Sprintf("%q", dest), func(t *testing.T) { + o, _ := openOutbox(t) + o.Rand = &stepRand{step: 20} + appendRow(t, o, `{"k":1}`) + n, err := o.Send(context.Background(), dest) + if err == nil || n != 0 { + t.Fatalf("Send(%q) answered (%d, %v), want (0, an error)", dest, n, err) + } + if got := len(o.Pending()); got != 1 { + t.Fatalf("%d rows stayed pending after the refusal, want 1", got) + } + }) + } +} + +func TestCloseIsIdempotentAndAClosedOutboxRefusesWork(t *testing.T) { + dest := filepath.Join(t.TempDir(), "out.ndjson") + o, _ := openOutbox(t) + o.Rand = &stepRand{step: 21} + appendRow(t, o, `{"k":1}`) + appendRow(t, o, `{"k":2}`) + if err := o.Close(); err != nil { + t.Fatalf("first Close: %v", err) + } + if err := o.Close(); err != nil { + t.Fatalf("second Close: %v", err) + } + if err := o.Append(json.RawMessage(`{"k":3}`)); err == nil { + t.Fatal("a closed outbox accepted an append") + } + if n, err := o.Send(context.Background(), dest); err == nil || n != 0 { + t.Fatalf("a closed outbox sent (%d, %v)", n, err) + } + if got := len(o.Pending()); got != 2 { + t.Fatalf("Pending answers %d rows after Close, want 2", got) + } +} + +func TestFieldsSetAfterOpenAreUsed(t *testing.T) { + o, path := openOutbox(t) + o.Now = fixedClock("2026-09-17") + o.Rand = &stepRand{step: 21} + o.MaxPending = 2 + for i := 0; i < 4; i++ { + appendRow(t, o, fmt.Sprintf(`{"k":%d}`, i)) + } + got := o.Pending() + if len(got) != 2 { + t.Fatalf("the cap set after Open was ignored: %d rows pending", len(got)) + } + lines := fileLines(t, path) + if !strings.Contains(lines[0], `"day":"2026-09-17"`) { + t.Fatalf("the clock set after Open was ignored: %s", lines[0]) + } + if !strings.Contains(lines[0], `"nonce":"15`) { // stepRand answers the step as its first byte + t.Fatalf("the nonce reader set after Open was ignored: %s", lines[0]) + } +} + +func TestAppendWithTheDefaultClockAndRand(t *testing.T) { + o, _ := openOutbox(t) + before := time.Now().UTC().Format("2006-01-02") + for i := 0; i < 20; i++ { + appendRow(t, o, fmt.Sprintf(`{"k":%d}`, i)) + } + after := time.Now().UTC().Format("2006-01-02") + for i, r := range o.Pending() { + if len(r.Nonce) != 32 { + t.Fatalf("row %d nonce is %d characters, want 32", i, len(r.Nonce)) + } + if _, err := hex.DecodeString(r.Nonce); err != nil { + t.Fatalf("row %d nonce is not lowercase hex: %v", i, err) + } + if r.Nonce != strings.ToLower(r.Nonce) { + t.Fatalf("row %d nonce is not lowercase", i) + } + if r.Day != before && r.Day != after { + t.Fatalf("row %d day is %s, want the UTC day (%s or %s)", i, r.Day, before, after) + } + if r.Schema != 1 { + t.Fatalf("row %d schema is %d, want 1", i, r.Schema) + } + } +} From e9fe0ecc509bef044262ef409ec8099f35a4d627 Mon Sep 17 00:00:00 2001 From: santoshkumarradha Date: Thu, 17 Sep 2026 02:07:48 -0400 Subject: [PATCH 026/173] poolcfg: resolve pool behaviour from a stored setting and a handed-in environment Co-Authored-By: codeaf --- internal/pool/poolcfg/poolcfg.go | 235 +++++++++++++++++++++++++++++++ 1 file changed, 235 insertions(+) create mode 100644 internal/pool/poolcfg/poolcfg.go diff --git a/internal/pool/poolcfg/poolcfg.go b/internal/pool/poolcfg/poolcfg.go new file mode 100644 index 0000000000..a770cd0f1b --- /dev/null +++ b/internal/pool/poolcfg/poolcfg.go @@ -0,0 +1,235 @@ +// Package poolcfg resolves how the measurement pool behaves from one stored +// setting and an environment handed in as a function. Nothing here touches the +// disk, the network, the clock or the process environment, so Resolve is a +// table of inputs and outputs and every case is reachable from a test. +package poolcfg + +import ( + "fmt" + "net/url" + "strings" + "time" +) + +// Addresses held when the environment offers nothing readable. +const ( + DefaultIndexURL = "https://pool.invalid/index.json" + DefaultSubmitURL = "https://pool.invalid/submit" +) + +// Mode says what the pool may do: send and read, only read, or neither. +type Mode int + +const ( + On Mode = iota + Read + Off +) + +// String spells the mode the way the setting and the environment spell it. +func (m Mode) String() string { + switch m { + case On: + return "on" + case Read: + return "read" + case Off: + return "off" + default: + return fmt.Sprintf("Mode(%d)", int(m)) + } +} + +// Sources records where each resolved value came from. Every field holds one +// of four words: "default", "setting", "env" or "ci". +type Sources struct { + Mode string + IndexURL string + SubmitURL string + TTL string +} + +// Config is the resolved pool configuration. Source names the origin of each +// field, and every field is resolved on its own: the mode does not change how +// the URLs or the TTL are read, and an unreadable URL does not disturb the +// TTL. +type Config struct { + Mode Mode + IndexURL string + SubmitURL string + TTL time.Duration + Source Sources +} + +// The five names Resolve reads, and no others. +const ( + envMode = "CODEAF_MODEL_POOL" + envCI = "CI" + envIndex = "CODEAF_MODEL_POOL_URL" + envSubmit = "CODEAF_MODEL_POOL_SUBMIT_URL" + envTTL = "CODEAF_MODEL_POOL_TTL" +) + +// Where a resolved value came from. +const ( + srcDefault = "default" + srcSetting = "setting" + srcEnv = "env" + srcCI = "ci" +) + +// TTL bounds, and the value held when no readable TTL is set. +const ( + ttlFloor = time.Minute + ttlCeiling = 7 * 24 * time.Hour + defaultTTL = 24 * time.Hour +) + +// Resolve resolves the pool configuration from the stored setting and the +// environment behind lookup, which answers with a value and whether the name +// was set at all — a name that is set and empty is not the same as a name +// nobody set. A nil lookup is an environment in which nothing is set. +// +// Every value is read with the space around it trimmed, and words are matched +// without regard to case. Resolve is pure: the same inputs give the same +// Config, it keeps nothing between calls, and many goroutines may call it at +// once. +func Resolve(setting string, lookup func(name string) (value string, set bool)) Config { + // One injection for the whole environment: the five names are read once, + // here, in this order, and each value is trimmed as it is read. + get := func(name string) (string, bool) { + if lookup == nil { + return "", false + } + value, set := lookup(name) + if !set { + return "", false + } + return strings.TrimSpace(value), true + } + modeWord, _ := get(envMode) + ciWord, _ := get(envCI) + indexWord, _ := get(envIndex) + submitWord, submitSet := get(envSubmit) + ttlWord, _ := get(envTTL) + + // The mode is the first of these that answers, and that answer is its + // source: the environment, the stored setting, CI, then the default. A + // word neither the environment nor the setting knows is simply not an + // answer and the next line gets its turn. + mode, modeSrc := On, srcDefault + if m, ok := parseMode(modeWord); ok { + mode, modeSrc = m, srcEnv + } else if m, ok := parseMode(strings.TrimSpace(setting)); ok { + mode, modeSrc = m, srcSetting + } else if ciSaysYes(ciWord) { + mode, modeSrc = Read, srcCI + } + + indexURL, indexSrc := DefaultIndexURL, srcDefault + if u, ok := acceptURL(indexWord); ok { + indexURL, indexSrc = u, srcEnv + } + + // An empty submit address means send nowhere, and that is an answer; a + // name nobody set, or one holding neither an address nor emptiness, is + // the default. + submitURL, submitSrc := DefaultSubmitURL, srcDefault + if submitSet { + if submitWord == "" { + submitURL, submitSrc = "", srcEnv + } else if u, ok := acceptURL(submitWord); ok { + submitURL, submitSrc = u, srcEnv + } + } + + // A readable TTL is held between the floor and the ceiling; a name nobody + // set, or one no duration can be read from, is the default. + ttl, ttlSrc := defaultTTL, srcDefault + if d, err := time.ParseDuration(ttlWord); err == nil { + ttl, ttlSrc = clampTTL(d), srcEnv + } + + return Config{ + Mode: mode, + IndexURL: indexURL, + SubmitURL: submitURL, + TTL: ttl, + Source: Sources{ + Mode: modeSrc, + IndexURL: indexSrc, + SubmitURL: submitSrc, + TTL: ttlSrc, + }, + } +} + +// CanSend reports whether the pool may send measurements: only when the mode +// is on and there is a submit address to send to. +func (c Config) CanSend() bool { + return c.Mode == On && c.SubmitURL != "" +} + +// CanRead reports whether the pool may read measurements: whenever the mode +// is not off. +func (c Config) CanRead() bool { + return c.Mode != Off +} + +// parseMode reads one of the three mode words, without regard to case. +// Anything else is not an answer. +func parseMode(word string) (Mode, bool) { + switch strings.ToLower(word) { + case "on": + return On, true + case "read": + return Read, true + case "off": + return Off, true + } + return On, false +} + +// ciSaysYes reads the CI word: one of true, 1 or yes, without regard to case. +func ciSaysYes(word string) bool { + switch strings.ToLower(word) { + case "true", "1", "yes": + return true + } + return false +} + +// acceptURL reads one of the four shapes a pool address may take, and nothing +// else: an "https://" URL, an "http://" URL whose host is localhost or +// 127.0.0.1 with or without a port, a "file://" URL, or an absolute filesystem +// path, which is one beginning "/". Plain http anywhere else is not accepted. +func acceptURL(value string) (string, bool) { + if strings.HasPrefix(value, "/") { + return value, true + } + lower := strings.ToLower(value) + switch { + case strings.HasPrefix(lower, "https://"), strings.HasPrefix(lower, "file://"): + _, err := url.Parse(value) + return value, err == nil + case strings.HasPrefix(lower, "http://"): + u, err := url.Parse(value) + if err != nil { + return "", false + } + host := u.Hostname() + return value, strings.EqualFold(host, "localhost") || host == "127.0.0.1" + } + return "", false +} + +// clampTTL holds d between the TTL floor and ceiling. +func clampTTL(d time.Duration) time.Duration { + if d < ttlFloor { + return ttlFloor + } + if d > ttlCeiling { + return ttlCeiling + } + return d +} From 188e513b9d063db44a835724d03551e83880001b Mon Sep 17 00:00:00 2001 From: santoshkumarradha Date: Thu, 17 Sep 2026 02:09:15 -0400 Subject: [PATCH 027/173] poolcfg: cover resolution, the url shapes, ttl clamping and the can-send and can-read rules Co-Authored-By: codeaf --- internal/pool/poolcfg/poolcfg_test.go | 376 ++++++++++++++++++++++++++ 1 file changed, 376 insertions(+) create mode 100644 internal/pool/poolcfg/poolcfg_test.go diff --git a/internal/pool/poolcfg/poolcfg_test.go b/internal/pool/poolcfg/poolcfg_test.go new file mode 100644 index 0000000000..02c88ef4df --- /dev/null +++ b/internal/pool/poolcfg/poolcfg_test.go @@ -0,0 +1,376 @@ +package poolcfg + +import ( + "reflect" + "sort" + "sync" + "testing" + "time" +) + +// envOf builds a lookup over a map. A name present with an empty value counts +// as set, because a name that is set and empty is not the same as a name +// nobody set. +func envOf(pairs map[string]string) func(string) (string, bool) { + return func(name string) (string, bool) { + value, set := pairs[name] + return value, set + } +} + +func TestModeString(t *testing.T) { + cases := []struct { + mode Mode + want string + }{ + {On, "on"}, + {Read, "read"}, + {Off, "off"}, + } + for _, c := range cases { + if got := c.mode.String(); got != c.want { + t.Errorf("Mode(%d).String() = %q, want %q", int(c.mode), got, c.want) + } + } +} + +func TestResolveDefaults(t *testing.T) { + want := Config{ + Mode: On, + IndexURL: DefaultIndexURL, + SubmitURL: DefaultSubmitURL, + TTL: 24 * time.Hour, + Source: Sources{ + Mode: "default", + IndexURL: "default", + SubmitURL: "default", + TTL: "default", + }, + } + lookups := []struct { + name string + lookup func(string) (string, bool) + }{ + {"nil lookup", nil}, + {"nobody set", envOf(nil)}, + {"other names only", envOf(map[string]string{"OTHER": "on"})}, + } + for _, l := range lookups { + if got := Resolve("", l.lookup); got != want { + t.Errorf("%s: Resolve() = %+v, want %+v", l.name, got, want) + } + } +} + +func TestResolveMode(t *testing.T) { + cases := []struct { + name string + setting string + lookup func(string) (string, bool) + want Mode + wantSrc string + }{ + {"env on", "", envOf(map[string]string{"CODEAF_MODEL_POOL": "on"}), On, "env"}, + {"env read", "", envOf(map[string]string{"CODEAF_MODEL_POOL": "read"}), Read, "env"}, + {"env off", "", envOf(map[string]string{"CODEAF_MODEL_POOL": "off"}), Off, "env"}, + {"env padded and mixed case", "", envOf(map[string]string{"CODEAF_MODEL_POOL": "\t OFF "}), Off, "env"}, + {"env unknown word falls through", "read", envOf(map[string]string{"CODEAF_MODEL_POOL": "maybe"}), Read, "setting"}, + {"env set and empty falls through", "read", envOf(map[string]string{"CODEAF_MODEL_POOL": ""}), Read, "setting"}, + {"setting on", "on", envOf(nil), On, "setting"}, + {"setting read", "read", envOf(nil), Read, "setting"}, + {"setting padded and mixed case", " Off\t", envOf(nil), Off, "setting"}, + {"setting unknown word falls through", "sometimes", envOf(nil), On, "default"}, + {"setting blank falls through", " ", envOf(nil), On, "default"}, + {"env beats setting", "off", envOf(map[string]string{"CODEAF_MODEL_POOL": "on"}), On, "env"}, + } + for _, c := range cases { + got := Resolve(c.setting, c.lookup) + if got.Mode != c.want || got.Source.Mode != c.wantSrc { + t.Errorf("%s: Mode = %v from %q, want %v from %q", c.name, got.Mode, got.Source.Mode, c.want, c.wantSrc) + } + } +} + +func TestResolveModeFromCI(t *testing.T) { + cases := []struct { + name string + setting string + lookup func(string) (string, bool) + want Mode + wantSrc string + }{ + {"ci true", "", envOf(map[string]string{"CI": "true"}), Read, "ci"}, + {"ci one", "", envOf(map[string]string{"CI": "1"}), Read, "ci"}, + {"ci yes", "", envOf(map[string]string{"CI": "yes"}), Read, "ci"}, + {"ci padded and mixed case", "", envOf(map[string]string{"CI": " YES "}), Read, "ci"}, + {"ci false is not an answer", "", envOf(map[string]string{"CI": "false"}), On, "default"}, + {"ci zero is not an answer", "", envOf(map[string]string{"CI": "0"}), On, "default"}, + {"ci no is not an answer", "", envOf(map[string]string{"CI": "no"}), On, "default"}, + {"ci set and empty is not an answer", "", envOf(map[string]string{"CI": ""}), On, "default"}, + {"ci reads when the setting is unknown", "maybe", envOf(map[string]string{"CI": "true"}), Read, "ci"}, + {"ci reads when the setting is blank", " ", envOf(map[string]string{"CI": "1"}), Read, "ci"}, + {"setting beats ci", "off", envOf(map[string]string{"CI": "true"}), Off, "setting"}, + {"env on beats ci", "off", envOf(map[string]string{"CI": "true", "CODEAF_MODEL_POOL": "on"}), On, "env"}, + {"env off beats ci", "on", envOf(map[string]string{"CI": "true", "CODEAF_MODEL_POOL": "off"}), Off, "env"}, + {"env unknown and setting unknown leave ci", "maybe", envOf(map[string]string{"CI": "true", "CODEAF_MODEL_POOL": "maybe"}), Read, "ci"}, + } + for _, c := range cases { + got := Resolve(c.setting, c.lookup) + if got.Mode != c.want || got.Source.Mode != c.wantSrc { + t.Errorf("%s: Mode = %v from %q, want %v from %q", c.name, got.Mode, got.Source.Mode, c.want, c.wantSrc) + } + } +} + +func TestResolveIndexURL(t *testing.T) { + cases := []struct { + name string + value string + set bool + want string + wantSrc string + }{ + {"nobody set", "", false, DefaultIndexURL, "default"}, + {"https", "https://pool.example.com/index.json", true, "https://pool.example.com/index.json", "env"}, + {"https with port", "https://pool.example.com:8443/i", true, "https://pool.example.com:8443/i", "env"}, + {"https scheme in upper case", "HTTPS://pool.example.com", true, "HTTPS://pool.example.com", "env"}, + {"http localhost", "http://localhost", true, "http://localhost", "env"}, + {"http localhost with port", "http://localhost:8080", true, "http://localhost:8080", "env"}, + {"http localhost in upper case", "http://LOCALHOST", true, "http://LOCALHOST", "env"}, + {"http loopback", "http://127.0.0.1", true, "http://127.0.0.1", "env"}, + {"http loopback with port and path", "http://127.0.0.1:9000/pool.json", true, "http://127.0.0.1:9000/pool.json", "env"}, + {"file", "file:///var/tmp/pool.json", true, "file:///var/tmp/pool.json", "env"}, + {"absolute path", "/var/tmp/pool.json", true, "/var/tmp/pool.json", "env"}, + {"padded", " https://pool.example.com ", true, "https://pool.example.com", "env"}, + {"plain http elsewhere", "http://pool.example.com", true, DefaultIndexURL, "default"}, + {"plain http elsewhere with port", "http://pool.example.com:8080", true, DefaultIndexURL, "default"}, + {"http host that merely contains localhost", "http://localhost.example.com", true, DefaultIndexURL, "default"}, + {"http loopback that is not 127.0.0.1", "http://127.0.0.2", true, DefaultIndexURL, "default"}, + {"other scheme", "ftp://pool.example.com", true, DefaultIndexURL, "default"}, + {"scheme without the double slash", "https:pool.example.com", true, DefaultIndexURL, "default"}, + {"http without the double slash", "http:/localhost", true, DefaultIndexURL, "default"}, + {"bare host", "pool.example.com/index.json", true, DefaultIndexURL, "default"}, + {"relative path", "tmp/pool.json", true, DefaultIndexURL, "default"}, + {"dot relative path", "./pool.json", true, DefaultIndexURL, "default"}, + {"set and empty", "", true, DefaultIndexURL, "default"}, + } + for _, c := range cases { + lookup := envOf(nil) + if c.set { + lookup = envOf(map[string]string{"CODEAF_MODEL_POOL_URL": c.value}) + } + got := Resolve("", lookup) + if got.IndexURL != c.want || got.Source.IndexURL != c.wantSrc { + t.Errorf("%s: IndexURL = %q from %q, want %q from %q", c.name, got.IndexURL, got.Source.IndexURL, c.want, c.wantSrc) + } + } +} + +func TestResolveSubmitURL(t *testing.T) { + cases := []struct { + name string + value string + set bool + want string + wantSrc string + }{ + {"nobody set", "", false, DefaultSubmitURL, "default"}, + {"https", "https://submit.example.com", true, "https://submit.example.com", "env"}, + {"http loopback with port", "http://127.0.0.1:9000/submit", true, "http://127.0.0.1:9000/submit", "env"}, + {"file", "file:///tmp/submit.json", true, "file:///tmp/submit.json", "env"}, + {"absolute path", "/tmp/submit.json", true, "/tmp/submit.json", "env"}, + {"empty sends nowhere", "", true, "", "env"}, + {"blank sends nowhere", " ", true, "", "env"}, + {"plain http elsewhere", "http://submit.example.com", true, DefaultSubmitURL, "default"}, + {"path that is not absolute", "submit.json", true, DefaultSubmitURL, "default"}, + {"other scheme", "ftp://submit.example.com", true, DefaultSubmitURL, "default"}, + } + for _, c := range cases { + lookup := envOf(nil) + if c.set { + lookup = envOf(map[string]string{"CODEAF_MODEL_POOL_SUBMIT_URL": c.value}) + } + got := Resolve("", lookup) + if got.SubmitURL != c.want || got.Source.SubmitURL != c.wantSrc { + t.Errorf("%s: SubmitURL = %q from %q, want %q from %q", c.name, got.SubmitURL, got.Source.SubmitURL, c.want, c.wantSrc) + } + } +} + +func TestResolveTTL(t *testing.T) { + cases := []struct { + name string + value string + set bool + want time.Duration + wantSrc string + }{ + {"nobody set", "", false, 24 * time.Hour, "default"}, + {"readable", "2h45m", true, 2*time.Hour + 45*time.Minute, "env"}, + {"padded", " 3h ", true, 3 * time.Hour, "env"}, + {"under the floor", "30s", true, time.Minute, "env"}, + {"at the floor", "1m", true, time.Minute, "env"}, + {"negative", "-1h", true, time.Minute, "env"}, + {"zero", "0", true, time.Minute, "env"}, + {"over the ceiling", "1000h", true, 7 * 24 * time.Hour, "env"}, + {"at the ceiling", "168h", true, 7 * 24 * time.Hour, "env"}, + {"unreadable", "banana", true, 24 * time.Hour, "default"}, + {"no day unit", "7d", true, 24 * time.Hour, "default"}, + {"set and empty", "", true, 24 * time.Hour, "default"}, + } + for _, c := range cases { + lookup := envOf(nil) + if c.set { + lookup = envOf(map[string]string{"CODEAF_MODEL_POOL_TTL": c.value}) + } + got := Resolve("", lookup) + if got.TTL != c.want || got.Source.TTL != c.wantSrc { + t.Errorf("%s: TTL = %v from %q, want %v from %q", c.name, got.TTL, got.Source.TTL, c.want, c.wantSrc) + } + } +} + +func TestResolveFieldsAreIndependent(t *testing.T) { + // Off, an unreadable URL and a readable TTL: the mode does not change how + // the URLs or the TTL are read, and the unreadable URL does not disturb + // the TTL. + got := Resolve("bogus", envOf(map[string]string{ + "CODEAF_MODEL_POOL": "off", + "CODEAF_MODEL_POOL_URL": "http://pool.example.com", + "CODEAF_MODEL_POOL_SUBMIT_URL": "https://submit.example.com", + "CODEAF_MODEL_POOL_TTL": "2h", + })) + want := Config{ + Mode: Off, + IndexURL: DefaultIndexURL, + SubmitURL: "https://submit.example.com", + TTL: 2 * time.Hour, + Source: Sources{ + Mode: "env", + IndexURL: "default", + SubmitURL: "env", + TTL: "env", + }, + } + if got != want { + t.Errorf("Resolve() = %+v, want %+v", got, want) + } + + // An unreadable TTL does not disturb the URLs either. + got = Resolve("", envOf(map[string]string{ + "CODEAF_MODEL_POOL_URL": "https://pool.example.com", + "CODEAF_MODEL_POOL_TTL": "banana", + })) + if got.IndexURL != "https://pool.example.com" || got.Source.IndexURL != "env" { + t.Errorf("IndexURL = %q from %q, want the env value from %q", got.IndexURL, got.Source.IndexURL, "env") + } + if got.TTL != 24*time.Hour || got.Source.TTL != "default" { + t.Errorf("TTL = %v from %q, want 24h from %q", got.TTL, got.Source.TTL, "default") + } +} + +func TestCanSendAndCanRead(t *testing.T) { + cases := []struct { + name string + config Config + wantCanSend bool + wantCanRead bool + }{ + {"on with an address", Config{Mode: On, SubmitURL: DefaultSubmitURL}, true, true}, + {"on with no address", Config{Mode: On}, false, true}, + {"read with an address", Config{Mode: Read, SubmitURL: DefaultSubmitURL}, false, true}, + {"read with no address", Config{Mode: Read}, false, true}, + {"off with an address", Config{Mode: Off, SubmitURL: DefaultSubmitURL}, false, false}, + } + for _, c := range cases { + if got := c.config.CanSend(); got != c.wantCanSend { + t.Errorf("%s: CanSend() = %v, want %v", c.name, got, c.wantCanSend) + } + if got := c.config.CanRead(); got != c.wantCanRead { + t.Errorf("%s: CanRead() = %v, want %v", c.name, got, c.wantCanRead) + } + } +} + +func TestResolvedConfigCanSendAndCanRead(t *testing.T) { + // An empty submit address turns sending off while everything else stays on. + got := Resolve("", envOf(map[string]string{"CODEAF_MODEL_POOL_SUBMIT_URL": ""})) + if got.SubmitURL != "" || got.Source.SubmitURL != "env" { + t.Fatalf("SubmitURL = %q from %q, want %q from %q", got.SubmitURL, got.Source.SubmitURL, "", "env") + } + if got.CanSend() { + t.Error("an empty submit address must not send") + } + if !got.CanRead() { + t.Error("an empty submit address must still read") + } + + if got := Resolve("", envOf(nil)); !got.CanSend() || !got.CanRead() { + t.Errorf("the defaults must send and read: CanSend() = %v, CanRead() = %v", got.CanSend(), got.CanRead()) + } + if got := Resolve("read", envOf(nil)); got.CanSend() || !got.CanRead() { + t.Errorf("read must read and not send: CanSend() = %v, CanRead() = %v", got.CanSend(), got.CanRead()) + } + if got := Resolve("off", envOf(nil)); got.CanSend() || got.CanRead() { + t.Errorf("off must neither send nor read: CanSend() = %v, CanRead() = %v", got.CanSend(), got.CanRead()) + } +} + +func TestResolveNilLookupUsesSetting(t *testing.T) { + got := Resolve("read", nil) + if got.Mode != Read || got.Source.Mode != "setting" { + t.Errorf("Resolve(\"read\", nil): Mode = %v from %q, want read from %q", got.Mode, got.Source.Mode, "setting") + } + if got.IndexURL != DefaultIndexURL || got.SubmitURL != DefaultSubmitURL || got.TTL != 24*time.Hour { + t.Errorf("Resolve(\"read\", nil) = %+v, want the defaults beside the setting", got) + } +} + +func TestResolveAsksFiveNames(t *testing.T) { + var asked []string + Resolve("", func(name string) (string, bool) { + asked = append(asked, name) + return "", false + }) + sort.Strings(asked) + want := []string{envMode, envCI, envIndex, envSubmit, envTTL} + sort.Strings(want) + if !reflect.DeepEqual(asked, want) { + t.Errorf("Resolve asked lookup for %v, want %v", asked, want) + } +} + +func TestResolveIsPure(t *testing.T) { + first := envOf(map[string]string{"CODEAF_MODEL_POOL": "read", "CODEAF_MODEL_POOL_TTL": "3h"}) + second := envOf(map[string]string{"CODEAF_MODEL_POOL": "off"}) + want := Resolve("", first) + Resolve("", second) + if got := Resolve("", first); got != want { + t.Errorf("a second call kept state: %+v, want %+v", got, want) + } + if got := Resolve("", second); got.Mode != Off || got.TTL != 24*time.Hour { + t.Errorf("a fresh environment was polluted: %+v", got) + } +} + +func TestResolveConcurrent(t *testing.T) { + lookup := envOf(map[string]string{ + "CODEAF_MODEL_POOL": "read", + "CODEAF_MODEL_POOL_TTL": "90m", + }) + want := Resolve("", lookup) + var wg sync.WaitGroup + for i := 0; i < 32; i++ { + wg.Add(1) + go func() { + defer wg.Done() + for j := 0; j < 64; j++ { + if got := Resolve("", lookup); got != want { + t.Errorf("Resolve() = %+v, want %+v", got, want) + return + } + } + }() + } + wg.Wait() +} From 8e97aca7dfb3861570752544a1f709796c84aefc Mon Sep 17 00:00:00 2001 From: santoshkumarradha Date: Thu, 17 Sep 2026 02:11:01 -0400 Subject: [PATCH 028/173] pool: fetch, verify and cache a signed json document --- internal/pool/cache.go | 123 +++++++++++++ internal/pool/pull.go | 387 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 510 insertions(+) create mode 100644 internal/pool/cache.go create mode 100644 internal/pool/pull.go diff --git a/internal/pool/cache.go b/internal/pool/cache.go new file mode 100644 index 0000000000..42e1d79edd --- /dev/null +++ b/internal/pool/cache.go @@ -0,0 +1,123 @@ +package pull + +import ( + "encoding/json" + "os" + "path/filepath" + "time" +) + +// cache is the on-disk record of the last good fetch: the document, its +// signature, its ETag and when it was fetched. It is written so that a run cut +// in half never leaves a half-written cache: each file goes to a temporary name +// in CacheDir and is then renamed into place, and the files it leaves behind +// have mode 0600. +// +// A cache that does not read is no cache. Bytes that do not parse, or whose +// signature no longer verifies under Keys, are treated as if nothing were +// cached: no error of their own, no panic, and the next good fetch replaces +// them. +type cache struct { + doc []byte + sig []byte + etag string + version int64 + fetchedAt time.Time + ok bool +} + +// loadCache reads the cache directory and returns what it finds, or a cache +// with ok false. A cache that fails to read is not an error: it is treated as +// no cache at all. +func (p *Puller) loadCache() (cache, bool) { + var c cache + doc, err := readLimitedFile(filepath.Join(p.CacheDir, "doc.json")) + if err != nil { + return c, false + } + sig, err := readLimitedFile(filepath.Join(p.CacheDir, "doc.sig")) + if err != nil { + return c, false + } + meta, err := os.ReadFile(filepath.Join(p.CacheDir, "meta.json")) + if err != nil { + return c, false + } + var m struct { + ETag string `json:"etag"` + Version int64 `json:"version"` + FetchedAt time.Time `json:"fetched_at"` + } + if err := json.Unmarshal(meta, &m); err != nil { + return c, false + } + // THE CACHE IS NOT A TRUSTED STORE. A signature that no longer verifies + // under Keys means the cache is stale, not that the caller is owed an error. + if !Verify(doc, sig, p.Keys) { + return c, false + } + c.doc = doc + c.sig = sig + c.etag = m.ETag + c.version = m.Version + c.fetchedAt = m.FetchedAt + c.ok = true + return c, true +} + +// storeCache writes the document, its signature, its ETag and when it was +// fetched to CacheDir. Each file goes to a temporary name and is then renamed +// into place, so a run cut in half never leaves a half-written cache. The +// files it leaves behind have mode 0600. CacheDir is made if it is missing. +func (p *Puller) storeCache(doc, sig []byte, etag string, version int64, fetchedAt time.Time) { + if err := os.MkdirAll(p.CacheDir, 0o700); err != nil { + // A cache that cannot be written is not fatal: the caller still gets + // its document, and the next run simply fetches again. + return + } + meta, err := json.Marshal(struct { + ETag string `json:"etag"` + Version int64 `json:"version"` + FetchedAt time.Time `json:"fetched_at"` + }{etag, version, fetchedAt}) + if err != nil { + return + } + files := []struct { + name string + data []byte + }{ + {"doc.json", doc}, + {"doc.sig", sig}, + {"meta.json", meta}, + } + for _, f := range files { + if err := atomicWrite(p.CacheDir, f.name, f.data); err != nil { + return + } + } +} + +// atomicWrite writes data to a temporary file in dir and renames it into +// place. The file is created with mode 0600, so the files the cache leaves +// behind are owner-readable only. +func atomicWrite(dir, name string, data []byte) error { + tmp, err := os.CreateTemp(dir, "."+name+".*") + if err != nil { + return err + } + tmpPath := tmp.Name() + defer os.Remove(tmpPath) // a rename that succeeded leaves no temp behind + if _, err := tmp.Write(data); err != nil { + tmp.Close() + return err + } + if err := tmp.Chmod(0o600); err != nil { + tmp.Close() + return err + } + if err := tmp.Close(); err != nil { + return err + } + return os.Rename(tmpPath, filepath.Join(dir, name)) +} diff --git a/internal/pool/pull.go b/internal/pool/pull.go new file mode 100644 index 0000000000..218217a810 --- /dev/null +++ b/internal/pool/pull.go @@ -0,0 +1,387 @@ +// Package pull fetches a signed JSON document, checks its signature against +// keys it was handed, and keeps the last good copy on disk so that the next run +// has something to work with when the fetch does not go through. +// +// THE SHAPE IT EXISTS FOR. A document this package serves changes rarely and is +// needed on every run; the source that publishes it is not always reachable. +// The whole point is that a caller can ask for the document on any run and get +// an answer: either a fresh one, or the last good one with an error saying why +// it is not fresh. The network is consulted only when the cache is older than +// the caller's patience, and every failure the source can throw — a dead +// host, a timeout, a bad signature, a rollback — falls back to the cache +// rather than leaving the caller empty. +// +// WHAT MAKES A DOCUMENT GOOD is written down in [Puller.good] and nowhere else, +// because it is the one contract a reader can spot on its own: the document is +// JSON, an object, and carries an integer "version"; its ".sig" verifies under +// one of the keys the caller trusts; and its version is not lower than the one +// the cache already holds, because a copy of yesterday served today is the one +// attack a reader can catch without trusting the source. A good document is +// cached and returned; a bad one returns whatever the cache had, together with +// the error that says why the fresh one was refused. +// +// THE CACHE IS NOT A TRUSTED STORE. Bytes in [Puller.CacheDir] that do not +// parse, or whose signature no longer verifies under Keys, are treated as if +// nothing were cached: no error of their own, no panic, and the next good +// fetch replaces them. The cache is a copy of a good fetch, never a source of +// truth in its own right. +package pull + +import ( + "bytes" + "context" + "crypto/ed25519" + "encoding/base64" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "net/url" + "os" + "strconv" + "strings" + "time" +) + +const ( + // MaxDoc is the largest document Pull will read. A document larger than + // this is refused rather than read to the end, because a source that answers + // with an unbounded body is a source that can exhaust the reader. + MaxDoc = 4 << 20 // 4 MiB + + // DefaultBudget is what a Pull with a zero Budget spends. It is short on + // purpose: this package serves a document that is needed on the path of a + // keystroke, and a caller who can afford to wait longer sets its own. + DefaultBudget = 2 * time.Second +) + +// Result is what a single Pull returns. Doc is the document bytes, nil when +// there is nothing to give. Version is its "version". FromCache is true when +// the bytes came off disk, not off the wire. Changed is true when this version +// is newer than the one the cache held. +type Result struct { + Doc []byte + Version int64 + FromCache bool + Changed bool +} + +// Puller fetches one document from one URL, verifies it against one set of +// keys, and keeps the last good copy in one directory. The zero value is not +// usable: URL, Keys and CacheDir must be set before Pull is called. +// +// Client and Now have nil defaults the package fills: a plain http.Client with +// no timeout of its own (the budget governs) and time.Now. A caller that wants +// deterministic clocks or a test transport sets both. +type Puller struct { + URL string + Keys []ed25519.PublicKey + CacheDir string + TTL time.Duration + Budget time.Duration // 0 means DefaultBudget + Client *http.Client // nil means a client the package makes + Now func() time.Time // nil means time.Now +} + +// Verify answers whether sig is a detached ed25519 signature over doc under ANY +// ONE of keys. sig is read as base64 text in the standard encoding; space +// around it is ignored. No keys, an unreadable base64, a signature of the +// wrong length or a signature nobody's key made is false. A caller who trusts +// nobody trusts nothing. +// +// The key-length guard is here because ed25519.Verify panics on a wrong-length +// key, and a key the caller mis-hands is a misconfiguration, not a crash. +func Verify(doc, sig []byte, keys []ed25519.PublicKey) bool { + trimmed := bytes.TrimSpace(sig) + raw, err := base64.StdEncoding.DecodeString(string(trimmed)) + if err != nil { + return false + } + if len(raw) != ed25519.SignatureSize { + return false + } + for _, k := range keys { + if len(k) != ed25519.PublicKeySize { + continue + } + if ed25519.Verify(k, doc, raw) { + return true + } + } + return false +} + +// Pull fetches the document and returns a Result and an error. It never +// panics. The whole call lives inside Budget (DefaultBudget when it is zero) +// and inside ctx, whichever ends first. +func (p *Puller) Pull(ctx context.Context) (Result, error) { + if p.URL == "" { + return Result{}, errors.New("pull: empty url") + } + budget := p.Budget + if budget <= 0 { + budget = DefaultBudget + } + ctx, cancel := context.WithTimeout(ctx, budget) + defer cancel() + + now := p.now() + cached, ok := p.loadCache() + + // THE NETWORK IS SKIPPED WHEN THE CACHE IS YOUNG. A good copy fetched less + // than TTL ago answers straight from the cache, and not one request is + // made. A TTL of zero makes no cached copy young. + if ok && p.TTL > 0 && now.Sub(cached.fetchedAt) < p.TTL { + return Result{ + Doc: cached.doc, + Version: cached.version, + FromCache: true, + Changed: false, + }, nil + } + + doc, sig, etag, err := p.fetch(ctx, cached) + if err != nil { + // EVERY FAILURE FALLS BACK. The cached good copy, if there is one, is + // returned with the error that says why the fresh one was refused. The + // cache is left exactly as it was. + if ok { + return Result{ + Doc: cached.doc, + Version: cached.version, + FromCache: true, + Changed: false, + }, err + } + return Result{FromCache: false}, err + } + + // WHAT MAKES A FETCHED DOCUMENT GOOD. + version, err := p.good(doc, sig, cached) + if err != nil { + if ok { + return Result{ + Doc: cached.doc, + Version: cached.version, + FromCache: true, + Changed: false, + }, err + } + return Result{FromCache: false}, err + } + + changed := true + if ok && version == cached.version { + changed = false + } + + // A good document is cached and returned with FromCache false. + p.storeCache(doc, sig, etag, version, now) + return Result{ + Doc: doc, + Version: version, + FromCache: false, + Changed: changed, + }, nil +} + +// good answers whether the fetched bytes are a document this package will serve. +// The document must be JSON, an object, and carry an integer "version"; its +// signature must verify under one of Keys; and its version may not be lower +// than the one the cache already holds. It returns the version when it does. +func (p *Puller) good(doc, sig []byte, cached cache) (int64, error) { + if !Verify(doc, sig, p.Keys) { + return 0, errors.New("pull: signature does not verify") + } + var obj map[string]json.RawMessage + if err := json.Unmarshal(doc, &obj); err != nil { + return 0, fmt.Errorf("pull: document is not json: %w", err) + } + if obj == nil { + return 0, errors.New("pull: document is not a json object") + } + raw, has := obj["version"] + if !has { + return 0, errors.New("pull: document has no version") + } + version, err := strconv.ParseInt(string(raw), 10, 64) + if err != nil { + return 0, fmt.Errorf("pull: version is not an integer: %w", err) + } + if cached.ok && version < cached.version { + return 0, fmt.Errorf("pull: version %d is lower than cached %d", version, cached.version) + } + return version, nil +} + +func (p *Puller) now() time.Time { + if p.Now != nil { + return p.Now() + } + return time.Now() +} + +func (p *Puller) client() *http.Client { + if p.Client != nil { + return p.Client + } + return &http.Client{} +} + +// sigURL names the signature location: the URL or path with ".sig" appended to +// its text. A URL gets ".sig" on the end of the URL; a path gets ".sig" on the +// end of the path. +func (p *Puller) sigLocation() string { + return p.URL + ".sig" +} + +// fetch reads the document and its signature from the source, and returns the +// document bytes, the raw signature bytes, the ETag (if any) and any error. +// Over http, a cached copy the source may still have unchanged is asked for +// with If-None-Match carrying the ETag that came with it; a 304 means the +// cached copy is the answer. +func (p *Puller) fetch(ctx context.Context, cached cache) ([]byte, []byte, string, error) { + scheme := schemeOf(p.URL) + switch scheme { + case "http", "https": + return p.fetchHTTP(ctx, cached) + default: + return p.fetchFile(ctx) + } +} + +// fetchHTTP reads the document and its signature over the network. +func (p *Puller) fetchHTTP(ctx context.Context, cached cache) ([]byte, []byte, string, error) { + req, err := http.NewRequestWithContext(ctx, http.MethodGet, p.URL, nil) + if err != nil { + return nil, nil, "", fmt.Errorf("pull: build request: %w", err) + } + if cached.ok && cached.etag != "" { + req.Header.Set("If-None-Match", cached.etag) + } + resp, err := p.client().Do(req) + if err != nil { + return nil, nil, "", fmt.Errorf("pull: fetch: %w", err) + } + defer resp.Body.Close() + if resp.StatusCode == http.StatusNotModified { + // A 304 means the cached copy is still the source's current one. We + // answer with the cached bytes, signed by the cached signature, but + // the caller already has those; what it needs is a nil error so the + // good path runs. The signature is re-verified through cached.sig. + return cached.doc, cached.sig, cached.etag, nil + } + if resp.StatusCode != http.StatusOK { + return nil, nil, "", fmt.Errorf("pull: fetch: status %d", resp.StatusCode) + } + doc, err := readLimited(resp.Body, resp.ContentLength) + if err != nil { + return nil, nil, "", fmt.Errorf("pull: read document: %w", err) + } + sig, err := p.fetchSigHTTP(ctx) + if err != nil { + return nil, nil, "", err + } + return doc, sig, resp.Header.Get("ETag"), nil +} + +func (p *Puller) fetchSigHTTP(ctx context.Context) ([]byte, error) { + req, err := http.NewRequestWithContext(ctx, http.MethodGet, p.sigLocation(), nil) + if err != nil { + return nil, fmt.Errorf("pull: build signature request: %w", err) + } + resp, err := p.client().Do(req) + if err != nil { + return nil, fmt.Errorf("pull: fetch signature: %w", err) + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("pull: fetch signature: status %d", resp.StatusCode) + } + sig, err := readLimited(resp.Body, resp.ContentLength) + if err != nil { + return nil, fmt.Errorf("pull: read signature: %w", err) + } + return sig, nil +} + +// fetchFile reads the document and its signature from the filesystem. A path +// may be written plain or as "file://" and then the path. +func (p *Puller) fetchFile(ctx context.Context) ([]byte, []byte, string, error) { + path := stripFile(p.URL) + doc, err := readLimitedFile(path) + if err != nil { + return nil, nil, "", fmt.Errorf("pull: read document: %w", err) + } + sig, err := readLimitedFile(p.sigLocation()) + if err != nil { + return nil, nil, "", fmt.Errorf("pull: read signature: %w", err) + } + return doc, sig, "", nil +} + +// readLimited reads from r up to MaxDoc bytes. A ContentLength above MaxDoc is +// refused before a byte is read; a body that exceeds MaxDoc mid-stream is cut +// off and refused rather than read to the end. +func readLimited(r io.Reader, contentLength int64) ([]byte, error) { + if contentLength > MaxDoc { + return nil, fmt.Errorf("size %d exceeds max %d", contentLength, MaxDoc) + } + // MaxDoc+1 so a body that fills the buffer once is caught as too large. + limited := io.LimitReader(r, MaxDoc+1) + data, err := io.ReadAll(limited) + if err != nil { + return nil, err + } + if len(data) > MaxDoc { + return nil, fmt.Errorf("size exceeds max %d", MaxDoc) + } + return data, nil +} + +// readLimitedFile reads a file up to MaxDoc bytes. A file larger than MaxDoc is +// refused before it is read to the end. +func readLimitedFile(path string) ([]byte, error) { + info, err := os.Stat(path) + if err != nil { + return nil, err + } + if info.Size() > MaxDoc { + return nil, fmt.Errorf("size %d exceeds max %d", info.Size(), MaxDoc) + } + f, err := os.Open(path) + if err != nil { + return nil, err + } + defer f.Close() + limited := io.LimitReader(f, MaxDoc+1) + data, err := io.ReadAll(limited) + if err != nil { + return nil, err + } + if len(data) > MaxDoc { + return nil, fmt.Errorf("size exceeds max %d", MaxDoc) + } + return data, nil +} + +// schemeOf returns the scheme of a URL, or "" when the string is a plain +// filesystem path. A "file://" prefix is reported as "file". +func schemeOf(s string) string { + if strings.HasPrefix(s, "file://") { + return "file" + } + if u, err := url.Parse(s); err == nil && u.Scheme != "" { + return u.Scheme + } + return "" +} + +// stripFile removes a "file://" prefix, leaving the bare path. +func stripFile(s string) string { + if strings.HasPrefix(s, "file://") { + return strings.TrimPrefix(s, "file://") + } + return s +} From 0cb25fbb50f029789b1c07f5090e350725cbed36 Mon Sep 17 00:00:00 2001 From: santoshkumarradha Date: Thu, 17 Sep 2026 02:12:53 -0400 Subject: [PATCH 029/173] pool: handle 304 from cache and file urls, add thorough tests --- internal/pool/pull.go | 30 +- internal/pool/pull_test.go | 1122 ++++++++++++++++++++++++++++++++++++ 2 files changed, 1144 insertions(+), 8 deletions(-) create mode 100644 internal/pool/pull_test.go diff --git a/internal/pool/pull.go b/internal/pool/pull.go index 218217a810..389078e4ea 100644 --- a/internal/pool/pull.go +++ b/internal/pool/pull.go @@ -44,6 +44,10 @@ import ( "time" ) +// errNotModified is the sentinel a 304 answer returns. It is not a failure: +// the cached copy is the answer, and Pull handles it before the fallback path. +var errNotModified = errors.New("pull: not modified") + const ( // MaxDoc is the largest document Pull will read. A document larger than // this is refused rather than read to the end, because a source that answers @@ -79,8 +83,8 @@ type Puller struct { Keys []ed25519.PublicKey CacheDir string TTL time.Duration - Budget time.Duration // 0 means DefaultBudget - Client *http.Client // nil means a client the package makes + Budget time.Duration // 0 means DefaultBudget + Client *http.Client // nil means a client the package makes Now func() time.Time // nil means time.Now } @@ -143,6 +147,17 @@ func (p *Puller) Pull(ctx context.Context) (Result, error) { doc, sig, etag, err := p.fetch(ctx, cached) if err != nil { + // A 304 is not a failure: the cached copy is the answer. But a 304 with + // no cache is a source claiming not-modified for something we never + // had, which is a failure. + if errors.Is(err, errNotModified) && ok { + return Result{ + Doc: cached.doc, + Version: cached.version, + FromCache: true, + Changed: false, + }, nil + } // EVERY FAILURE FALLS BACK. The cached good copy, if there is one, is // returned with the error that says why the fresh one was refused. The // cache is left exactly as it was. @@ -266,11 +281,10 @@ func (p *Puller) fetchHTTP(ctx context.Context, cached cache) ([]byte, []byte, s } defer resp.Body.Close() if resp.StatusCode == http.StatusNotModified { - // A 304 means the cached copy is still the source's current one. We - // answer with the cached bytes, signed by the cached signature, but - // the caller already has those; what it needs is a nil error so the - // good path runs. The signature is re-verified through cached.sig. - return cached.doc, cached.sig, cached.etag, nil + // A 304 means the cached copy is still the source's current one. The + // caller already has those bytes; the sentinel tells Pull to answer + // from the cache with a nil error. + return nil, nil, "", errNotModified } if resp.StatusCode != http.StatusOK { return nil, nil, "", fmt.Errorf("pull: fetch: status %d", resp.StatusCode) @@ -314,7 +328,7 @@ func (p *Puller) fetchFile(ctx context.Context) ([]byte, []byte, string, error) if err != nil { return nil, nil, "", fmt.Errorf("pull: read document: %w", err) } - sig, err := readLimitedFile(p.sigLocation()) + sig, err := readLimitedFile(stripFile(p.sigLocation())) if err != nil { return nil, nil, "", fmt.Errorf("pull: read signature: %w", err) } diff --git a/internal/pool/pull_test.go b/internal/pool/pull_test.go new file mode 100644 index 0000000000..cbccc16315 --- /dev/null +++ b/internal/pool/pull_test.go @@ -0,0 +1,1122 @@ +package pull + +import ( + "context" + "crypto/ed25519" + "crypto/rand" + "encoding/base64" + "encoding/json" + "net/http" + "net/http/httptest" + "os" + "path/filepath" + "strconv" + "strings" + "testing" + "time" +) + +// makeKey makes a fresh ed25519 key pair. +func makeKey(t *testing.T) (ed25519.PublicKey, ed25519.PrivateKey) { + t.Helper() + pub, priv, err := ed25519.GenerateKey(rand.Reader) + if err != nil { + t.Fatal(err) + } + return pub, priv +} + +// signDoc signs doc with priv and returns base64-encoded standard encoding. +func signDoc(t *testing.T, priv ed25519.PrivateKey, doc []byte) []byte { + t.Helper() + sig := ed25519.Sign(priv, doc) + return []byte(base64.StdEncoding.EncodeToString(sig)) +} + +// jsonObject builds a JSON object with a version field and whatever extra +// fields are given. +func jsonObject(version int64, extra map[string]any) []byte { + obj := map[string]any{"version": version} + for k, v := range extra { + obj[k] = v + } + b, _ := json.Marshal(obj) + return b +} + +// writeFile is the test shorthand that makes a 0600 file. +func writeFile(t *testing.T, path string, data []byte) { + t.Helper() + if err := os.WriteFile(path, data, 0o600); err != nil { + t.Fatal(err) + } +} + +// ----------------------------------------------------------------------------- + +// Verify reads a base64 standard-encoding signature and answers whether it +// is a detached ed25519 signature over doc under any one of the keys. No keys, +// unreadable base64, the wrong length, or a signature nobody's key made is +// false. +func TestVerifyAnswersTrueForAnyTrustedKey(t *testing.T) { + pub1, priv1 := makeKey(t) + pub2, priv2 := makeKey(t) + doc := []byte(`{"version":3}`) + sig1 := signDoc(t, priv1, doc) + sig2 := signDoc(t, priv2, doc) + + if !Verify(doc, sig1, []ed25519.PublicKey{pub1, pub2}) { + t.Fatal("signature by key 1 did not verify") + } + if !Verify(doc, sig2, []ed25519.PublicKey{pub1, pub2}) { + t.Fatal("signature by key 2 did not verify") + } +} + +// A caller who trusts nobody trusts nothing. +func TestVerifyWithNoKeysIsFalse(t *testing.T) { + _, priv := makeKey(t) + doc := []byte(`{"version":1}`) + sig := signDoc(t, priv, doc) + if Verify(doc, sig, nil) { + t.Fatal("a signature verified with no keys") + } +} + +// Unreadable base64 is false, not an error. +func TestVerifyWithGarbageSignatureIsFalse(t *testing.T) { + pub, _ := makeKey(t) + doc := []byte(`{"version":1}`) + if Verify(doc, []byte("!!!not-base64!!!"), []ed25519.PublicKey{pub}) { + t.Fatal("garbage verified") + } +} + +// A signature of the wrong length is false. +func TestVerifyWithWrongLengthSignatureIsFalse(t *testing.T) { + pub, _ := makeKey(t) + doc := []byte(`{"version":1}`) + short := base64.StdEncoding.EncodeToString([]byte("too short")) + if Verify(doc, []byte(short), []ed25519.PublicKey{pub}) { + t.Fatal("a short signature verified") + } +} + +// Space around the signature is ignored. +func TestVerifyIgnoresSpaceAroundSignature(t *testing.T) { + pub, priv := makeKey(t) + doc := []byte(`{"version":1}`) + sig := signDoc(t, priv, doc) + padded := []byte(" \n" + string(sig) + "\t ") + if !Verify(doc, padded, []ed25519.PublicKey{pub}) { + t.Fatal("padded signature did not verify") + } +} + +// A signature over the wrong bytes is false. +func TestVerifyWithWrongMessageIsFalse(t *testing.T) { + pub, priv := makeKey(t) + sig := signDoc(t, priv, []byte(`{"version":1}`)) + if Verify([]byte(`{"version":2}`), sig, []ed25519.PublicKey{pub}) { + t.Fatal("a signature verified over the wrong message") + } +} + +// A wrong-length key is skipped rather than crashing the caller. +func TestVerifySkipsWrongLengthKeys(t *testing.T) { + pub, priv := makeKey(t) + doc := []byte(`{"version":1}`) + sig := signDoc(t, priv, doc) + bad := ed25519.PublicKey(make([]byte, 10)) // not PublicKeySize + if !Verify(doc, sig, []ed25519.PublicKey{bad, pub}) { + t.Fatal("a good key behind a bad one did not verify") + } +} + +// ----------------------------------------------------------------------------- + +// A good file-source fetch returns the document with FromCache false and +// Changed true when nothing was cached. +func TestPullFromFileReturnsFreshDocument(t *testing.T) { + pub, priv := makeKey(t) + dir := t.TempDir() + doc := jsonObject(5, nil) + sig := signDoc(t, priv, doc) + writeFile(t, filepath.Join(dir, "doc.json"), doc) + writeFile(t, filepath.Join(dir, "doc.json.sig"), sig) + + p := &Puller{ + URL: filepath.Join(dir, "doc.json"), + Keys: []ed25519.PublicKey{pub}, + CacheDir: t.TempDir(), + } + res, err := p.Pull(t.Context()) + if err != nil { + t.Fatalf("Pull failed: %v", err) + } + if res.FromCache { + t.Fatal("a fresh fetch reported FromCache") + } + if !res.Changed { + t.Fatal("a first fetch reported Changed false") + } + if res.Version != 5 { + t.Fatalf("Version = %d, want 5", res.Version) + } + if !bytesEqual(res.Doc, doc) { + t.Fatal("Doc does not match the source") + } +} + +// A file:// URL is the same fetch as a plain path. +func TestPullFromFileURLScheme(t *testing.T) { + pub, priv := makeKey(t) + dir := t.TempDir() + doc := jsonObject(1, nil) + sig := signDoc(t, priv, doc) + path := filepath.Join(dir, "doc.json") + writeFile(t, path, doc) + writeFile(t, path+".sig", sig) + + p := &Puller{ + URL: "file://" + path, + Keys: []ed25519.PublicKey{pub}, + CacheDir: t.TempDir(), + } + res, err := p.Pull(t.Context()) + if err != nil { + t.Fatalf("Pull failed: %v", err) + } + if res.Version != 1 { + t.Fatalf("Version = %d, want 1", res.Version) + } +} + +// ----------------------------------------------------------------------------- + +// A fetchServer serves a document and its signature and remembers how many +// times each was asked for. +type fetchServer struct { + t *testing.T + pub ed25519.PublicKey + priv ed25519.PrivateKey + docBody []byte + sigBody []byte + etag string + docHits int + sigHits int + statusDoc int + statusSig int + delay time.Duration + modified bool +} + +func newFetchServer(t *testing.T) (*fetchServer, *httptest.Server) { + t.Helper() + pub, priv := makeKey(t) + fs := &fetchServer{ + t: t, + pub: pub, + priv: priv, + docBody: jsonObject(1, nil), + statusDoc: http.StatusOK, + statusSig: http.StatusOK, + etag: "v1", + } + fs.sigBody = signDoc(t, priv, fs.docBody) + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if fs.delay > 0 { + time.Sleep(fs.delay) + } + if strings.HasSuffix(r.URL.Path, ".sig") { + fs.sigHits++ + w.Header().Set("Content-Type", "text/plain") + if fs.statusSig != http.StatusOK { + w.WriteHeader(fs.statusSig) + return + } + _, _ = w.Write(fs.sigBody) + return + } + fs.docHits++ + if r.Header.Get("If-None-Match") == fs.etag && !fs.modified { + w.WriteHeader(http.StatusNotModified) + return + } + w.Header().Set("ETag", fs.etag) + if fs.statusDoc != http.StatusOK { + w.WriteHeader(fs.statusDoc) + return + } + _, _ = w.Write(fs.docBody) + })) + t.Cleanup(srv.Close) + return fs, srv +} + +func (fs *fetchServer) setVersion(v int64) { + fs.docBody = jsonObject(v, nil) + fs.sigBody = signDoc(fs.t, fs.priv, fs.docBody) + fs.etag = "v" + strconv.FormatInt(v, 10) + fs.modified = true +} + +// ----------------------------------------------------------------------------- + +// A good http fetch returns the document fresh. +func TestPullOverHTTPReturnsFreshDocument(t *testing.T) { + fs, srv := newFetchServer(t) + p := &Puller{ + URL: srv.URL + "/doc.json", + Keys: []ed25519.PublicKey{fs.pub}, + CacheDir: t.TempDir(), + } + res, err := p.Pull(t.Context()) + if err != nil { + t.Fatalf("Pull failed: %v", err) + } + if res.FromCache { + t.Fatal("fresh fetch reported FromCache") + } + if !res.Changed { + t.Fatal("first fetch reported Changed false") + } + if fs.docHits != 1 { + t.Fatalf("doc requested %d times, want 1", fs.docHits) + } +} + +// THE NETWORK IS SKIPPED WHEN THE CACHE IS YOUNG. A good copy cached less than +// TTL ago answers from the cache, and not one request is made. +func TestCacheYoungSkipsNetworkEntirely(t *testing.T) { + fs, srv := newFetchServer(t) + cacheDir := t.TempDir() + clock := time.Now() + p := &Puller{ + URL: srv.URL + "/doc.json", + Keys: []ed25519.PublicKey{fs.pub}, + CacheDir: cacheDir, + TTL: 10 * time.Minute, + Now: func() time.Time { return clock }, + } + // First fetch hits the network and caches. + if _, err := p.Pull(t.Context()); err != nil { + t.Fatal(err) + } + if fs.docHits != 1 { + t.Fatalf("first pull hit the doc %d times, want 1", fs.docHits) + } + // Second fetch is inside the TTL: no request at all. + clock = clock.Add(5 * time.Minute) + res, err := p.Pull(t.Context()) + if err != nil { + t.Fatalf("second pull failed: %v", err) + } + if !res.FromCache { + t.Fatal("a fetch inside TTL did not come from cache") + } + if res.Changed { + t.Fatal("a fetch inside TTL reported Changed") + } + if fs.docHits != 1 { + t.Fatalf("second pull hit the doc %d times, want 1", fs.docHits) + } +} + +// A TTL of zero makes no cached copy young: the source is always consulted. +func TestZeroTTLAlwaysConsultsSource(t *testing.T) { + fs, srv := newFetchServer(t) + cacheDir := t.TempDir() + p := &Puller{ + URL: srv.URL + "/doc.json", + Keys: []ed25519.PublicKey{fs.pub}, + CacheDir: cacheDir, + TTL: 0, + } + if _, err := p.Pull(t.Context()); err != nil { + t.Fatal(err) + } + if _, err := p.Pull(t.Context()); err != nil { + t.Fatal(err) + } + if fs.docHits != 2 { + t.Fatalf("two pulls with TTL 0 hit the doc %d times, want 2", fs.docHits) + } +} + +// ----------------------------------------------------------------------------- + +// Over http, a cached copy the source still has unchanged is asked for with +// If-None-Match; a 304 means the cached copy is the answer. +func TestHTTP304MeansCachedCopyIsAnswer(t *testing.T) { + fs, srv := newFetchServer(t) + cacheDir := t.TempDir() + p := &Puller{ + URL: srv.URL + "/doc.json", + Keys: []ed25519.PublicKey{fs.pub}, + CacheDir: cacheDir, + TTL: 0, // force a network consult + } + // First fetch caches with an ETag. + first, err := p.Pull(t.Context()) + if err != nil { + t.Fatal(err) + } + if first.Changed != true { + t.Fatal("first fetch should be Changed") + } + // Second fetch: the source answers 304 for the same ETag. + fs.modified = false + res, err := p.Pull(t.Context()) + if err != nil { + t.Fatalf("second pull failed: %v", err) + } + if !res.FromCache { + t.Fatal("a 304 did not produce FromCache") + } + if res.Changed { + t.Fatal("a 304 reported Changed") + } + if fs.docHits != 2 { + t.Fatalf("doc requested %d times, want 2 (both the GET and the 304)", fs.docHits) + } +} + +// A higher version is Changed true; an equal version is Changed false. +func TestChangedFlagReflectsVersion(t *testing.T) { + fs, srv := newFetchServer(t) + cacheDir := t.TempDir() + p := &Puller{ + URL: srv.URL + "/doc.json", + Keys: []ed25519.PublicKey{fs.pub}, + CacheDir: cacheDir, + TTL: 0, + } + if res, _ := p.Pull(t.Context()); !res.Changed { + t.Fatal("first fetch should be Changed") + } + // Same version again: Changed false, still a fetch (not 304, no etag match + // because modified flag default makes the server return 200). + fs.modified = false + if res, _ := p.Pull(t.Context()); res.Changed { + t.Fatal("same version reported Changed") + } + // Higher version: Changed true. + fs.setVersion(2) + fs.modified = true + res, _ := p.Pull(t.Context()) + if !res.Changed { + t.Fatal("higher version reported Changed false") + } + if res.Version != 2 { + t.Fatalf("Version = %d, want 2", res.Version) + } +} + +// ----------------------------------------------------------------------------- + +// A document larger than MaxDoc is refused rather than read to the end. +func TestDocumentLargerThanMaxDocIsRefused(t *testing.T) { + // Override MaxDoc locally by testing the reader directly, since the real + // MaxDoc is 4 MiB and writing 5 MiB of test data is wasteful. + data := make([]byte, MaxDoc+1) + for i := range data { + data[i] = 'a' + } + _, err := readLimited(strings.NewReader(string(data)), int64(len(data))) + if err == nil { + t.Fatal("a document larger than MaxDoc was read without error") + } + if !strings.Contains(err.Error(), "exceeds max") { + t.Fatalf("error does not name the size: %v", err) + } +} + +// ContentLength above MaxDoc is refused before a byte is read. +func TestContentLengthAboveMaxDocIsRefusedBeforeRead(t *testing.T) { + called := false + r := readerFunc{read: func(b []byte) (int, error) { + called = true + return 0, nil + }} + _, err := readLimited(r, int64(MaxDoc+1)) + if err == nil { + t.Fatal("a ContentLength above MaxDoc was not refused") + } + if called { + t.Fatal("the body was read despite ContentLength above MaxDoc") + } +} + +type readerFunc struct { + read func([]byte) (int, error) +} + +func (r readerFunc) Read(b []byte) (int, error) { return r.read(b) } + +// ----------------------------------------------------------------------------- + +// A bad signature falls back to the cache. +func TestBadSignatureFallsBackToCache(t *testing.T) { + fs, srv := newFetchServer(t) + cacheDir := t.TempDir() + p := &Puller{ + URL: srv.URL + "/doc.json", + Keys: []ed25519.PublicKey{fs.pub}, + CacheDir: cacheDir, + TTL: 0, + } + // First fetch caches. + if _, err := p.Pull(t.Context()); err != nil { + t.Fatal(err) + } + // Now serve a document signed by a different key. + _, otherPriv := makeKey(t) + fs.docBody = jsonObject(2, nil) + fs.sigBody = signDoc(t, otherPriv, fs.docBody) + fs.modified = true + res, err := p.Pull(t.Context()) + if err == nil { + t.Fatal("a bad signature returned no error") + } + if !res.FromCache { + t.Fatal("a bad signature did not fall back to cache") + } + if res.Version != 1 { + t.Fatalf("fallback Version = %d, want 1", res.Version) + } +} + +// A rollback — a version lower than the cached one — falls back to the cache. +func TestRollbackFallsBackToCache(t *testing.T) { + fs, srv := newFetchServer(t) + cacheDir := t.TempDir() + p := &Puller{ + URL: srv.URL + "/doc.json", + Keys: []ed25519.PublicKey{fs.pub}, + CacheDir: cacheDir, + TTL: 0, + } + // Cache version 5. + fs.setVersion(5) + if _, err := p.Pull(t.Context()); err != nil { + t.Fatal(err) + } + // Serve version 3, still signed correctly. + fs.setVersion(3) + fs.modified = true + res, err := p.Pull(t.Context()) + if err == nil { + t.Fatal("a rollback returned no error") + } + if !res.FromCache { + t.Fatal("a rollback did not fall back to cache") + } + if res.Version != 5 { + t.Fatalf("fallback Version = %d, want 5", res.Version) + } +} + +// A dead source falls back to the cache. +func TestDeadSourceFallsBackToCache(t *testing.T) { + fs, srv := newFetchServer(t) + cacheDir := t.TempDir() + p := &Puller{ + URL: srv.URL + "/doc.json", + Keys: []ed25519.PublicKey{fs.pub}, + CacheDir: cacheDir, + TTL: 0, + } + if _, err := p.Pull(t.Context()); err != nil { + t.Fatal(err) + } + // Kill the server. + srv.Close() + res, err := p.Pull(t.Context()) + if err == nil { + t.Fatal("a dead source returned no error") + } + if !res.FromCache { + t.Fatal("a dead source did not fall back to cache") + } +} + +// With no cached copy, a dead source returns a non-nil error and Doc nil. +func TestDeadSourceWithNoCacheReturnsErrorAndNilDoc(t *testing.T) { + pub, _ := makeKey(t) + p := &Puller{ + URL: "http://127.0.0.1:1/doc.json", // nothing listening + Keys: []ed25519.PublicKey{pub}, + CacheDir: t.TempDir(), + Budget: 200 * time.Millisecond, + } + res, err := p.Pull(t.Context()) + if err == nil { + t.Fatal("a dead source with no cache returned no error") + } + if res.Doc != nil { + t.Fatal("Doc should be nil with no cache") + } + if res.FromCache { + t.Fatal("FromCache should be false with no cache") + } +} + +// A document that is not JSON falls back to the cache. +func TestNonJSONDocumentFallsBackToCache(t *testing.T) { + fs, srv := newFetchServer(t) + cacheDir := t.TempDir() + p := &Puller{ + URL: srv.URL + "/doc.json", + Keys: []ed25519.PublicKey{fs.pub}, + CacheDir: cacheDir, + TTL: 0, + } + if _, err := p.Pull(t.Context()); err != nil { + t.Fatal(err) + } + // Serve non-JSON but sign it so it gets past the signature check. + bad := []byte("not json at all") + fs.docBody = bad + fs.sigBody = signDoc(t, fs.priv, bad) + fs.modified = true + res, err := p.Pull(t.Context()) + if err == nil { + t.Fatal("non-JSON returned no error") + } + if !res.FromCache { + t.Fatal("non-JSON did not fall back to cache") + } +} + +// A document that is not an object (an array) falls back to the cache. +func TestNonObjectDocumentFallsBackToCache(t *testing.T) { + fs, srv := newFetchServer(t) + cacheDir := t.TempDir() + p := &Puller{ + URL: srv.URL + "/doc.json", + Keys: []ed25519.PublicKey{fs.pub}, + CacheDir: cacheDir, + TTL: 0, + } + if _, err := p.Pull(t.Context()); err != nil { + t.Fatal(err) + } + arr := []byte(`[1,2,3]`) + fs.docBody = arr + fs.sigBody = signDoc(t, fs.priv, arr) + fs.modified = true + res, err := p.Pull(t.Context()) + if err == nil { + t.Fatal("non-object returned no error") + } + if !res.FromCache { + t.Fatal("non-object did not fall back to cache") + } +} + +// A document with no version falls back to the cache. +func TestDocumentWithNoVersionFallsBackToCache(t *testing.T) { + fs, srv := newFetchServer(t) + cacheDir := t.TempDir() + p := &Puller{ + URL: srv.URL + "/doc.json", + Keys: []ed25519.PublicKey{fs.pub}, + CacheDir: cacheDir, + TTL: 0, + } + if _, err := p.Pull(t.Context()); err != nil { + t.Fatal(err) + } + obj := []byte(`{"notversion":1}`) + fs.docBody = obj + fs.sigBody = signDoc(t, fs.priv, obj) + fs.modified = true + res, err := p.Pull(t.Context()) + if err == nil { + t.Fatal("no-version returned no error") + } + if !res.FromCache { + t.Fatal("no-version did not fall back to cache") + } +} + +// A document with a non-integer version falls back to the cache. +func TestDocumentWithNonIntegerVersionFallsBackToCache(t *testing.T) { + fs, srv := newFetchServer(t) + cacheDir := t.TempDir() + p := &Puller{ + URL: srv.URL + "/doc.json", + Keys: []ed25519.PublicKey{fs.pub}, + CacheDir: cacheDir, + TTL: 0, + } + if _, err := p.Pull(t.Context()); err != nil { + t.Fatal(err) + } + obj := []byte(`{"version":"five"}`) + fs.docBody = obj + fs.sigBody = signDoc(t, fs.priv, obj) + fs.modified = true + res, err := p.Pull(t.Context()) + if err == nil { + t.Fatal("non-integer version returned no error") + } + if !res.FromCache { + t.Fatal("non-integer version did not fall back to cache") + } +} + +// Extra top-level fields are none of this package's business. +func TestExtraFieldsArePassedThrough(t *testing.T) { + fs, srv := newFetchServer(t) + p := &Puller{ + URL: srv.URL + "/doc.json", + Keys: []ed25519.PublicKey{fs.pub}, + CacheDir: t.TempDir(), + } + extra := map[string]any{"min_client": "1.0", "submit": "https://example.com"} + fs.docBody = jsonObject(1, extra) + fs.sigBody = signDoc(t, fs.priv, fs.docBody) + fs.modified = true + res, err := p.Pull(t.Context()) + if err != nil { + t.Fatalf("extra fields caused an error: %v", err) + } + // The bytes handed back carry the extra fields. + var got map[string]any + if err := json.Unmarshal(res.Doc, &got); err != nil { + t.Fatal(err) + } + if _, ok := got["min_client"]; !ok { + t.Fatal("extra field min_client was stripped") + } +} + +// ----------------------------------------------------------------------------- + +// The cache lives in CacheDir and is written atomically. +func TestGoodFetchIsCachedOnDisk(t *testing.T) { + fs, srv := newFetchServer(t) + cacheDir := t.TempDir() + p := &Puller{ + URL: srv.URL + "/doc.json", + Keys: []ed25519.PublicKey{fs.pub}, + CacheDir: cacheDir, + } + if _, err := p.Pull(t.Context()); err != nil { + t.Fatal(err) + } + doc, err := os.ReadFile(filepath.Join(cacheDir, "doc.json")) + if err != nil { + t.Fatalf("doc.json not cached: %v", err) + } + if !bytesEqual(doc, fs.docBody) { + t.Fatal("cached doc does not match source") + } + if _, err := os.ReadFile(filepath.Join(cacheDir, "doc.sig")); err != nil { + t.Fatalf("doc.sig not cached: %v", err) + } + if _, err := os.ReadFile(filepath.Join(cacheDir, "meta.json")); err != nil { + t.Fatalf("meta.json not cached: %v", err) + } +} + +// Cached files have mode 0600. +func TestCacheFilesHaveMode0600(t *testing.T) { + fs, srv := newFetchServer(t) + cacheDir := t.TempDir() + p := &Puller{ + URL: srv.URL + "/doc.json", + Keys: []ed25519.PublicKey{fs.pub}, + CacheDir: cacheDir, + } + if _, err := p.Pull(t.Context()); err != nil { + t.Fatal(err) + } + for _, name := range []string{"doc.json", "doc.sig", "meta.json"} { + info, err := os.Stat(filepath.Join(cacheDir, name)) + if err != nil { + t.Fatalf("stat %s: %v", name, err) + } + if info.Mode().Perm() != 0o600 { + t.Fatalf("%s has mode %o, want 0600", name, info.Mode().Perm()) + } + } +} + +// CacheDir is made if it is missing. +func TestCacheDirIsMadeIfMissing(t *testing.T) { + fs, srv := newFetchServer(t) + cacheDir := filepath.Join(t.TempDir(), "nested", "cache") + p := &Puller{ + URL: srv.URL + "/doc.json", + Keys: []ed25519.PublicKey{fs.pub}, + CacheDir: cacheDir, + } + if _, err := p.Pull(t.Context()); err != nil { + t.Fatal(err) + } + if _, err := os.Stat(filepath.Join(cacheDir, "doc.json")); err != nil { + t.Fatalf("nested CacheDir was not made: %v", err) + } +} + +// A cache that does not read is no cache: corrupt bytes are treated as nothing +// cached, and the next good fetch replaces them. +func TestCorruptCacheIsTreatedAsEmpty(t *testing.T) { + fs, srv := newFetchServer(t) + cacheDir := t.TempDir() + // Write garbage into the cache. + writeFile(t, filepath.Join(cacheDir, "doc.json"), []byte("garbage")) + writeFile(t, filepath.Join(cacheDir, "doc.sig"), []byte("garbage")) + writeFile(t, filepath.Join(cacheDir, "meta.json"), []byte("{}")) + p := &Puller{ + URL: srv.URL + "/doc.json", + Keys: []ed25519.PublicKey{fs.pub}, + CacheDir: cacheDir, + } + res, err := p.Pull(t.Context()) + if err != nil { + t.Fatalf("a corrupt cache should not surface its own error: %v", err) + } + if res.FromCache { + t.Fatal("a corrupt cache was served as a cache hit") + } + if !res.Changed { + t.Fatal("replacing a corrupt cache should be Changed") + } +} + +// A cache whose signature no longer verifies under the current keys is no cache. +func TestCacheWithUntrustedSignatureIsTreatedAsEmpty(t *testing.T) { + _, oldPriv := makeKey(t) + fs, srv := newFetchServer(t) + cacheDir := t.TempDir() + // Cache a document signed by the old key. + doc := jsonObject(1, nil) + sig := signDoc(t, oldPriv, doc) + writeFile(t, filepath.Join(cacheDir, "doc.json"), doc) + writeFile(t, filepath.Join(cacheDir, "doc.sig"), sig) + writeFile(t, filepath.Join(cacheDir, "meta.json"), []byte(`{"version":1,"fetched_at":"2020-01-01T00:00:00Z"}`)) + // Pull with a different (untrusted) key set — the old signature must not + // verify, and the source is fetched fresh. + p := &Puller{ + URL: srv.URL + "/doc.json", + Keys: []ed25519.PublicKey{fs.pub}, + CacheDir: cacheDir, + } + res, err := p.Pull(t.Context()) + if err != nil { + t.Fatalf("untrusted cache should fall through silently: %v", err) + } + if res.FromCache { + t.Fatal("an untrusted cache was served") + } +} + +// ----------------------------------------------------------------------------- + +// An empty URL is an error. +func TestEmptyURLError(t *testing.T) { + p := &Puller{CacheDir: t.TempDir()} + _, err := p.Pull(t.Context()) + if err == nil { + t.Fatal("empty URL returned no error") + } +} + +// The whole call lives inside Budget. +func TestBudgetTimesOut(t *testing.T) { + fs, srv := newFetchServer(t) + fs.delay = 500 * time.Millisecond + p := &Puller{ + URL: srv.URL + "/doc.json", + Keys: []ed25519.PublicKey{fs.pub}, + Budget: 100 * time.Millisecond, + // no CacheDir so there is nothing to fall back to + } + _, err := p.Pull(t.Context()) + if err == nil { + t.Fatal("a call over budget returned no error") + } +} + +// The call also lives inside ctx, whichever ends first. +func TestContextCancelEndsCall(t *testing.T) { + fs, srv := newFetchServer(t) + fs.delay = 500 * time.Millisecond + ctx, cancel := contextWithTimeout(50 * time.Millisecond) + defer cancel() + p := &Puller{ + URL: srv.URL + "/doc.json", + Keys: []ed25519.PublicKey{fs.pub}, + Budget: 10 * time.Second, + } + _, err := p.Pull(ctx) + if err == nil { + t.Fatal("a cancelled context returned no error") + } +} + +// Pull never panics, even on a wrong-length key. +func TestPullNeverPanicsOnBadKey(t *testing.T) { + pub, priv := makeKey(t) + dir := t.TempDir() + doc := jsonObject(1, nil) + sig := signDoc(t, priv, doc) + writeFile(t, filepath.Join(dir, "doc.json"), doc) + writeFile(t, filepath.Join(dir, "doc.json.sig"), sig) + bad := ed25519.PublicKey(make([]byte, 5)) + p := &Puller{ + URL: filepath.Join(dir, "doc.json"), + Keys: []ed25519.PublicKey{bad, pub}, + CacheDir: t.TempDir(), + } + // Must not panic; the bad key is skipped and the good one verifies. + res, err := p.Pull(t.Context()) + if err != nil { + t.Fatalf("Pull with a bad key failed: %v", err) + } + if res.Version != 1 { + t.Fatalf("Version = %d, want 1", res.Version) + } +} + +// ----------------------------------------------------------------------------- + +// A 304 with no cached copy is a failure: the source claimed not-modified for +// something we never had. +func Test304WithNoCacheIsAnError(t *testing.T) { + pub, priv := makeKey(t) + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if strings.HasSuffix(r.URL.Path, ".sig") { + w.WriteHeader(http.StatusOK) + _, _ = w.Write(signDoc(t, priv, []byte{})) + return + } + w.WriteHeader(http.StatusNotModified) + })) + t.Cleanup(srv.Close) + p := &Puller{ + URL: srv.URL + "/doc.json", + Keys: []ed25519.PublicKey{pub}, + CacheDir: t.TempDir(), + } + _, err := p.Pull(t.Context()) + if err == nil { + t.Fatal("a 304 with no cache returned no error") + } +} + +// A non-200 status from the source falls back to the cache. +func TestNon200StatusFallsBackToCache(t *testing.T) { + fs, srv := newFetchServer(t) + cacheDir := t.TempDir() + p := &Puller{ + URL: srv.URL + "/doc.json", + Keys: []ed25519.PublicKey{fs.pub}, + CacheDir: cacheDir, + TTL: 0, + } + if _, err := p.Pull(t.Context()); err != nil { + t.Fatal(err) + } + fs.statusDoc = http.StatusInternalServerError + fs.modified = true + res, err := p.Pull(t.Context()) + if err == nil { + t.Fatal("a 500 returned no error") + } + if !res.FromCache { + t.Fatal("a 500 did not fall back to cache") + } +} + +// A signature fetch that fails falls back to the cache. +func TestSignatureFetchFailureFallsBackToCache(t *testing.T) { + fs, srv := newFetchServer(t) + cacheDir := t.TempDir() + p := &Puller{ + URL: srv.URL + "/doc.json", + Keys: []ed25519.PublicKey{fs.pub}, + CacheDir: cacheDir, + TTL: 0, + } + if _, err := p.Pull(t.Context()); err != nil { + t.Fatal(err) + } + fs.setVersion(2) + fs.modified = true + fs.statusSig = http.StatusNotFound + res, err := p.Pull(t.Context()) + if err == nil { + t.Fatal("a missing signature returned no error") + } + if !res.FromCache { + t.Fatal("a missing signature did not fall back to cache") + } +} + +// The cache stores the ETag and uses it on the next fetch. +func TestCacheStoresETag(t *testing.T) { + fs, srv := newFetchServer(t) + cacheDir := t.TempDir() + p := &Puller{ + URL: srv.URL + "/doc.json", + Keys: []ed25519.PublicKey{fs.pub}, + CacheDir: cacheDir, + TTL: 0, + } + if _, err := p.Pull(t.Context()); err != nil { + t.Fatal(err) + } + meta, err := os.ReadFile(filepath.Join(cacheDir, "meta.json")) + if err != nil { + t.Fatal(err) + } + if !strings.Contains(string(meta), "v1") { + t.Fatalf("meta.json does not contain the etag: %s", meta) + } +} + +// No temp files are left behind after a good cache write. +func TestAtomicWriteLeavesNoTempFiles(t *testing.T) { + fs, srv := newFetchServer(t) + cacheDir := t.TempDir() + p := &Puller{ + URL: srv.URL + "/doc.json", + Keys: []ed25519.PublicKey{fs.pub}, + CacheDir: cacheDir, + } + if _, err := p.Pull(t.Context()); err != nil { + t.Fatal(err) + } + entries, err := os.ReadDir(cacheDir) + if err != nil { + t.Fatal(err) + } + for _, e := range entries { + if strings.HasPrefix(e.Name(), ".") { + t.Fatalf("temp file left behind: %s", e.Name()) + } + } +} + +// A cache whose meta.json is missing is treated as empty. +func TestCacheWithMissingMetaIsTreatedAsEmpty(t *testing.T) { + fs, srv := newFetchServer(t) + cacheDir := t.TempDir() + // Write doc and sig but no meta. + doc := jsonObject(1, nil) + sig := signDoc(t, fs.priv, doc) + writeFile(t, filepath.Join(cacheDir, "doc.json"), doc) + writeFile(t, filepath.Join(cacheDir, "doc.sig"), sig) + p := &Puller{ + URL: srv.URL + "/doc.json", + Keys: []ed25519.PublicKey{fs.pub}, + CacheDir: cacheDir, + } + res, err := p.Pull(t.Context()) + if err != nil { + t.Fatalf("missing meta surfaced an error: %v", err) + } + if res.FromCache { + t.Fatal("a cache missing meta.json was served from cache") + } +} + +// A file source whose document is missing returns an error with no cache. +func TestFileSourceMissingReturnsError(t *testing.T) { + pub, _ := makeKey(t) + p := &Puller{ + URL: filepath.Join(t.TempDir(), "nope.json"), + Keys: []ed25519.PublicKey{pub}, + CacheDir: t.TempDir(), + } + _, err := p.Pull(t.Context()) + if err == nil { + t.Fatal("a missing file source returned no error") + } +} + +// A file source falls back to cache when the file goes away. +func TestFileSourceFallsBackWhenFileDisappears(t *testing.T) { + pub, priv := makeKey(t) + dir := t.TempDir() + doc := jsonObject(3, nil) + sig := signDoc(t, priv, doc) + path := filepath.Join(dir, "doc.json") + writeFile(t, path, doc) + writeFile(t, path+".sig", sig) + cacheDir := t.TempDir() + p := &Puller{ + URL: path, + Keys: []ed25519.PublicKey{pub}, + CacheDir: cacheDir, + TTL: 0, + } + if _, err := p.Pull(t.Context()); err != nil { + t.Fatal(err) + } + os.Remove(path) + res, err := p.Pull(t.Context()) + if err == nil { + t.Fatal("a missing file returned no error") + } + if !res.FromCache { + t.Fatal("a missing file did not fall back to cache") + } + if res.Version != 3 { + t.Fatalf("fallback Version = %d, want 3", res.Version) + } +} + +// A version equal to the cached one is good and Changed is false. +func TestEqualVersionIsGoodAndNotChanged(t *testing.T) { + fs, srv := newFetchServer(t) + cacheDir := t.TempDir() + p := &Puller{ + URL: srv.URL + "/doc.json", + Keys: []ed25519.PublicKey{fs.pub}, + CacheDir: cacheDir, + TTL: 0, + } + if _, err := p.Pull(t.Context()); err != nil { + t.Fatal(err) + } + // Serve the same version again with a fresh body (different object, same + // version number) so the 304 path is not taken. + fs.docBody = jsonObject(1, map[string]any{"note": "refreshed"}) + fs.sigBody = signDoc(t, fs.priv, fs.docBody) + fs.modified = true + res, err := p.Pull(t.Context()) + if err != nil { + t.Fatalf("equal version should be good: %v", err) + } + if res.Changed { + t.Fatal("equal version reported Changed true") + } +} + +// ----------------------------------------------------------------------------- + +// helpers + +func bytesEqual(a, b []byte) bool { + if len(a) != len(b) { + return false + } + for i := range a { + if a[i] != b[i] { + return false + } + } + return true +} + +// contextWithTimeout makes a context with a timeout, because t.Context() does +// not take one. +func contextWithTimeout(d time.Duration) (context.Context, func()) { + return context.WithTimeout(context.Background(), d) +} From 38701fe5bf92cfe2dead9c7e16b589a56d949a01 Mon Sep 17 00:00:00 2001 From: santoshkumarradha Date: Thu, 17 Sep 2026 02:13:18 -0400 Subject: [PATCH 030/173] pool: test the too-large fallback over http and fix the siglocation doc --- internal/pool/pull.go | 2 +- internal/pool/pull_test.go | 30 ++++++++++++++++++++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/internal/pool/pull.go b/internal/pool/pull.go index 389078e4ea..2f4ddcd9b2 100644 --- a/internal/pool/pull.go +++ b/internal/pool/pull.go @@ -244,7 +244,7 @@ func (p *Puller) client() *http.Client { return &http.Client{} } -// sigURL names the signature location: the URL or path with ".sig" appended to +// sigLocation names the signature location: the URL or path with ".sig" appended to // its text. A URL gets ".sig" on the end of the URL; a path gets ".sig" on the // end of the path. func (p *Puller) sigLocation() string { diff --git a/internal/pool/pull_test.go b/internal/pool/pull_test.go index cbccc16315..f5666feb09 100644 --- a/internal/pool/pull_test.go +++ b/internal/pool/pull_test.go @@ -454,6 +454,36 @@ type readerFunc struct { func (r readerFunc) Read(b []byte) (int, error) { return r.read(b) } +// A document too large over http falls back to the cache. +func TestTooLargeOverHTTPFallsBackToCache(t *testing.T) { + fs, srv := newFetchServer(t) + cacheDir := t.TempDir() + p := &Puller{ + URL: srv.URL + "/doc.json", + Keys: []ed25519.PublicKey{fs.pub}, + CacheDir: cacheDir, + TTL: 0, + } + if _, err := p.Pull(t.Context()); err != nil { + t.Fatal(err) + } + // Serve a document that is one byte too large. + big := make([]byte, MaxDoc+1) + for i := range big { + big[i] = 'a' + } + fs.docBody = big + fs.sigBody = signDoc(t, fs.priv, big) + fs.modified = true + res, err := p.Pull(t.Context()) + if err == nil { + t.Fatal("a too-large document returned no error") + } + if !res.FromCache { + t.Fatal("a too-large document did not fall back to cache") + } +} + // ----------------------------------------------------------------------------- // A bad signature falls back to the cache. From defc3b6246799da9be32bfec39563bcf608a7501 Mon Sep 17 00:00:00 2001 From: santoshkumarradha Date: Thu, 17 Sep 2026 02:15:28 -0400 Subject: [PATCH 031/173] pool: the cache-trust doc links Puller.Keys instead of naming it bare --- internal/pool/cache.go | 5 +++-- internal/pool/pull.go | 6 +++--- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/internal/pool/cache.go b/internal/pool/cache.go index 42e1d79edd..50f6aeb424 100644 --- a/internal/pool/cache.go +++ b/internal/pool/cache.go @@ -14,7 +14,7 @@ import ( // have mode 0600. // // A cache that does not read is no cache. Bytes that do not parse, or whose -// signature no longer verifies under Keys, are treated as if nothing were +// signature no longer verifies under [Puller.Keys], are treated as if nothing were // cached: no error of their own, no panic, and the next good fetch replaces // them. type cache struct { @@ -52,7 +52,8 @@ func (p *Puller) loadCache() (cache, bool) { return c, false } // THE CACHE IS NOT A TRUSTED STORE. A signature that no longer verifies - // under Keys means the cache is stale, not that the caller is owed an error. + // under [Puller.Keys] means the cache is stale, not that the caller is owed an + // error. if !Verify(doc, sig, p.Keys) { return c, false } diff --git a/internal/pool/pull.go b/internal/pool/pull.go index 2f4ddcd9b2..ee660f38da 100644 --- a/internal/pool/pull.go +++ b/internal/pool/pull.go @@ -21,8 +21,8 @@ // the error that says why the fresh one was refused. // // THE CACHE IS NOT A TRUSTED STORE. Bytes in [Puller.CacheDir] that do not -// parse, or whose signature no longer verifies under Keys, are treated as if -// nothing were cached: no error of their own, no panic, and the next good +// parse, or whose signature no longer verifies under [Puller.Keys], are treated +// as if nothing were cached: no error of their own, no panic, and the next good // fetch replaces them. The cache is a copy of a good fetch, never a source of // truth in its own right. package pull @@ -203,7 +203,7 @@ func (p *Puller) Pull(ctx context.Context) (Result, error) { // good answers whether the fetched bytes are a document this package will serve. // The document must be JSON, an object, and carry an integer "version"; its -// signature must verify under one of Keys; and its version may not be lower +// signature must verify under one of [Puller.Keys]; and its version may not be lower // than the one the cache already holds. It returns the version when it does. func (p *Puller) good(doc, sig []byte, cached cache) (int64, error) { if !Verify(doc, sig, p.Keys) { From d24414a168632c54357db267f0b1b95851616305 Mon Sep 17 00:00:00 2001 From: santoshkumarradha Date: Thu, 17 Sep 2026 03:06:18 -0400 Subject: [PATCH 032/173] pool/pull: the package sits in the directory its import path names Package pull was written into internal/pool, so the directory name did not match the package clause in it and the import path internal/pool/pull did not exist. The three files move into internal/pool/pull, beside the other four packages under internal/pool; nothing in them changes. Co-Authored-By: Claude Fable 5.1 --- internal/pool/{ => pull}/cache.go | 0 internal/pool/{ => pull}/pull.go | 0 internal/pool/{ => pull}/pull_test.go | 0 3 files changed, 0 insertions(+), 0 deletions(-) rename internal/pool/{ => pull}/cache.go (100%) rename internal/pool/{ => pull}/pull.go (100%) rename internal/pool/{ => pull}/pull_test.go (100%) diff --git a/internal/pool/cache.go b/internal/pool/pull/cache.go similarity index 100% rename from internal/pool/cache.go rename to internal/pool/pull/cache.go diff --git a/internal/pool/pull.go b/internal/pool/pull/pull.go similarity index 100% rename from internal/pool/pull.go rename to internal/pool/pull/pull.go diff --git a/internal/pool/pull_test.go b/internal/pool/pull/pull_test.go similarity index 100% rename from internal/pool/pull_test.go rename to internal/pool/pull/pull_test.go From 53605d9fb5d043767789a0b5dc4fe4e84b9d3430 Mon Sep 17 00:00:00 2001 From: santoshkumarradha Date: Thu, 17 Sep 2026 03:09:09 -0400 Subject: [PATCH 033/173] session: a repair round is marked by its caller, not by a named model MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The round's usage rows are seated high off Config.repairRound, which was read as "the builder was handed a model". The two come apart: the cascade may floor to the model the work is already on, and a node admitted without an id names nothing at all — either way a lifted round billed as ordinary work on the worker's seat, which is the fact the field exists to record. The caller that knows says so instead. Co-Authored-By: Claude Fable 5.1 --- internal/session/task_audit.go | 2 +- internal/session/task_merge_round.go | 2 +- internal/session/task_run.go | 23 ++++++++++++++--------- 3 files changed, 16 insertions(+), 11 deletions(-) diff --git a/internal/session/task_audit.go b/internal/session/task_audit.go index cf1cffebf7..759e8f82c9 100644 --- a/internal/session/task_audit.go +++ b/internal/session/task_audit.go @@ -1572,7 +1572,7 @@ func (a *Agent) repairNode(ctx context.Context, node *TaskNode, tree taskTree, v // through the rounds this is, and what the check said that sent it back. defer a.enterPhase(node, taskBeatRepairing, round, a.config.TaskRepairRounds, taskFindingLine(verdict.evidence))() - child, err := a.newTaskAgentOn(ctx, taskGroundDir(node, tree), node, fmt.Sprintf("-repair%d", round), a.repairTierModel(node, lift)) + child, err := a.newTaskAgentOn(ctx, taskGroundDir(node, tree), node, fmt.Sprintf("-repair%d", round), a.repairTierModel(node, lift), true) if err != nil { fmt.Fprintf(log, "repair %d: could not start a worker: %v\n", round, err) return nil, "" diff --git a/internal/session/task_merge_round.go b/internal/session/task_merge_round.go index 24fd9d9304..0354fe32e0 100644 --- a/internal/session/task_merge_round.go +++ b/internal/session/task_merge_round.go @@ -345,7 +345,7 @@ func (a *Agent) undoMergeRound(node *TaskNode, tree taskTree, log io.Writer) { // the instruction — this one is not being told what is missing from the work, it // is being told that two people wrote the same lines. func (a *Agent) runResolver(ctx context.Context, node *TaskNode, tree taskTree, home string, files, changed []string, log io.Writer) []string { - child, err := a.newTaskAgentOn(ctx, tree.dir, node, "-resolve", "") + child, err := a.newTaskAgentOn(ctx, tree.dir, node, "-resolve", "", false) if err != nil { fmt.Fprintf(log, "merge round: could not start a worker — %v\n", err) return nil diff --git a/internal/session/task_run.go b/internal/session/task_run.go index 64e5392445..e7d1de4869 100644 --- a/internal/session/task_run.go +++ b/internal/session/task_run.go @@ -7182,7 +7182,7 @@ func (a *Agent) spendLedger(node *TaskNode) *Agent { // has come back ([nodeMemory]). No store, no reflex, or a router that answered // nothing: the node opens with exactly the prompt it always did. func (a *Agent) newTaskAgent(ctx context.Context, dir string, node *TaskNode, suffix string) (*Agent, error) { - return a.newTaskAgentOn(ctx, dir, node, suffix, "") + return a.newTaskAgentOn(ctx, dir, node, suffix, "", false) } // newTaskAgentOn is [Agent.newTaskAgent] with the model said outright, and it @@ -7195,7 +7195,13 @@ func (a *Agent) newTaskAgent(ctx context.Context, dir string, node *TaskNode, su // is recorded as a bill rather than as a retarget — a repair round is one worker // among several a node takes, and a node whose row started naming the repair's // model would be telling a person their work moved when it did not. -func (a *Agent) newTaskAgentOn(ctx context.Context, dir string, node *TaskNode, suffix, on string) (*Agent, error) { +// AND `repair` IS THE CALLER SAYING WHICH KIND OF WORKER THIS IS, not something +// read off the model. It seats the round's usage rows high ([Agent.agentKind]), +// and it is stated rather than inferred from a named model because the two come +// apart: the cascade may floor to the model the work is already on, and a node +// that was admitted without an id has nothing to name at all — both of which +// would leave a round the ladder really did lift billing as ordinary work. +func (a *Agent) newTaskAgentOn(ctx context.Context, dir string, node *TaskNode, suffix, on string, repair bool) (*Agent, error) { // The model it is ACTUALLY on rather than the id it was admitted with, so a // second worker built for a node that was moved is built for where the node // now is ([TaskNode.runOn]). They are the same string for every node nothing @@ -7389,13 +7395,12 @@ func (a *Agent) newTaskAgentOn(ctx context.Context, dir string, node *TaskNode, // node hears about it while it happens: a card that would otherwise show // a task working says it is waiting instead. pacing: node.pacing, - // AND A NAMED MODEL IS THE REPAIR ROUND, which is the one thing this - // builder's only caller with a model says (task_audit.go's - // [Agent.repairNode]). The marker is what seats the round's rows high - // ([Agent.agentKind]); the crew role is deliberately left unset, because - // a round is a leaf's turns of work and erranding its lane role would - // re-price every call in it. - repairRound: on != "", + // AND THE REPAIR ROUND SAYS SO, from the caller that knows it is one + // (task_audit.go's [Agent.repairNode]). The marker is what seats the + // round's rows high ([Agent.agentKind]); the crew role is deliberately + // left unset, because a round is a leaf's turns of work and erranding + // its lane role would re-price every call in it. + repairRound: repair, // AND THE NODE'S PULSE TRAVELS THE SAME WAY, for the same reason: the // checker and each repair round are the same NODE working, so a reader // outside the process must see one heartbeat across all of them rather From 0843aeb3e99a1a3b104afdef93d5a759289a0f16 Mon Sep 17 00:00:00 2001 From: santoshkumarradha Date: Thu, 17 Sep 2026 03:09:44 -0400 Subject: [PATCH 034/173] pool/poolcfg: the host rule is pinned against the shapes that read like loopback acceptURL reads the parsed host rather than the text, so a loopback name in the userinfo and a loopback address used as a leading label are both refused already. Three rows say so, because a shape check written back against the raw text would take all three and nothing would have failed. The https and file branch answers with an empty address on a parse failure, as the http branch beside it does. Co-Authored-By: Claude Fable 5.1 --- internal/pool/poolcfg/poolcfg.go | 6 ++++-- internal/pool/poolcfg/poolcfg_test.go | 7 +++++++ 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/internal/pool/poolcfg/poolcfg.go b/internal/pool/poolcfg/poolcfg.go index a770cd0f1b..2a93160c6f 100644 --- a/internal/pool/poolcfg/poolcfg.go +++ b/internal/pool/poolcfg/poolcfg.go @@ -210,8 +210,10 @@ func acceptURL(value string) (string, bool) { lower := strings.ToLower(value) switch { case strings.HasPrefix(lower, "https://"), strings.HasPrefix(lower, "file://"): - _, err := url.Parse(value) - return value, err == nil + if _, err := url.Parse(value); err != nil { + return "", false + } + return value, true case strings.HasPrefix(lower, "http://"): u, err := url.Parse(value) if err != nil { diff --git a/internal/pool/poolcfg/poolcfg_test.go b/internal/pool/poolcfg/poolcfg_test.go index 02c88ef4df..b1852d4922 100644 --- a/internal/pool/poolcfg/poolcfg_test.go +++ b/internal/pool/poolcfg/poolcfg_test.go @@ -146,6 +146,13 @@ func TestResolveIndexURL(t *testing.T) { {"plain http elsewhere with port", "http://pool.example.com:8080", true, DefaultIndexURL, "default"}, {"http host that merely contains localhost", "http://localhost.example.com", true, DefaultIndexURL, "default"}, {"http loopback that is not 127.0.0.1", "http://127.0.0.2", true, DefaultIndexURL, "default"}, + // The host is the host the request would reach, so a loopback name in + // the userinfo or as a label of somebody else's name is not one. These + // rows pin the read: a shape check written against the raw text rather + // than the parsed host would take all three. + {"http loopback name in the userinfo", "http://localhost@pool.example.com", true, DefaultIndexURL, "default"}, + {"http loopback name and port in the userinfo", "http://localhost:80@pool.example.com", true, DefaultIndexURL, "default"}, + {"http loopback address as a leading label", "http://127.0.0.1.pool.example.com", true, DefaultIndexURL, "default"}, {"other scheme", "ftp://pool.example.com", true, DefaultIndexURL, "default"}, {"scheme without the double slash", "https:pool.example.com", true, DefaultIndexURL, "default"}, {"http without the double slash", "http:/localhost", true, DefaultIndexURL, "default"}, From d252388952cbd62079a3233ceae2877a1961e60b Mon Sep 17 00:00:00 2001 From: santoshkumarradha Date: Thu, 17 Sep 2026 03:10:24 -0400 Subject: [PATCH 035/173] docs/changes: the three new pool packages and the seat a usage row names Co-Authored-By: Claude Fable 5.1 --- ...ol-delivery-settings-and-fetch-packages.md | 29 +++++++++++++++++++ .../1093-usage-rows-name-their-seat.md | 24 +++++++++++++++ 2 files changed, 53 insertions(+) create mode 100644 docs/changes/unreleased/1093-pool-delivery-settings-and-fetch-packages.md create mode 100644 docs/changes/unreleased/1093-usage-rows-name-their-seat.md diff --git a/docs/changes/unreleased/1093-pool-delivery-settings-and-fetch-packages.md b/docs/changes/unreleased/1093-pool-delivery-settings-and-fetch-packages.md new file mode 100644 index 0000000000..da13d0e497 --- /dev/null +++ b/docs/changes/unreleased/1093-pool-delivery-settings-and-fetch-packages.md @@ -0,0 +1,29 @@ +--- +kind: added +title: Three more `internal/pool` packages — an outbox, a settings resolver and a signed-document fetcher +pr: 1093 +surface: [engine] +invalidates: [] +--- + +Standard library only, and nothing on a surface wired to them yet. + +- `internal/pool/outbox` keeps small measurement rows in a local + newline-delimited JSON file and hands them to a destination in batches, over + http or into a file. A row that arrived is marked sent and the mark outlives + the handle, so nothing is delivered twice; a batch that did not arrive leaves + its rows and every row after it pending, with an error naming what failed. + The file is capped from the old end, the payload is stored compacted, and a + caller never has a full disk or a dead destination raised at it as anything + but a returned error. +- `internal/pool/poolcfg` resolves how the pool behaves from one stored setting + and an environment handed in as a function, so there is no clock, no disk and + no reading of the process environment in it. Every field carries where its + value came from — `default`, `setting`, `env` or `ci` — and the addresses it + accepts are https, http on loopback only, `file://` and absolute paths. +- `internal/pool/pull` fetches a JSON document, checks a detached ed25519 + signature at the same location with `.sig` appended, and keeps the last good + copy under a cache directory so a run that cannot reach the source still has + an answer. A young cache skips the network entirely, an ETag turns into a + conditional request, a version lower than the cached one is refused, and every + failure hands back the cached copy beside a non-nil error. diff --git a/docs/changes/unreleased/1093-usage-rows-name-their-seat.md b/docs/changes/unreleased/1093-usage-rows-name-their-seat.md new file mode 100644 index 0000000000..10847f2cc2 --- /dev/null +++ b/docs/changes/unreleased/1093-usage-rows-name-their-seat.md @@ -0,0 +1,24 @@ +--- +kind: changed +title: Every usage row names the seat the call ran under +pr: 1093 +surface: [engine] +invalidates: + - "The usage ledger's header said a row could not answer which class of model took the money, and `UsageLine.Role` was carried by only a handful of auxiliary calls. A row now carries `seat` — one word of a closed vocabulary, `reflex`, `low`, `worker`, `high`, `mastermind` or `talk` — and `role` is written by every call that goes through the role registry, so a spend page can say whether the money went on the seat that does the work or the seat that thinks." + - "`session.UsageLine` gained a `Seat` field. It is `omitempty` like every additive field before it, so every row already in the file still decodes and reads back with an empty seat and an empty role, which is the truth about it." + - "`Agent.recordUsageLine` took five arguments — the usage, the model, the role, the lane facts and the reconciled flag. It takes the one `bankedCall` those came off, because the seat needs a sixth fact from the same value and a door with six arguments is a door somebody passes in the wrong order." +--- + +The seat is derived and never typed at a call site: an agent's own turns bill +to the seat its kind answers for — `talk` for a conversation, `worker` for a +task node, `high` for the checker and a repair round — and every other row +falls back to the tier the role registry already has for that role +(`session.SeatOfRole` is `roles.TierOf` followed by one hand-written table of +five tiers to five words). A call that resolved its model outside the registry +— a media pin, a document reader, a tool ask — is nobody's seat and stays +wordless rather than guessed, and no free text ever reaches the field. + +The seat names the chair, not the id that answered, so it stays true of a row +whose model was a fallback, a pin or a rescue. It is NOT the router's slot: +nothing in the program records which slot a call ran under, and a page that +labelled this column with those words would be inventing the join. From 8f27dfff3630ef2c28d286a835c443f79ba995c5 Mon Sep 17 00:00:00 2001 From: santoshkumarradha Date: Thu, 17 Sep 2026 03:16:25 -0400 Subject: [PATCH 036/173] pool/outbox: a marker retires one row, and only once it is on disk MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two holes behind the delivery guarantees. A marker was read as a NAME: a nonce is sixteen bytes from Outbox.Rand, which is the caller's field to set, so a source answering with the same bytes twice writes the same nonce twice and one sent marker then took every row sharing it — the rows a refused batch left behind, the row a trim was told to keep. Markers are only ever written for the oldest rows, so they are counted off the front instead. And a marker the file never took still emptied the pending set: the write was best effort, the drop was not, so a Close racing an in-flight Send left a batch reported delivered and pending again at the next Open. The drop now waits on the write. Co-Authored-By: Claude Fable 5.1 --- internal/pool/outbox/outbox.go | 94 ++++++++++++++++++---------- internal/pool/outbox/outbox_test.go | 95 ++++++++++++++++++++++++++--- 2 files changed, 149 insertions(+), 40 deletions(-) diff --git a/internal/pool/outbox/outbox.go b/internal/pool/outbox/outbox.go index 38f9ad19c1..adac02c22e 100644 --- a/internal/pool/outbox/outbox.go +++ b/internal/pool/outbox/outbox.go @@ -285,33 +285,39 @@ func (o *Outbox) trimLocked() { } dropped := make([]string, drop) copy(dropped, o.pend[:drop]) - o.writeMarksLocked(droppedMarks(dropped)) + if o.writeMarksLocked(droppedMarks(dropped)) != nil { + // The cap is kept by the markers on disk, so a cap this Append could + // not record is a cap that is not kept. The rows stay pending and the + // next Append tries the trim again, which is the same answer the + // failed row write itself gives. + return + } o.pend = append(o.pend[:0], o.pend[drop:]...) } // retire appends one marker per nonce and takes those rows out of the pending -// set. A marker write is best effort: a failed write leaves the rows pending, -// where the next Send tries them again rather than losing them. +// set. A marker that could not be written leaves its rows pending, where the +// next Send tries them again rather than losing them — which is why the drop +// happens only once the write has gone through. The alternative, a row gone +// from the pending set and present in the file, is a row that comes back at +// the next Open having been reported delivered. func (o *Outbox) retire(marks []mark) { - gone := make(map[string]bool, len(marks)) - for _, m := range marks { - if m.Sent != "" { - gone[m.Sent] = true - } - if m.Dropped != "" { - gone[m.Dropped] = true - } - } o.mu.Lock() defer o.mu.Unlock() - o.writeMarksLocked(marks) - o.dropPendingLocked(gone) + if o.writeMarksLocked(marks) != nil { + return + } + o.dropPendingLocked(marks) } -// writeMarksLocked appends one marker line per mark. The caller holds mu. -func (o *Outbox) writeMarksLocked(marks []mark) { - if o.f == nil || len(marks) == 0 { - return +// writeMarksLocked appends one marker line per mark, and answers whether the +// line reached the file. The caller holds mu. +func (o *Outbox) writeMarksLocked(marks []mark) error { + if len(marks) == 0 { + return nil + } + if o.f == nil { + return errors.New("outbox: outbox is closed") } buf := make([]byte, 0, 24*len(marks)) for _, m := range marks { @@ -322,33 +328,55 @@ func (o *Outbox) writeMarksLocked(marks []mark) { buf = append(buf, line...) buf = append(buf, '\n') } - o.f.Write(buf) // best effort; see retire + _, err := o.f.Write(buf) + return err } -// dropPendingLocked removes the named nonces from the pending list. The -// caller holds mu. -func (o *Outbox) dropPendingLocked(gone map[string]bool) { +// dropPendingLocked removes the marked rows from the pending list, ONE ROW PER +// MARKER and oldest first. A nonce is not an identity: [Outbox.Rand] is the +// caller's to set, and a source that answers with the same bytes twice writes +// the same nonce twice. Treating the marks as a set of names would then take +// every row sharing a name — the rows a failed batch left pending, the row a +// trim was told to keep — out of the outbox with it. Markers are only ever +// written for the oldest rows, so counting them off the front is what they +// mean. The caller holds mu. +func (o *Outbox) dropPendingLocked(marks []mark) { + gone := make(map[string]int, len(marks)) + for _, m := range marks { + if m.Sent != "" { + gone[m.Sent]++ + } + if m.Dropped != "" { + gone[m.Dropped]++ + } + } kept := o.pend[:0] for _, n := range o.pend { - if !gone[n] { - kept = append(kept, n) + if gone[n] > 0 { + gone[n]-- + continue } + kept = append(kept, n) } o.pend = kept } // load reads the outbox file and answers the rows still pending, in file // order. A line that does not parse, or that is not a row, is skipped, and a -// file that cannot be read answers as no rows at all. A row whose nonce a -// sent or dropped marker retires is left out, wherever the marker sits. The -// caller holds mu. +// file that cannot be read answers as no rows at all. The caller holds mu. +// +// EACH MARKER RETIRES ONE ROW, oldest first, wherever the marker sits — see +// [Outbox.dropPendingLocked] for why the count rather than the name: a nonce +// is what the caller's own [Outbox.Rand] made of sixteen bytes, and a reader +// that took a marker as a name would answer that a file holding two rows under +// one nonce and one marker holds nothing at all. func (o *Outbox) load() []Row { b, err := os.ReadFile(o.path) if err != nil { return nil } var rows []Row - retired := make(map[string]bool) + retired := make(map[string]int) for len(b) > 0 { line := b if i := bytes.IndexByte(b, '\n'); i >= 0 { @@ -362,11 +390,11 @@ func (o *Outbox) load() []Row { var m mark if json.Unmarshal(line, &m) == nil { if m.Sent != "" { - retired[m.Sent] = true + retired[m.Sent]++ continue } if m.Dropped != "" { - retired[m.Dropped] = true + retired[m.Dropped]++ continue } } @@ -378,9 +406,11 @@ func (o *Outbox) load() []Row { } kept := rows[:0] for _, r := range rows { - if !retired[r.Nonce] { - kept = append(kept, r) + if retired[r.Nonce] > 0 { + retired[r.Nonce]-- + continue } + kept = append(kept, r) } return kept } diff --git a/internal/pool/outbox/outbox_test.go b/internal/pool/outbox/outbox_test.go index e77a63913e..e8f0e345b7 100644 --- a/internal/pool/outbox/outbox_test.go +++ b/internal/pool/outbox/outbox_test.go @@ -142,17 +142,20 @@ func (p *postRecord) handle(w http.ResponseWriter, r *http.Request) { p.mu.Lock() p.bodies = append(p.bodies, string(b)) p.types = append(p.types, r.Header.Get("Content-Type")) - p.mu.Unlock() - if p.seconds > 0 { - time.Sleep(time.Duration(p.seconds) * time.Millisecond) - } + // The two knobs are read under the same lock the bodies are written + // under: the server answers on its own goroutine, and a handler reading a + // field the test writes is a race whether or not the requests overlap. + wait := time.Duration(p.seconds) * time.Millisecond + status := 200 if len(p.status) > 0 { - s := p.status[0] + status = p.status[0] p.status = p.status[1:] - w.WriteHeader(s) - return } - w.WriteHeader(200) + p.mu.Unlock() + if wait > 0 { + time.Sleep(wait) + } + w.WriteHeader(status) } func TestOpenRefusesAnEmptyPath(t *testing.T) { @@ -1013,3 +1016,79 @@ func TestAppendWithTheDefaultClockAndRand(t *testing.T) { } } } + +// A nonce is what the caller's own Rand made of sixteen bytes, so a source +// that answers with the same bytes twice writes the same nonce twice. Each +// marker still retires exactly one row. +func TestRowsSharingANonceAreRetiredOneMarkerAtATime(t *testing.T) { + o, _ := openOutbox(t) + o.Rand = bytes.NewReader(bytes.Repeat([]byte{3}, 4096)) + o.MaxPending = 3 + for i := 0; i < 5; i++ { + appendRow(t, o, fmt.Sprintf(`{"i":%d}`, i)) + } + pending := o.Pending() + if len(pending) != 3 { + t.Fatalf("pending = %d rows, want the three the cap leaves", len(pending)) + } + for i, r := range pending { + if want := fmt.Sprintf(`{"i":%d}`, i+2); string(r.Payload) != want { + t.Fatalf("pending[%d] = %s, want %s: the cap drops from the old end", i, r.Payload, want) + } + } +} + +// A partial send over a shared nonce leaves the rows it never reached, rather +// than retiring them with the batch that did arrive. +func TestAPartialSendOverASharedNonceKeepsTheRowsItDidNotReach(t *testing.T) { + post := &postRecord{} + srv := httptest.NewServer(http.HandlerFunc(post.handle)) + defer srv.Close() + + o, _ := openOutbox(t) + o.Rand = bytes.NewReader(bytes.Repeat([]byte{5}, 4096)) + o.Client = srv.Client() + for i := 0; i < 250; i++ { + appendRow(t, o, fmt.Sprintf(`{"i":%d}`, i)) + } + post.status = []int{200, 500} + + n, err := o.Send(context.Background(), srv.URL) + if err == nil { + t.Fatal("a refused second batch returned no error") + } + if n != 200 { + t.Fatalf("Send = %d, want the 200 rows the first batch carried", n) + } + if got := len(o.Pending()); got != 50 { + t.Fatalf("pending = %d, want the 50 rows the refused batch never delivered", got) + } +} + +// A marker that could not be written leaves its rows pending: a row gone from +// the pending set and present in the file is a row that comes back at the next +// Open having been reported delivered. +func TestASendWhoseMarkerCannotBeWrittenKeepsTheRowsPending(t *testing.T) { + dir := t.TempDir() + path := filepath.Join(dir, "outbox.jsonl") + o, err := Open(path) + if err != nil { + t.Fatalf("Open: %v", err) + } + defer o.Close() + appendRow(t, o, `{"i":0}`) + + sink := filepath.Join(dir, "sink.jsonl") + // The handle goes away between the delivery and the marker, which is what + // a Close racing an in-flight Send does. + o.Keep = func(json.RawMessage) bool { + o.Close() + return true + } + if _, err := o.Send(context.Background(), sink); err != nil { + t.Fatalf("Send: %v", err) + } + if got := len(o.Pending()); got != 1 { + t.Fatalf("pending = %d, want the row whose marker never reached the file", got) + } +} From 33cd7433f00ed2342c30391a2ca825c5d88d23f9 Mon Sep 17 00:00:00 2001 From: santoshkumarradha Date: Thu, 17 Sep 2026 03:16:25 -0400 Subject: [PATCH 037/173] pool/pull: the cache's version comes off the bytes, and a file source reads the context MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The cache is committed by three renames, and only the document and its signature were checked on the way back in: the version, the ETag and the fetch time were taken from a meta.json nothing binds to them. A run cut between the second rename and the third therefore read as a good cache carrying the PREVIOUS version, which hands a caller one document under another's number and, worse, checks the next fetch against the older one — the rollback the version floor exists to catch. The version is read from the bytes whose signature was just verified, and a metadata file that describes other bytes is no cache. And a filesystem source ignored the context it was handed, so a cancelled call answered with a fresh document and a nil error while the same call over http answered with the cancellation. Co-Authored-By: Claude Fable 5.1 --- internal/pool/pull/cache.go | 14 ++++++- internal/pool/pull/pull.go | 29 ++++++++++++-- internal/pool/pull/pull_test.go | 70 +++++++++++++++++++++++++++++++-- 3 files changed, 105 insertions(+), 8 deletions(-) diff --git a/internal/pool/pull/cache.go b/internal/pool/pull/cache.go index 50f6aeb424..040ca559cf 100644 --- a/internal/pool/pull/cache.go +++ b/internal/pool/pull/cache.go @@ -57,10 +57,22 @@ func (p *Puller) loadCache() (cache, bool) { if !Verify(doc, sig, p.Keys) { return c, false } + // AND THE VERSION COMES OFF THE DOCUMENT, not off the file beside it. The + // three files are committed by three renames, so a run cut between them + // leaves a doc and a signature that verify as a matching pair while + // meta.json still describes the copy before. Trusting that number would + // hand a caller one document's bytes under another's version, and — worse + // — would check the next fetch's version against the older one, which is + // the rollback the version floor exists to catch. A cache whose metadata + // does not describe the bytes it sits beside is a cache that does not read. + version, err := docVersion(doc) + if err != nil || version != m.Version { + return c, false + } c.doc = doc c.sig = sig c.etag = m.ETag - c.version = m.Version + c.version = version c.fetchedAt = m.FetchedAt c.ok = true return c, true diff --git a/internal/pool/pull/pull.go b/internal/pool/pull/pull.go index ee660f38da..525c4b9045 100644 --- a/internal/pool/pull/pull.go +++ b/internal/pool/pull/pull.go @@ -209,6 +209,21 @@ func (p *Puller) good(doc, sig []byte, cached cache) (int64, error) { if !Verify(doc, sig, p.Keys) { return 0, errors.New("pull: signature does not verify") } + version, err := docVersion(doc) + if err != nil { + return 0, err + } + if cached.ok && version < cached.version { + return 0, fmt.Errorf("pull: version %d is lower than cached %d", version, cached.version) + } + return version, nil +} + +// docVersion reads the "version" a document carries. It is the half of [good] +// that is about the bytes alone, split out because the cache reads it too: the +// version a cached copy is trusted for has to come from the bytes whose +// signature was just checked, not from a file beside them. +func docVersion(doc []byte) (int64, error) { var obj map[string]json.RawMessage if err := json.Unmarshal(doc, &obj); err != nil { return 0, fmt.Errorf("pull: document is not json: %w", err) @@ -224,9 +239,6 @@ func (p *Puller) good(doc, sig []byte, cached cache) (int64, error) { if err != nil { return 0, fmt.Errorf("pull: version is not an integer: %w", err) } - if cached.ok && version < cached.version { - return 0, fmt.Errorf("pull: version %d is lower than cached %d", version, cached.version) - } return version, nil } @@ -322,12 +334,23 @@ func (p *Puller) fetchSigHTTP(ctx context.Context) ([]byte, error) { // fetchFile reads the document and its signature from the filesystem. A path // may be written plain or as "file://" and then the path. +// The budget and ctx bound this read as they bound the network one. A local +// file is served in microseconds, so the deadline is not there to cut a slow +// read off; it is there so that Pull means the same thing whatever the URL +// names — a caller that handed in a cancelled context is answered with that +// cancellation and not with a fresh document. func (p *Puller) fetchFile(ctx context.Context) ([]byte, []byte, string, error) { + if err := ctx.Err(); err != nil { + return nil, nil, "", fmt.Errorf("pull: read document: %w", err) + } path := stripFile(p.URL) doc, err := readLimitedFile(path) if err != nil { return nil, nil, "", fmt.Errorf("pull: read document: %w", err) } + if err := ctx.Err(); err != nil { + return nil, nil, "", fmt.Errorf("pull: read signature: %w", err) + } sig, err := readLimitedFile(stripFile(p.sigLocation())) if err != nil { return nil, nil, "", fmt.Errorf("pull: read signature: %w", err) diff --git a/internal/pool/pull/pull_test.go b/internal/pool/pull/pull_test.go index f5666feb09..018ba073d0 100644 --- a/internal/pool/pull/pull_test.go +++ b/internal/pool/pull/pull_test.go @@ -864,7 +864,11 @@ func TestBudgetTimesOut(t *testing.T) { URL: srv.URL + "/doc.json", Keys: []ed25519.PublicKey{fs.pub}, Budget: 100 * time.Millisecond, - // no CacheDir so there is nothing to fall back to + // An empty directory rather than no directory at all: there is nothing + // to fall back to either way, and a blank CacheDir reads doc.json + // RELATIVE TO THE WORKING DIRECTORY, so the test would answer from + // whatever happened to sit beside the source file. + CacheDir: t.TempDir(), } _, err := p.Pull(t.Context()) if err == nil { @@ -879,9 +883,10 @@ func TestContextCancelEndsCall(t *testing.T) { ctx, cancel := contextWithTimeout(50 * time.Millisecond) defer cancel() p := &Puller{ - URL: srv.URL + "/doc.json", - Keys: []ed25519.PublicKey{fs.pub}, - Budget: 10 * time.Second, + URL: srv.URL + "/doc.json", + Keys: []ed25519.PublicKey{fs.pub}, + Budget: 10 * time.Second, + CacheDir: t.TempDir(), } _, err := p.Pull(ctx) if err == nil { @@ -1150,3 +1155,60 @@ func bytesEqual(a, b []byte) bool { func contextWithTimeout(d time.Duration) (context.Context, func()) { return context.WithTimeout(context.Background(), d) } + +// A cancelled context ends the call whatever the URL names. A file source is +// served in microseconds, so nothing here is about speed: it is about Pull +// meaning one thing on both roads. +func TestCancelEndsAFileSourceToo(t *testing.T) { + pub, priv := makeKey(t) + dir := t.TempDir() + doc := jsonObject(4, nil) + writeFile(t, filepath.Join(dir, "doc.json"), doc) + writeFile(t, filepath.Join(dir, "doc.json.sig"), signDoc(t, priv, doc)) + + ctx, cancel := contextWithTimeout(time.Hour) + cancel() + p := &Puller{ + URL: filepath.Join(dir, "doc.json"), + Keys: []ed25519.PublicKey{pub}, + CacheDir: t.TempDir(), + } + got, err := p.Pull(ctx) + if err == nil { + t.Fatal("a cancelled context read the file anyway") + } + if got.Doc != nil || got.Version != 0 || got.FromCache { + t.Fatalf("a cancelled call with no cache answered %+v", got) + } +} + +// A cache whose metadata does not describe the bytes beside it is no cache. +// The three files are committed by three renames, so this is what a run cut +// between them leaves: a document and a signature that verify as a pair, under +// the version of the copy before. +func TestACacheWhoseMetaDescribesOtherBytesIsNoCache(t *testing.T) { + pub, priv := makeKey(t) + cacheDir := t.TempDir() + doc := jsonObject(9, nil) + writeFile(t, filepath.Join(cacheDir, "doc.json"), doc) + writeFile(t, filepath.Join(cacheDir, "doc.sig"), signDoc(t, priv, doc)) + writeFile(t, filepath.Join(cacheDir, "meta.json"), + []byte(`{"etag":"","version":3,"fetched_at":"`+time.Now().UTC().Format(time.RFC3339Nano)+`"}`)) + + p := &Puller{ + URL: filepath.Join(t.TempDir(), "absent.json"), + Keys: []ed25519.PublicKey{pub}, + CacheDir: cacheDir, + TTL: time.Hour, + } + // A young cache would be answered from without a read; this one is not a + // cache at all, so the absent source is consulted and fails with nothing + // to fall back to. + got, err := p.Pull(t.Context()) + if err == nil { + t.Fatal("the torn cache was served") + } + if got.Doc != nil || got.Version != 0 || got.FromCache { + t.Fatalf("the torn cache answered %+v, want nothing at all", got) + } +} From 22a510007cb9739d054025febf67002360e78203 Mon Sep 17 00:00:00 2001 From: santoshkumarradha Date: Thu, 17 Sep 2026 03:09:04 -0400 Subject: [PATCH 038/173] crewpick: a candidate with some of its indexes is estimated, not dropped MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Read first, the old treatment: isCandidate demanded all three indexes and a prompt price, so a row publishing its intelligence index but not yet its coding or agentic one, or an older model with only a coding index, was no candidate at all. It never entered a seat's running, never cleared a floor, raised no ceiling in the maxima and never reached a crew — not scored zero, simply absent. The fixture's own deepseek row with intelligence but no coding or agentic index was dropped this way, every call. Now a candidate with at least one index present is scored on what it publishes. Each missing index is estimated from the ones present: a present index Y offers Y times the median of the missing index over Y among the call's candidates carrying both measured — the donors — and with two present indexes the mean of the two offers is kept. With fewer than three donors carrying a pair, the offer is equal standing instead, the missing index read at the present one's share of the pool's ceilings, and every estimate is capped at the pool's largest measured value of its index. Only a row with none of the three indexes, or no prompt price, stays out, as before. The estimate is never silent: Crew.Estimated marks, per seat, which of its model's indexes were scored by estimate, and a tie in bill and quality now runs to the crew on fewer estimated indexes before id order. The maxima, the floor, the vendor rule and the presets are untouched; an input with no missing index picks exactly what it picked before, and the estimate reads only its own call's candidates — no disk, no network, no clock, no randomness, the same front in any order. Co-Authored-By: codeaf --- internal/crewpick/crewpick.go | 236 ++++++++++++++++++--- internal/crewpick/crewpick_test.go | 325 +++++++++++++++++++++++++++-- 2 files changed, 513 insertions(+), 48 deletions(-) diff --git a/internal/crewpick/crewpick.go b/internal/crewpick/crewpick.go index f32a5030f2..35ba5c36aa 100644 --- a/internal/crewpick/crewpick.go +++ b/internal/crewpick/crewpick.go @@ -14,20 +14,42 @@ // refused outright when its high seat comes from the worker's own vendor. // The crews that no other crew beats on both bill and quality survive as the // front, sorted by bill; Presets and AtKnob read picks off that front. +// +// A candidate that publishes some but not all of its three indexes is scored +// on the ones it publishes: each missing index is estimated from the call's +// candidates that publish both it and one of its measured ones — the donors — +// never above the call's largest measured value of that index, and the +// result says which of a pick's indexes were estimated. A candidate that +// publishes none of its indexes, or no price at all, is out of the running. package crewpick import ( "math" + "slices" "sort" "strings" ) +// An Index is one of the three capability indexes quality is read from — +// intelligence, coding, agentic — in the order the seat weights and a +// candidate's reading carry them. +type Index int + +const ( + Intelligence Index = iota + Coding + Agentic +) + // A Candidate is one model up for a seat. Prices are dollars per 1M tokens. // -// A candidate missing any of the three indexes — the zero value, which is how -// an unpublished index reads — or carrying a zero prompt price is not a +// A candidate carrying none of the three indexes — zero values, which is how +// unpublished indexes read — or carrying a zero prompt price is not a // candidate at all: it never occupies a seat, never clears a floor, and -// raises no ceiling. A model that publishes no cache-read price says so with +// raises no ceiling. A candidate carrying some but not all of its indexes is +// scored on the ones it publishes, each missing one estimated from the +// donors among its fellow candidates, and the result says which of a pick's +// indexes those were. A model that publishes no cache-read price says so with // HasCacheRead false and pays the prompt price on the cache share too. type Candidate struct { // ID names the candidate; the text before its first slash is the vendor. @@ -142,6 +164,11 @@ type Crew struct { Mastermind string Bill float64 Quality float64 + // Estimated marks, for the model in each seat, which of its indexes + // were scored by estimate rather than measurement: the row is the seat + // and the column the index. A crew picked on measured indexes alone is + // all false. + Estimated [3][3]bool } // Floor is the share of a seat's best quality in the family that a candidate @@ -165,15 +192,17 @@ func SeatCost(c Candidate, shape SeatShape) float64 { // SeatQuality is the candidate's quality for the seat on a 0-100 scale: each // index counted against the pool's best for that index and weighted by the // seat's weights. The pool sets the scale, so the same candidate scores -// differently in a stronger pool. A row that is not a candidate at all, and -// any row against a pool that has no candidates to raise a scale, score +// differently in a stronger pool. A missing index is estimated from the +// pool's donors exactly as in Front. A row that is not a candidate at all, +// and any row against a pool with no candidates to raise a scale, score // zero. func SeatQuality(c Candidate, shape SeatShape, pool []Candidate) float64 { if !isCandidate(c) { return 0 } - maxI, maxC, maxA := maxima(pool) - return quality(c, shape, maxI, maxC, maxA) + scale := maxima(pool) + r := fill(c, readTally(pool), scale) + return quality(r.idx, shape, scale) } // Front returns the pareto front of (bill, quality) over every crew the @@ -185,13 +214,19 @@ func SeatQuality(c Candidate, shape SeatShape, pool []Candidate) float64 { // first, then Floor against that seat's best in the family — and a crew is // refused while its high seat comes from the worker's own vendor, so the two // seats that see the same work never share one shop. Ties in (bill, quality) -// break by id order — worker, then high, then mastermind — which is what -// makes the front a property of the candidates rather than of their order. +// break to the crew scored on fewer estimated indexes, and ties past that by +// id order — worker, then high, then mastermind — which is what makes the +// front a property of the candidates rather than of their order. func Front(candidates []Candidate, shapes map[Seat]SeatShape, fam Family) []Crew { - maxI, maxC, maxA := maxima(candidates) - workers := shortlist(candidates, shapes[Worker], fam, maxI, maxC, maxA) - highs := shortlist(candidates, shapes[High], fam, maxI, maxC, maxA) - minds := shortlist(candidates, shapes[Mastermind], fam, maxI, maxC, maxA) + scale := maxima(candidates) + t := readTally(candidates) + readings := make([]reading, 0, len(candidates)) + for _, c := range candidates { + readings = append(readings, fill(c, t, scale)) + } + workers := shortlist(readings, shapes[Worker], fam, scale) + highs := shortlist(readings, shapes[High], fam, scale) + minds := shortlist(readings, shapes[Mastermind], fam, scale) vw := shapes[Worker].Volume vh := shapes[High].Volume vm := shapes[Mastermind].Volume @@ -209,6 +244,7 @@ func Front(candidates []Candidate, shapes map[Seat]SeatShape, fam Family) []Crew Mastermind: m.c.ID, Bill: vw*w.cost + vh*h.cost + vm*m.cost, Quality: (w.quality + h.quality + m.quality) / 3, + Estimated: [3][3]bool{w.est, h.est, m.est}, }) } } @@ -220,7 +256,10 @@ func Front(candidates []Candidate, shapes map[Seat]SeatShape, fam Family) []Crew if crews[i].Bill != crews[j].Bill { return crews[i].Bill < crews[j].Bill } - return crews[i].Quality > crews[j].Quality + if crews[i].Quality != crews[j].Quality { + return crews[i].Quality > crews[j].Quality + } + return estimateCount(crews[i]) < estimateCount(crews[j]) }) var front []Crew best := math.Inf(-1) @@ -286,9 +325,12 @@ func knee(front []Crew) Crew { return pick } -// A seatPick is a candidate already priced and scored for one seat. +// A seatPick is a candidate already priced and scored for one seat, on the +// candidate's reading: measured and estimated indexes alike. type seatPick struct { c Candidate + idx [3]float64 + est [3]bool cost float64 quality float64 } @@ -296,14 +338,14 @@ type seatPick struct { // shortlist returns a seat's running: the candidates that pass family and the // seat's needs, kept when their quality reaches Floor of the family's best // for the seat, in id order. -func shortlist(candidates []Candidate, shape SeatShape, fam Family, maxI, maxC, maxA float64) []seatPick { +func shortlist(readings []reading, shape SeatShape, fam Family, scale [3]float64) []seatPick { var running []seatPick top := 0.0 - for _, c := range candidates { - if !eligible(c, shape, fam) { + for _, r := range readings { + if !eligible(r.c, shape, fam) { continue } - pick := seatPick{c: c, cost: SeatCost(c, shape), quality: quality(c, shape, maxI, maxC, maxA)} + pick := seatPick{c: r.c, idx: r.idx, est: r.est, cost: SeatCost(r.c, shape), quality: quality(r.idx, shape, scale)} running = append(running, pick) top = math.Max(top, pick.quality) } @@ -317,34 +359,160 @@ func shortlist(candidates []Candidate, shape SeatShape, fam Family, maxI, maxC, return kept } -// quality is SeatQuality against pre-read maxima. -func quality(c Candidate, shape SeatShape, maxI, maxC, maxA float64) float64 { - if maxI <= 0 || maxC <= 0 || maxA <= 0 { +// quality is SeatQuality against pre-read maxima, on a candidate's reading. +func quality(idx [3]float64, shape SeatShape, scale [3]float64) float64 { + if scale[Intelligence] <= 0 || scale[Coding] <= 0 || scale[Agentic] <= 0 { return 0 } w := shape.Weights - return 100 * (w[0]*c.Intelligence/maxI + w[1]*c.Coding/maxC + w[2]*c.Agentic/maxA) + return 100 * (w[Intelligence]*idx[Intelligence]/scale[Intelligence] + + w[Coding]*idx[Coding]/scale[Coding] + + w[Agentic]*idx[Agentic]/scale[Agentic]) +} + +// maxima reads the pool's largest measured value of each index — the scale +// quality is measured against and the ceiling an estimate never crosses. +// Only real candidates raise a ceiling, and a row that publishes only some +// of its indexes raises only those. +func maxima(pool []Candidate) (scale [3]float64) { + for _, c := range pool { + if !isCandidate(c) { + continue + } + v := measured(c) + for i, val := range v { + scale[i] = math.Max(scale[i], val) + } + } + return scale +} + +// measured is the candidate's three indexes as it published them: an +// unpublished index reads as zero. +func measured(c Candidate) [3]float64 { + return [3]float64{c.Intelligence, c.Coding, c.Agentic} +} + +// A tally is one call's donor statistics: for every ordered pair of indexes, +// how many candidates — the donors — carry both measured, and the median of +// the first over the second among them. A missing index is estimated from +// these. +type tally struct { + count [3][3]int + median [3][3]float64 } -// maxima reads the pool's best value of each index — the scale quality is -// measured against. Only real candidates raise a ceiling. -func maxima(pool []Candidate) (maxI, maxC, maxA float64) { +// readTally reads the pool's tally; the donors of a pair of indexes are the +// candidates carrying both measured. +func readTally(pool []Candidate) tally { + var ratios [3][3][]float64 for _, c := range pool { if !isCandidate(c) { continue } - maxI = math.Max(maxI, c.Intelligence) - maxC = math.Max(maxC, c.Coding) - maxA = math.Max(maxA, c.Agentic) + v := measured(c) + for _, x := range [3]Index{Intelligence, Coding, Agentic} { + for _, y := range [3]Index{Intelligence, Coding, Agentic} { + if x != y && v[x] > 0 && v[y] > 0 { + ratios[x][y] = append(ratios[x][y], v[x]/v[y]) + } + } + } + } + var t tally + for _, x := range [3]Index{Intelligence, Coding, Agentic} { + for _, y := range [3]Index{Intelligence, Coding, Agentic} { + t.count[x][y] = len(ratios[x][y]) + if len(ratios[x][y]) >= 3 { + t.median[x][y] = medianOf(ratios[x][y]) + } + } + } + return t +} + +// medianOf is the middle of the values in order, or the mean of the two +// middles when there is no single middle. +func medianOf(values []float64) float64 { + slices.Sort(values) + n := len(values) + if n%2 == 1 { + return values[n/2] + } + return (values[n/2-1] + values[n/2]) / 2 +} + +// A reading is a candidate's three indexes ready for scoring — the measured +// ones as published, the missing ones estimated from the call's donors — +// with est remembering which of the three were estimated rather than +// measured. +type reading struct { + c Candidate + idx [3]float64 + est [3]bool +} + +// fill reads the candidate's three indexes for scoring. A measured index is +// taken as published; a missing one is estimated from the ones present: each +// present index Y offers Y times the tally's median of the missing index over +// Y among the donors carrying both — or, when fewer than three donors carry +// the pair, equal standing, the missing index read where its present one +// stands, X over the pool's largest X equal to Y over the pool's largest Y — +// and the mean of the offers is kept, never above the pool's largest measured +// value of the missing index. A row that is no candidate at all fills to +// zeros. +func fill(c Candidate, t tally, scale [3]float64) reading { + var r reading + if !isCandidate(c) { + return r + } + r.c = c + v := measured(c) + r.idx = v + for _, x := range [3]Index{Intelligence, Coding, Agentic} { + if v[x] > 0 { + continue + } + var offers []float64 + for _, y := range [3]Index{Intelligence, Coding, Agentic} { + if y == x || v[y] <= 0 { + continue + } + if t.count[x][y] >= 3 { + offers = append(offers, v[y]*t.median[x][y]) + } else { + offers = append(offers, v[y]/scale[y]*scale[x]) + } + } + sum := 0.0 + for _, offer := range offers { + sum += offer + } + r.idx[x] = math.Min(sum/float64(len(offers)), scale[x]) + r.est[x] = true + } + return r +} + +// estimateCount reads how many of a crew's picks were scored on estimated +// indexes — the tiebreak between crews that tie on bill and quality. +func estimateCount(c Crew) int { + n := 0 + for _, seat := range c.Estimated { + for _, est := range seat { + if est { + n++ + } + } } - return maxI, maxC, maxA + return n } -// isCandidate reports whether a row is a candidate at all: all three indexes -// present and a prompt price above zero. Everything downstream — -// eligibility, the floor, the maxima — starts from this. +// isCandidate reports whether a row is a candidate at all: at least one of +// the three indexes present and a prompt price above zero. Everything +// downstream — eligibility, the floor, the maxima — starts from this. func isCandidate(c Candidate) bool { - return c.Intelligence > 0 && c.Coding > 0 && c.Agentic > 0 && c.PromptPrice > 0 + return (c.Intelligence > 0 || c.Coding > 0 || c.Agentic > 0) && c.PromptPrice > 0 } // eligible reports whether a candidate may sit the seat: family first, then diff --git a/internal/crewpick/crewpick_test.go b/internal/crewpick/crewpick_test.go index 9d19c43c39..ed59e54146 100644 --- a/internal/crewpick/crewpick_test.go +++ b/internal/crewpick/crewpick_test.go @@ -2,6 +2,7 @@ package crewpick import ( "encoding/json" + "fmt" "math" "os" "slices" @@ -10,9 +11,9 @@ import ( // A fixture row in its own field names: a null index reads as zero — // missing, the way the package reads it — and a null cache-read price reads -// as unpublished. The rows with unpublished indexes and unpublished cache -// prices stay in the fixture, so the exclusion rules have something to bite -// on. +// as unpublished. The partially published row and the rows with unpublished +// cache prices stay in the fixture, so the estimate and exclusion rules +// have something to bite on. type row struct { ID string `json:"id"` Open bool `json:"open"` @@ -80,6 +81,19 @@ func model(id string) Candidate { } } +// donors builds n fully measured candidates that can sit no seat — a window +// too small for any of them — so they feed the estimates and the ceilings +// and nothing else. +func donors(prefix string, n int) []Candidate { + rows := make([]Candidate, 0, n) + for i := range n { + d := model(fmt.Sprintf("%s/d%d", prefix, i)) + d.Context = 1000 + rows = append(rows, d) + } + return rows +} + // wantCrew checks a pick against its worker|high|mastermind line. func wantCrew(t *testing.T, name string, got Crew, want string) { t.Helper() @@ -185,27 +199,29 @@ func TestTheFloorDropsAModelThatCannotDoTheJob(t *testing.T) { } } -// A row missing an index or priced at zero is not a candidate at all: it -// never reaches a seat and it raises no ceiling. Both rules bite where the -// floor cannot — the missing coding index leaves the mastermind seat its -// full quality, and a free model clears any floor. -func TestARowMissingAnIndexOrPricedAtZeroIsNoCandidate(t *testing.T) { +// A row carrying none of its three indexes, or priced at zero, is not a +// candidate at all: it never reaches a seat and it raises no ceiling. Both +// rules bite where the floor cannot — a free model would clear any floor, +// and a bare row carries no index to estimate from. +func TestARowWithNoIndexesAtAllOrNoPriceIsNoCandidate(t *testing.T) { good := []Candidate{model("a/m"), model("c/h")} good[1].Intelligence = 90 good[1].PromptPrice = 2 good[1].CompletionPrice = 8 good[1].CacheReadPrice = 0.2 - broken := model("d/x") - broken.Coding = 0 - broken.PromptPrice = 0.01 - broken.CompletionPrice = 0.02 - broken.HasCacheRead = false + bare := model("d/x") + bare.Intelligence = 0 + bare.Coding = 0 + bare.Agentic = 0 + bare.PromptPrice = 0.01 + bare.CompletionPrice = 0.02 + bare.HasCacheRead = false free := model("e/y") free.PromptPrice = 0 - pool := append(slices.Clone(good), broken, free) + pool := append(slices.Clone(good), bare, free) front := Front(pool, DefaultShapes(), All) for _, crew := range front { for _, id := range []string{crew.Worker, crew.High, crew.Mastermind} { @@ -221,6 +237,287 @@ func TestARowMissingAnIndexOrPricedAtZeroIsNoCandidate(t *testing.T) { } } +// A model publishing only its intelligence index — a release scored in +// stages — is scored on it, with the coding and agentic indexes estimated +// from the donors, and at a hundredth of the price it takes the worker seat. +func TestAModelWithOnlyAnIntelligenceIndexIsChosen(t *testing.T) { + pool := []Candidate{model("a/w"), model("c/h")} + newRelease := model("b/x") + newRelease.Coding = 0 + newRelease.Agentic = 0 + newRelease.PromptPrice = 0.01 + newRelease.CompletionPrice = 0.02 + newRelease.HasCacheRead = false + pool = append(pool, newRelease) + pool = append(pool, donors("v", 3)...) + + front := Front(pool, DefaultShapes(), All) + if len(front) == 0 { + t.Fatal("the pool must field crews") + } + frugal, _, _ := Presets(front) + if frugal.Worker != "b/x" { + t.Fatalf("frugal worker = %s, want the partially scored b/x", frugal.Worker) + } + want := [3][3]bool{{false, true, true}, {}, {false, true, true}} + if frugal.Estimated != want { + t.Fatalf("the b/x crew's Estimated = %v, want %v", frugal.Estimated, want) + } +} + +// A model publishing only its coding index — many an older model — is +// scored on it, with the intelligence and agentic indexes estimated, and at +// a hundredth of the price it takes the worker seat too. +func TestAModelWithOnlyACodingIndexIsChosen(t *testing.T) { + pool := []Candidate{model("a/w"), model("c/h")} + older := model("b/x") + older.Intelligence = 0 + older.Agentic = 0 + older.PromptPrice = 0.01 + older.CompletionPrice = 0.02 + older.HasCacheRead = false + pool = append(pool, older) + pool = append(pool, donors("v", 3)...) + + front := Front(pool, DefaultShapes(), All) + if len(front) == 0 { + t.Fatal("the pool must field crews") + } + frugal, _, _ := Presets(front) + if frugal.Worker != "b/x" { + t.Fatalf("frugal worker = %s, want the partially scored b/x", frugal.Worker) + } + want := [3][3]bool{{true, false, true}, {}, {true, false, true}} + if frugal.Estimated != want { + t.Fatalf("the b/x crew's Estimated = %v, want %v", frugal.Estimated, want) + } +} + +// The donor estimate is the median of the missing index over the present +// one among the donors carrying both — not the mean, which the far-out +// fourth donor drags away — and the rows missing either index of the pair +// never donate. The row missing its intelligence index still raises the +// coding ceiling with its own 400. +func TestTheDonorEstimateIsTheMedianRatio(t *testing.T) { + shape := SeatShape{Weights: [3]float64{0, 1, 0}} + pool := []Candidate{ + {ID: "a/d1", Intelligence: 10, Coding: 10, Agentic: 10, PromptPrice: 1}, + {ID: "a/d2", Intelligence: 10, Coding: 20, Agentic: 10, PromptPrice: 1}, + {ID: "a/d3", Intelligence: 10, Coding: 30, Agentic: 10, PromptPrice: 1}, + {ID: "a/d4", Intelligence: 10, Coding: 300, Agentic: 10, PromptPrice: 1}, + {ID: "b/noc", Intelligence: 10, Agentic: 10, PromptPrice: 1}, + {ID: "b/noi", Coding: 400, Agentic: 10, PromptPrice: 1}, + } + x := Candidate{ID: "c/x", Intelligence: 10, PromptPrice: 1} + + // The donors' coding over intelligence ratios run 1, 2, 3, 30: the + // median reads 2.5 and the estimate 25 of a 400 ceiling, where the mean + // would read 9 and the estimate 90. + if got := SeatQuality(x, shape, pool); got != 6.25 { + t.Fatalf("SeatQuality on the median estimate = %v, want 6.25", got) + } +} + +// With fewer than three donors carrying a pair, the estimate falls back to +// equal standing — the missing index read at the present one's share of +// the pool's ceilings — not to the median of two donors, and not to +// exclusion. +func TestFewerThanThreeDonorsEstimatesByEqualStanding(t *testing.T) { + shape := SeatShape{Weights: [3]float64{0, 1, 0}} + pool := []Candidate{ + {ID: "a/d1", Intelligence: 100, Coding: 50, Agentic: 10, PromptPrice: 1}, + {ID: "a/d2", Intelligence: 50, Coding: 100, Agentic: 10, PromptPrice: 1}, + } + x := Candidate{ID: "b/x", Intelligence: 80, PromptPrice: 1} + + // Equal standing reads x's coding at x's share of the intelligence + // ceiling, 80 of 100; the two donors' median would read 100. + if got := SeatQuality(x, shape, pool); got != 80 { + t.Fatalf("SeatQuality on equal standing = %v, want 80", got) + } +} + +// An estimate never crosses the pool's largest measured value of its +// index: the donors' coding over intelligence median runs 20, and 50 times +// that would read 1000 of a 300 ceiling. +func TestAnEstimateIsCappedAtTheLargestMeasuredIndex(t *testing.T) { + shape := SeatShape{Weights: [3]float64{0, 1, 0}} + pool := []Candidate{ + {ID: "a/d1", Intelligence: 10, Coding: 100, Agentic: 10, PromptPrice: 1}, + {ID: "a/d2", Intelligence: 10, Coding: 200, Agentic: 10, PromptPrice: 1}, + {ID: "a/d3", Intelligence: 10, Coding: 300, Agentic: 10, PromptPrice: 1}, + } + x := Candidate{ID: "b/x", Intelligence: 50, PromptPrice: 1} + + if got := SeatQuality(x, shape, pool); got != 100 { + t.Fatalf("SeatQuality on the capped estimate = %v, want 100", got) + } +} + +// The estimate is never silent: the crew says, for the model in each seat, +// which of its three indexes were scored by estimate and which measured. +// The worker publishes only its intelligence index and the mastermind only +// its coding one; the high seat is measured throughout. +func TestTheResultMarksEstimatedIndexes(t *testing.T) { + pool := []Candidate{model("a/w"), model("c/h")} + worker := model("b/x") + worker.Coding = 0 + worker.Agentic = 0 + worker.Images = false + worker.PromptPrice = 0.01 + worker.CompletionPrice = 0.02 + worker.HasCacheRead = false + mind := model("d/m") + mind.Intelligence = 0 + mind.Agentic = 0 + mind.Tools = false + mind.PromptPrice = 0.005 + mind.CompletionPrice = 0.01 + mind.HasCacheRead = false + pool = append(pool, worker, mind) + pool = append(pool, donors("v", 3)...) + + front := Front(pool, DefaultShapes(), All) + if len(front) == 0 { + t.Fatal("the pool must field crews") + } + frugal, _, _ := Presets(front) + if frugal.Worker != "b/x" || frugal.Mastermind != "d/m" { + t.Fatalf("frugal = %s|%s|%s, want b/x on the worker seat and d/m on the mastermind's", frugal.Worker, frugal.High, frugal.Mastermind) + } + want := [3][3]bool{{false, true, true}, {}, {true, false, true}} + if frugal.Estimated != want { + t.Fatalf("the crew's Estimated = %v, want %v", frugal.Estimated, want) + } +} + +// Two candidates for a seat otherwise tied on quality and cost run to the +// one with fewer estimated indexes — even when its id reads later, and +// whichever way round the pool arrives. +func TestATieOnQualityAndCostRunsToFewerEstimatedIndexes(t *testing.T) { + pool := []Candidate{model("a/w"), model("c/h")} + // Twins for the mastermind seat, identical in every number the picker + // reads, but aaa runs on two estimated indexes and zzz on none. + aaa := model("aaa/two") + aaa.Intelligence = 0 + aaa.Coding = 80 + aaa.Agentic = 0 + aaa.Tools = false + aaa.Images = false + aaa.PromptPrice = 0.1 + aaa.CompletionPrice = 0.2 + aaa.HasCacheRead = false + zzz := aaa + zzz.ID = "zzz/one" + zzz.Intelligence = 80 + zzz.Agentic = 80 + pool = append(pool, aaa, zzz) + pool = append(pool, donors("v", 3)...) + + front := Front(pool, DefaultShapes(), All) + if len(front) == 0 { + t.Fatal("the pool must field crews") + } + if front[0].Mastermind != "zzz/one" { + t.Fatalf("a tie went to the estimated twin: %+v", front[0]) + } + for _, crew := range front { + if crew.Mastermind == "aaa/two" { + t.Fatalf("the estimated twin survived its tie: %+v", crew) + } + } + + reversed := slices.Clone(pool) + slices.Reverse(reversed) + if two := Front(reversed, DefaultShapes(), All); !slices.Equal(front, two) { + t.Fatal("the tie broke differently when the pool arrived reversed") + } +} + +// Estimates, caps and ties read the same whatever order the candidates +// arrive in: a pool of partially published models gives the same front +// shuffled as it does in hand order. +func TestPartialCandidatesGiveTheSameFrontInAnyOrder(t *testing.T) { + newRelease := model("b/new") + newRelease.Coding = 0 + newRelease.Agentic = 0 + newRelease.PromptPrice = 0.01 + newRelease.CompletionPrice = 0.02 + newRelease.HasCacheRead = false + older := model("d/old") + older.Intelligence = 0 + older.Agentic = 0 + older.Tools = false + staged := model("e/staged") + staged.Agentic = 0 + pool := []Candidate{model("a/w"), model("c/h"), newRelease, older, staged} + pool = append(pool, donors("v", 3)...) + + one := Front(pool, DefaultShapes(), All) + if len(one) == 0 { + t.Fatal("the pool must field crews") + } + if one[0].Estimated == ([3][3]bool{}) { + t.Fatal("the cheapest crew carries no estimated index; the partial candidates never reached it") + } + reversed := slices.Clone(pool) + slices.Reverse(reversed) + if two := Front(reversed, DefaultShapes(), All); !slices.Equal(one, two) { + t.Fatal("the front changed when the partial candidates arrived reversed") + } + rotated := append(slices.Clone(pool[3:]), pool[:3]...) + if three := Front(rotated, DefaultShapes(), All); !slices.Equal(one, three) { + t.Fatal("the front changed when the pool arrived rotated") + } +} + +// An input with no index missing picks on measurements alone: the fixture's +// partially published row left out, both fronts hold the same sizes and +// the same frugal, balanced and max picks the whole fixture holds, and no +// estimated flag is set anywhere — the partial row never raised a ceiling, +// so leaving it out moves nothing measured. +func TestNothingMissingLeavesEveryPickUnchanged(t *testing.T) { + cands := loadCandidates(t) + var full []Candidate + for _, c := range cands { + if c.Intelligence > 0 && c.Coding > 0 && c.Agentic > 0 { + full = append(full, c) + } + } + shapes := DefaultShapes() + + open := Front(full, shapes, Open) + if len(open) != 7 { + t.Fatalf("the full open front holds %d crews, want 7", len(open)) + } + frugal, balanced, max := Presets(open) + wantCrew(t, "open frugal", frugal, "deepseek/deepseek-v4-flash-0731|z-ai/glm-5.3-flash|z-ai/glm-5.3-flash") + wantCrew(t, "open balanced", balanced, "z-ai/glm-5.3-flash|moonshotai/kimi-k3|z-ai/glm-5.3") + wantCrew(t, "open max", max, "z-ai/glm-5.3|moonshotai/kimi-k3|z-ai/glm-5.3") + + all := Front(full, shapes, All) + if len(all) != 18 { + t.Fatalf("the full all front holds %d crews, want 18", len(all)) + } + frugal, balanced, max = Presets(all) + wantCrew(t, "all frugal", frugal, "z-ai/glm-5.3-flash|google/gemini-3.8-flash|z-ai/glm-5.3-flash") + wantCrew(t, "all balanced", balanced, "z-ai/glm-5.3-flash|anthropic/claude-fable-5.1|anthropic/claude-fable-5.1") + wantCrew(t, "all max", max, "anthropic/claude-fable-5.1|openai/gpt-6-astra|anthropic/claude-fable-5.1") + if math.Abs(balanced.Bill-0.590) > 0.005 { + t.Fatalf("all balanced bill = %f, want about 0.590", balanced.Bill) + } + if math.Abs(balanced.Quality-95.4) > 0.05 { + t.Fatalf("all balanced quality = %f, want about 95.4", balanced.Quality) + } + for _, front := range [][]Crew{open, all} { + for _, crew := range front { + if crew.Estimated != ([3][3]bool{}) { + t.Fatalf("a fully measured pool raised an estimated flag: %+v", crew) + } + } + } +} + // The knob runs from one end of the front to the other: at 0 the budget is // the cheapest bill and the pick is frugal, at 1 it is the dearest and the // pick is max. From b6723bf57d3bb0c9ea38dd5bc3b4c98e76b69e04 Mon Sep 17 00:00:00 2001 From: santoshkumarradha Date: Thu, 17 Sep 2026 03:18:16 -0400 Subject: [PATCH 039/173] docs/changes: the crewpick note says what a partly-scored candidate does Co-Authored-By: Claude Fable 5.1 --- .../1093-crew-picking-and-pool-reading-packages.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/docs/changes/unreleased/1093-crew-picking-and-pool-reading-packages.md b/docs/changes/unreleased/1093-crew-picking-and-pool-reading-packages.md index 93f0dc5e06..e665704bc9 100644 --- a/docs/changes/unreleased/1093-crew-picking-and-pool-reading-packages.md +++ b/docs/changes/unreleased/1093-crew-picking-and-pool-reading-packages.md @@ -12,7 +12,13 @@ them yet. - `internal/crewpick` scores every (worker, high, mastermind) combination a candidate list can field by the bill its seat volumes run up against the mean of its seat qualities, and reads the frugal, balanced and max picks — and a - knob between them — off the non-dominated front. + knob between them — off the non-dominated front. A candidate that publishes + some but not all of its three indexes is scored on the ones it publishes, + each missing one estimated from the call's own candidates carrying the pair + and never above the largest measured value of that index; `Crew.Estimated` + says which of a pick's indexes were estimated, and a tie in bill and quality + runs to the crew on fewer of them. Only a candidate publishing no index at + all is out of the running. - `internal/pool/tally` keeps per-address sufficient statistics (n, sum, sum of squares) and paired-comparison counts, so two sheets merge by addition in any order and marshal to a byte-identical schema-1 document. From 61352ebaae5212734824664ac5cc5e56f0c4d0d9 Mon Sep 17 00:00:00 2001 From: santoshkumarradha Date: Thu, 17 Sep 2026 03:19:49 -0400 Subject: [PATCH 040/173] config: the four pool names are registered as operator plumbing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The registry covers every CODEAF_ name the tree spells, and internal/pool/ poolcfg spells four the settings sheet had never heard of, so the branch failed the coverage test even though the package reads no environment of its own — the names are a table in it and the values are handed in. They go on the operator list: two endpoints, a cadence, and the word itself, which has no row because it has no stored setting for a row to hold yet. Co-Authored-By: Claude Fable 5.1 --- internal/config/settings.go | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/internal/config/settings.go b/internal/config/settings.go index 77826d3e3f..d5674aa2d3 100644 --- a/internal/config/settings.go +++ b/internal/config/settings.go @@ -1243,6 +1243,24 @@ var OperatorEnvPins = []string{ // shape, under `make demo-home`'s terms. A row offering to persist a // fixture would put a demo question in front of a person every morning. "CODEAF_QUESTION_DEMO", + // The four names the measurement pool resolves from (internal/pool/poolcfg, + // which names them and reads none of them: the environment reaches that + // package as a function the caller hands in). CODEAF_MODEL_POOL is the + // on/read/off word, the two URLs are where the index is read and where + // measurements are handed to, and the TTL is how long a read copy stays + // young. + // + // They are plumbing on the terms the other addresses here are on: + // CODEAF_MODEL_POOL_URL and CODEAF_MODEL_POOL_SUBMIT_URL are endpoints, + // exactly as CODEAF_BASE_URL and CODEAF_RELAY are, and the TTL is a + // cadence nobody sets to express a preference. The word itself is the one + // of the four a sheet row could own, and it has no row because it has no + // stored setting yet: poolcfg takes the setting as an argument and nothing + // in the binary calls it. The row is owed on the day something does. + "CODEAF_MODEL_POOL", + "CODEAF_MODEL_POOL_URL", + "CODEAF_MODEL_POOL_SUBMIT_URL", + "CODEAF_MODEL_POOL_TTL", } // Defaults the registry owns beyond the ones config.go already declares. From e50ed25fc7d472b551b16b7ea3b636abe818e423 Mon Sep 17 00:00:00 2001 From: santoshkumarradha Date: Thu, 17 Sep 2026 04:08:54 -0400 Subject: [PATCH 041/173] config: the crew tables move to the measured seats, and the shipped mastermind follows Co-Authored-By: codeaf --- internal/config/auto_test.go | 61 +++++++++++++++++++---------------- internal/config/crew.go | 35 +++++++++----------- internal/config/crew_test.go | 30 +++++++++-------- internal/config/seats_test.go | 4 +-- internal/config/settings.go | 2 +- 5 files changed, 69 insertions(+), 63 deletions(-) diff --git a/internal/config/auto_test.go b/internal/config/auto_test.go index d2ae892e66..b7a1ee0735 100644 --- a/internal/config/auto_test.go +++ b/internal/config/auto_test.go @@ -214,35 +214,38 @@ func TestTheInvocationRungsStillOutrankAnAutoRow(t *testing.T) { // THE PRESET AN AUTO SEAT RUNS AT IS READ FROM THE STORED ROWS, never through // seat resolution — the seam cannot ask the ladder that is asking it — and a -// row that says auto matches whichever preset is being compared. Four pinned -// rows of one preset plus an auto row are that preset. +// row that says auto matches whichever preset the other rows name. +// +// MAX DIFFERS FROM BALANCED ONLY IN THE WORKER SEAT NOW, so an auto row on the +// worker can no longer tell the two apart — balanced, the default preset, wins +// that tie — and the identifying shape is the other way round: the worker +// pinned to max's own id, which only max names, with the auto row on a seat +// above it. func TestThePresetAnAutoSeatRunsAtIsReadFromTheStoredRows(t *testing.T) { t.Setenv(ModelEnv, "") t.Setenv(PlanModelEnv, "") - for _, preset := range CrewPresets { - models, _ := CrewModelsForSource(DefaultCrewSource, preset) - rows := map[string]string{KeyTierWorkerModel: AutoValue} - for _, tier := range ModelTiers { - if tier != ModelTierWorker { - rows[tierKeyFor(tier)] = models[tier] - } - } - dir := writeProfileRows(t, rows) - restore := AutoModels - AutoModels = func() []catalog.Model { return autoTestRows() } - seat := TierSeatAt(dir, ModelTierWorker) - AutoModels = restore - if seat.Crew != preset { - t.Errorf("four %s rows and an auto row read as %q", preset, seat.Crew) - } - if seat.Rung() != "crew "+preset+", computed" { - t.Errorf("the rung reads %q, want crew %s, computed", seat.Rung(), preset) + models, _ := CrewModelsForSource(DefaultCrewSource, CrewMax) + rows := map[string]string{KeyTierMastermindModel: AutoValue} + for _, tier := range ModelTiers { + if tier != ModelTierMastermind { + rows[tierKeyFor(tier)] = models[tier] } } + dir := writeProfileRows(t, rows) + restore := AutoModels + AutoModels = func() []catalog.Model { return autoTestRows() } + seat := TierSeatAt(dir, ModelTierMastermind) + AutoModels = restore + if seat.Crew != CrewMax { + t.Errorf("max's rows and an auto row read as %q", seat.Crew) + } + if seat.Rung() != "crew "+CrewMax+", computed" { + t.Errorf("the rung reads %q, want crew max, computed", seat.Rung()) + } // A profile with no auto row anywhere reads as it always read — the // default rung, no crew word — which is the unchanged-behaviour law: the // seam fires only on a row that says the word. - seat := TierSeatAt(t.TempDir(), ModelTierWorker) + seat = TierSeatAt(t.TempDir(), ModelTierWorker) if seat.Rung() != "default" || seat.Crew != "" { t.Errorf("an untouched profile reads %q (%s), want the default rung as before", seat.Rung(), seat.Crew) } @@ -288,22 +291,24 @@ func TestAutoNowhereChangesNothing(t *testing.T) { } // THE SEAM'S TABLE RUNG IS THE PRESET'S OWN ID, not merely a non-empty -// answer: with the catalog gone, an auto row under a pinned preset resolves -// to that preset's id for the tier, not to the default preset's. +// answer: with the catalog gone, an auto row under rows that pin max reads +// to that preset's id for the tier, not to the default preset's. The worker +// is the pinned seat — it is the one cell where max differs from balanced — +// and the auto row rides the mastermind. func TestTheTableRungAnswersThePresetTheRowsName(t *testing.T) { t.Setenv(ModelEnv, "") t.Setenv(PlanModelEnv, "") models, _ := CrewModelsForSource(DefaultCrewSource, CrewMax) - rows := map[string]string{KeyTierWorkerModel: AutoValue} + rows := map[string]string{KeyTierMastermindModel: AutoValue} for _, tier := range ModelTiers { - if tier != ModelTierWorker { + if tier != ModelTierMastermind { rows[tierKeyFor(tier)] = models[tier] } } dir := writeProfileRows(t, rows) - seat := TierSeatAt(dir, ModelTierWorker) - if seat.Model != models[ModelTierWorker] || seat.Source != SeatTable { - t.Errorf("with no catalog the seat reads %q (%s), want max's own worker id on the table rung", + seat := TierSeatAt(dir, ModelTierMastermind) + if seat.Model != models[ModelTierMastermind] || seat.Source != SeatTable { + t.Errorf("with no catalog the seat reads %q (%s), want max's own mastermind id on the table rung", seat.Model, seat.Rung()) } } diff --git a/internal/config/crew.go b/internal/config/crew.go index 6c007ee153..dda4f7ed35 100644 --- a/internal/config/crew.go +++ b/internal/config/crew.go @@ -90,13 +90,12 @@ const DefaultCrewSource = CrewSourceAll // crewModels is the open-weight table: one row per preset, one model per class. // // THE WORKER COLUMN IS THE DIAL. It climbs one step per preset — -// deepseek-v4-flash, glm-5.3-flash, glm-5.3 — because it is the seat that pays +// glm-5.3-flash, glm-5.3-flash, glm-5.3 — because it is the seat that pays // most of a task's bill, and a preset that moved every other seat while leaving // it alone would change everything about a task except its cost. The careful -// column is always a DIFFERENT VENDOR from the worker and always sees images -// (the vision role rides it). The reflex and low columns never vary: they are -// the same near-free models in all three presets, and a column that never -// varies is not a dial. +// column always sees images (the vision role rides it). The reflex and low +// columns never vary: they are the same near-free models in all three presets, +// and a column that never varies is not a dial. // // HOW THE IDS WERE READ OFF, on 2026-09-16 and seat by seat. Every open-weight // row of the catalog was placed on two axes: the expected bill that seat's own @@ -123,7 +122,7 @@ var crewModels = map[string]map[string]string{ CrewFrugal: { ModelTierReflex: "mistralai/mistral-nemo", ModelTierLow: "deepseek/deepseek-v4-flash-0731", - ModelTierWorker: "deepseek/deepseek-v4-flash-0731", + ModelTierWorker: "z-ai/glm-5.3-flash", ModelTierHigh: "z-ai/glm-5.3-flash", ModelTierMastermind: "z-ai/glm-5.3-flash", }, @@ -161,17 +160,15 @@ var crewModels = map[string]map[string]string{ // shape of this table. The worker seat carries most of a task's tokens, so a // step there multiplies through the entire bill while a step on the careful or // the mastermind seat is paid a handful of times. The money therefore goes to -// the two low-volume seats first: frugal to balanced moves careful from -// gemini-3.8-flash to claude-fable-5.1 and the mastermind from glm-5.3-flash to -// claude-fable-5.1, and only max moves the worker itself. Max's mastermind then -// sits on the worker's own model because at that bill the one-shot front has no -// separate planner above it. +// the two low-volume seats first: frugal to balanced moves the careful seat to +// claude-fable-5.1 and the mastermind to claude-opus-5, and max moves the +// worker itself, with the careful seat staying on fable beside it. var crewAllModels = map[string]map[string]string{ CrewFrugal: { ModelTierReflex: "google/gemini-2.5-flash", ModelTierLow: "deepseek/deepseek-v4-flash-0731", ModelTierWorker: "z-ai/glm-5.3-flash", - ModelTierHigh: "google/gemini-3.8-flash", + ModelTierHigh: "qwen/qwen3.8-max-0902", ModelTierMastermind: "z-ai/glm-5.3-flash", }, CrewBalanced: { @@ -179,14 +176,14 @@ var crewAllModels = map[string]map[string]string{ ModelTierLow: "deepseek/deepseek-v4-flash-0731", ModelTierWorker: "z-ai/glm-5.3-flash", ModelTierHigh: "anthropic/claude-fable-5.1", - ModelTierMastermind: "anthropic/claude-fable-5.1", + ModelTierMastermind: "anthropic/claude-opus-5", }, CrewMax: { ModelTierReflex: "google/gemini-2.5-flash", ModelTierLow: "deepseek/deepseek-v4-flash-0731", - ModelTierWorker: "anthropic/claude-fable-5.1", - ModelTierHigh: "openai/gpt-6-astra", - ModelTierMastermind: "anthropic/claude-fable-5.1", + ModelTierWorker: "z-ai/glm-5.3", + ModelTierHigh: "anthropic/claude-fable-5.1", + ModelTierMastermind: "anthropic/claude-opus-5", }, } @@ -263,9 +260,9 @@ var crewLines = map[string]map[string]string{ CrewMax: "glm-5.3 works and thinks, kimi-k3 checks", }, CrewSourceAll: { - CrewFrugal: "glm-flash works and thinks, gemini-flash checks", - CrewBalanced: "glm-flash works, fable checks and thinks", - CrewMax: "fable works and thinks, astra checks", + CrewFrugal: "glm-flash works and thinks, qwen-max checks", + CrewBalanced: "glm-flash works, fable checks, opus thinks", + CrewMax: "glm-5.3 works, fable checks, opus thinks", }, } diff --git a/internal/config/crew_test.go b/internal/config/crew_test.go index 64dc1ce484..ce97e14151 100644 --- a/internal/config/crew_test.go +++ b/internal/config/crew_test.go @@ -47,7 +47,7 @@ func TestCrewPresetsNameTheApprovedModels(t *testing.T) { CrewFrugal: { ModelTierReflex: "mistralai/mistral-nemo", ModelTierLow: "deepseek/deepseek-v4-flash-0731", - ModelTierWorker: "deepseek/deepseek-v4-flash-0731", + ModelTierWorker: "z-ai/glm-5.3-flash", ModelTierHigh: "z-ai/glm-5.3-flash", ModelTierMastermind: "z-ai/glm-5.3-flash", }, @@ -217,11 +217,12 @@ func TestTheCrewRowRefusesAWordThatIsNotAPreset(t *testing.T) { // ── the family the three words draw from ──────────────────────────────────── -// THE CAREFUL SEAT IS ALWAYS A SECOND VENDOR, IN EVERY PRESET OF EVERY FAMILY. +// THE CAREFUL SEAT IS A SECOND VENDOR, IN EVERY PRESET OF THE ALL FAMILY. // A check from the same vendor as the work it checks is a check that shares -// that vendor's blind spots, and the two tables are one law: a preset that -// moved to the all family and put the same vendor on worker and careful would -// break the property the open crew was built on while pretending to keep it. +// that vendor's blind spots. The open family's frugal row is the one standing +// exception: worker and careful both sit on glm-5.3-flash there, because at +// that preset's bill the open-weight front has no second vendor to take the +// careful seat. func TestTheCarefulSeatIsAlwaysASecondVendorInEveryFamily(t *testing.T) { vendor := func(id string) string { if at := strings.Index(id, "/"); at > 0 { @@ -231,6 +232,9 @@ func TestTheCarefulSeatIsAlwaysASecondVendorInEveryFamily(t *testing.T) { } for _, family := range CrewSources { for _, preset := range CrewPresets { + if family == CrewSourceOpen && preset == CrewFrugal { + continue + } models, ok := CrewModelsForSource(family, preset) if !ok { t.Fatalf("there is no %s preset in the %s family", preset, family) @@ -253,7 +257,7 @@ func TestTheAllFamilyNamesTheLockedModels(t *testing.T) { ModelTierReflex: "google/gemini-2.5-flash", ModelTierLow: "deepseek/deepseek-v4-flash-0731", ModelTierWorker: "z-ai/glm-5.3-flash", - ModelTierHigh: "google/gemini-3.8-flash", + ModelTierHigh: "qwen/qwen3.8-max-0902", ModelTierMastermind: "z-ai/glm-5.3-flash", }, CrewBalanced: { @@ -261,14 +265,14 @@ func TestTheAllFamilyNamesTheLockedModels(t *testing.T) { ModelTierLow: "deepseek/deepseek-v4-flash-0731", ModelTierWorker: "z-ai/glm-5.3-flash", ModelTierHigh: "anthropic/claude-fable-5.1", - ModelTierMastermind: "anthropic/claude-fable-5.1", + ModelTierMastermind: "anthropic/claude-opus-5", }, CrewMax: { ModelTierReflex: "google/gemini-2.5-flash", ModelTierLow: "deepseek/deepseek-v4-flash-0731", - ModelTierWorker: "anthropic/claude-fable-5.1", - ModelTierHigh: "openai/gpt-6-astra", - ModelTierMastermind: "anthropic/claude-fable-5.1", + ModelTierWorker: "z-ai/glm-5.3", + ModelTierHigh: "anthropic/claude-fable-5.1", + ModelTierMastermind: "anthropic/claude-opus-5", }, } for preset, expected := range want { @@ -298,9 +302,9 @@ func TestTheAllFamilyNamesTheLockedModels(t *testing.T) { CrewMax: "glm-5.3 works and thinks, kimi-k3 checks", }, CrewSourceAll: { - CrewFrugal: "glm-flash works and thinks, gemini-flash checks", - CrewBalanced: "glm-flash works, fable checks and thinks", - CrewMax: "fable works and thinks, astra checks", + CrewFrugal: "glm-flash works and thinks, qwen-max checks", + CrewBalanced: "glm-flash works, fable checks, opus thinks", + CrewMax: "glm-5.3 works, fable checks, opus thinks", }, } for family, lines := range wantLines { diff --git a/internal/config/seats_test.go b/internal/config/seats_test.go index 223f5fcd4a..cf84e5b4c5 100644 --- a/internal/config/seats_test.go +++ b/internal/config/seats_test.go @@ -78,8 +78,8 @@ func TestTheSeatLadderAnswersInItsOwnOrder(t *testing.T) { // An operator can still put a level on a flag or tier row. name: "the crew leaves generation behavior to the model", crew: CrewMax, - work: "anthropic/claude-fable-5.1", workRung: "crew max", - plan: "anthropic/claude-fable-5.1", planRung: "crew max", + work: "z-ai/glm-5.3", workRung: "crew max", + plan: "anthropic/claude-opus-5", planRung: "crew max", }, { // And a flag carrying one is not shortened either, so the two rungs diff --git a/internal/config/settings.go b/internal/config/settings.go index d5674aa2d3..c9753f996b 100644 --- a/internal/config/settings.go +++ b/internal/config/settings.go @@ -1031,7 +1031,7 @@ const ( DefaultHighModel = "anthropic/claude-fable-5.1" // The mastermind names a capable planning model. Its generation behavior is // left to the provider unless an operator adds a level to the model id. - DefaultMastermindModel = "anthropic/claude-fable-5.1" + DefaultMastermindModel = "anthropic/claude-opus-5" ) // DocumentEngines are the four rungs CODEAF_DOC_ENGINE accepts. From 2dcfc54a633ff5d327b0868c5bb57bb3bbe6bcd8 Mon Sep 17 00:00:00 2001 From: santoshkumarradha Date: Thu, 17 Sep 2026 04:18:02 -0400 Subject: [PATCH 042/173] manual: the crew pages name the measured seats Co-Authored-By: codeaf --- internal/manual/chat/commands.md | 16 ++++++------ internal/manual/chat/models-and-cost.md | 34 ++++++++++++------------- 2 files changed, 25 insertions(+), 25 deletions(-) diff --git a/internal/manual/chat/commands.md b/internal/manual/chat/commands.md index eb4ac685b2..363b4625c4 100644 --- a/internal/manual/chat/commands.md +++ b/internal/manual/chat/commands.md @@ -671,7 +671,7 @@ The `crew` line sits directly under `model` and reads the preset word — or `cu the three classes: ``` -crew max · brain claude-fable-5.1 · hands claude-fable-5.1 · checks gpt-6-astra +crew max · brain claude-opus-5 · hands glm-5.3 · checks claude-fable-5.1 ``` The crew is **not on the status line**. It was one short segment there — `crew max`, or @@ -1423,12 +1423,12 @@ opens the six-seat reading, bottom-anchored like the model picker. From the top: the five models codeaf uses on its own behalf — not the one you chat with you talk to · deepseek-v4-flash family ‹ open models · all models › - frugal — glm-flash works and thinks, gemini-flash checks - reflex google/gemini-2.5-flash · small work deepseek/deepseek-v4-flash-0731 · worker z-ai/glm-5.3-flash · careful work google/gemini-3.8-flash · mastermind z-ai/glm-5.3-flash -› balanced — glm-flash works, fable checks and thinks - reflex google/gemini-2.5-flash · small work deepseek/deepseek-v4-flash-0731 · worker z-ai/glm-5.3-flash · careful work anthropic/claude-fable-5.1 · mastermind anthropic/claude-fable-5.1 - max — fable works and thinks, astra checks - reflex google/gemini-2.5-flash · small work deepseek/deepseek-v4-flash-0731 · worker anthropic/claude-fable-5.1 · careful work openai/gpt-6-astra · mastermind anthropic/claude-fable-5.1 + frugal — glm-flash works and thinks, qwen-max checks + reflex google/gemini-2.5-flash · small work deepseek/deepseek-v4-flash-0731 · worker z-ai/glm-5.3-flash · careful work qwen/qwen3.8-max-0902 · mastermind z-ai/glm-5.3-flash +› balanced — glm-flash works, fable checks, opus thinks + reflex google/gemini-2.5-flash · small work deepseek/deepseek-v4-flash-0731 · worker z-ai/glm-5.3-flash · careful work anthropic/claude-fable-5.1 · mastermind anthropic/claude-opus-5 + max — glm-5.3 works, fable checks, opus thinks + reflex google/gemini-2.5-flash · small work deepseek/deepseek-v4-flash-0731 · worker z-ai/glm-5.3 · careful work anthropic/claude-fable-5.1 · mastermind anthropic/claude-opus-5 each of the five can be pinned on its own in /settings → Providers ``` @@ -1455,7 +1455,7 @@ page. `/crew frugal`, `/crew balanced` or `/crew max` sets the five and confirms in one line: ``` -crew → max · brain claude-fable-5.1 · hands claude-fable-5.1 · checks gpt-6-astra · you are still talking to deepseek-v4-flash — /model changes that +crew → max · brain claude-opus-5 · hands glm-5.3 · checks claude-fable-5.1 · you are still talking to deepseek-v4-flash — /model changes that ``` **The model ids are drawn brighter than the words around them.** `crew →`, the preset diff --git a/internal/manual/chat/models-and-cost.md b/internal/manual/chat/models-and-cost.md index 375418b665..7a1e867a5a 100644 --- a/internal/manual/chat/models-and-cost.md +++ b/internal/manual/chat/models-and-cost.md @@ -335,7 +335,7 @@ preset: | small work | `deepseek/deepseek-v4-flash-0731` | | worker | `z-ai/glm-5.3-flash` | | careful work | `anthropic/claude-fable-5.1` | -| mastermind | `anthropic/claude-fable-5.1` | +| mastermind | `anthropic/claude-opus-5` | None of them is the model you are talking to. A crew that followed your conversation would put the most expensive model in the build on the cheapest questions in it — a call made @@ -383,13 +383,13 @@ not climb together in either of them. In the `all` family the worker stays on `glm-5.3-flash` through `balanced`, because the worker seat carries most of a task's tokens: a step there multiplies through the whole bill, where a step on the careful or the mastermind seat is paid a handful of times. So `frugal` to -`balanced` spends on those two low-volume seats, and only `max` moves the worker itself. The -`max` mastermind then sits on the worker's own model, because at that bill the one-shot front -has no separate planner above it. +`balanced` spends on those two low-volume seats, and `max` moves the worker itself. The +careful and mastermind seats are settled by `balanced` and stay there through `max`. -The careful class is always a different vendor from the worker and always sees images, in -every preset of both families: a check from a second vendor catches what the first vendor's -blind spots let through, and the vision role rides that row. The small-work row is pinned to +The careful class always sees images — the vision role rides that row — and is a different +vendor from the worker in every preset of the `all` family; the open family's `frugal` row +is the one standing exception, with worker and careful both on `glm-5.3-flash`, because at +that bill the open-weight front has no second vendor for the careful seat. The small-work row is pinned to the July build of DeepSeek V4 Flash on purpose: the bare `deepseek/deepseek-v4-flash` id resolves to the April build, and the July build costs the same. @@ -407,13 +407,13 @@ default one, `all`: | --- | --- | --- | --- | | reflex | `gemini-2.5-flash` | `gemini-2.5-flash` | `gemini-2.5-flash` | | small work | `deepseek-v4-flash-0731` | `deepseek-v4-flash-0731` | `deepseek-v4-flash-0731` | -| worker | `glm-5.3-flash` | `glm-5.3-flash` | `claude-fable-5.1` | -| careful work | `gemini-3.8-flash` | `claude-fable-5.1` | `gpt-6-astra` | -| mastermind | `glm-5.3-flash` | `claude-fable-5.1` | `claude-fable-5.1` | +| worker | `glm-5.3-flash` | `glm-5.3-flash` | `glm-5.3` | +| careful work | `qwen3.8-max-0902` | `claude-fable-5.1` | `claude-fable-5.1` | +| mastermind | `glm-5.3-flash` | `claude-opus-5` | `claude-opus-5` | -- **frugal**: glm-flash works and thinks, gemini-flash checks -- **balanced**: glm-flash works, fable checks and thinks -- **max**: fable works and thinks, astra checks +- **frugal**: glm-flash works and thinks, qwen-max checks +- **balanced**: glm-flash works, fable checks, opus thinks +- **max**: glm-5.3 works, fable checks, opus thinks The reflex and small-work columns never vary — they are the same near-free models in all three — so `/crew` never names them: the confirmation and @@ -519,7 +519,7 @@ running keeps the model it was admitted on. **Where to read the crew back:** - `/status` prints a `crew` line directly under `model`: - `crew max · brain claude-fable-5.1 · hands claude-fable-5.1 · checks gpt-6-astra`. The word is + `crew max · brain claude-opus-5 · hands glm-5.3 · checks claude-fable-5.1`. The word is the preset, or `custom` when the five classes are your own arrangement. **brain** is the mastermind, **hands** is the worker, **checks** is the careful class. - `/settings` → Providers has the **crew** row above the five class rows. @@ -548,7 +548,7 @@ the crew — or `/model` — half way through does not move work already going. 4. the model you are talking to, only when the worker row is blank. So on the shipped `balanced` crew a task runs on `z-ai/glm-5.3-flash` whatever you are -chatting on, and `/crew max` moves the next task onto `anthropic/claude-fable-5.1`. The task's row on the +chatting on, and `/crew max` moves the next task onto `z-ai/glm-5.3`. The task's row on the roster, its room's status line and its finished card all name the model it actually ran on. The worker of an adaptive run's nodes is the same seat, and so is the work model of `codeaf do` — one row, every door. @@ -687,7 +687,7 @@ No. `/crew max` moves the five crew seats and leaves the model you talk to exact was. The confirmation names it: ``` -crew → max · brain claude-fable-5.1 · hands claude-fable-5.1 · checks gpt-6-astra · you are still talking to deepseek-v4-flash — /model changes that +crew → max · brain claude-opus-5 · hands glm-5.3 · checks claude-fable-5.1 · you are still talking to deepseek-v4-flash — /model changes that ``` `/model`, `/model ` or the model row in `/settings` are the only ways to change the @@ -2255,7 +2255,7 @@ The `crew` line sits directly under `model` and reads the preset word — or `cu the three classes after it: ``` -crew max · brain claude-fable-5.1 · hands claude-fable-5.1 · checks gpt-6-astra +crew max · brain claude-opus-5 · hands glm-5.3 · checks claude-fable-5.1 ``` On the live status line the crew is one short segment — `crew max`, or `crew custom` — at From 655989921502b2888bc2e42db50fec7dc88ff47f Mon Sep 17 00:00:00 2001 From: santoshkumarradha Date: Thu, 17 Sep 2026 04:18:21 -0400 Subject: [PATCH 043/173] docs/changes: the six crew-table cells that moved Co-Authored-By: codeaf --- ...1093-crew-tables-move-to-the-measured-seats.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 docs/changes/unreleased/1093-crew-tables-move-to-the-measured-seats.md diff --git a/docs/changes/unreleased/1093-crew-tables-move-to-the-measured-seats.md b/docs/changes/unreleased/1093-crew-tables-move-to-the-measured-seats.md new file mode 100644 index 0000000000..cadb6817ca --- /dev/null +++ b/docs/changes/unreleased/1093-crew-tables-move-to-the-measured-seats.md @@ -0,0 +1,15 @@ +--- +kind: changed +title: Six cells of the fixed crew tables moved to the measured seats +pr: 1093 +surface: [chat, engine] +invalidates: + - "The all family's frugal careful seat is `qwen/qwen3.8-max-0902` (was `google/gemini-3.8-flash`). Its balanced mastermind is `anthropic/claude-opus-5` (was `anthropic/claude-fable-5.1`). Its max row works on `z-ai/glm-5.3` (was `anthropic/claude-fable-5.1`), checks on `anthropic/claude-fable-5.1` (was `openai/gpt-6-astra`) and plans on `anthropic/claude-opus-5` (was `anthropic/claude-fable-5.1`). The open family's frugal row works on `z-ai/glm-5.3-flash` (was `deepseek/deepseek-v4-flash-0731`)." + - "`config.DefaultMastermindModel` is `anthropic/claude-opus-5`, so the five shipped defaults are still exactly the default family's balanced row. An untouched profile plans on opus-5 and reads the crew as `balanced`; a profile that pinned the mastermind on the old default now reads `custom` until it pins again." + - "Max now differs from balanced only in the worker seat, so an `auto` row on the worker can no longer tell the two presets apart — the stored-rows reading answers `balanced`, the default preset, and the worker's own id is what identifies max to the seam. The open family's frugal row has worker and careful on the same vendor, `glm-5.3-flash`, the one standing exception to the second-vendor law." +--- + +The cells move to the same measured plot the tables already name: expected bill +under each seat's call shape against published quality, nothing quoted. Max +spends on the seat that pays most of a task's bill; the careful and mastermind +seats settle at balanced and hold through max. From bfcc7ecd891b18ef75e29701db18ae358166b587 Mon Sep 17 00:00:00 2001 From: santoshkumarradha Date: Thu, 17 Sep 2026 04:37:22 -0400 Subject: [PATCH 044/173] config: the crew comments carry the moved shape, and the worker tie is pinned Co-Authored-By: codeaf --- internal/config/auto_test.go | 19 +++++++++++++++++++ internal/config/crew.go | 20 ++++++++++++-------- 2 files changed, 31 insertions(+), 8 deletions(-) diff --git a/internal/config/auto_test.go b/internal/config/auto_test.go index b7a1ee0735..e8052e8fc8 100644 --- a/internal/config/auto_test.go +++ b/internal/config/auto_test.go @@ -242,6 +242,25 @@ func TestThePresetAnAutoSeatRunsAtIsReadFromTheStoredRows(t *testing.T) { if seat.Rung() != "crew "+CrewMax+", computed" { t.Errorf("the rung reads %q, want crew max, computed", seat.Rung()) } + // And the tie itself, pinned: max's four other rows are balanced's, so the + // auto row on the worker reads the default preset — balanced wins it — and + // the worker runs at balanced's budget. + rows = map[string]string{KeyTierWorkerModel: AutoValue} + for _, tier := range ModelTiers { + if tier != ModelTierWorker { + rows[tierKeyFor(tier)] = models[tier] + } + } + dir = writeProfileRows(t, rows) + AutoModels = func() []catalog.Model { return autoTestRows() } + seat = TierSeatAt(dir, ModelTierWorker) + AutoModels = restore + if seat.Crew != CrewBalanced { + t.Errorf("max's rows with an auto worker read as %q, want balanced, the default preset", seat.Crew) + } + if seat.Rung() != "crew "+CrewBalanced+", computed" { + t.Errorf("the rung reads %q, want crew balanced, computed", seat.Rung()) + } // A profile with no auto row anywhere reads as it always read — the // default rung, no crew word — which is the unchanged-behaviour law: the // seam fires only on a row that says the word. diff --git a/internal/config/crew.go b/internal/config/crew.go index dda4f7ed35..8a9f61e1dd 100644 --- a/internal/config/crew.go +++ b/internal/config/crew.go @@ -89,13 +89,16 @@ const DefaultCrewSource = CrewSourceAll // crewModels is the open-weight table: one row per preset, one model per class. // -// THE WORKER COLUMN IS THE DIAL. It climbs one step per preset — -// glm-5.3-flash, glm-5.3-flash, glm-5.3 — because it is the seat that pays +// THE WORKER COLUMN IS THE DIAL. It holds glm-5.3-flash through balanced, and +// max is the preset that takes it to glm-5.3, because it is the seat that pays // most of a task's bill, and a preset that moved every other seat while leaving // it alone would change everything about a task except its cost. The careful -// column always sees images (the vision role rides it). The reflex and low -// columns never vary: they are the same near-free models in all three presets, -// and a column that never varies is not a dial. +// column always sees images (the vision role rides it) and is a second vendor +// from balanced upward; frugal keeps worker and careful on the same +// glm-5.3-flash, because at that bill the open-weight front has no second +// vendor to take the careful seat. The reflex and low columns never vary: they +// are the same near-free models in all three presets, and a column that never +// varies is not a dial. // // HOW THE IDS WERE READ OFF, on 2026-09-16 and seat by seat. Every open-weight // row of the catalog was placed on two axes: the expected bill that seat's own @@ -145,9 +148,10 @@ var crewModels = map[string]map[string]string{ // crewAllModels is the same three presets answered from the whole catalog // rather than its open-weight shelf, which is what the `all` family of // [KeyCrewSource] draws from AND WHAT A PROFILE THAT HAS ANSWERED NOTHING -// RESOLVES ([DefaultCrewSource]). The careful column is the same law here as -// there: a DIFFERENT VENDOR from the worker in every preset, and the reflex -// and low columns still never vary. Closed models live here and only here. +// RESOLVES ([DefaultCrewSource]). The careful column is a DIFFERENT VENDOR +// from the worker in every preset here — the open family's frugal row is the +// one standing exception — and the reflex and low columns still never vary. +// Closed models live here and only here. // // The ids come off the same plot [crewModels] describes, run on 2026-09-16 over // every row of the catalog rather than the open-weight ones: expected task bill From 84f923c8a3ff55913c9e75f8194422027de30bf4 Mon Sep 17 00:00:00 2001 From: santoshkumarradha Date: Thu, 17 Sep 2026 04:40:28 -0400 Subject: [PATCH 045/173] config: the open frugal line and its mirrors name the worker the row picks Co-Authored-By: codeaf --- internal/config/crew.go | 2 +- internal/config/crew_test.go | 2 +- internal/manual/chat/models-and-cost.md | 8 ++++---- internal/tui3/onboarding.go | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/internal/config/crew.go b/internal/config/crew.go index 8a9f61e1dd..69bd735185 100644 --- a/internal/config/crew.go +++ b/internal/config/crew.go @@ -259,7 +259,7 @@ func CrewModelsForSource(source, preset string) (map[string]string, bool) { // contradiction the chooser exists to prevent. var crewLines = map[string]map[string]string{ CrewSourceOpen: { - CrewFrugal: "deepseek works, glm-flash checks and thinks · pennies a day", + CrewFrugal: "glm-flash works, checks and thinks · pennies a day", CrewBalanced: "glm-flash works, kimi-k3 checks, glm-5.3 thinks", CrewMax: "glm-5.3 works and thinks, kimi-k3 checks", }, diff --git a/internal/config/crew_test.go b/internal/config/crew_test.go index ce97e14151..f397415ef5 100644 --- a/internal/config/crew_test.go +++ b/internal/config/crew_test.go @@ -297,7 +297,7 @@ func TestTheAllFamilyNamesTheLockedModels(t *testing.T) { // pick is the contradiction the chooser exists to prevent. wantLines := map[string]map[string]string{ CrewSourceOpen: { - CrewFrugal: "deepseek works, glm-flash checks and thinks · pennies a day", + CrewFrugal: "glm-flash works, checks and thinks · pennies a day", CrewBalanced: "glm-flash works, kimi-k3 checks, glm-5.3 thinks", CrewMax: "glm-5.3 works and thinks, kimi-k3 checks", }, diff --git a/internal/manual/chat/models-and-cost.md b/internal/manual/chat/models-and-cost.md index 7a1e867a5a..09feab052f 100644 --- a/internal/manual/chat/models-and-cost.md +++ b/internal/manual/chat/models-and-cost.md @@ -360,13 +360,13 @@ Under `open`, the same three words resolve to these: | --- | --- | --- | --- | | reflex | `mistralai/mistral-nemo` | `mistralai/mistral-nemo` | `mistralai/mistral-nemo` | | small work | `deepseek/deepseek-v4-flash-0731` | `deepseek/deepseek-v4-flash-0731` | `deepseek/deepseek-v4-flash-0731` | -| worker | `deepseek/deepseek-v4-flash-0731` | `z-ai/glm-5.3-flash` | `z-ai/glm-5.3` | +| worker | `z-ai/glm-5.3-flash` | `z-ai/glm-5.3-flash` | `z-ai/glm-5.3` | | careful work | `z-ai/glm-5.3-flash` | `moonshotai/kimi-k3` | `moonshotai/kimi-k3` | | mastermind | `z-ai/glm-5.3-flash` | `z-ai/glm-5.3` | `z-ai/glm-5.3` | -The worker column is the dial in that family — it climbs one step per preset, because it is -the seat that pays most of a task's bill and a preset that moved every other seat would -change everything about a task except its cost. +The worker column is the dial in that family — it holds `glm-5.3-flash` through `balanced`, and +`max` moves it to `glm-5.3` — because it is the seat that pays most of a task's bill and a +preset that moved every other seat would change everything about a task except its cost. **Why these ids.** Every row of the catalog was placed on two axes on 2026-09-16: the bill a seat's own call shape runs up, built from the catalog's published prompt, completion and diff --git a/internal/tui3/onboarding.go b/internal/tui3/onboarding.go index d48cbaa4c5..6881df2030 100644 --- a/internal/tui3/onboarding.go +++ b/internal/tui3/onboarding.go @@ -1468,8 +1468,8 @@ const setupNoMatchWord = "nothing matches · backspace widens it" // is deliberately NOT [config.CrewLine]. // // The registry's line is written for somebody who already has the vocabulary: -// `deepseek works, glm-flash checks and thinks · pennies a day` names four -// models and makes a claim about money. Both halves are wrong here. Nobody meeting codeaf for the +// `glm-flash works, checks and thinks · pennies a day` names one model three +// times and makes a claim about money. Both halves are wrong here. Nobody meeting codeaf for the // first time can rank `glm-flash` against `kimi-k3`, and — the harder rule — // THIS SCREEN MAKES NO CLAIM ABOUT WHAT ANYTHING COSTS. "Pennies a day" is a // forecast about somebody else's usage on somebody else's pricing, said on the From e18ad36f8d79ed46302df2b4e842a0fe2e23b483 Mon Sep 17 00:00:00 2001 From: santoshkumarradha Date: Thu, 17 Sep 2026 04:54:58 -0400 Subject: [PATCH 046/173] config: the default preset wins when the stored rows match two presets Co-Authored-By: Claude Fable 5.1 --- ...093-tier-row-auto-computes-from-catalog.md | 7 ++ internal/config/auto.go | 8 ++ internal/config/auto_test.go | 73 +++++++++++++++---- internal/manual/chat/models-and-cost.md | 24 ++++++ 4 files changed, 96 insertions(+), 16 deletions(-) diff --git a/docs/changes/unreleased/1093-tier-row-auto-computes-from-catalog.md b/docs/changes/unreleased/1093-tier-row-auto-computes-from-catalog.md index 9f2d6b87dc..0d3ae8a28f 100644 --- a/docs/changes/unreleased/1093-tier-row-auto-computes-from-catalog.md +++ b/docs/changes/unreleased/1093-tier-row-auto-computes-from-catalog.md @@ -24,6 +24,13 @@ stays on disk; the id is derived on every read. to empty. - A seat on either new rung names the preset it ran at: `crew frugal, computed` on a run's receipt. +- The preset an auto row runs at is read from the profile's other stored + rows, an auto row matching whichever preset is being compared. **When the + stored rows match more than one preset, the default preset — `balanced` — + wins the tie**, which is also what a profile matching none of them reads. + Since `max` differs from `balanced` only in the worker seat, a crew with + `auto` on the worker and the shipped rows elsewhere matches both and reads + as `balanced`; the worker's own id is what identifies `max` to the seam. - Rows that don't say auto are unchanged, and so is everything above the crew: a flag, `--plan-model` and the environment still outrank an auto row, and a flag whose text is `auto` is handed on whole. diff --git a/internal/config/auto.go b/internal/config/auto.go index b94211a2c0..1cb6be3c69 100644 --- a/internal/config/auto.go +++ b/internal/config/auto.go @@ -203,6 +203,14 @@ func autoCatalogRows() []catalog.Model { // The answer is the first preset whose every row matches, the default preset // tried first and then the crew's own order; when none matches, the default — // the budget an undecided profile runs at is the budget an auto seat runs at. +// +// THE DEFAULT PRESET WINS EVERY TIE, and ties are ordinary rather than rare: +// the presets differ in only a few cells, so an auto row on a seat the two +// presets share leaves the rest of the rows matching both. Max differs from +// balanced only in the worker seat today, which makes a crew with an auto +// worker and the shipped rows elsewhere exactly that tie — it reads balanced. +// Trying the default first is what decides it, so the order above is the rule +// and not an accident of iteration. func crewPresetUnder(profileDir, family string) string { defaults := crewTableFor(family)[DefaultCrew] read := make(map[string]string, len(ModelTiers)) diff --git a/internal/config/auto_test.go b/internal/config/auto_test.go index e8052e8fc8..dfcfccfe79 100644 --- a/internal/config/auto_test.go +++ b/internal/config/auto_test.go @@ -242,31 +242,72 @@ func TestThePresetAnAutoSeatRunsAtIsReadFromTheStoredRows(t *testing.T) { if seat.Rung() != "crew "+CrewMax+", computed" { t.Errorf("the rung reads %q, want crew max, computed", seat.Rung()) } - // And the tie itself, pinned: max's four other rows are balanced's, so the - // auto row on the worker reads the default preset — balanced wins it — and - // the worker runs at balanced's budget. - rows = map[string]string{KeyTierWorkerModel: AutoValue} + // A profile with no auto row anywhere reads as it always read — the + // default rung, no crew word — which is the unchanged-behaviour law: the + // seam fires only on a row that says the word. + seat = TierSeatAt(t.TempDir(), ModelTierWorker) + if seat.Rung() != "default" || seat.Crew != "" { + t.Errorf("an untouched profile reads %q (%s), want the default rung as before", seat.Rung(), seat.Crew) + } +} + +// THE DEFAULT PRESET WINS EVERY TIE. An auto row matches whichever preset is +// being compared, so a profile whose OTHER rows match two presets at once has +// no single answer from the rows alone — and the answer is the default preset, +// balanced, the budget an undecided profile already runs at. +// +// The tie is built here rather than assumed: the worker holds auto and the +// other four rows are taken from max, and the test first states that those +// four are balanced's rows too, which is what makes this a tie at all. Should +// the tables move so the two presets differ somewhere above the worker, that +// first check fails and says so, rather than the test quietly pinning nothing. +func TestTheDefaultPresetWinsWhenTheStoredRowsMatchTwoPresets(t *testing.T) { + t.Setenv(ModelEnv, "") + t.Setenv(PlanModelEnv, "") + balanced, ok := CrewModelsForSource(DefaultCrewSource, CrewBalanced) + if !ok { + t.Fatalf("there is no %s preset in the %s family", CrewBalanced, DefaultCrewSource) + } + max, ok := CrewModelsForSource(DefaultCrewSource, CrewMax) + if !ok { + t.Fatalf("there is no %s preset in the %s family", CrewMax, DefaultCrewSource) + } + rows := map[string]string{KeyTierWorkerModel: AutoValue} for _, tier := range ModelTiers { - if tier != ModelTierWorker { - rows[tierKeyFor(tier)] = models[tier] + if tier == ModelTierWorker { + continue + } + if balanced[tier] != max[tier] { + t.Fatalf("the %s seat differs between balanced (%s) and max (%s), so an auto worker is no longer a tie", + tier, balanced[tier], max[tier]) } + rows[tierKeyFor(tier)] = max[tier] } - dir = writeProfileRows(t, rows) + if balanced[ModelTierWorker] == max[ModelTierWorker] { + t.Fatalf("balanced and max name the same worker, so the two presets are not two") + } + + dir := writeProfileRows(t, rows) + restore := AutoModels AutoModels = func() []catalog.Model { return autoTestRows() } - seat = TierSeatAt(dir, ModelTierWorker) + seat := TierSeatAt(dir, ModelTierWorker) AutoModels = restore + if seat.Crew != CrewBalanced { - t.Errorf("max's rows with an auto worker read as %q, want balanced, the default preset", seat.Crew) + t.Errorf("rows matching both presets read as %q, want %s, the default preset", seat.Crew, CrewBalanced) } if seat.Rung() != "crew "+CrewBalanced+", computed" { - t.Errorf("the rung reads %q, want crew balanced, computed", seat.Rung()) + t.Errorf("the rung reads %q, want crew %s, computed", seat.Rung(), CrewBalanced) } - // A profile with no auto row anywhere reads as it always read — the - // default rung, no crew word — which is the unchanged-behaviour law: the - // seam fires only on a row that says the word. - seat = TierSeatAt(t.TempDir(), ModelTierWorker) - if seat.Rung() != "default" || seat.Crew != "" { - t.Errorf("an untouched profile reads %q (%s), want the default rung as before", seat.Rung(), seat.Crew) + // And the same tie on the table rung, with nothing to compute from: the + // default preset decides the fallback id too, not just the budget word. + dir = writeProfileRows(t, rows) + seat = TierSeatAt(dir, ModelTierWorker) + if seat.Crew != CrewBalanced || seat.Source != SeatTable { + t.Errorf("with no catalog the tie reads %q (%s), want balanced on the table rung", seat.Crew, seat.Rung()) + } + if seat.Model != balanced[ModelTierWorker] { + t.Errorf("the tie's table id is %q, want balanced's own worker %q", seat.Model, balanced[ModelTierWorker]) } } diff --git a/internal/manual/chat/models-and-cost.md b/internal/manual/chat/models-and-cost.md index 09feab052f..97ff65aad5 100644 --- a/internal/manual/chat/models-and-cost.md +++ b/internal/manual/chat/models-and-cost.md @@ -437,6 +437,30 @@ puts all five back in one write. A profile that applied a crew before the worker existed reads `custom` until a preset is applied again, because its four old rows and the new fifth are not any of the three. +### A class row that says `auto` + +Any of the five class rows may hold the bare word `auto` instead of a model id, case +folded. The seat's model is then **computed from the catalog** — the three published +capability indexes against the three published prices, under that seat's own call shape — +every time the row is read. The word stays on disk; the id is worked out on every read, so +a catalog that moves moves the seat with it and your profile never holds a model id this +build chose for you. A row with nothing to compute from — no catalog yet — falls back to +the table row for your preset, never to `auto` and never to empty. + +**The budget it computes at comes from the other four rows.** `auto` has no opinion about +cost of its own, so it runs at whatever preset the rows around it name: four rows that are +`balanced`'s make an `auto` row a balanced seat, four that are `max`'s make it a max seat. + +**When the other rows match more than one preset, `balanced` wins.** It is the default +preset, and the budget an undecided profile runs at is the budget an `auto` seat runs at. +This is not a rare corner: `max` differs from `balanced` only in the worker seat, so a +crew with `auto` on the worker and the other four rows as shipped matches both, and reads +as `balanced`. Pin the worker to `max`'s own id and put the `auto` row on a seat above it +if you want a computed seat at `max`'s budget. + +A seat on a computed row names both facts where a seat is shown — `crew balanced, +computed` — so the reading is never a guess. + ### The roles under each class Directly under the **pinned roles** row the panel lists **every registered role**, grouped From 980945761ae96d4495cbc1ca1778d642beb51b8b Mon Sep 17 00:00:00 2001 From: santoshkumarradha Date: Thu, 17 Sep 2026 05:00:30 -0400 Subject: [PATCH 047/173] config, session: the crew examples in the comments name the moved seats Co-Authored-By: Claude Fable 5.1 --- internal/config/crew.go | 6 +++--- internal/session/promptprofile.go | 5 +++-- internal/session/promptprofile_test.go | 4 ++-- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/internal/config/crew.go b/internal/config/crew.go index 69bd735185..68f35a1829 100644 --- a/internal/config/crew.go +++ b/internal/config/crew.go @@ -408,7 +408,7 @@ func SetCrewSource(profileDir, source string) error { // CrewSummary is the one line a crew change confirms itself with: // -// crew → balanced · brain claude-fable-5.1 · hands glm-5.3-flash · checks claude-fable-5.1 +// crew → balanced · brain claude-opus-5 · hands glm-5.3-flash · checks claude-fable-5.1 // // The three names are the classes a person actually asked about — what thinks, // what works, what checks — and HANDS IS THE WORKER: the seat that does the @@ -423,7 +423,7 @@ func CrewSummary(profileDir string) string { // CrewClasses is the three class names alone: // -// brain claude-fable-5.1 · hands glm-5.3-flash · checks claude-fable-5.1 +// brain claude-opus-5 · hands glm-5.3-flash · checks claude-fable-5.1 // // It is the tail of [CrewSummary] lifted out because a second surface prints the // crew now — /status, where the word already has a label of its own and "crew →" @@ -439,7 +439,7 @@ func CrewClasses(profileDir string) string { // CrewClassModels is the three ids [CrewClasses] names, in that order and // without the role words in front of them: // -// claude-fable-5.1, glm-5.3-flash, claude-fable-5.1 +// claude-opus-5, glm-5.3-flash, claude-fable-5.1 // // It exists because a surface drawing the crew line has to be able to say which // runs of it are the ANSWER — the ids a person typed /crew to change — and which diff --git a/internal/session/promptprofile.go b/internal/session/promptprofile.go index bc636c473d..607d000b44 100644 --- a/internal/session/promptprofile.go +++ b/internal/session/promptprofile.go @@ -29,8 +29,9 @@ package session // An earlier draft of this file had a second trigger — the crew's `worker` row, // on the reasoning that an open-weight model reasons like a worker whatever its // card claims. THAT WAS WRONG, and it is stated here so nobody restores it. -// `deepseek-v4-flash` and `glm-5.3-flash` are the open-weight models the frugal -// and balanced presets put in that seat, and served through a provider they have +// `glm-5.3-flash` and `glm-5.3` are the open-weight models the presets put in +// that seat — the first through balanced, the second at max — and served +// through a provider they have // a hundred and twenty-eight thousand tokens of room. That is a NORMAL SYSTEM and // it must get the full page: the seat rule would have taken sections off the page, // shelved `propose_task` and `tasks`, and turned saved memories OFF for anybody diff --git a/internal/session/promptprofile_test.go b/internal/session/promptprofile_test.go index 240f0c83a6..60b95d0cc7 100644 --- a/internal/session/promptprofile_test.go +++ b/internal/session/promptprofile_test.go @@ -75,8 +75,8 @@ func TestTheCatalogsWindowOutranksTheConfiguredOne(t *testing.T) { // THE CREW'S WORKER SEAT IS NOT A TRIGGER, AND A BIG WINDOW ON IT GETS THE WHOLE // PAGE. An earlier draft went lean whenever the conversation rode the crew's -// `worker` model, which is `deepseek/deepseek-v4-flash-0731` on the frugal preset -// and `z-ai/glm-5.3-flash` on the balanced one — models served with a hundred and +// `worker` model, which is `z-ai/glm-5.3-flash` through the balanced preset and +// `z-ai/glm-5.3` at max — models served with a hundred and // twenty-eight thousand tokens of room. That would have dropped sections, shelved // `propose_task` and turned saved memories off for anybody who picked a preset and // then chose that same model in chat, with nothing on screen saying so. Open From beffd415fb43e80dcb6e4fc986b36339ea37ae16 Mon Sep 17 00:00:00 2001 From: santoshkumarradha Date: Thu, 17 Sep 2026 05:00:30 -0400 Subject: [PATCH 048/173] manual, docs: the headless model line and the reflex default match the tables Co-Authored-By: Claude Fable 5.1 --- docs/HEADLESS.md | 2 +- internal/manual/chat/models-and-cost.md | 4 ++-- internal/manual/chat/what-i-remember.md | 3 ++- internal/manual/pages/models.md | 2 +- 4 files changed, 6 insertions(+), 5 deletions(-) diff --git a/docs/HEADLESS.md b/docs/HEADLESS.md index feb3cd749c..99a06632d7 100644 --- a/docs/HEADLESS.md +++ b/docs/HEADLESS.md @@ -153,7 +153,7 @@ kimi-k3:low` asked OpenRouter for a model id nobody publishes.) **Every run says which rung answered**, on stderr, before anything else: ``` -models: work deepseek/deepseek-v4-flash (crew frugal) · plan qwen/qwen3.8-27b (crew frugal) +models: work z-ai/glm-5.3-flash (crew frugal) · plan z-ai/glm-5.3-flash (crew frugal) models: work anthropic/whatever (--model) · plan follows the work model (default) ``` diff --git a/internal/manual/chat/models-and-cost.md b/internal/manual/chat/models-and-cost.md index 97ff65aad5..2a281ecbc1 100644 --- a/internal/manual/chat/models-and-cost.md +++ b/internal/manual/chat/models-and-cost.md @@ -3532,8 +3532,8 @@ on the `prompt profile` row or pinned it for the launch. Nothing about a model's licence, its vendor, its name or which crew seat it sits in makes a session lean. -So an open-weight model with a large window is NOT lean. `deepseek-v4-flash` and -`glm-5.3-flash` are served with 128,000 tokens of room, so they get the full +So an open-weight model with a large window is NOT lean. `glm-5.3-flash` and +`glm-5.3` are served with 128,000 tokens of room, so they get the full page, the full tool list and saved memories, exactly like any other 128,000-token model — including when they are the model your crew preset picked for the `worker` seat, and including when you then choose that same model in diff --git a/internal/manual/chat/what-i-remember.md b/internal/manual/chat/what-i-remember.md index c5775d05d5..e35c945df7 100644 --- a/internal/manual/chat/what-i-remember.md +++ b/internal/manual/chat/what-i-remember.md @@ -539,7 +539,8 @@ here. The two that ride every message are both on the cheapest of the five crew classes — the `reflex` class, which exists precisely because a call made twice a turn is a different economy from one made once a session. It ships pointed at -`mistralai/mistral-nemo`. Each goes out with a short +`google/gemini-2.5-flash`, and at `mistralai/mistral-nemo` under the `open` +crew family. Each goes out with a short prompt and a 200-token ceiling, and each is asked to answer in a few words of JSON. diff --git a/internal/manual/pages/models.md b/internal/manual/pages/models.md index b757745a76..dedb696413 100644 --- a/internal/manual/pages/models.md +++ b/internal/manual/pages/models.md @@ -94,7 +94,7 @@ the build's default. `CODEAF_HOME` and `CODEAF_PROFILE_DIR` decide which profile Every one of those runs opens with a line on stderr naming both seats and what chose each: ``` -models: work deepseek/deepseek-v4-flash (crew frugal) · plan qwen/qwen3.8-27b (crew frugal) +models: work z-ai/glm-5.3-flash (crew frugal) · plan z-ai/glm-5.3-flash (crew frugal) ``` `codeaf do --json` carries the same four facts as `model`, `plan_model`, `model_source` From ec4e56b13d8aa250ba7b917f1451c61e2fd22e41 Mon Sep 17 00:00:00 2001 From: santoshkumarradha Date: Thu, 17 Sep 2026 05:09:29 -0400 Subject: [PATCH 049/173] exec: the failed lane is read off the wire's own fact, not a status failedLane decided that a relayed error blamed its upstream by comparing the status against 500, which is the shape the taxonomy law refuses: only internal/taxonomy and internal/provider may turn a status into a move. The provider package already stamps the fact the retry needs: the router names the endpoint exactly when it is relaying that endpoint's refusal (APIError.FromUpstream). The retry reads that and nothing else, so an upstream that refused a call is the lane the next attempt avoids, whatever number it refused under, and a router's own refusal still names nobody. Co-Authored-By: Claude Fable 5.1 --- internal/exec/linear.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/internal/exec/linear.go b/internal/exec/linear.go index 8820634ce2..09615e4b63 100644 --- a/internal/exec/linear.go +++ b/internal/exec/linear.go @@ -1716,9 +1716,13 @@ func (l *Linear) complete(ctx context.Context, messages []ai.Message, definition // implicates nobody. A router's own refusal carries no provider name, a // transport fault names no machine, and an empty name is a fact the retry law // keeps (provider's retryavoid.go): the retry goes where it always went. +// +// Whether the lane is to blame is the wire's fact ([provider.APIError.FromUpstream]), +// never a status read here: the router names the endpoint exactly when it is +// relaying that endpoint's refusal, and the taxonomy owns what a status means. func failedLane(err error) string { var relayed *provider.APIError - if errors.As(err, &relayed) && relayed.Status >= 500 { + if errors.As(err, &relayed) && relayed.FromUpstream() { return strings.TrimSpace(relayed.Provider) } var cut *provider.StreamCut From ac11b3bcf6e2df0e7d04304d4644d22a7d3a4c1b Mon Sep 17 00:00:00 2001 From: santoshkumarradha Date: Thu, 17 Sep 2026 05:14:25 -0400 Subject: [PATCH 050/173] exec: the lane to avoid is the provider's fact, read as a question The retry loop named a lane to avoid by comparing a relayed status to 500, which only the taxonomy and the provider may do. Reading the provider name alone went too far the other way: a relayed 4xx is the endpoint's reading of the request, and the loop's own test holds that it names no lane. APIError.UpstreamFault answers the question in the package that owns the status: a named endpoint whose answer was its own fault (5xx). failedLane asks it and reads no number. A test pins each shape. Co-Authored-By: Claude Fable 5.1 --- internal/exec/linear.go | 8 ++++---- internal/provider/client.go | 13 +++++++++++++ internal/provider/refusal_test.go | 25 +++++++++++++++++++++++++ 3 files changed, 42 insertions(+), 4 deletions(-) diff --git a/internal/exec/linear.go b/internal/exec/linear.go index 09615e4b63..ca324ada16 100644 --- a/internal/exec/linear.go +++ b/internal/exec/linear.go @@ -1717,12 +1717,12 @@ func (l *Linear) complete(ctx context.Context, messages []ai.Message, definition // transport fault names no machine, and an empty name is a fact the retry law // keeps (provider's retryavoid.go): the retry goes where it always went. // -// Whether the lane is to blame is the wire's fact ([provider.APIError.FromUpstream]), -// never a status read here: the router names the endpoint exactly when it is -// relaying that endpoint's refusal, and the taxonomy owns what a status means. +// Whether the lane is to blame is the wire's fact ([provider.APIError.UpstreamFault]), +// never a status read here: the provider owns what a relayed status means, and +// a relayed 4xx is that endpoint's reading of the request, not a lane to leave. func failedLane(err error) string { var relayed *provider.APIError - if errors.As(err, &relayed) && relayed.FromUpstream() { + if errors.As(err, &relayed) && relayed.UpstreamFault() { return strings.TrimSpace(relayed.Provider) } var cut *provider.StreamCut diff --git a/internal/provider/client.go b/internal/provider/client.go index 9739e7e289..477d03bb99 100644 --- a/internal/provider/client.go +++ b/internal/provider/client.go @@ -2512,6 +2512,19 @@ func (e *APIError) FromUpstream() bool { return e != nil && strings.TrimSpace(e.Provider) != "" } +// UpstreamFault reports that THE NAMED ENDPOINT FAILED ON ITS OWN ACCOUNT: it +// was asked, it answered, and its answer was a fault of its own (5xx). It is the +// one relayed refusal that names a lane worth steering the SAME request away +// from on its next attempt: a relayed 4xx is that endpoint's reading of the +// request, which the next endpoint may read the same way, and a 429 is pacing +// with its own patience (retry.go). +// +// It lives here because a status is a fact only this package and the taxonomy +// may read; a caller asks the question and never the number. +func (e *APIError) UpstreamFault() bool { + return e.FromUpstream() && e.Status >= 500 +} + // OurRequest reports that THE REQUEST IS WHAT IS WRONG, so no endpoint will do // better with it. // diff --git a/internal/provider/refusal_test.go b/internal/provider/refusal_test.go index 8c377afd1b..0b20ed3dd3 100644 --- a/internal/provider/refusal_test.go +++ b/internal/provider/refusal_test.go @@ -53,6 +53,31 @@ func TestAProviderRefusalCarriesTheUpstreamAndItsOwnWords(t *testing.T) { } } +// A LANE TO AVOID IS A NAMED ENDPOINT THAT FAILED ON ITS OWN ACCOUNT. The +// retry asks this question rather than reading the status itself. +func TestOnlyANamedUpstreamsOwnFaultNamesALaneToAvoid(t *testing.T) { + fault := &APIError{Status: 502, Message: "upstream broke", Provider: "Alpha"} + if !fault.UpstreamFault() { + t.Error("a relayed 502 named no lane to avoid") + } + refused := &APIError{Status: 400, Message: "too long", Provider: "Alpha"} + if refused.UpstreamFault() { + t.Error("a relayed 400 was read as the endpoint's own fault") + } + paced := &APIError{Status: 429, Message: "slow down", Provider: "Alpha"} + if paced.UpstreamFault() { + t.Error("relayed pacing was read as the endpoint's own fault") + } + router := &APIError{Status: 502, Message: "bad gateway"} + if router.UpstreamFault() { + t.Error("a 502 that named nobody was blamed on an upstream") + } + var none *APIError + if none.UpstreamFault() { + t.Error("a nil refusal named a lane") + } +} + // AND THE TWO KINDS OF 4xx ARE TOLD APART BY SHAPE, NEVER BY A STATUS LIST. // // An upstream refused: another endpoint may serve, so the ladder goes on. The From c13a39a9fe0428accc6b6762780b35186ab1786b Mon Sep 17 00:00:00 2001 From: santoshkumarradha Date: Thu, 17 Sep 2026 05:24:38 -0400 Subject: [PATCH 051/173] orchestrate: the fuel table holds the ids this build ships Co-Authored-By: codeaf --- ...093-fuel-asks-an-installed-tariff-first.md | 2 +- internal/orchestrate/fuel.go | 20 +++++++++++++------ 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/docs/changes/unreleased/1093-fuel-asks-an-installed-tariff-first.md b/docs/changes/unreleased/1093-fuel-asks-an-installed-tariff-first.md index 652e1e6470..bd93fad0d5 100644 --- a/docs/changes/unreleased/1093-fuel-asks-an-installed-tariff-first.md +++ b/docs/changes/unreleased/1093-fuel-asks-an-installed-tariff-first.md @@ -5,7 +5,7 @@ pr: 1093 surface: [engine] invalidates: - "`orchestrate.PriceOf` answered from one package-level table and nothing else. An installed `PriceSource` is asked first and the table is the fallback behind it; `UsePrices` installs or removes one, and `CatalogPrices` adapts a per-token catalog reader into the per-million shape the meter uses." - - "The fuel table's rows were `deepseek/deepseek-v4-flash`, `z-ai/glm-5.2`, `moonshotai/kimi-k3`, `anthropic/claude-opus` and `openai/gpt-5`. The first two are gone and `deepseek/deepseek-v4-flash-0731`, `z-ai/glm-5.3` and `z-ai/glm-5.3-flash` are there instead, so the two retired ids now meter at the unpriced rate rather than at a row of their own." + - "The fuel table now holds exactly the nine ids the shipped defaults and crew tables name: `google/gemini-2.5-flash`, `mistralai/mistral-nemo`, `deepseek/deepseek-v4-flash-0731`, `z-ai/glm-5.3-flash`, `z-ai/glm-5.3`, `moonshotai/kimi-k3`, `qwen/qwen3.8-max-0902`, `anthropic/claude-fable-5.1` and `anthropic/claude-opus-5`. `anthropic/claude-opus` and `openai/gpt-5` are gone, and both now meter at the unpriced rate rather than at a row of their own." - "An orchestrated run metered against the fuel table whenever a call reported no cost of its own. It meters against the catalog's published price when the session has one, and falls back to the table only for a model the catalog publishes nothing for." --- diff --git a/internal/orchestrate/fuel.go b/internal/orchestrate/fuel.go index 5b48fec175..651a69feab 100644 --- a/internal/orchestrate/fuel.go +++ b/internal/orchestrate/fuel.go @@ -93,13 +93,21 @@ func CatalogPrices(priceNow func(model string) (prompt, completion float64, know // cannot know. [Meter] is what answers when a response carries no figure at // all, which is common enough that a run metering only reported costs would // have a tank that never empties. +// +// The rows are the ids the shipped defaults and crew tables name, each copied +// from the catalog's published price on the day its row was written; the +// installed tariff ([CatalogPrices]) is what answers when a catalog is present, +// and this table is only the fallback behind it. var prices = map[string]Price{ - "deepseek/deepseek-v4-flash-0731": {In: 0.14, Out: 0.28}, - "z-ai/glm-5.3": {In: 0.60, Out: 2.20}, - "z-ai/glm-5.3-flash": {In: 0.10, Out: 0.40}, - "moonshotai/kimi-k3": {In: 0.60, Out: 2.50}, - "anthropic/claude-opus": {In: 15.00, Out: 75.00}, - "openai/gpt-5": {In: 1.25, Out: 10.00}, + "google/gemini-2.5-flash": {In: 0.30, Out: 2.50}, + "mistralai/mistral-nemo": {In: 0.02, Out: 0.03}, + "deepseek/deepseek-v4-flash-0731": {In: 0.06, Out: 0.12}, + "z-ai/glm-5.3-flash": {In: 0.09, Out: 0.30}, + "z-ai/glm-5.3": {In: 1.40, Out: 4.40}, + "moonshotai/kimi-k3": {In: 3.00, Out: 15.00}, + "qwen/qwen3.8-max-0902": {In: 2.00, Out: 6.00}, + "anthropic/claude-fable-5.1": {In: 10.00, Out: 50.00}, + "anthropic/claude-opus-5": {In: 5.00, Out: 25.00}, } // unpriced is what a model nobody has a row for costs. From 2bf0cda87e0ca1267d90ee0a7aab61939e10e6b5 Mon Sep 17 00:00:00 2001 From: santoshkumarradha Date: Thu, 17 Sep 2026 05:24:47 -0400 Subject: [PATCH 052/173] orchestrate, session: every shipped model is pinned to a fuel row Co-Authored-By: codeaf --- internal/orchestrate/fuel_test.go | 15 ++++++----- internal/session/fuelrows_test.go | 43 +++++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+), 6 deletions(-) create mode 100644 internal/session/fuelrows_test.go diff --git a/internal/orchestrate/fuel_test.go b/internal/orchestrate/fuel_test.go index fdcbade220..3a01197dfa 100644 --- a/internal/orchestrate/fuel_test.go +++ b/internal/orchestrate/fuel_test.go @@ -10,10 +10,10 @@ import ( ) func TestMeter(t *testing.T) { - if got, want := MeterCall(1_000_000, 0, "openai/gpt-5"), 1.25; math.Abs(got-want) > 1e-9 { + if got, want := MeterCall(1_000_000, 0, "google/gemini-2.5-flash"), 0.30; math.Abs(got-want) > 1e-9 { t.Fatalf("input tokens: got %v want %v", got, want) } - if got, want := MeterCall(0, 1_000_000, "anthropic/claude-opus"), 75.0; math.Abs(got-want) > 1e-9 { + if got, want := MeterCall(0, 1_000_000, "anthropic/claude-fable-5.1"), 50.0; math.Abs(got-want) > 1e-9 { t.Fatalf("output tokens: got %v want %v", got, want) } // Meter has one number and charges it at the dear rate on purpose. @@ -318,8 +318,8 @@ func TestASourceThatDoesNotKnowFallsThroughToTheTable(t *testing.T) { UsePrices(func(string) (Price, bool) { return Price{}, false }) defer UsePrices(nil) - price, known := PriceOf("openai/gpt-5") - if !known || price != (Price{In: 1.25, Out: 10.00}) { + price, known := PriceOf("z-ai/glm-5.3") + if !known || price != (Price{In: 1.40, Out: 4.40}) { t.Fatalf("a refusing source leaves the table in charge: %+v, %v", price, known) } if _, known := PriceOf("somebody/new-model"); known { @@ -332,7 +332,7 @@ func TestASourceThatDoesNotKnowFallsThroughToTheTable(t *testing.T) { func TestUsePricesNilLeavesTheTableAlone(t *testing.T) { UsePrices(func(string) (Price, bool) { return Price{In: 99, Out: 99}, true }) UsePrices(nil) - if price, known := PriceOf("moonshotai/kimi-k3"); !known || price != (Price{In: 0.60, Out: 2.50}) { + if price, known := PriceOf("moonshotai/kimi-k3"); !known || price != (Price{In: 3.00, Out: 15.00}) { t.Fatalf("a removed source unmasks the table: %+v, %v", price, known) } } @@ -381,7 +381,10 @@ func TestTheMeterRidesTheInstalledSource(t *testing.T) { // TestRetiredIdsAreStrangers: an id the table no longer holds meters at the // unpriced rate, exactly as one it never had. func TestRetiredIdsAreStrangers(t *testing.T) { - for _, id := range []string{"z-ai/glm-5.2", "deepseek/deepseek-v4-flash"} { + for _, id := range []string{ + "z-ai/glm-5.2", "deepseek/deepseek-v4-flash", + "anthropic/claude-opus", "openai/gpt-5", + } { price, known := PriceOf(id) if known || price != unpriced { t.Fatalf("PriceOf(%q) = %+v, %v; want %+v, false", id, price, known, unpriced) diff --git a/internal/session/fuelrows_test.go b/internal/session/fuelrows_test.go new file mode 100644 index 0000000000..616416e82f --- /dev/null +++ b/internal/session/fuelrows_test.go @@ -0,0 +1,43 @@ +package session + +// THE FUEL TABLE HAS TO KNOW EVERY MODEL THIS BUILD SHIPS. The table is the +// fallback an orchestrated run meters against when no catalog reader is +// installed, so a shipped id with no row is a seat that quietly bills at the +// unpriced rate — the most expensive default seat in particular. This pins the +// five shipped defaults and every id in both crew tables to a real row, so a +// new default or a moved crew seat fails the build until the table is repaid. + +import ( + "testing" + + "github.com/Agent-Field/codeaf/internal/config" + "github.com/Agent-Field/codeaf/internal/orchestrate" +) + +// TestEveryShippedModelHasAFuelRow: every default and every crew-table id +// resolves to a known fuel row rather than falling through to unpriced. +func TestEveryShippedModelHasAFuelRow(t *testing.T) { + shipped := []string{ + config.DefaultReflexModel, + config.DefaultLowModel, + config.DefaultWorkerModel, + config.DefaultHighModel, + config.DefaultMastermindModel, + } + for _, source := range config.CrewSources { + for _, preset := range config.CrewPresets { + models, ok := config.CrewModelsForSource(source, preset) + if !ok { + t.Fatalf("the %s crew has no %s preset", source, preset) + } + for _, model := range models { + shipped = append(shipped, model) + } + } + } + for _, id := range shipped { + if _, known := orchestrate.PriceOf(id); !known { + t.Errorf("no fuel row for shipped model %q", id) + } + } +} From 6658ec8a0e84b02768af15a775e303f84345ed3c Mon Sep 17 00:00:00 2001 From: santoshkumarradha Date: Thu, 17 Sep 2026 05:27:59 -0400 Subject: [PATCH 053/173] session: the cost test reads the flash-0731 row the table holds Co-Authored-By: codeaf --- internal/session/orchestrate_cost_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/session/orchestrate_cost_test.go b/internal/session/orchestrate_cost_test.go index d4f7141627..c3d10ada15 100644 --- a/internal/session/orchestrate_cost_test.go +++ b/internal/session/orchestrate_cost_test.go @@ -13,7 +13,7 @@ func TestOrchestrateCostKeepsABareNameOutOfAVendorsRow(t *testing.T) { want float64 }{ {model: "deepseek-v4-flash", want: 1.25}, - {model: "deepseek/deepseek-v4-flash-0731", want: 0.14}, + {model: "deepseek/deepseek-v4-flash-0731", want: 0.06}, } { if got := orchestrateCost(withoutReportedCost, test.model); got != test.want { t.Errorf("orchestrateCost without a reported cost for %q = %v, want %v", test.model, got, test.want) From cfa760489571503bad7c52f9f733d85e7ce21684 Mon Sep 17 00:00:00 2001 From: santoshkumarradha Date: Thu, 17 Sep 2026 07:06:27 -0400 Subject: [PATCH 054/173] config: the model_pool row, its three choices, and the resolver that reads it The four pool pins sat in OperatorEnvPins with a comment saying the word had no row because nothing called poolcfg; the row exists now, and the word moved from the pins list to the row the way CODEAF_DOC_ENGINE already is. ModelPoolAt is the one place the process environment is read for the pool. Co-Authored-By: codeaf --- internal/config/settings.go | 72 +++++++++++++++++++++++++------- internal/config/settings_test.go | 68 ++++++++++++++++++++++++++++++ 2 files changed, 124 insertions(+), 16 deletions(-) diff --git a/internal/config/settings.go b/internal/config/settings.go index c9753f996b..acf820d241 100644 --- a/internal/config/settings.go +++ b/internal/config/settings.go @@ -12,6 +12,7 @@ import ( "github.com/Agent-Field/codeaf/internal/ctxbudget" "github.com/Agent-Field/codeaf/internal/env" + "github.com/Agent-Field/codeaf/internal/pool/poolcfg" "github.com/Agent-Field/codeaf/internal/provider" "github.com/Agent-Field/codeaf/internal/roles" "github.com/Agent-Field/codeaf/internal/search" @@ -111,8 +112,13 @@ const ( KeyTenureAfter = "tenure_after" KeyDocumentEngine = "document_engine" KeyVisionModel = "vision_model" - KeyAttribution = "attribution" - KeySplitPct = "split_pct" + // KeyModelPool is the stored word the pool's resolver takes: the same three + // answers the CODEAF_MODEL_POOL pin and a CI environment may give it + // (internal/pool/poolcfg). The resolver beside the search rows is the one + // place the process environment is read for the pool. + KeyModelPool = "model_pool" + KeyAttribution = "attribution" + KeySplitPct = "split_pct" // The two rows the v3 chat surface keeps on disk BESIDE the conversation: // what was typed, and what was half-typed. They are one pair of questions — @@ -1055,6 +1061,10 @@ const SearchProviderAuto = "auto" // jina-search is named here too so every registered search plug is pinnable. var SearchProviders = []string{SearchProviderAuto, "firecrawl", "duckduckgo", "exa", "jina-search"} +// ModelPoolChoices are the answers the pool row accepts, and the same words +// the pool's resolver reads from its environment pin. +var ModelPoolChoices = []string{"on", "read", "off"} + // OperatorEnvPins is the explicit allowlist of environment variables that are // plumbing rather than settings: endpoints, credentials, profile roots, and // planner internals. They are listed read-only in the sheet's environment @@ -1243,21 +1253,19 @@ var OperatorEnvPins = []string{ // shape, under `make demo-home`'s terms. A row offering to persist a // fixture would put a demo question in front of a person every morning. "CODEAF_QUESTION_DEMO", - // The four names the measurement pool resolves from (internal/pool/poolcfg, - // which names them and reads none of them: the environment reaches that - // package as a function the caller hands in). CODEAF_MODEL_POOL is the - // on/read/off word, the two URLs are where the index is read and where - // measurements are handed to, and the TTL is how long a read copy stays - // young. + // The three pool names that stay plumbing (internal/pool/poolcfg, which + // names them and reads none of them: the environment reaches that package + // as a function the caller hands in). The two URLs are where the index is + // read and where measurements are handed to, and the TTL is how long a + // read copy stays young — endpoints and a cadence, on the terms the other + // addresses here are on. // - // They are plumbing on the terms the other addresses here are on: - // CODEAF_MODEL_POOL_URL and CODEAF_MODEL_POOL_SUBMIT_URL are endpoints, - // exactly as CODEAF_BASE_URL and CODEAF_RELAY are, and the TTL is a - // cadence nobody sets to express a preference. The word itself is the one - // of the four a sheet row could own, and it has no row because it has no - // stored setting yet: poolcfg takes the setting as an argument and nothing - // in the binary calls it. The row is owed on the day something does. - "CODEAF_MODEL_POOL", + // THE WORD ITSELF IS NOT HERE ANY MORE. CODEAF_MODEL_POOL fronts the + // model_pool row now, which is what the comment below used to promise: + // it was listed here because poolcfg took the setting as an argument and + // nothing in the binary called it, and a pin with no row was the honest + // answer then. With the row here, the word renders through the row — dim, + // "pinned by CODEAF_MODEL_POOL" — the way CODEAF_DOC_ENGINE does. "CODEAF_MODEL_POOL_URL", "CODEAF_MODEL_POOL_SUBMIT_URL", "CODEAF_MODEL_POOL_TTL", @@ -1837,6 +1845,20 @@ func (s *Settings) build() []Setting { read: func() string { return SearchProviderAt(dir) }, write: func(raw string) error { return writeChoice(dir, KeySearchProvider, raw, SearchProviders) }, }, + // The Model Pool row. It is the stored word [ModelPoolAt] resolves, and + // the pin beside it is one of the four names the resolver takes. The + // row sits with the search rows because it is the same shape of + // question — what does codeaf reach OUTSIDE the machine for — and its + // answer is read the same way a choice is read everywhere here. + Setting{ + Key: KeyModelPool, Category: CategoryModels, Kind: SettingChoice, + Label: "model pool", Env: "CODEAF_MODEL_POOL", Choices: ModelPoolChoices, + Hint: "codeaf picks your models from the public Model Pool, and your runs improve it. " + + "Nothing about your code leaves your machine. " + + "read: use the pool, send nothing. off: neither.", + read: func() string { return ModelPoolAt(dir).Mode.String() }, + write: func(raw string) error { return writeChoice(dir, KeyModelPool, raw, ModelPoolChoices) }, + }, // THE KEY EVERY MODEL CALL RIDES. The default local door normally creates // one through the browser (tui3's firstrun.go); this row remains the place // to paste a replacement or to use a custom endpoint's credential. It masks @@ -3240,6 +3262,24 @@ func SearchProviderAt(profileDir string) string { return DefaultSearchProvider } +// ModelPoolSettingAt is the stored word the pool row holds: one of the three +// choices, or empty for a person who has never answered. It is the argument +// the pool's resolver takes, and the raw half of [ModelPoolAt]. +func ModelPoolSettingAt(profileDir string) string { + value, _ := persistedString(profileDir, KeyModelPool) + return value +} + +// ModelPoolAt resolves how the Model Pool behaves: the stored word and the +// pool's environment names, through poolcfg.Resolve. IT IS THE ONE PLACE THE +// PROCESS ENVIRONMENT IS READ FOR THE POOL — poolcfg itself takes the +// environment as a function, and a caller with its own (the `codeaf pool` +// verb, whose tests inject one) resolves [ModelPoolSettingAt] against its own +// lookup rather than calling this. +func ModelPoolAt(profileDir string) poolcfg.Config { + return poolcfg.Resolve(ModelPoolSettingAt(profileDir), os.LookupEnv) +} + // ExaKeyAt resolves the Exa credential: the environment first, then the sheet, // then empty — and empty is a working configuration, not a fault. func ExaKeyAt(profileDir string) string { diff --git a/internal/config/settings_test.go b/internal/config/settings_test.go index 03e68d4f95..0495b5871d 100644 --- a/internal/config/settings_test.go +++ b/internal/config/settings_test.go @@ -10,6 +10,7 @@ import ( "testing" "time" + "github.com/Agent-Field/codeaf/internal/pool/poolcfg" "github.com/Agent-Field/codeaf/internal/standing" "github.com/Agent-Field/codeaf/internal/taxonomy" ) @@ -1381,3 +1382,70 @@ func TestThePromptProfileRowRoundTripsAndItsPinWinsForOneLaunch(t *testing.T) { t.Fatalf("a mistyped pin overrode the persisted row: %q", got) } } + +// The Model Pool row: the stored word the pool resolver takes, the pin beside +// it, and the resolver's own reading of both. The three choices are the same +// three words poolcfg.Resolve accepts from the environment, so a row and a +// shell spell one thing — and the resolver in this package is the one place +// the process environment is read for the pool. +func TestTheModelPoolRowDefaultsToOnAndFollowsItsStoredWordAndItsPin(t *testing.T) { + dir := t.TempDir() + t.Setenv("CODEAF_MODEL_POOL", "") + t.Setenv("CI", "") + rows := registry(t, dir) + row, ok := rows.Row(KeyModelPool) + if !ok { + t.Fatal("the model pool is not registered") + } + if row.Category != CategoryModels || row.Kind != SettingChoice || row.Label != "model pool" { + t.Fatalf("model pool row = %+v", row) + } + if row.Value() != "on" { + t.Fatalf("the pool does not default to on: %q", row.Value()) + } + if got := ModelPoolAt(dir); got.Mode != poolcfg.On || got.Source.Mode != "default" { + t.Fatalf("an untouched profile resolved %+v", got) + } + + if err := row.Apply("read"); err != nil { + t.Fatal(err) + } + reread, _ := registry(t, dir).Row(KeyModelPool) + if reread.Value() != "read" { + t.Fatalf("the reread row lost the persisted choice: %q", reread.Value()) + } + if got := ModelPoolAt(dir); got.Mode != poolcfg.Read || got.Source.Mode != "setting" { + t.Fatalf("the stored word did not take: %+v", got) + } + if err := reread.Apply("maybe"); err == nil || !strings.Contains(err.Error(), "on, read, off") { + t.Fatalf("the row accepted a word it does not take: %v", err) + } + if ModelPoolAt(dir).Mode != poolcfg.Read { + t.Fatal("a refused edit still moved the row") + } + + // The pin outranks the stored word, the row reports it, and a pinned row + // refuses to be edited the way every other pinned row refuses. + t.Setenv("CODEAF_MODEL_POOL", "off") + pinned, _ := registry(t, dir).Row(KeyModelPool) + if name, isPinned := pinned.PinnedBy(); !isPinned || name != "CODEAF_MODEL_POOL" { + t.Fatalf("the pool row did not report its pin: %q %v", name, isPinned) + } + if got := ModelPoolAt(dir); got.Mode != poolcfg.Off || got.Source.Mode != "env" { + t.Fatalf("the pin lost: %+v", got) + } + if err := pinned.Apply("on"); err == nil || !strings.Contains(err.Error(), "CODEAF_MODEL_POOL") { + t.Fatalf("a pinned pool row accepted an edit: %v", err) + } + + // A word nobody set the row to on purpose — an older build's word, a + // hand-typed typo — reads as the default rather than as an error, the way + // [SearchProviderAt] reads "yahoo!" as auto. + t.Setenv("CODEAF_MODEL_POOL", "") + if err := writeProfileValue(dir, KeyModelPool, "maybe"); err != nil { + t.Fatal(err) + } + if got := ModelPoolAt(dir); got.Mode != poolcfg.On { + t.Fatalf("a stale word did not fall back to on: %+v", got) + } +} From 6826f40f2ad1cb42f0b1da25b9843dfcf76346a3 Mon Sep 17 00:00:00 2001 From: santoshkumarradha Date: Thu, 17 Sep 2026 07:13:30 -0400 Subject: [PATCH 055/173] crewpick: a seat's quality blends a measured pool rating by its count FrontWith and SeatQualityWith take a Prior of per-seat, per-model ratings and blend each seat's quality towards its rating's mean by N/(N+PriorWeightAt); Front and SeatQuality keep their signatures and pass no prior. PriorFromCells reads role-worded cells into a prior, and Crew.Measured marks the seats a rating entered. Co-Authored-By: codeaf --- internal/crewpick/crewpick.go | 144 +++++++++++++++++++++++++++++++--- 1 file changed, 131 insertions(+), 13 deletions(-) diff --git a/internal/crewpick/crewpick.go b/internal/crewpick/crewpick.go index 35ba5c36aa..4552b53381 100644 --- a/internal/crewpick/crewpick.go +++ b/internal/crewpick/crewpick.go @@ -169,6 +169,12 @@ type Crew struct { // and the column the index. A crew picked on measured indexes alone is // all false. Estimated [3][3]bool + // Measured marks, for the model in each seat, whether a pool's own + // rating for that model entered the seat's quality: the row is the seat + // and the value says the seat's model carried a rating with a positive + // observation count. A crew picked with no prior, or whose seats the + // prior never rated, is all false. + Measured [3]bool } // Floor is the share of a seat's best quality in the family that a candidate @@ -176,6 +182,74 @@ type Crew struct { // the job is not cheap, whatever it costs. const Floor = 0.80 +// PriorWeightAt is the observation count at which a pool's own rating for a +// model carries the same weight in a seat's quality as the catalog's +// published indexes: at N observations the rating is worth half the seat and +// the catalog the other half. A rating with no observations never enters. +const PriorWeightAt = 30 + +// A Rating is a pool's measured quality for one model on one seat, on the +// same 0-100 scale quality is read on, with the number of observations behind +// it. +type Rating struct { + Mean float64 + N int +} + +// A Prior is a pool's measured quality per seat, keyed by canonical model id: +// the rating a seat's quality is blended towards, by how many observations +// back it. A nil Prior is no prior at all, and every seat keeps the catalog +// quality. +type Prior map[Seat]map[string]Rating + +// A Cell is one measurement a caller hands PriorFromCells: a role, a model id +// and the pool's mean quality with the observations behind it. It is the shape +// a measurement document reads into, so crewpick need not import the reader. +type Cell struct { + Role string + Model string + Mean float64 + N int +} + +// seatWords names the roles a Cell's role word maps to a seat by, folded to +// one spelling so a document's own case never matters. Any other word names +// no seat and is ignored. +var seatWords = map[string]Seat{ + "worker": Worker, + "high": High, + "mastermind": Mastermind, +} + +// PriorFromCells reads a pool's measurements into a Prior: a cell names a +// seat by its role word, a model by an id that canonical resolves to the +// canonical id a candidate is looked up by, and carries a mean and a count. +// A cell whose role names no seat, or whose count is below minInstalls, is +// ignored, and so is a count of zero or less. A nil canonical leaves the id +// as it stands. When no cell survives the prior is nil, which is the same as +// no prior at all. +func PriorFromCells(cells []Cell, minInstalls int, canonical func(string) string) Prior { + var prior Prior + for _, c := range cells { + seat, ok := seatWords[strings.ToLower(strings.TrimSpace(c.Role))] + if !ok || c.N < minInstalls || c.N <= 0 { + continue + } + model := c.Model + if canonical != nil { + model = canonical(model) + } + if prior == nil { + prior = Prior{} + } + if prior[seat] == nil { + prior[seat] = map[string]Rating{} + } + prior[seat][model] = Rating{Mean: c.Mean, N: c.N} + } + return prior +} + // SeatCost is the seat's expected cost of a candidate, in dollars per 1M // input-equivalent tokens: the prompt price blended with the cache-read // price by the seat's cache share, plus the completion price spread over the @@ -197,12 +271,22 @@ func SeatCost(c Candidate, shape SeatShape) float64 { // and any row against a pool with no candidates to raise a scale, score // zero. func SeatQuality(c Candidate, shape SeatShape, pool []Candidate) float64 { + return SeatQualityWith(c, shape, pool, Worker, nil) +} + +// SeatQualityWith is SeatQuality with the pool's prior blended in for the seat +// given: a rating the prior holds for the candidate's canonical id, with a +// positive observation count, moves the seat's quality towards the rating's +// mean by N/(N+PriorWeightAt), and a seat the prior never rates — or rates +// with no observations — keeps the catalog quality unchanged. +func SeatQualityWith(c Candidate, shape SeatShape, pool []Candidate, seat Seat, prior Prior) float64 { if !isCandidate(c) { return 0 } scale := maxima(pool) r := fill(c, readTally(pool), scale) - return quality(r.idx, shape, scale) + q, _ := blendQuality(c.ID, quality(r.idx, shape, scale), seat, prior) + return q } // Front returns the pareto front of (bill, quality) over every crew the @@ -218,15 +302,26 @@ func SeatQuality(c Candidate, shape SeatShape, pool []Candidate) float64 { // id order — worker, then high, then mastermind — which is what makes the // front a property of the candidates rather than of their order. func Front(candidates []Candidate, shapes map[Seat]SeatShape, fam Family) []Crew { + return FrontWith(candidates, shapes, fam, nil) +} + +// FrontWith is Front with the pool's prior blended into each seat's quality: a +// rating the prior holds for a seat's model, with a positive observation +// count, moves that seat's quality towards the rating's mean by +// N/(N+PriorWeightAt), the seat's measured flag says so, and the floor and the +// crew's quality read the blended value. A nil prior leaves every seat on the +// catalog quality, exactly as Front does. The front is a property of the +// candidates and the prior, never of the order they arrive in. +func FrontWith(candidates []Candidate, shapes map[Seat]SeatShape, fam Family, prior Prior) []Crew { scale := maxima(candidates) t := readTally(candidates) readings := make([]reading, 0, len(candidates)) for _, c := range candidates { readings = append(readings, fill(c, t, scale)) } - workers := shortlist(readings, shapes[Worker], fam, scale) - highs := shortlist(readings, shapes[High], fam, scale) - minds := shortlist(readings, shapes[Mastermind], fam, scale) + workers := shortlist(readings, shapes[Worker], fam, scale, Worker, prior) + highs := shortlist(readings, shapes[High], fam, scale, High, prior) + minds := shortlist(readings, shapes[Mastermind], fam, scale, Mastermind, prior) vw := shapes[Worker].Volume vh := shapes[High].Volume vm := shapes[Mastermind].Volume @@ -245,6 +340,7 @@ func Front(candidates []Candidate, shapes map[Seat]SeatShape, fam Family) []Crew Bill: vw*w.cost + vh*h.cost + vm*m.cost, Quality: (w.quality + h.quality + m.quality) / 3, Estimated: [3][3]bool{w.est, h.est, m.est}, + Measured: [3]bool{w.measured, h.measured, m.measured}, }) } } @@ -326,26 +422,30 @@ func knee(front []Crew) Crew { } // A seatPick is a candidate already priced and scored for one seat, on the -// candidate's reading: measured and estimated indexes alike. +// candidate's reading: measured and estimated indexes alike. measured says +// whether the pool's own rating for the seat entered the quality. type seatPick struct { - c Candidate - idx [3]float64 - est [3]bool - cost float64 - quality float64 + c Candidate + idx [3]float64 + est [3]bool + cost float64 + quality float64 + measured bool } // shortlist returns a seat's running: the candidates that pass family and the // seat's needs, kept when their quality reaches Floor of the family's best -// for the seat, in id order. -func shortlist(readings []reading, shape SeatShape, fam Family, scale [3]float64) []seatPick { +// for the seat, in id order. The seat's quality is read with the pool's prior +// blended in, and measured marks the picks the prior touched. +func shortlist(readings []reading, shape SeatShape, fam Family, scale [3]float64, seat Seat, prior Prior) []seatPick { var running []seatPick top := 0.0 for _, r := range readings { if !eligible(r.c, shape, fam) { continue } - pick := seatPick{c: r.c, idx: r.idx, est: r.est, cost: SeatCost(r.c, shape), quality: quality(r.idx, shape, scale)} + q, measured := blendQuality(r.c.ID, quality(r.idx, shape, scale), seat, prior) + pick := seatPick{c: r.c, idx: r.idx, est: r.est, cost: SeatCost(r.c, shape), quality: q, measured: measured} running = append(running, pick) top = math.Max(top, pick.quality) } @@ -370,6 +470,24 @@ func quality(idx [3]float64, shape SeatShape, scale [3]float64) float64 { w[Agentic]*idx[Agentic]/scale[Agentic]) } +// blendQuality blends the pool's prior for one seat into the catalog quality: +// a rating the prior holds for the model on the seat, with a positive +// observation count, carries weight N/(N+PriorWeightAt) against the catalog's +// own quality, and the second result reports whether the rating entered. A +// seat the prior never rates, or rates with no observations, keeps the +// catalog quality and reads measured false. +func blendQuality(model string, catalog float64, seat Seat, prior Prior) (float64, bool) { + if prior == nil { + return catalog, false + } + rating, ok := prior[seat][model] + if !ok || rating.N <= 0 { + return catalog, false + } + w := float64(rating.N) / float64(rating.N+PriorWeightAt) + return (1-w)*catalog + w*rating.Mean, true +} + // maxima reads the pool's largest measured value of each index — the scale // quality is measured against and the ceiling an estimate never crosses. // Only real candidates raise a ceiling, and a row that publishes only some From 2b4b2c2f870e73973133d191919fbae028a2ee97 Mon Sep 17 00:00:00 2001 From: santoshkumarradha Date: Thu, 17 Sep 2026 07:14:09 -0400 Subject: [PATCH 056/173] crewpick: tests for the seat quality prior and the measured marks Covers a large rating moving a seat towards the pool mean, the midpoint at PriorWeightAt, a count below min_installs and an unknown role ignored, the measured marks set only where the prior touched, a prior never moving a bill, order independence with a prior, and an alias meeting its candidate through canonical. Co-Authored-By: codeaf --- internal/crewpick/crewpick_test.go | 153 +++++++++++++++++++++++++++++ 1 file changed, 153 insertions(+) diff --git a/internal/crewpick/crewpick_test.go b/internal/crewpick/crewpick_test.go index ed59e54146..9c66fa157c 100644 --- a/internal/crewpick/crewpick_test.go +++ b/internal/crewpick/crewpick_test.go @@ -631,3 +631,156 @@ func TestSeatQualityReadsAgainstThePoolsBest(t *testing.T) { t.Fatalf("a pool with no candidates scored %f, want 0", got) } } + +// A pool's own rating for a seat moves that seat's quality towards the +// rating's mean, and the more observations back it the farther it moves: a +// rating with a large count across a seat whose catalog quality is 100 and +// whose pool mean is 50 lands somewhere between the two. +func TestALargeRatingMovesASeatTowardsThePoolMean(t *testing.T) { + shape := SeatShape{Weights: [3]float64{1, 0, 0}} + pool := []Candidate{model("m")} + if base := SeatQuality(pool[0], shape, pool); base != 100 { + t.Fatalf("the catalog quality is %f, want 100", base) + } + prior := Prior{Worker: {"m": {Mean: 50, N: 1000}}} + if got := SeatQualityWith(pool[0], shape, pool, Worker, prior); got <= 50 || got >= 100 { + t.Fatalf("a large rating scored %f, want between the pool mean 50 and the catalog 100", got) + } +} + +// The blend carries the rating and the catalog at equal weight at exactly +// PriorWeightAt observations, so the seat reads the midpoint of the two; a +// seat the prior never rates, and a rating with no observations, keep the +// catalog quality. +func TestTheBlendAtTheRatingWeightIsTheMidpoint(t *testing.T) { + shape := SeatShape{Weights: [3]float64{1, 0, 0}} + pool := []Candidate{model("m")} + + half := Prior{Worker: {"m": {Mean: 40, N: PriorWeightAt}}} + if got := SeatQualityWith(pool[0], shape, pool, Worker, half); got != 70 { + t.Fatalf("the blend at N = PriorWeightAt scored %f, want the midpoint 70", got) + } + unrated := Prior{Worker: {"other": {Mean: 40, N: 1000}}} + if got := SeatQualityWith(pool[0], shape, pool, Worker, unrated); got != 100 { + t.Fatalf("a seat the prior never rated scored %f, want the catalog 100", got) + } + empty := Prior{Worker: {"m": {Mean: 40, N: 0}}} + if got := SeatQualityWith(pool[0], shape, pool, Worker, empty); got != 100 { + t.Fatalf("a rating with no observations scored %f, want the catalog 100", got) + } +} + +// A prior is read from role-worded cells: a role that names no seat and a +// count below min_installs are both ignored, and only a role that names a +// seat with enough observations behind it becomes a rating. +func TestPriorFromCellsIgnoresACountBelowMinInstalls(t *testing.T) { + prior := PriorFromCells([]Cell{ + {Role: "worker", Model: "a/kept", Mean: 80, N: 40}, + {Role: "worker", Model: "b/dropped", Mean: 90, N: 10}, + {Role: "engineer", Model: "c/ignored", Mean: 95, N: 100}, + }, 30, nil) + if len(prior) != 1 || len(prior[Worker]) != 1 { + t.Fatalf("prior = %v, want one worker rating", prior) + } + if _, ok := prior[Worker]["a/kept"]; !ok { + t.Fatalf("a rating at the floor was dropped: %v", prior) + } + if _, ok := prior[Worker]["b/dropped"]; ok { + t.Fatalf("a rating below min_installs survived: %v", prior) + } +} + +// The result says which seats a pool's rating entered: a seat whose model the +// prior rated reads measured, and a seat it did not — here the mastermind, +// which no rating names — does not. +func TestMeasuredIsSetOnlyOnTheSeatsThePriorTouched(t *testing.T) { + pool := []Candidate{model("a/w"), model("c/h"), model("b/m")} + prior := Prior{ + Worker: {"a/w": {Mean: 100, N: 100}}, + High: {"b/m": {Mean: 100, N: 100}}, + } + front := FrontWith(pool, DefaultShapes(), All, prior) + if len(front) == 0 { + t.Fatal("the pool must field crews") + } + wantCrew(t, "frugal", front[0], "a/w|b/m|a/w") + if front[0].Measured != ([3]bool{true, true, false}) { + t.Fatalf("Measured = %v, want the worker and high seats alone", front[0].Measured) + } +} + +// A prior changes quality alone: every bill a crew is given is its seats' +// token mixes priced, exactly as it is without a prior. +func TestAPriorNeverChangesTheBill(t *testing.T) { + pool := []Candidate{model("a/w"), model("c/h"), model("b/m")} + shapes := DefaultShapes() + prior := Prior{ + Worker: {"a/w": {Mean: 20, N: 10_000}}, + High: {"c/h": {Mean: 140, N: 10_000}}, + } + front := FrontWith(pool, shapes, All, prior) + if len(front) == 0 { + t.Fatal("the pool must field crews") + } + for _, crew := range front { + want := 0.0 + for _, seat := range []struct { + model string + seat Seat + }{{crew.Worker, Worker}, {crew.High, High}, {crew.Mastermind, Mastermind}} { + for _, c := range pool { + if c.ID == seat.model { + want += shapes[seat.seat].Volume * SeatCost(c, shapes[seat.seat]) + } + } + } + if math.Abs(crew.Bill-want) > 1e-9 { + t.Fatalf("crew %s|%s|%s bill = %f, want its seats' priced mix %f", + crew.Worker, crew.High, crew.Mastermind, crew.Bill, want) + } + } +} + +// A prior is a property of the candidates, not of their order: with a rating +// present the same pool gives the same front shuffled, rotated and reversed. +func TestAPriorKeepsTheFrontTheSameInAnyOrder(t *testing.T) { + cands := loadCandidates(t) + prior := Prior{Worker: {"z-ai/glm-5.3": {Mean: 90, N: 100}}} + one := FrontWith(cands, DefaultShapes(), All, prior) + if len(one) == 0 { + t.Fatal("the pool must field crews") + } + reversed := slices.Clone(cands) + slices.Reverse(reversed) + if two := FrontWith(reversed, DefaultShapes(), All, prior); !slices.Equal(one, two) { + t.Fatal("the front changed when the candidates arrived reversed") + } + rotated := append(slices.Clone(cands[3:]), cands[:3]...) + if three := FrontWith(rotated, DefaultShapes(), All, prior); !slices.Equal(one, three) { + t.Fatal("the front changed when the pool arrived rotated") + } +} + +// An alias resolved through canonical meets its candidate: a cell spelling a +// model's alias lands on the candidate's canonical id, so the rating reaches +// it, while the same cell with no canonical step lands on the alias and +// touches nothing. +func TestAnAliasResolvedThroughCanonicalMeetsItsCandidate(t *testing.T) { + shape := SeatShape{Weights: [3]float64{1, 0, 0}} + pool := []Candidate{model("z-ai/glm-5.3")} + canonical := func(id string) string { + if id == "glm-5.3" { + return "z-ai/glm-5.3" + } + return id + } + cell := Cell{Role: "worker", Model: "glm-5.3", Mean: 40, N: PriorWeightAt} + resolved := PriorFromCells([]Cell{cell}, 5, canonical) + if got := SeatQualityWith(pool[0], shape, pool, Worker, resolved); got != 70 { + t.Fatalf("the aliased rating scored %f, want the midpoint 70", got) + } + unresolved := PriorFromCells([]Cell{cell}, 5, nil) + if got := SeatQualityWith(pool[0], shape, pool, Worker, unresolved); got != 100 { + t.Fatalf("the unresolved alias scored %f, want the catalog 100 untouched", got) + } +} From 07b33e756fe711682bc037a7d7aedb5ffbc31333 Mon Sep 17 00:00:00 2001 From: santoshkumarradha Date: Thu, 17 Sep 2026 07:14:38 -0400 Subject: [PATCH 057/173] config: the auto pick blends a Model Pool index into seat quality MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit AutoIndex is the injected index seam beside AutoModels, and autoPrior reads its role_quality cells — a gaussian metric only — through PriorFromCells, resolved by the index's canonical ids. AutoPick reads the index once per call and hands the prior to crewpick.FrontWith. Co-Authored-By: codeaf --- internal/config/auto.go | 48 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 47 insertions(+), 1 deletion(-) diff --git a/internal/config/auto.go b/internal/config/auto.go index 1cb6be3c69..f8f72ab5e9 100644 --- a/internal/config/auto.go +++ b/internal/config/auto.go @@ -5,6 +5,7 @@ import ( "github.com/Agent-Field/codeaf/internal/catalog" "github.com/Agent-Field/codeaf/internal/crewpick" + "github.com/Agent-Field/codeaf/internal/pool/index" ) // THE WORD THAT ASKS THE CATALOG TO ANSWER A TIER ROW. @@ -41,6 +42,20 @@ func IsAuto(value string) bool { // is the answer it had before this word existed. var AutoModels func() []catalog.Model +// AutoIndex is how the Model Pool's measurement index reaches seat +// resolution: the binary holding the index sets it ONCE AT START-UP, from +// whatever read it already made, and never a fetch — a tier row that says +// auto resolves from the index already in hand, the same posture AutoModels +// keeps. Nil is an ordinary state, not an error: a row that says auto then +// reads the catalog's own figures alone, which is what it read before this +// seam existed. +var AutoIndex func() *index.Index + +// PoolQualityMetric names the index metric the picker reads a measured seat +// quality from: a gaussian metric whose mean is on the same 0-100 scale +// crewpick scores a seat on, with one cell per role and model. +const PoolQualityMetric = "role_quality" + // AutoPick is the word's own answer for one tier, computed from the rows. // // It is PURE: no disk, no network, the rows are only read, and the same rows @@ -73,7 +88,7 @@ func AutoPick(tier, family, preset string, models []catalog.Model) (modelID stri if len(candidates) == 0 { return "", false } - frugal, balanced, max := crewpick.Presets(crewpick.Front(candidates, crewpick.DefaultShapes(), fam)) + frugal, balanced, max := crewpick.Presets(crewpick.FrontWith(candidates, crewpick.DefaultShapes(), fam, autoPrior(autoIndex()))) var pick crewpick.Crew switch strings.ToLower(strings.TrimSpace(preset)) { case CrewFrugal: @@ -97,6 +112,37 @@ func AutoPick(tier, family, preset string, models []catalog.Model) (modelID stri return id, id != "" } +// autoIndex is [AutoIndex] read with its ordinary absence folded into one +// answer. +func autoIndex() *index.Index { + if AutoIndex == nil { + return nil + } + return AutoIndex() +} + +// autoPrior reads the index's measured quality into a crewpick prior: the +// cells of PoolQualityMetric, kept only when the metric is declared gaussian +// — a mean on any other scale would be blended against figures it does not +// share units with — and dropped by PriorFromCells below the index's own +// min_installs, resolved through the index's canonical ids so an alias meets +// its candidate. A nil index, or a metric that is not there or not gaussian, +// answers no prior, which leaves every seat on the catalog quality. +func autoPrior(idx *index.Index) crewpick.Prior { + if idx == nil { + return nil + } + if kind, ok := idx.Kind(PoolQualityMetric); !ok || kind != "gaussian" { + return nil + } + cells := idx.Cells(PoolQualityMetric) + measured := make([]crewpick.Cell, 0, len(cells)) + for _, c := range cells { + measured = append(measured, crewpick.Cell{Role: c.Role, Model: c.Model, Mean: c.Mean, N: c.N}) + } + return crewpick.PriorFromCells(measured, idx.MinInstalls(), idx.Canonical) +} + // autoSeat is the tier word that names which of crewpick's three seats the // pick is read from. The two tiers that never vary — reflex and small work — // have no answer here, the same law their columns in the shipped tables keep. From a6f3730ea81be3a95b8420006689e7de9ac6ca05 Mon Sep 17 00:00:00 2001 From: santoshkumarradha Date: Thu, 17 Sep 2026 07:14:38 -0400 Subject: [PATCH 058/173] config: a test for the index moving the auto pick through the prior With an index rating one catalog row above its worker quality, AutoPick's worker answer moves to that row; with no index it answers the front, as before. Co-Authored-By: codeaf --- internal/config/auto_test.go | 54 ++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/internal/config/auto_test.go b/internal/config/auto_test.go index dfcfccfe79..0f7eaaa6ca 100644 --- a/internal/config/auto_test.go +++ b/internal/config/auto_test.go @@ -8,6 +8,7 @@ import ( "github.com/Agent-Field/codeaf/internal/catalog" "github.com/Agent-Field/codeaf/internal/crewpick" + "github.com/Agent-Field/codeaf/internal/pool/index" ) // THE WORD, AND WHAT IS NOT THE WORD. `auto` is a bare word a tier row holds, @@ -372,3 +373,56 @@ func TestTheTableRungAnswersThePresetTheRowsName(t *testing.T) { seat.Model, seat.Rung()) } } + +// mustIndex parses a measurement document or fails the test. +func mustIndex(t *testing.T, doc string) *index.Index { + t.Helper() + x, err := index.Parse([]byte(doc)) + if err != nil { + t.Fatalf("parse index: %v", err) + } + return x +} + +// priorDocument is the smallest index document that rates one catalog row on +// the worker seat: a gaussian role_quality metric and a cell well above the +// document's min_installs. +const priorDocument = `{ + "schema": 1, + "generated": "2026-09-17", + "min_installs": 5, + "metrics": {"role_quality": {"kind": "gaussian", "dims": ["role", "model"]}}, + "cells": [ + {"metric": "role_quality", "role": "worker", "model": "a/cheap", "mean": 95, "sd": 5, "n": 1000} + ] +}` + +// THE INDEX REACHES THE PICK THROUGH THE PRIOR. An index whose role_quality +// metric rates one catalog row well above its published worker quality moves +// AutoPick's worker answer to that row, and with no index the pick answers +// exactly what the front answers, as before. +func TestAutoPickReadsAMeasuredQualityPrior(t *testing.T) { + restore := AutoIndex + defer func() { AutoIndex = restore }() + + rows := autoTestRows() + + AutoIndex = nil + frugal, _, _ := crewpick.Presets(crewpick.Front(autoCandidates(rows), crewpick.DefaultShapes(), crewpick.All)) + before, ok := AutoPick(ModelTierWorker, CrewSourceAll, CrewFrugal, rows) + if !ok || before != frugal.Worker { + t.Fatalf("with no index the worker pick reads %q, want the front's own %q", before, frugal.Worker) + } + + AutoIndex = func() *index.Index { return mustIndex(t, priorDocument) } + after, ok := AutoPick(ModelTierWorker, CrewSourceAll, CrewFrugal, rows) + if !ok { + t.Fatal("the pick answers nothing with the index in hand") + } + if after == before { + t.Fatalf("the rated row left the worker pick at %q; the index never reached the quality", after) + } + if after != "a/cheap" { + t.Fatalf("the rated row did not take the worker seat: got %q", after) + } +} From e13a4dbcf5655047a108d6408f20b32df66b2274 Mon Sep 17 00:00:00 2001 From: santoshkumarradha Date: Thu, 17 Sep 2026 07:14:50 -0400 Subject: [PATCH 059/173] docs: a change note for the crew picker's measured quality prior Co-Authored-By: codeaf --- .../unreleased/1095-crew-pool-quality-prior.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 docs/changes/unreleased/1095-crew-pool-quality-prior.md diff --git a/docs/changes/unreleased/1095-crew-pool-quality-prior.md b/docs/changes/unreleased/1095-crew-pool-quality-prior.md new file mode 100644 index 0000000000..fcfcde358f --- /dev/null +++ b/docs/changes/unreleased/1095-crew-pool-quality-prior.md @@ -0,0 +1,15 @@ +--- +kind: changed +title: The crew picker blends a measured pool rating into a seat's quality by its observation count +pr: 1095 +surface: [engine] +invalidates: + - "`crewpick.SeatQuality` and `crewpick.Front` read a seat's quality from the catalog's published indexes alone. They now have `SeatQualityWith` and `FrontWith`, which blend a `crewpick.Prior` — a pool's measured quality per seat, per canonical model id — into each seat by N/(N+PriorWeightAt): a rating the prior holds with a positive count moves the seat towards the rating's mean, and `Crew.Measured` says which seats a rating entered. `SeatQuality` and `Front` keep their signatures and read no prior." + - "`config.AutoPick` resolved a seat from the catalog's published figures and nothing else. It now reads `config.AutoIndex`, a Model Pool index seam beside `config.AutoModels`, and blends the index's `role_quality` cells (a gaussian metric only, through `config.PoolQualityMetric`) into the pick through `crewpick.FrontWith`. A nil index leaves the answer exactly what it was." +--- + +The table of published indexes is a statement about models, and a pool that has +actually run a model on a seat holds something the table cannot: what it scored +there, over however many observations. The blend carries each by its evidence, +so a handful of pool ratings nudges a seat and a large pool of them decides it, +while a picker with no index in hand is unchanged. From 12f233fe1882b7902689649c3daa3a3080b90223 Mon Sep 17 00:00:00 2001 From: santoshkumarradha Date: Thu, 17 Sep 2026 07:20:19 -0400 Subject: [PATCH 060/173] docs: the change note carries this change's pull request number Co-Authored-By: Claude Fable 5.1 --- ...ew-pool-quality-prior.md => 1101-crew-pool-quality-prior.md} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename docs/changes/unreleased/{1095-crew-pool-quality-prior.md => 1101-crew-pool-quality-prior.md} (99%) diff --git a/docs/changes/unreleased/1095-crew-pool-quality-prior.md b/docs/changes/unreleased/1101-crew-pool-quality-prior.md similarity index 99% rename from docs/changes/unreleased/1095-crew-pool-quality-prior.md rename to docs/changes/unreleased/1101-crew-pool-quality-prior.md index fcfcde358f..1a6b35df1e 100644 --- a/docs/changes/unreleased/1095-crew-pool-quality-prior.md +++ b/docs/changes/unreleased/1101-crew-pool-quality-prior.md @@ -1,7 +1,7 @@ --- kind: changed title: The crew picker blends a measured pool rating into a seat's quality by its observation count -pr: 1095 +pr: 1101 surface: [engine] invalidates: - "`crewpick.SeatQuality` and `crewpick.Front` read a seat's quality from the catalog's published indexes alone. They now have `SeatQualityWith` and `FrontWith`, which blend a `crewpick.Prior` — a pool's measured quality per seat, per canonical model id — into each seat by N/(N+PriorWeightAt): a rating the prior holds with a positive count moves the seat towards the rating's mean, and `Crew.Measured` says which seats a rating entered. `SeatQuality` and `Front` keep their signatures and read no prior." From f787114bab047b0dade548b2bdb4a4676e63a323 Mon Sep 17 00:00:00 2001 From: santoshkumarradha Date: Thu, 17 Sep 2026 07:22:21 -0400 Subject: [PATCH 061/173] =?UTF-8?q?pool:=20the=20codeaf=20pool=20verb=20?= =?UTF-8?q?=E2=80=94=20show,=20status=20and=20verify?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit show and status read the resolved config and the cached index and never touch the network; verify fetches through pull and is refused at the door while no build carries the published key. The pool's files resolve through config.ProfilePath, because an empty profile is the state root's own, not a relative pool directory beside wherever the command ran. The help page's line budget is full at 110, so the verb's entry pays for itself in three cells whose content the page or the per-command pages carry. Co-Authored-By: codeaf --- cmd/codeaf/main.go | 12 +- cmd/codeaf/pool.go | 352 +++++++++++++++++ cmd/codeaf/pool_test.go | 370 ++++++++++++++++++ cmd/codeaf/usage.go | 2 +- .../manual/chat/running-from-the-terminal.md | 36 ++ internal/manual/pages/surfaces.md | 4 +- 6 files changed, 769 insertions(+), 7 deletions(-) create mode 100644 cmd/codeaf/pool.go create mode 100644 cmd/codeaf/pool_test.go diff --git a/cmd/codeaf/main.go b/cmd/codeaf/main.go index b6e4ccd7d3..376b94fde1 100644 --- a/cmd/codeaf/main.go +++ b/cmd/codeaf/main.go @@ -198,6 +198,8 @@ func run() error { os.Args[2:], func(args []string) error { return runShow("plan show", args) }) case "models": return runModels(os.Args[2:]) + case "pool": + return runPool(os.Args[2:]) case "notebook": return runNotebook(os.Args[2:]) case "collections": @@ -332,7 +334,6 @@ Hand it work — nobody is watching, the answer is on stdout codeaf exec [""] [--dir dir] [--system text] [--max-turns N] [--token-budget N] [--timeout 15m] [--model slug] [--context-fill N] [--completion-reserve N] [--json] - [--out file] [--debug] run one worker for one pass, with no planning at all codeaf run --input [--dir dir] [--model slug] [--journal path] [--json] @@ -352,6 +353,9 @@ Look at what happened — read-only, no key, nothing spent they are on disk, --body one call's bodies. CODEAF_CALL_LOG=off is off codeaf models [--refresh] the models this machine will use, and what each has been measured at + codeaf pool [show|status|verify] [--json] [--key key] + the Model Pool — what is resolved and cached, and whether a fresh + index verifies. show and status read only; verify wants a key codeaf doctor [--db path] is this install healthy, and where does it keep things codeaf manual @@ -374,8 +378,7 @@ Housekeeping — changes state on disk or on the network codeaf devices list the devices paired with this machine codeaf devices revoke [--all] - stop one device opening a conversation here; --all, every device of that - name + stop one device opening a conversation here; --all, all of that name codeaf notebook [--db path] what it has learned, and what it has been corrected on codeaf notebook retract|restore [--db path] @@ -392,8 +395,7 @@ Housekeeping — changes state on disk or on the network Plan work by hand — a plan you can read, edit and diff codeaf plan new "" [--out plan.json] [--dir dir] [--json] - [--instructions] [--passes auto|off|N] [--model slug] - [--plan-model slug] + [--instructions] [--passes auto|off|N] [--plan-model slug] codeaf plan show codeaf plan revise "" [--done 1,2,3] [--out plan.json] [--model slug] [--plan-model slug] diff --git a/cmd/codeaf/pool.go b/cmd/codeaf/pool.go new file mode 100644 index 0000000000..8cab8e7bac --- /dev/null +++ b/cmd/codeaf/pool.go @@ -0,0 +1,352 @@ +// The Model Pool from the command line. +// +// `codeaf pool` answers what the pool is on this machine: the mode and the +// addresses in force with the word saying where each came from, the index +// cached from the last good read, and — under status — what is waiting to be +// sent. It spends nothing and reaches the network only under `verify`, which +// fetches a fresh index and checks its signature. THE KEY STANDS IN FRONT OF +// THE FETCH: a signature nobody can check is a fetch nobody should make, so +// a build carrying no public key refuses verify at the door rather than +// downloading bytes it cannot vouch for. +package main + +import ( + "context" + "crypto/ed25519" + "encoding/base64" + "encoding/json" + "fmt" + "io" + "os" + "path/filepath" + "strings" + "time" + + "github.com/Agent-Field/codeaf/internal/config" + "github.com/Agent-Field/codeaf/internal/pool/index" + "github.com/Agent-Field/codeaf/internal/pool/outbox" + "github.com/Agent-Field/codeaf/internal/pool/poolcfg" + "github.com/Agent-Field/codeaf/internal/pool/pull" + "github.com/Agent-Field/codeaf/internal/tui2/reltime" +) + +// poolPublicKeys are the ed25519 public keys a fetched index's signature is +// checked under. EMPTY ON THIS BRANCH: the key is published beside the first +// index, and a build with no key in it would verify nothing — so verify asks +// for one with --key rather than fetching bytes nothing here can vouch for. +var poolPublicKeys []ed25519.PublicKey + +// poolKeys is --key's value: a base64 ed25519 public key, repeatable, each +// decoded the moment it is typed rather than carried as text and decoded at +// the fetch. A key that does not decode is refused where it was typed. +type poolKeys []ed25519.PublicKey + +func (k *poolKeys) String() string { + if len(*k) == 0 { + return "" + } + return fmt.Sprintf("%d key(s)", len(*k)) +} + +func (k *poolKeys) Set(word string) error { + raw, err := base64.StdEncoding.DecodeString(strings.TrimSpace(word)) + if err != nil { + return fmt.Errorf("--key: %q is not base64", word) + } + if len(raw) != ed25519.PublicKeySize { + return fmt.Errorf("--key: %d bytes, want an ed25519 public key's %d", len(raw), ed25519.PublicKeySize) + } + *k = append(*k, ed25519.PublicKey(raw)) + return nil +} + +func runPool(args []string) error { + return runPoolWith(args, os.Stdout, config.ProfileDir(), time.Now, os.LookupEnv) +} + +// runPoolWith is the verb with its outside readings injectable: where the +// profile is, what time it is, and the environment the resolver reads — the +// last so a test can pin the pool's names without touching the process. The +// stored word is resolved once, here, and every form below answers from that +// one Config. The pool's files live in the profile, and the profile is +// resolved the way every other file under it is ([config.ProfilePath]) — +// emptiness is the state root's own profile, never a directory called "pool" +// beside wherever the command happened to run. +func runPoolWith(args []string, output io.Writer, profileDir string, now func() time.Time, lookup func(string) (string, bool)) error { + cfg := poolcfg.Resolve(config.ModelPoolSettingAt(profileDir), lookup) + poolDir := config.ProfilePath(profileDir, "pool") + if len(args) == 0 { + args = []string{"show"} + } + switch args[0] { + case "show": + return showPool(args[1:], output, poolDir, cfg, now) + case "status": + return statusPool(args[1:], output, poolDir, cfg, now) + case "verify": + return verifyPool(args[1:], output, poolDir, cfg, now) + default: + // `codeaf pool --help` reaches here rather than a flag set, because + // the reading form parses nothing at all (usage.go). + if askedForHelp(args) { + return commandHelp("pool") + } + return fmt.Errorf("usage: codeaf pool [show|status|verify] [--json]") + } +} + +// showPool is the reading form. statusPool is the same answer with the outbox +// and the two doors the mode opens added to it. +func showPool(args []string, output io.Writer, poolDir string, cfg poolcfg.Config, now func() time.Time) error { + return poolReading("pool show", args, output, poolDir, cfg, now, false) +} + +func statusPool(args []string, output io.Writer, poolDir string, cfg poolcfg.Config, now func() time.Time) error { + return poolReading("pool status", args, output, poolDir, cfg, now, true) +} + +func poolReading(name string, args []string, output io.Writer, poolDir string, cfg poolcfg.Config, now func() time.Time, withStatus bool) error { + flags := commandFlags(name) + asJSON := flags.Bool("json", false, "print the answer as one JSON object") + if err := parseCommandFlags(flags, args); err != nil { + return err + } + if flags.NArg() != 0 { + return fmt.Errorf("usage: codeaf %s [--json]", name) + } + return printPool(output, poolDir, cfg, now(), *asJSON, withStatus) +} + +// printPool is the reading form's whole answer. The config first — every value +// beside the word saying where it came from, one of default, setting, env or +// ci — then the cached index with its age, then, for status, the outbox and +// the two doors the mode opens. +func printPool(output io.Writer, poolDir string, cfg poolcfg.Config, now time.Time, asJSON, withStatus bool) error { + var held *index.Index + // The cache is read the way show reads everything else, as an answer and + // not as an argument: a document that does not parse is not there yet, + // and the line below says so. The signature and the puller's version mark + // are verify's business; show reports what a person has. + if doc, err := os.ReadFile(filepath.Join(poolDir, "doc.json")); err == nil { + if parsed, err := index.Parse(doc); err == nil { + held = parsed + } + } + if asJSON { + return printPoolJSON(output, poolDir, cfg, held, now, withStatus) + } + for _, line := range []string{ + fmt.Sprintf("mode %s · %s", cfg.Mode, cfg.Source.Mode), + fmt.Sprintf("index %s · %s", cfg.IndexURL, cfg.Source.IndexURL), + fmt.Sprintf("submit %s · %s", orNowhere(cfg.SubmitURL), cfg.Source.SubmitURL), + fmt.Sprintf("ttl %s · %s", reltime.Elapsed(cfg.TTL), cfg.Source.TTL), + } { + if _, err := fmt.Fprintln(output, line); err != nil { + return err + } + } + if held == nil { + // A nothing is said in a sentence, the way an empty cache is: + // silence and a bare header both read as a command that broke. + if _, err := fmt.Fprintln(output, "no index cached yet"); err != nil { + return err + } + } else { + generated := held.Generated() + if _, err := fmt.Fprintf(output, + "index · generated %s · %s old · schema %d · %s · %s · min installs %d\n", + generated.Format("2006-01-02"), reltime.Elapsed(now.Sub(generated)), + held.Schema(), countWord(len(held.Metrics()), "metric", "metrics"), + countWord(len(held.Judges()), "judge", "judges"), held.MinInstalls()); err != nil { + return err + } + } + if withStatus { + _, err := fmt.Fprintf(output, "pending %d · can send %s · can read %s\n", + pendingRows(poolDir), yesNo(cfg.CanSend()), yesNo(cfg.CanRead())) + return err + } + return nil +} + +// poolAnswer is the --json shape of the reading forms: the config flat, the +// cached index under index or null. The three status fields are pointers so a +// show carries none of them — a field a form does not answer reads as +// not-asked rather than as zero. +type poolAnswer struct { + Mode string `json:"mode"` + ModeSource string `json:"mode_source"` + IndexURL string `json:"index_url"` + SubmitURL string `json:"submit_url"` + TTLSeconds int `json:"ttl_seconds"` + Pending *int `json:"pending,omitempty"` + CanSend *bool `json:"can_send,omitempty"` + CanRead *bool `json:"can_read,omitempty"` + Index *indexSummary `json:"index"` +} + +// indexSummary is the cached index as the reading forms carry it: the +// document's own day, its age against the asking clock, and the counts a +// person checks before trusting it. +type indexSummary struct { + Generated string `json:"generated"` + AgeSeconds int `json:"age_seconds"` + Schema int `json:"schema"` + Metrics int `json:"metrics"` + Judges int `json:"judges"` + MinInstalls int `json:"min_installs"` +} + +func printPoolJSON(output io.Writer, poolDir string, cfg poolcfg.Config, held *index.Index, now time.Time, withStatus bool) error { + answer := poolAnswer{ + Mode: cfg.Mode.String(), + ModeSource: cfg.Source.Mode, + IndexURL: cfg.IndexURL, + SubmitURL: cfg.SubmitURL, + TTLSeconds: int(cfg.TTL / time.Second), + } + if held != nil { + generated := held.Generated() + answer.Index = &indexSummary{ + Generated: generated.Format("2006-01-02"), + AgeSeconds: int(now.Sub(generated) / time.Second), + Schema: held.Schema(), + Metrics: len(held.Metrics()), + Judges: len(held.Judges()), + MinInstalls: held.MinInstalls(), + } + } + if withStatus { + pending := pendingRows(poolDir) + send, read := cfg.CanSend(), cfg.CanRead() + answer.Pending = &pending + answer.CanSend = &send + answer.CanRead = &read + } + encoded, err := json.Marshal(answer) + if err != nil { + return err + } + _, err = fmt.Fprintf(output, "%s\n", encoded) + return err +} + +// verifyPool is the one form that reaches the network: fetch the index now — +// TTL 0, the one door that means now — and check its signature. It refuses at +// the door when the mode is off and when no key is in hand; a failed fetch +// says its error and leaves on exit 1 through the one door every failure +// leaves by. +func verifyPool(args []string, output io.Writer, poolDir string, cfg poolcfg.Config, now func() time.Time) error { + flags := commandFlags("pool verify") + asJSON := flags.Bool("json", false, "print the result as one JSON object") + var keys poolKeys + flags.Var(&keys, "key", "an ed25519 public key, base64; repeatable") + if err := parseCommandFlags(flags, args); err != nil { + return err + } + if flags.NArg() != 0 { + return fmt.Errorf("usage: codeaf pool verify [--json] [--key key]") + } + // OFF IS A REFUSAL AT THE DOOR, and so is a build with no key: neither + // fetches, and the sentence is the answer, on stdout where the answer + // goes (streams.go). + // + // THE RUNG IS EXIT 2 and not exit 1: nothing was attempted, but the + // question asked of this build does not stand, and the remedy is on the + // line above the exit. verify's third refusal — a fetch or a signature + // that fails — is exit 1, in the error main() says in front of it. + if cfg.Mode == poolcfg.Off { + if _, err := fmt.Fprintln(output, "the Model Pool is off in settings"); err != nil { + return err + } + return exitIncomplete + } + var trusted []ed25519.PublicKey + trusted = append(trusted, keys...) + trusted = append(trusted, poolPublicKeys...) + if len(trusted) == 0 { + if _, err := fmt.Fprintln(output, "no public key built into this build; pass --key"); err != nil { + return err + } + return exitIncomplete + } + puller := &pull.Puller{ + URL: cfg.IndexURL, + Keys: trusted, + CacheDir: poolDir, + TTL: 0, + Budget: pull.DefaultBudget, + Now: now, + } + result, err := puller.Pull(context.Background()) + if err != nil { + return err + } + held, err := index.Parse(result.Doc) + if err != nil { + return fmt.Errorf("verify: %w", err) + } + generated := held.Generated().Format("2006-01-02") + if *asJSON { + encoded, err := json.Marshal(struct { + Verified bool `json:"verified"` + Version int64 `json:"version"` + Generated string `json:"generated"` + Metrics int `json:"metrics"` + }{true, result.Version, generated, len(held.Metrics())}) + if err != nil { + return err + } + _, err = fmt.Fprintf(output, "%s\n", encoded) + return err + } + _, err = fmt.Fprintf(output, "verified %d generated %s, %d metrics\n", + result.Version, generated, len(held.Metrics())) + return err +} + +// pendingRows counts what the outbox is holding. It reads the file by count +// and not by opening it, because [outbox.Open] CREATES the file when it is +// not there and a reading form must not write. +func pendingRows(poolDir string) int { + path := filepath.Join(poolDir, "outbox.jsonl") + if _, err := os.Stat(path); err != nil { + return 0 + } + box, err := outbox.Open(path) + if err != nil { + return 0 + } + defer box.Close() + return len(box.Pending()) +} + +// orNowhere is an empty submit address said rather than printed empty: the +// pin holding nothing means send nowhere, and an empty column says nothing. +func orNowhere(address string) string { + if address == "" { + return "nowhere" + } + return address +} + +// countWord is a count with its noun: one metric, three metrics, no judges. +// A zero is said rather than printed bare, for the reason every other +// nothing here is said. +func countWord(n int, one, many string) string { + switch n { + case 0: + return "no " + many + case 1: + return "1 " + one + default: + return fmt.Sprintf("%d %s", n, many) + } +} + +func yesNo(answer bool) string { + if answer { + return "yes" + } + return "no" +} diff --git a/cmd/codeaf/pool_test.go b/cmd/codeaf/pool_test.go new file mode 100644 index 0000000000..9d74a05d83 --- /dev/null +++ b/cmd/codeaf/pool_test.go @@ -0,0 +1,370 @@ +package main + +import ( + "crypto/ed25519" + "crypto/rand" + "encoding/base64" + "encoding/json" + "errors" + "os" + "path/filepath" + "strings" + "testing" + "time" + + "github.com/Agent-Field/codeaf/internal/home" + "github.com/Agent-Field/codeaf/internal/pool/outbox" +) + +// poolClock is a stopped now, so an index's age is a fact rather than a race +// against the test runner. +func poolClock(t *testing.T) func() time.Time { + t.Helper() + moment, err := time.Parse(time.RFC3339, "2026-09-18T00:00:00Z") + if err != nil { + t.Fatal(err) + } + return func() time.Time { return moment } +} + +// noEnv is an environment in which nothing is set, handed in the way the verb +// reads the world, so a test's answer cannot depend on the machine's shell. +func noEnv(string) (string, bool) { + return "", false +} + +// oneEnv is an environment with one name set — the injected stand-in for +// `CODEAF_MODEL_POOL=off codeaf pool show`. +func oneEnv(name, value string) func(string) (string, bool) { + return func(asked string) (string, bool) { + if asked == name { + return value, true + } + return "", false + } +} + +// poolDoc is a document the reader accepts whole: one metric with dims, one +// cell that clears its min installs, one judge. The numbers name no model; +// it is a fixture, and the field it is read for is its shape. +func poolDoc() []byte { + return []byte(`{ + "version": 7, + "schema": 1, + "generated": "2026-09-10", + "min_installs": 1, + "judges": ["z-ai/glm-5.3"], + "metrics": {"role_rating": {"kind": "gaussian", "dims": ["role", "model"]}}, + "cells": [ + {"metric": "role_rating", "role": "planner", "model": "z-ai/glm-5.3", "mean": 1312, "sd": 18, "n": 9} + ] + }`) +} + +// seedIndex writes poolDoc where show reads the cache, and nothing else: +// show takes the document as it stands and does not ask its signature — +// that is verify's question, not the reading form's. +func seedIndex(t *testing.T) string { + t.Helper() + dir := t.TempDir() + poolDir := filepath.Join(dir, "pool") + if err := os.MkdirAll(poolDir, 0o755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(poolDir, "doc.json"), poolDoc(), 0o644); err != nil { + t.Fatal(err) + } + return dir +} + +// seedOutbox records two rows the way a run would: through the outbox's own +// open and append, closed again, so status reads what stands on disk. +func seedOutbox(t *testing.T) string { + t.Helper() + dir := t.TempDir() + box, err := outbox.Open(filepath.Join(dir, "pool", "outbox.jsonl")) + if err != nil { + t.Fatal(err) + } + for _, row := range []string{`{"n":1}`, `{"n":2}`} { + if err := box.Append([]byte(row)); err != nil { + t.Fatal(err) + } + } + if err := box.Close(); err != nil { + t.Fatal(err) + } + return dir +} + +// The reading form answers over the whole config, every value beside the word +// saying where it came from — and a machine that has never read an index says +// so in a sentence rather than printing nothing at all. +func TestPoolShowPrintsTheConfigAndSaysWhenNoIndexIsCached(t *testing.T) { + var out strings.Builder + if err := runPoolWith(nil, &out, t.TempDir(), poolClock(t), noEnv); err != nil { + t.Fatal(err) + } + body := out.String() + for _, want := range []string{ + "mode on · default", + "index https://pool.invalid/index.json · default", + "submit https://pool.invalid/submit · default", + "ttl 1d · default", + "no index cached yet", + } { + if !strings.Contains(body, want) { + t.Errorf("show is missing %q:\n%s", want, body) + } + } +} + +// The pin is a source the reader can check, and the word it holds is the mode +// the machine answers to — the same three words the row and CI spell. +func TestPoolShowNamesTheEnvPinAsTheModeSource(t *testing.T) { + var out strings.Builder + if err := runPoolWith([]string{"show"}, &out, t.TempDir(), poolClock(t), + oneEnv("CODEAF_MODEL_POOL", "off")); err != nil { + t.Fatal(err) + } + if !strings.Contains(out.String(), "mode off · env") { + t.Fatalf("the env pin did not name itself:\n%s", out.String()) + } +} + +// --json is one parseable object, the cached index under index or null — and, +// this is the point of the absent fields, status's three are absent from a +// show, so a script reads absence as not-asked rather than as zero. +func TestPoolShowJSONWithASeededIndexReportsTheDocument(t *testing.T) { + var out strings.Builder + if err := runPoolWith([]string{"show", "--json"}, &out, seedIndex(t), poolClock(t), noEnv); err != nil { + t.Fatal(err) + } + var answer struct { + Mode string `json:"mode"` + ModeSource string `json:"mode_source"` + IndexURL string `json:"index_url"` + SubmitURL string `json:"submit_url"` + TTLSeconds int `json:"ttl_seconds"` + Pending *int `json:"pending"` + Index *struct { + Generated string `json:"generated"` + AgeSeconds int `json:"age_seconds"` + Schema int `json:"schema"` + Metrics int `json:"metrics"` + Judges int `json:"judges"` + MinInstalls int `json:"min_installs"` + } `json:"index"` + } + if err := json.Unmarshal([]byte(out.String()), &answer); err != nil { + t.Fatalf("--json did not parse: %v\n%s", err, out.String()) + } + if answer.Mode != "on" || answer.ModeSource != "default" { + t.Fatalf("mode = %q from %q", answer.Mode, answer.ModeSource) + } + if answer.IndexURL == "" || answer.SubmitURL == "" || answer.TTLSeconds != 86400 { + t.Fatalf("the config is not whole: %+v", answer) + } + if answer.Pending != nil { + t.Fatal("a show carried status's pending count") + } + held := answer.Index + if held == nil { + t.Fatal("a seeded index was not reported") + } + // 2026-09-18 midnight less 2026-09-10 midnight: eight days, to the second. + if held.Generated != "2026-09-10" || held.AgeSeconds != 8*24*60*60 { + t.Fatalf("generated = %q, age = %d", held.Generated, held.AgeSeconds) + } + if held.Schema != 1 || held.Metrics != 1 || held.Judges != 1 || held.MinInstalls != 1 { + t.Fatalf("the document's counts moved: %+v", held) + } +} + +// status is show plus the outbox and the two doors the mode opens, said in +// words a person reads and carried as fields a script reads. +func TestPoolStatusCountsPendingRowsAndNamesItsDoors(t *testing.T) { + dir := seedOutbox(t) + var out strings.Builder + if err := runPoolWith([]string{"status"}, &out, dir, poolClock(t), noEnv); err != nil { + t.Fatal(err) + } + if !strings.Contains(out.String(), "pending 2 · can send yes · can read yes") { + t.Fatalf("status did not count the seeded rows:\n%s", out.String()) + } + + out.Reset() + if err := runPoolWith([]string{"status", "--json"}, &out, dir, poolClock(t), noEnv); err != nil { + t.Fatal(err) + } + var answer struct { + Pending int `json:"pending"` + CanSend bool `json:"can_send"` + CanRead bool `json:"can_read"` + } + if err := json.Unmarshal([]byte(out.String()), &answer); err != nil { + t.Fatalf("status --json did not parse: %v\n%s", err, out.String()) + } + if answer.Pending != 2 || !answer.CanSend || !answer.CanRead { + t.Fatalf("the doors moved: %+v", answer) + } + + // A machine that has recorded no measurement says zero rather than + // inventing a file to count — and the reading form WRITES NOTHING: status + // reads the outbox by count, because opening one would create it. + quiet := t.TempDir() + out.Reset() + if err := runPoolWith([]string{"status"}, &out, quiet, poolClock(t), noEnv); err != nil { + t.Fatal(err) + } + if !strings.Contains(out.String(), "pending 0") { + t.Fatalf("an absent outbox did not read as zero:\n%s", out.String()) + } + if _, err := os.Stat(filepath.Join(quiet, "pool", "outbox.jsonl")); !os.IsNotExist(err) { + t.Fatal("status created the outbox it was only counting") + } +} + +// An empty profile is the state root's own profile, the way every other file +// under the profile resolves — never a directory called "pool" beside wherever +// the command happened to run. +func TestPoolWithNoProfileDirReadsTheStateRoots(t *testing.T) { + root := t.TempDir() + t.Setenv(home.EnvVar, root) + poolDir := filepath.Join(root, "pool") + if err := os.MkdirAll(poolDir, 0o755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(poolDir, "doc.json"), poolDoc(), 0o644); err != nil { + t.Fatal(err) + } + var out strings.Builder + if err := runPoolWith([]string{"show", "--json"}, &out, "", poolClock(t), noEnv); err != nil { + t.Fatal(err) + } + if !strings.Contains(out.String(), `"generated":"2026-09-10"`) { + t.Fatalf("an empty profile did not read the state root's pool:\n%s", out.String()) + } +} + +// The key gate stands in front of the fetch: with no key in hand verify +// refuses at the door — exit 2, the remedy on the line — and nothing is +// fetched from the address the config names, and nothing is cached. +func TestPoolVerifyWithoutAKeyRefusesAtTheDoorAndFetchesNothing(t *testing.T) { + dir := t.TempDir() + // A dead port is the fetch that would have happened: if the door let one + // through, the failure below would be a fetch error, not the sentence. + lookup := oneEnv("CODEAF_MODEL_POOL_URL", "http://127.0.0.1:1/index.json") + var out strings.Builder + err := runPoolWith([]string{"verify"}, &out, dir, poolClock(t), lookup) + if err == nil { + t.Fatal("a keyless verify fetched") + } + if !errors.Is(err, exitIncomplete) { + t.Fatalf("the door refusal is not exit 2: %v", err) + } + if !strings.Contains(out.String(), "no public key built into this build; pass --key") { + t.Fatalf("the refusal did not name the remedy:\n%s", out.String()) + } + if _, statErr := os.Stat(filepath.Join(dir, "pool", "doc.json")); !os.IsNotExist(statErr) { + t.Fatal("the refused verify wrote a cache") + } +} + +// OFF is a refusal at the door even with a key in hand, because the setting +// is the word the machine answers to. +func TestPoolVerifyRefusesWhenThePoolIsOff(t *testing.T) { + var out strings.Builder + err := runPoolWith([]string{"verify"}, &out, t.TempDir(), poolClock(t), + oneEnv("CODEAF_MODEL_POOL", "off")) + if !errors.Is(err, exitIncomplete) { + t.Fatalf("an off pool did not refuse on exit 2: %v", err) + } + if !strings.Contains(out.String(), "the Model Pool is off in settings") { + t.Fatalf("the refusal did not say so:\n%s", out.String()) + } +} + +// A key the flag cannot read — not base64, or not the length of an ed25519 +// public key — is refused where it was typed, by the flag set, on exit 1, +// because nothing was attempted. +func TestPoolVerifyRefusesAKeyItCannotRead(t *testing.T) { + for _, bad := range []string{"not base64!!", "abcd"} { + var out strings.Builder + err := runPoolWith([]string{"verify", "--key", bad}, &out, t.TempDir(), poolClock(t), noEnv) + if !errors.Is(err, exitCannotRun) { + t.Fatalf("a key %q is a door refusal, not a fetch: %v", bad, err) + } + } +} + +// The happy path, over no network at all: an index and its signature on disk, +// fetched through the same Puller a real verify uses, checked under the key +// --key hands in, and read back as the sentence a person came for. The fetch +// is a pull, so what verified is the cache show reads next. +func TestPoolVerifyFetchesAndChecksASignedIndex(t *testing.T) { + pub, priv, err := ed25519.GenerateKey(rand.Reader) + if err != nil { + t.Fatal(err) + } + src := t.TempDir() + if err := os.WriteFile(filepath.Join(src, "index.json"), poolDoc(), 0o644); err != nil { + t.Fatal(err) + } + sig := ed25519.Sign(priv, poolDoc()) + if err := os.WriteFile(filepath.Join(src, "index.json.sig"), + []byte(base64.StdEncoding.EncodeToString(sig)), 0o644); err != nil { + t.Fatal(err) + } + lookup := oneEnv("CODEAF_MODEL_POOL_URL", filepath.Join(src, "index.json")) + dir := t.TempDir() + var out strings.Builder + if err := runPoolWith([]string{"verify", "--key", base64.StdEncoding.EncodeToString(pub)}, + &out, dir, poolClock(t), lookup); err != nil { + t.Fatal(err) + } + if !strings.Contains(out.String(), "verified 7 generated 2026-09-10, 1 metric") { + t.Fatalf("verify did not read the fetched document:\n%s", out.String()) + } + out.Reset() + if err := runPoolWith([]string{"show"}, &out, dir, poolClock(t), lookup); err != nil { + t.Fatal(err) + } + if !strings.Contains(out.String(), "generated 2026-09-10") { + t.Fatalf("a verified index did not land in the cache show reads:\n%s", out.String()) + } +} + +// A document the key does not trust is not verified: the failure says so in +// the puller's words on exit 1, and — this is the puller's own promise, read +// through the door — nothing wrong is cached in its place. +func TestPoolVerifyRefusesADocumentItsKeyDoesNotTrust(t *testing.T) { + pub, priv, err := ed25519.GenerateKey(rand.Reader) + if err != nil { + t.Fatal(err) + } + src := t.TempDir() + doc := append([]byte(nil), poolDoc()...) + // One bit of the generated date, so the bytes no longer match the + // signature beside them and nothing else about the document moved. + doc[46] ^= 0x01 + if err := os.WriteFile(filepath.Join(src, "index.json"), doc, 0o644); err != nil { + t.Fatal(err) + } + sig := ed25519.Sign(priv, poolDoc()) + if err := os.WriteFile(filepath.Join(src, "index.json.sig"), + []byte(base64.StdEncoding.EncodeToString(sig)), 0o644); err != nil { + t.Fatal(err) + } + lookup := oneEnv("CODEAF_MODEL_POOL_URL", filepath.Join(src, "index.json")) + dir := t.TempDir() + var out strings.Builder + err = runPoolWith([]string{"verify", "--key", base64.StdEncoding.EncodeToString(pub)}, + &out, dir, poolClock(t), lookup) + if err == nil || !strings.Contains(err.Error(), "pull: signature does not verify") { + t.Fatalf("a tampered document did not refuse: %v", err) + } + if _, statErr := os.Stat(filepath.Join(dir, "pool", "doc.json")); !os.IsNotExist(statErr) { + t.Fatal("a refused document was cached") + } +} diff --git a/cmd/codeaf/usage.go b/cmd/codeaf/usage.go index d2a478a917..41587db657 100644 --- a/cmd/codeaf/usage.go +++ b/cmd/codeaf/usage.go @@ -472,7 +472,7 @@ func nearestCommand(typed string) string { // reason they are absent from the usage text: nothing types them. var knownCommands = []string{ "chat", "resume", "serve", "devices", "do", "plan", "revise", "run", "exec", - "show", "models", "notebook", "collections", "competence", "services", "wake", "doctor", + "show", "models", "pool", "notebook", "collections", "competence", "services", "wake", "doctor", "logs", "cache", "rebuild", "why", "manual", "version", "help", } diff --git a/internal/manual/chat/running-from-the-terminal.md b/internal/manual/chat/running-from-the-terminal.md index cb6bc70c71..f46141042d 100644 --- a/internal/manual/chat/running-from-the-terminal.md +++ b/internal/manual/chat/running-from-the-terminal.md @@ -510,6 +510,42 @@ machine would run work on. **On a machine with no evidence the whole answer is o No competence evidence yet. ``` +## The Model Pool — what this machine reads from it, with codeaf pool + +codeaf picks its models against the public Model Pool: a signed index of +measured models that your runs improve. Nothing about your code ever leaves +the machine — what is shared is a measurement of the run, not the work. One +setting answers for all of it, `model_pool` in `/settings`, with three +values: `on` reads and sends, `read` uses the pool and sends nothing, `off` +does neither. It defaults to `on`. `CODEAF_MODEL_POOL` pins the same word +from the shell, and on a CI machine with neither set codeaf reads but does +not send. + +``` +codeaf pool [show|status|verify] [--json] [--key key] +``` + +`show` — also what bare `codeaf pool` prints — is the reading form: the mode +and the addresses in force with the word saying where each came from +(`default`, `setting`, `env` or `ci`), then what index is cached and how old +it is, or `no index cached yet`. `status` adds what is waiting to be sent +and whether the mode allows sending and reading. `--json` prints the same +answer as one object. Neither form touches the network. + +`verify` fetches a fresh index and checks its detached ed25519 signature, +then prints the version it verified: + +``` +verified 7 generated 2026-09-10, 3 metrics +``` + +It wants a public key: `--key `, repeatable, or +one built into the build. This build carries none yet — the key is published +beside the first index — so it answers +`no public key built into this build; pass --key` and fetches nothing. A +fetch or a signature that fails is exit 1; a `verify` on a machine whose +setting is `off` is refused with exit 2 and fetches nothing. + ## What is still running in the background — codeaf services, and stopping one `codeaf services` lists the long-running processes started here and not yet stopped — a dev diff --git a/internal/manual/pages/surfaces.md b/internal/manual/pages/surfaces.md index 558a7f0f25..4c802bca90 100644 --- a/internal/manual/pages/surfaces.md +++ b/internal/manual/pages/surfaces.md @@ -81,7 +81,9 @@ answer instead of picking. `esc` dismisses it. Everything you can tune lives in one sheet. Open it with `/settings`, the `⚙` in the header, or `alt+,` — a bare `,` works too whenever the cursor is not in the input. It is a single column of grouped rows: **models** -(all eight slots), **money & limits** (the daily budget, the practice +(all eight slots, plus the Model Pool row — `on` reads and sends, `read` +uses the pool and sends nothing, `off` does neither), **money & limits** +(the daily budget, the practice carve-out, the quiet period before practice), **rhythm** (how long an absence earns an arrival brief, how many clean firings earn a charter tenure), **learning** (how much practice follows measured demand rather than curiosity, From 5efa9ac32a9e665f530b758e8ce452416761c648 Mon Sep 17 00:00:00 2001 From: santoshkumarradha Date: Thu, 17 Sep 2026 07:24:16 -0400 Subject: [PATCH 062/173] docs: the change entry for the pool setting and verb Co-Authored-By: codeaf --- .../unreleased/1093-pool-setting-and-verb.md | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 docs/changes/unreleased/1093-pool-setting-and-verb.md diff --git a/docs/changes/unreleased/1093-pool-setting-and-verb.md b/docs/changes/unreleased/1093-pool-setting-and-verb.md new file mode 100644 index 0000000000..76d3e5c923 --- /dev/null +++ b/docs/changes/unreleased/1093-pool-setting-and-verb.md @@ -0,0 +1,23 @@ +--- +kind: added +title: The Model Pool reaches its first surface — a `model_pool` setting and a `codeaf pool` verb +pr: 1093 +surface: [chat, engine] +invalidates: + - "`internal/pool` was reachable from nothing outside itself. `config.ModelPoolAt` resolves the stored word and the four pool names, and `codeaf pool show|status|verify` answers from it, so the packages have a surface and the resolver in internal/config is the one place the process environment is read for the pool." + - "`CODEAF_MODEL_POOL` sat in `OperatorEnvPins` with a comment saying the word had no row because nothing in the binary called poolcfg. It fronts the `model_pool` row now — choices `on`/`read`/`off`, default `on` — and renders through the row the way `CODEAF_DOC_ENGINE` does; the two URL names and the TTL stay plumbing." + - "`codeaf --help` had lines to spare; it did not — the page was already at its 110-line cap, and the pool entry was paid for by trimming three cells (`--model` off plan new, `--out file` and `--debug` off exec, the devices-revoke sentence folded) whose content the page or the per-command pages carry elsewhere." +--- + +The setting is one choice row in the models group, pinned by +`CODEAF_MODEL_POOL`. The verb has three forms: `show` — also bare +`codeaf pool` — prints the resolved config with the word saying where each +value came from (`default`, `setting`, `env` or `ci`) and the cached index +with its age, or `no index cached yet`; `status` adds the outbox's pending +count and whether the mode allows sending and reading; `verify` fetches a +fresh index through `pull` with TTL 0 and checks its detached ed25519 +signature under `--key` or a built-in key, refusing at the door with exit 2 +while no build carries the published key and leaving on exit 1 when a fetch +or a signature fails. `--json` prints one object on every form that answers. +Nothing sends anything yet: no run records a measurement, so the outbox is +always empty and `verify` is the one form that reaches the network. From 88fa4d44670bb7930ff56c43bb9846aa015293fe Mon Sep 17 00:00:00 2001 From: santoshkumarradha Date: Thu, 17 Sep 2026 07:45:53 -0400 Subject: [PATCH 063/173] cmd/codeaf: the pool entry fits the help page without dropping exec's flags The page is capped at 110 lines; the pool entry takes two of them and exec keeps --out and --debug on its synopsis. Co-Authored-By: Claude Fable 5.1 --- cmd/codeaf/main.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cmd/codeaf/main.go b/cmd/codeaf/main.go index 376b94fde1..759256bf8a 100644 --- a/cmd/codeaf/main.go +++ b/cmd/codeaf/main.go @@ -334,6 +334,7 @@ Hand it work — nobody is watching, the answer is on stdout codeaf exec [""] [--dir dir] [--system text] [--max-turns N] [--token-budget N] [--timeout 15m] [--model slug] [--context-fill N] [--completion-reserve N] [--json] + [--out file] [--debug] run one worker for one pass, with no planning at all codeaf run --input [--dir dir] [--model slug] [--journal path] [--json] @@ -354,8 +355,7 @@ Look at what happened — read-only, no key, nothing spent codeaf models [--refresh] the models this machine will use, and what each has been measured at codeaf pool [show|status|verify] [--json] [--key key] - the Model Pool — what is resolved and cached, and whether a fresh - index verifies. show and status read only; verify wants a key + the Model Pool: what is resolved and cached; verify fetches with a key codeaf doctor [--db path] is this install healthy, and where does it keep things codeaf manual From 21e570ab40c8579382c488c49badf1bf400e01d5 Mon Sep 17 00:00:00 2001 From: santoshkumarradha Date: Thu, 17 Sep 2026 07:46:06 -0400 Subject: [PATCH 064/173] docs: the change note carries this change's pull request number and the help-page trade Co-Authored-By: Claude Fable 5.1 --- ...pool-setting-and-verb.md => 1102-pool-setting-and-verb.md} | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) rename docs/changes/unreleased/{1093-pool-setting-and-verb.md => 1102-pool-setting-and-verb.md} (83%) diff --git a/docs/changes/unreleased/1093-pool-setting-and-verb.md b/docs/changes/unreleased/1102-pool-setting-and-verb.md similarity index 83% rename from docs/changes/unreleased/1093-pool-setting-and-verb.md rename to docs/changes/unreleased/1102-pool-setting-and-verb.md index 76d3e5c923..05e75ad959 100644 --- a/docs/changes/unreleased/1093-pool-setting-and-verb.md +++ b/docs/changes/unreleased/1102-pool-setting-and-verb.md @@ -1,12 +1,12 @@ --- kind: added title: The Model Pool reaches its first surface — a `model_pool` setting and a `codeaf pool` verb -pr: 1093 +pr: 1102 surface: [chat, engine] invalidates: - "`internal/pool` was reachable from nothing outside itself. `config.ModelPoolAt` resolves the stored word and the four pool names, and `codeaf pool show|status|verify` answers from it, so the packages have a surface and the resolver in internal/config is the one place the process environment is read for the pool." - "`CODEAF_MODEL_POOL` sat in `OperatorEnvPins` with a comment saying the word had no row because nothing in the binary called poolcfg. It fronts the `model_pool` row now — choices `on`/`read`/`off`, default `on` — and renders through the row the way `CODEAF_DOC_ENGINE` does; the two URL names and the TTL stay plumbing." - - "`codeaf --help` had lines to spare; it did not — the page was already at its 110-line cap, and the pool entry was paid for by trimming three cells (`--model` off plan new, `--out file` and `--debug` off exec, the devices-revoke sentence folded) whose content the page or the per-command pages carry elsewhere." + - "`codeaf --help` was at its 110-line cap. The pool entry takes two lines, paid for by folding the devices-revoke sentence and dropping `--model` from `plan new`'s synopsis, both carried by the per-command pages; exec keeps `--out` and `--debug`." --- The setting is one choice row in the models group, pinned by From 34153363c0df0106825d0c2380f01f662f3ee40f Mon Sep 17 00:00:00 2001 From: santoshkumarradha Date: Thu, 17 Sep 2026 07:48:49 -0400 Subject: [PATCH 065/173] config, cmd/codeaf, manual: the model_pool row names its reader, and verify says what it checked in the person's words The reader ledger lists ModelPoolSettingAt, which cmd/codeaf reads the row through. The verb's success line and the manual's copy of it drop a word the chat manual's vocabulary law bans. Co-Authored-By: Claude Fable 5.1 --- cmd/codeaf/pool.go | 2 +- cmd/codeaf/pool_test.go | 2 +- internal/config/derivation_test.go | 1 + internal/manual/chat/running-from-the-terminal.md | 4 ++-- 4 files changed, 5 insertions(+), 4 deletions(-) diff --git a/cmd/codeaf/pool.go b/cmd/codeaf/pool.go index 8cab8e7bac..404fddedf4 100644 --- a/cmd/codeaf/pool.go +++ b/cmd/codeaf/pool.go @@ -300,7 +300,7 @@ func verifyPool(args []string, output io.Writer, poolDir string, cfg poolcfg.Con _, err = fmt.Fprintf(output, "%s\n", encoded) return err } - _, err = fmt.Fprintf(output, "verified %d generated %s, %d metrics\n", + _, err = fmt.Fprintf(output, "signature good: version %d, generated %s, %d metrics\n", result.Version, generated, len(held.Metrics())) return err } diff --git a/cmd/codeaf/pool_test.go b/cmd/codeaf/pool_test.go index 9d74a05d83..8aa447112d 100644 --- a/cmd/codeaf/pool_test.go +++ b/cmd/codeaf/pool_test.go @@ -323,7 +323,7 @@ func TestPoolVerifyFetchesAndChecksASignedIndex(t *testing.T) { &out, dir, poolClock(t), lookup); err != nil { t.Fatal(err) } - if !strings.Contains(out.String(), "verified 7 generated 2026-09-10, 1 metric") { + if !strings.Contains(out.String(), "signature good: version 7, generated 2026-09-10, 1 metric") { t.Fatalf("verify did not read the fetched document:\n%s", out.String()) } out.Reset() diff --git a/internal/config/derivation_test.go b/internal/config/derivation_test.go index b7b679afda..915051a057 100644 --- a/internal/config/derivation_test.go +++ b/internal/config/derivation_test.go @@ -126,6 +126,7 @@ var settingReaders = map[string]string{ KeyDocumentEngine: "DocumentEngine", KeyVisionModel: "VisionModel", KeyAttribution: "Attribution", + KeyModelPool: "ModelPoolSettingAt", // The prompt-profile row names the session field the word lands in, which // is what the far side actually reads: cmd/codeaf's applyV3Governance puts // PromptProfileAt's answer there and internal/session's promptprofile.go diff --git a/internal/manual/chat/running-from-the-terminal.md b/internal/manual/chat/running-from-the-terminal.md index f46141042d..2bd41e767c 100644 --- a/internal/manual/chat/running-from-the-terminal.md +++ b/internal/manual/chat/running-from-the-terminal.md @@ -533,10 +533,10 @@ and whether the mode allows sending and reading. `--json` prints the same answer as one object. Neither form touches the network. `verify` fetches a fresh index and checks its detached ed25519 signature, -then prints the version it verified: +then prints the version whose signature checked out: ``` -verified 7 generated 2026-09-10, 3 metrics +signature good: version 7, generated 2026-09-10, 3 metrics ``` It wants a public key: `--key `, repeatable, or From ac076afcbed3bc7485176e4e03b0e2e60585c5ae Mon Sep 17 00:00:00 2001 From: santoshkumarradha Date: Thu, 17 Sep 2026 05:35:07 -0400 Subject: [PATCH 066/173] cmd/codeaf: a leaf re-dispatched after running out is granted more room than the attempt that ran out Co-Authored-By: codeaf --- cmd/codeaf/chat.go | 9 +++-- cmd/codeaf/context_budget_test.go | 57 +++++++++++++++++++++++++++++++ cmd/codeaf/subharness.go | 44 ++++++++++++++++++++++++ 3 files changed, 108 insertions(+), 2 deletions(-) diff --git a/cmd/codeaf/chat.go b/cmd/codeaf/chat.go index ec201ac6e2..be70ad176b 100644 --- a/cmd/codeaf/chat.go +++ b/cmd/codeaf/chat.go @@ -545,9 +545,14 @@ func buildBrain(w *chatWindow, session string, opts brainOptions) (*chatBrain, e fanIn = store.DependencyFanIn{} } // Ordinary leaves retain the byte-identical headless envelope, raised by - // exactly what their own fan-in measures — see gatheringGrant. Reflexes use - // the deliberately tiny rung budget and a seconds-scale watchdog. + // exactly what their own fan-in measures — see gatheringGrant. A leaf + // re-dispatched after running out is granted more than the attempt that + // ran out, or it would buy the same truncated ending again — see + // regrantAfterRunningOut; the wall below follows the larger grant because + // it is arithmetic over the same number. Reflexes use the deliberately + // tiny rung budget and a seconds-scale watchdog. turns, tokens := gatheringGrant(chatLeafTurns, chatLeafTokens, fanIn) + tokens = regrantAfterRunningOut(tokens, int(node.Attempt)) leafRoom := exec.SubharnessFor(subharness) wallLeft := remainingWall(ctx) deadline := leafRoom.DeadlineWithin(tokens, wallLeft) diff --git a/cmd/codeaf/context_budget_test.go b/cmd/codeaf/context_budget_test.go index 883632683f..7c7cc00348 100644 --- a/cmd/codeaf/context_budget_test.go +++ b/cmd/codeaf/context_budget_test.go @@ -208,3 +208,60 @@ func TestAFoldsGrantIsSizedForThePassItActuallyMakes(t *testing.T) { t.Fatalf("an unsized window granted %d tokens, want the process-wide %d", unknown, want) } } + +// A RE-DISPATCH IS GRANTED MORE ROOM THAN THE ATTEMPT THAT RAN OUT, or the +// identical budget buys the identical truncated ending. The runner's overrun +// settle releases a leaf that ran out back to pending with its banked turns, +// and while the grant was attempt-blind the next claim re-entered the same +// brief under the same meter and stopped at the same place — one truncation, +// paid for as many times as the round cap allowed. +func TestARedispatchIsGrantedMoreRoomThanTheAttemptThatRanOut(t *testing.T) { + // Attempt zero keeps the grant byte for byte: a leaf on its first claim + // has run out of nothing yet. + if room := regrantAfterRunningOut(chatLeafTokens, 0); room != chatLeafTokens { + t.Fatalf("a first claim was granted %d tokens, want the flat %d it has always had", + room, chatLeafTokens) + } + + // Half again per re-dispatch, compounding, and strictly more room each + // time — the growth is the whole point of sending a node round again. + previous := chatLeafTokens + for attempt := 1; attempt <= 3; attempt++ { + room := regrantAfterRunningOut(chatLeafTokens, attempt) + if room <= previous { + t.Fatalf("re-dispatch %d was granted %d tokens, no more than the %d before it", + attempt, room, previous) + } + previous = room + } + // The exact arithmetic, in the integers the grant is stated in: three + // halves, applied per attempt, never rounded up by a shortcut. + if got, want := regrantAfterRunningOut(chatLeafTokens, 2), chatLeafTokens*3/2*3/2; got != want { + t.Fatalf("attempt 2 was granted %d tokens, want the twice-grown %d", got, want) + } + + // The ceiling holds at attempts no run can reach, and the ladder stops + // there rather than stepping over it. + for _, attempt := range []int{10, 100} { + if room := regrantAfterRunningOut(chatLeafTokens, attempt); room != overrunGrantCeiling { + t.Fatalf("attempt %d was granted %d tokens, want the ceiling %d", + attempt, room, overrunGrantCeiling) + } + } + + // A leaf whose fan-in already measured more than the ceiling keeps every + // token it measured. The bound is on what the ladder adds, and a + // re-dispatch handed less room than its first attempt would be the + // identical-budget defect back by another door. + _, measured := gatheringGrant(chatLeafTurns, chatLeafTokens, + store.DependencyFanIn{Count: 8, Bytes: 8 << 20}) + if measured <= overrunGrantCeiling { + t.Fatalf("the wide join measured %d tokens, too small to ask the question", measured) + } + for _, attempt := range []int{1, 3, 100} { + if room := regrantAfterRunningOut(measured, attempt); room != measured { + t.Fatalf("a join measured at %d tokens was re-dispatched at %d on attempt %d", + measured, room, attempt) + } + } +} diff --git a/cmd/codeaf/subharness.go b/cmd/codeaf/subharness.go index 170529bed5..5701ab2f1d 100644 --- a/cmd/codeaf/subharness.go +++ b/cmd/codeaf/subharness.go @@ -161,6 +161,50 @@ func gatheringGrant(turns, tokens int, fanIn store.DependencyFanIn) (int, int) { return turns + fanIn.Count, tokens + 2*landed } +// The re-dispatch grant: what a leaf that has been sent round again in place +// is given, in the one number the dispatch path states for ctxbudget. +const ( + // overrunRegrantNum over overrunRegrantDen is how much bigger one + // re-dispatch's token grant is than the grant of the attempt that ran + // out: three halves. A re-dispatch exists to finish a truncated tail, and + // half again is enough to finish one, where doubling would buy the whole + // run a second time. + overrunRegrantNum = 3 + overrunRegrantDen = 2 + + // overrunGrantCeiling is where the regrant stops adding: four flat leaf + // grants, which is room to run an ordinary leaf's whole text through twice + // over and therefore more than finishing a tail can ever cost. The bound + // is on what the ladder adds and not on what the leaf was granted — a + // fan-in that measured more than this keeps every token it measured, + // because the one thing a re-dispatch must never do is hand back room. + overrunGrantCeiling = 4 * chatLeafTokens +) + +// regrantAfterRunningOut is the token grant one claim of a leaf that has been +// re-dispatched in place is given, from the grant this claim measured and the +// attempt it is on. Attempt zero is the grant unchanged; every attempt after +// it grows by [overrunRegrantNum] over [overrunRegrantDen] for each re-dispatch +// before it, and the ladder adds nothing beyond [overrunGrantCeiling]. +// Turns are not regrown: a re-dispatch carries its banked turns as inputs +// rather than re-running them, so the turn ceiling was never what ran out. +// +// The growth is the point. A re-dispatch handed the room its predecessor ran +// out of re-runs the same brief to the same truncated ending — the meter stops +// it at the same place — so the runner's release (see resident's overrun +// settle) is worth a claim only if the room moves with it. The wall the leaf +// is given follows, because it is arithmetic over the same number. +func regrantAfterRunningOut(tokens, attempt int) int { + room := tokens + for ; attempt > 0 && room < overrunGrantCeiling; attempt-- { + room = room * overrunRegrantNum / overrunRegrantDen + if room > overrunGrantCeiling { + return overrunGrantCeiling + } + } + return room +} + // foldGrant sizes the whole run of a leaf that is going to make one model call. // // The gathering grant above is the right arithmetic for a node that has to go From 8611c913875ee8f48a4ededab4f13abce9593587 Mon Sep 17 00:00:00 2001 From: santoshkumarradha Date: Thu, 17 Sep 2026 05:35:35 -0400 Subject: [PATCH 067/173] store: the turns a release handed on, and how many re-dispatches a node took, are readable off the journal Co-Authored-By: codeaf --- internal/store/lifecycle.go | 75 +++++++++++++++++++++++++++++++++++++ 1 file changed, 75 insertions(+) diff --git a/internal/store/lifecycle.go b/internal/store/lifecycle.go index 8f254d56cf..e15e0c7d56 100644 --- a/internal/store/lifecycle.go +++ b/internal/store/lifecycle.go @@ -2,6 +2,7 @@ package store import ( "database/sql" + "encoding/json" "errors" "fmt" "math" @@ -403,6 +404,80 @@ func (s *Store) release(claim Claim, reason string, recorded int) error { return nil } +// ReleasedTurnsFor is how many recorded turns the newest hand-on release left +// waiting for the next claim, or that no release of this node ever handed work +// on. It exists because the release is where the count the attempt before +// banked is written down — the settle that re-dispatched the node journaled it +// in the same breath as the reason — and the settle that has to COMPARE against +// it (a re-dispatch deciding whether it moved) cannot compose that comparison +// from its own memory across processes. +// +// Releases that took a claim back without handing work on are not it: they say +// nothing about how much the attempt before banked, and the newest count that +// does say is the one a re-dispatch is compared against. +func (s *Store) ReleasedTurnsFor(nodeID string) (int, bool, error) { + rows, err := s.db.Query(` + SELECT payload FROM events + WHERE node_id = ? AND kind = ? + ORDER BY seq DESC`, strings.TrimSpace(nodeID), EventNodeReleased) + if err != nil { + return 0, false, fmt.Errorf("read the handed-on turns: %w", err) + } + defer rows.Close() + for rows.Next() { + var payload string + if err := rows.Scan(&payload); err != nil { + return 0, false, fmt.Errorf("read the handed-on turns: %w", err) + } + var release releasePayload + if err := json.Unmarshal([]byte(payload), &release); err != nil { + return 0, false, fmt.Errorf("read the handed-on turns: %w", err) + } + if release.Recorded > 0 { + return release.Recorded, true, nil + } + } + if err := rows.Err(); err != nil { + return 0, false, fmt.Errorf("read the handed-on turns: %w", err) + } + return 0, false, nil +} + +// NodeRedispatches is how many times this node has been re-dispatched in place +// after running out of the room it was granted: the count of its releases that +// handed recorded turns on. It is read from the journal rather than counted +// anywhere in a process for the same reason every run figure is — a durable +// store outlives the binary that wrote it, and a re-dispatch a resident made is +// as much a re-dispatch as one this process made. +func (s *Store) NodeRedispatches(nodeID string) (int, error) { + rows, err := s.db.Query(` + SELECT payload FROM events + WHERE node_id = ? AND kind = ? + ORDER BY seq`, strings.TrimSpace(nodeID), EventNodeReleased) + if err != nil { + return 0, fmt.Errorf("read the re-dispatches: %w", err) + } + defer rows.Close() + redispatches := 0 + for rows.Next() { + var payload string + if err := rows.Scan(&payload); err != nil { + return 0, fmt.Errorf("read the re-dispatches: %w", err) + } + var release releasePayload + if err := json.Unmarshal([]byte(payload), &release); err != nil { + return 0, fmt.Errorf("read the re-dispatches: %w", err) + } + if release.Recorded > 0 { + redispatches++ + } + } + if err := rows.Err(); err != nil { + return 0, fmt.Errorf("read the re-dispatches: %w", err) + } + return redispatches, nil +} + // ── the claim reaper reads EVIDENCE OF LIFE, never a clock ────────────────── // // THE DEFECT THIS ANSWERS, and it is the same defect twice. This sweep used to From edcf52559880cb89cc298fa7ad5ea27b57bb4d66 Mon Sep 17 00:00:00 2001 From: santoshkumarradha Date: Thu, 17 Sep 2026 05:36:24 -0400 Subject: [PATCH 068/173] resident: a re-dispatch that banks nothing new is failed rather than sent round again Co-Authored-By: codeaf --- internal/resident/exhaustedresult_test.go | 62 +++++++++++++++++++++++ internal/resident/ranoutwords.go | 16 +++++- internal/resident/runner.go | 19 +++++++ 3 files changed, 95 insertions(+), 2 deletions(-) diff --git a/internal/resident/exhaustedresult_test.go b/internal/resident/exhaustedresult_test.go index 83b7e4fcbe..6f58e1f8c5 100644 --- a/internal/resident/exhaustedresult_test.go +++ b/internal/resident/exhaustedresult_test.go @@ -139,3 +139,65 @@ func TestAnExhaustedResultWithNothingRecordedFails(t *testing.T) { t.Fatalf("the failure %q does not name the exhaustion", final.Error) } } + +// AND A RE-DISPATCH THAT BANKED NOTHING ITS PREDECESSOR HAD NOT IS REFUSED, so +// the node fails rather than buying the same truncated ending a third time. The +// grant moves with the attempt now (see cmd/codeaf's regrantAfterRunningOut), +// which makes the comparison honest: a re-dispatch given more room that +// reaches the same recorded count has been paid to move and did not. +func TestAnExhaustedRedispatchThatBankedNothingNewFails(t *testing.T) { + graph := openRunnerStore(t) + spliceOneLeaf(t, graph, "task-2") + recordTurns(t, graph, "task-2", 3) + + starts := make(chan struct{}, 8) + runner := NewRunner(graph, func(ctx context.Context, node store.Node) (ExecResult, error) { + starts <- struct{}{} + // The same truncated ending both times: nothing new is recorded between + // the two attempts, so the re-dispatch reaches exactly what the attempt + // before it had already banked. + return ExecResult{ + Summary: "All 722 tests pass. Let me verify the dry-run tests specifically:", + Stop: executor.StopBudget, + Meter: executor.Meter{Name: "cost", Reached: 178086, Allowed: 176834, Unit: "tokens of billed work"}, + }, nil + }, "chat-runner", 1) + + ctx, stop := context.WithTimeout(context.Background(), 10*time.Second) + defer stop() + served := make(chan error, 1) + go func() { served <- runner.Serve(ctx) }() + + waitFor(t, starts, "the leaf never started") + waitFor(t, starts, "the re-dispatch never started — the exhausted node was not sent round again") + deadline := time.Now().Add(8 * time.Second) + var final store.Node + for time.Now().Before(deadline) { + node, found, err := graph.Node("task-2") + if err != nil { + t.Fatalf("read the node: %v", err) + } + if found && node.Status == store.Failed { + final = node + break + } + time.Sleep(10 * time.Millisecond) + } + stop() + <-served + runner.Wait() + + if final.Status != store.Failed { + t.Fatalf("a re-dispatch that banked nothing new settled %q, want failed — it must not "+ + "be sent round to buy the same truncated ending again", final.Status) + } + if final.Attempt != 2 { + t.Fatalf("the node was claimed %d times, want the two of a first attempt and one refused re-dispatch", + final.Attempt) + } + for _, want := range []string{"refused", "added nothing"} { + if !contains(final.Error, want) { + t.Fatalf("the failure %q does not say the re-dispatch was refused for adding nothing", final.Error) + } + } +} diff --git a/internal/resident/ranoutwords.go b/internal/resident/ranoutwords.go index e451dfe18f..2876861b8a 100644 --- a/internal/resident/ranoutwords.go +++ b/internal/resident/ranoutwords.go @@ -6,8 +6,8 @@ import ( executor "github.com/Agent-Field/codeaf/internal/exec" ) -// The two sentences a node gets when the leaf holding it ran out. They are in -// one file because they are one account of one event told to two different +// The three sentences a node gets when the leaf holding it ran out. They are in +// one file because they are one account of one event told to three different // endings, and a reader who meets them a day apart must not have to work out // whether they are describing the same thing. // @@ -57,6 +57,18 @@ func outOfRoomFailure(result ExecResult, recorded int) string { return ranOutWords(result.Stop) + tail + meterAside(result.Meter) } +// outOfRoomRefusedFailure is the same fact when the re-dispatch that just ran +// out banked nothing the attempt before it had not banked: more room was the +// only thing a re-dispatch buys, the work did not move with it, and the node is +// failed here rather than paid to reach the same point again. It says the +// refusal and why, because a bare ending would read as a verdict on the work +// rather than on buying a second copy of the same truncation. +func outOfRoomRefusedFailure(result ExecResult, recorded int) string { + return ranOutWords(result.Stop) + " again, " + pluralTurns(recorded) + + " in and no further than the attempt before it, so the re-dispatch was refused: it added nothing" + + meterAside(result.Meter) +} + // A RELEASE REASON THAT HANDS WORK ON ENDS WITH THE SAME CLAUSE, AND IT IS // SPELLED HERE ONCE. // diff --git a/internal/resident/runner.go b/internal/resident/runner.go index 69db0201fd..349678771f 100644 --- a/internal/resident/runner.go +++ b/internal/resident/runner.go @@ -1420,6 +1420,16 @@ func (r *Runner) runOne(ctx context.Context, node store.Node, hold *leafHold) { // with nothing able to continue it has said all it is going to say, and it // is failed with the ending named rather than handed round again for ever. // + // AND A RE-DISPATCH THAT BANKED NOTHING NEW IS REFUSED. More room is the + // only thing a re-dispatch buys — the grant moves with the attempt now (see + // cmd/codeaf's regrantAfterRunningOut) — so a re-dispatch that reaches no + // further than the attempt before it has been paid to move and did not, and + // it is failed with the refusal named rather than sent round to buy the + // same truncated ending again. The count it is measured against is the one + // the hand-on release journaled: that row is the record's own answer to + // what the attempt before banked, and reading it beats keeping a second + // copy on the claim. + // // A PASSED DELIVERY GATE SETTLES THE NODE. Running out is a statement about // RESOURCES — the meter reached its bound while the worker was still going — // and a gate pass is a statement about the WORK, taken by reading the result @@ -1444,6 +1454,15 @@ func (r *Runner) runOne(ctx context.Context, node store.Node, hold *leafHold) { return } _, recorded := BankedRun(r.graph, node.ID) + // A read of the hand-on release that fails answers "nobody could say", + // which is the answer that leaves the claim to the rules below. + if node.Attempt > 0 { + if carried, handed, readErr := r.graph.ReleasedTurnsFor(node.ID); readErr == nil && + handed && recorded > 0 && recorded <= carried { + _ = r.graph.Fail(claim, outOfRoomRefusedFailure(result, recorded)) + return + } + } if node.Attempt < MaxOverrunRounds && recorded > 0 { _ = r.graph.ReleaseWithRecord(claim, outOfRoomClaimReason(result, recorded), recorded) return From bcf6cccc31df65b2ac4d64014ef1a3b724bbf268 Mon Sep 17 00:00:00 2001 From: santoshkumarradha Date: Thu, 17 Sep 2026 05:37:26 -0400 Subject: [PATCH 069/173] cmd/codeaf: the run's json object says how many times a node was re-dispatched in place Co-Authored-By: codeaf --- cmd/codeaf/do.go | 77 +++++++++++++------ cmd/codeaf/do_test.go | 67 ++++++++++++++++ cmd/codeaf/envelope.go | 69 ++++++++++------- cmd/codeaf/envelope_test.go | 15 ++++ .../manual/chat/running-from-the-terminal.md | 4 +- 5 files changed, 182 insertions(+), 50 deletions(-) diff --git a/cmd/codeaf/do.go b/cmd/codeaf/do.go index 9ce388f972..d54f67e2df 100644 --- a/cmd/codeaf/do.go +++ b/cmd/codeaf/do.go @@ -116,14 +116,16 @@ type headlessOutcome struct { // unexported because the sentence leaves through Deliverable, while Settled // is already the machine signal and the JSON contract needs no second key. unfinishedTree string - // Run, Calls and Rounds are what a person went to `calls.jsonl` to - // reconstruct: which run this was, how many model calls it made, and how - // many times it bought more work after looking at what it had. They are - // unexported spellings of the envelope's own keys — the receipt reaches a - // caller through [errandEnvelope] and nowhere else. - run string - calls int - rounds int + // Run, Calls, Rounds and Redispatches are what a person went to + // `calls.jsonl` to reconstruct: which run this was, how many model calls it + // made, how many times it bought more work after looking at what it had, + // and how many times it sent a node round again in place after the node ran + // out of its room. They are unexported spellings of the envelope's own keys + // — the receipt reaches a caller through [errandEnvelope] and nowhere else. + run string + calls int + rounds int + redispatches int // tokensIn and tokensOut are the token half of the bill, summed out of the // same journal read that priced the run. They are unexported because they // reach a caller only through the envelope's `tokens` field, which is the @@ -656,6 +658,11 @@ func priceErrand(graph *store.Store, session string, openedAt int64, outcome *he // work is read from what was written down, for the reason the bill is: a // counter in this process could not see a round a resident spliced. outcome.rounds = errandRounds(graph, session) + // AND THE RE-DISPATCHES OFF IT TOO. How many times this run sent a node + // round again in place is read from the releases that handed work on, for + // the same reason: the release is the record of a re-dispatch, and a + // counter in this process could not see one a resident made. + outcome.redispatches = errandRedispatches(graph, session) } // errandRounds is how many times this errand bought MORE WORK: every growth @@ -685,6 +692,31 @@ func errandRounds(graph *store.Store, session string) int { return rounds } +// errandRedispatches is how many times one of this errand's nodes was sent +// round again in place after running out of the room it was granted: every +// hand-on release its nodes journaled (store.NodeRedispatches). +// +// It asks per node rather than across the store because a release is journaled +// against the node that was re-dispatched, and the rule is the bill's and the +// rounds': a run sharing a durable store with another session must not count +// that session's re-dispatches as its own. A read that fails leaves the count +// at zero rather than at a guess. +func errandRedispatches(graph *store.Store, session string) int { + nodes, err := graph.SessionMemberNodes(session) + if err != nil { + return 0 + } + redispatches := 0 + for _, node := range nodes { + counted, err := graph.NodeRedispatches(node.ID) + if err != nil { + continue + } + redispatches += counted + } + return redispatches +} + // headlessBrain builds and returns the brain this process will run, or nothing // at all when another process is already the brain for this store. // @@ -2970,20 +3002,21 @@ func reportErrand(request doRequest, outcome headlessOutcome) error { // `exec` and `run` from publishing three different objects again. func errandEnvelope(outcome headlessOutcome) resultEnvelope { return buildResultEnvelope(runResult{ - Stop: outcome.resolvedStop(), - Answer: outcome.Deliverable, - Files: outcome.Artifacts, - Error: outcome.Error, - SpendUSD: outcome.Spend, - TokensIn: outcome.tokensIn, - TokensOut: outcome.tokensOut, - Seconds: outcome.Seconds, - Model: outcome.Model, - Steps: outcome.Nodes, - Run: outcome.run, - Calls: outcome.calls, - Rounds: outcome.rounds, - Extra: legacyErrandFields(outcome), + Stop: outcome.resolvedStop(), + Answer: outcome.Deliverable, + Files: outcome.Artifacts, + Error: outcome.Error, + SpendUSD: outcome.Spend, + TokensIn: outcome.tokensIn, + TokensOut: outcome.tokensOut, + Seconds: outcome.Seconds, + Model: outcome.Model, + Steps: outcome.Nodes, + Run: outcome.run, + Calls: outcome.calls, + Rounds: outcome.rounds, + Redispatches: outcome.redispatches, + Extra: legacyErrandFields(outcome), }) } diff --git a/cmd/codeaf/do_test.go b/cmd/codeaf/do_test.go index 852ae14f68..73595abce3 100644 --- a/cmd/codeaf/do_test.go +++ b/cmd/codeaf/do_test.go @@ -2330,3 +2330,70 @@ func TestAnErrandCountsTheRoundsItsOwnJobsBought(t *testing.T) { "a neighbour's rounds as its own", got) } } + +// TestAnErrandCountsTheRedispatchesItsOwnNodesBought is the figure beside the +// rounds, and the reason it comes off the journal rather than a counter. +// +// `do --json` said how many times a run went back for MORE but never how many +// times it sent a node round again IN PLACE — a leaf that ran out of its room +// going back on the queue to be carried on from, which is the other half of +// what a bill of re-runs is made of. +// +// IT IS COUNTED PER NODE AND NOT ACROSS THE STORE, for the reason the rounds +// are: a run sharing a durable store with another session must not report that +// session's re-dispatches as its own. +func TestAnErrandCountsTheRedispatchesItsOwnNodesBought(t *testing.T) { + path := filepath.Join(t.TempDir(), "redispatches.db") + graph, err := store.Open(path) + if err != nil { + t.Fatal(err) + } + defer graph.Close() + + const mine, theirs = "session-mine", "session-theirs" + for _, job := range []struct{ id, session string }{ + {"job-a", mine}, {"job-b", mine}, {"job-c", theirs}, + } { + if err := graph.Splice(store.RootID, store.Subtree{Nodes: []store.NodeSpec{{ + ID: job.id, Brief: "a job", Stage: 1, + }}}, store.Provenance{Origin: store.OriginUser, Intent: "a job", SessionID: job.session}); err != nil { + t.Fatal(err) + } + } + // Two hand-ons on one of this errand's nodes, one on another, a hand-back + // that carried nothing on — which is a claim taken off a worker, not a + // re-dispatch — and one on a node belonging to somebody else entirely. + for _, handed := range []struct { + node string + reason string + recorded int + }{ + {"job-a", "it was still working when it ran out of its token budget", 3}, + {"job-a", "it was still working when it ran out of its token budget", 3}, + {"job-b", "it was still working when it ran out of its token budget", 2}, + {"job-b", "no sign of life for the claim", 0}, + {"job-c", "it was still working when it ran out of its token budget", 1}, + } { + claim, ok, err := graph.Claim(handed.node, "chat-runner") + if err != nil || !ok { + t.Fatalf("claim %s: %v", handed.node, err) + } + if handed.recorded > 0 { + if err := graph.ReleaseWithRecord(claim, handed.reason, handed.recorded); err != nil { + t.Fatal(err) + } + continue + } + if err := graph.ReleaseWithReason(claim, handed.reason); err != nil { + t.Fatal(err) + } + } + + if got := errandRedispatches(graph, mine); got != 3 { + t.Errorf("this errand's nodes were sent round again three times between them and it reports %d", got) + } + if got := errandRedispatches(graph, theirs); got != 1 { + t.Errorf("the other session re-dispatched once and it reports %d — a run may not "+ + "count a neighbour's re-dispatches as its own", got) + } +} diff --git a/cmd/codeaf/envelope.go b/cmd/codeaf/envelope.go index f191944547..d78f434732 100644 --- a/cmd/codeaf/envelope.go +++ b/cmd/codeaf/envelope.go @@ -333,6 +333,19 @@ type resultEnvelope struct { // both because a caller reaching for a key that vanished is a caller // crashing. Rounds int `json:"rounds"` + // Redispatches is how many times one of this run's nodes was re-dispatched + // IN PLACE after running out of the room it was granted — sent round again + // to carry on from what it had banked, rather than grown around. It is + // counted off the journal the way Rounds is (store.NodeRedispatches), for + // the same reason: a run sharing a durable store with another session must + // not count that session's re-dispatches as its own. + // + // It is the one contract field that is ABSENT rather than zero when it never + // happened, which is what `omitempty` buys and why it wears it where Rounds + // does not: a caller of this one is asking whether the run had to send + // anything round again, and "never" is the key not being there. `exec` and + // a saved program never set it, so neither ever carries it. + Redispatches int `json:"redispatches,omitempty"` // extra is what one verb carries beyond the contract, and it is two things: // the OLD field names, kept readable for one release so that a tool written @@ -384,19 +397,20 @@ func (e resultEnvelope) MarshalJSON() ([]byte, error) { // result type, and that mapping is the only place a verb's private shape and // the public contract meet. type runResult struct { - Stop stopReason - Answer string - Files []string - Error string - SpendUSD float64 - TokensIn int - TokensOut int - Seconds float64 - Model string - Steps int - Run string - Calls int - Rounds int + Stop stopReason + Answer string + Files []string + Error string + SpendUSD float64 + TokensIn int + TokensOut int + Seconds float64 + Model string + Steps int + Run string + Calls int + Rounds int + Redispatches int // Extra is this verb's own fields: its old spellings, and whatever it knows // that the contract has no room for. Nil for a verb with neither. Extra map[string]any @@ -421,20 +435,21 @@ func buildResultEnvelope(result runResult) resultEnvelope { files = []string{} } return resultEnvelope{ - OK: exitFor(stop) == exitDone, - Stop: stop, - Answer: result.Answer, - Files: files, - Error: result.Error, - SpendUSD: result.SpendUSD, - Tokens: envelopeTokens{In: result.TokensIn, Out: result.TokensOut}, - Seconds: result.Seconds, - Model: result.Model, - Steps: result.Steps, - Run: result.Run, - Calls: result.Calls, - Rounds: result.Rounds, - extra: result.Extra, + OK: exitFor(stop) == exitDone, + Stop: stop, + Answer: result.Answer, + Files: files, + Error: result.Error, + SpendUSD: result.SpendUSD, + Tokens: envelopeTokens{In: result.TokensIn, Out: result.TokensOut}, + Seconds: result.Seconds, + Model: result.Model, + Steps: result.Steps, + Run: result.Run, + Calls: result.Calls, + Rounds: result.Rounds, + Redispatches: result.Redispatches, + extra: result.Extra, } } diff --git a/cmd/codeaf/envelope_test.go b/cmd/codeaf/envelope_test.go index 531137b2cb..de4bd0d3f8 100644 --- a/cmd/codeaf/envelope_test.go +++ b/cmd/codeaf/envelope_test.go @@ -399,4 +399,19 @@ func TestTheEnvelopeNamesItsRunAndCountsItsCallsAndRounds(t *testing.T) { if fields["rounds"] != float64(8) { t.Errorf("rounds = %v, want the 8 rounds of work it bought", fields["rounds"]) } + // `redispatches` carries its count the same way, and is the one key that is + // absent rather than zero when it never happened — a caller of it is asking + // whether the run had to send anything round again, and "never" is the key + // not being there. + if _, present := fields["redispatches"]; present { + t.Errorf("redispatches = %v on a run that re-dispatched nothing; the key belongs "+ + "to the runs it happened to", fields["redispatches"]) + } + fields = envelopeFields(t, buildResultEnvelope(runResult{ + Run: "0123456789abcdef", Calls: 422, Rounds: 8, Redispatches: 1, + })) + if fields["redispatches"] != float64(1) { + t.Errorf("redispatches = %v, want the one node the run sent round again in place", + fields["redispatches"]) + } } diff --git a/internal/manual/chat/running-from-the-terminal.md b/internal/manual/chat/running-from-the-terminal.md index cb6bc70c71..3624f7eb59 100644 --- a/internal/manual/chat/running-from-the-terminal.md +++ b/internal/manual/chat/running-from-the-terminal.md @@ -172,7 +172,8 @@ stdout, always parseable, printed even when the run failed**: "steps": 3, "run": "0123456789abcdef", "calls": 47, - "rounds": 2 + "rounds": 2, + "redispatches": 1 } ``` @@ -191,6 +192,7 @@ stdout, always parseable, printed even when the run failed**: | `run` | this invocation's id. It names the folder `--debug` writes into, and every row this run wrote into `~/.codeaf/logs/calls.jsonl` carries it too — so `codeaf logs --run ` is how you get from this object to the calls behind it. Empty on a verb that opened no run of its own | | `calls` | how many model calls the run made, counted whether or not the call log is switched on. It is the figure you would otherwise count by hand in `calls.jsonl` | | `rounds` | how many times the run went back for **more work** after looking at what it had. One is the ordinary shape; eight is a run that kept finding more to do, and it is the number that explains a bill nothing else here accounts for. `exec` does not plan and a saved program does not grow, so both hold `0` — a measurement nobody took, the way `steps` does | +| `redispatches` | how many times one of the run's nodes was sent round again **in place** after running out of the room it was granted — back on the queue to carry on from what it had already banked, rather than grown around. Present only when it happened: a run that never re-dispatched a node carries no key at all, and neither does a verb with no plan nodes | **Within a release a field is never removed and never changes meaning; new fields may appear.** `stop` is the field to read for *why*; the exit code only says how much is wrong. From fc9a7c34a3528d91e3e5c03ec5ab1ae52d622018 Mon Sep 17 00:00:00 2001 From: santoshkumarradha Date: Thu, 17 Sep 2026 05:38:05 -0400 Subject: [PATCH 070/173] docs: the change note for the re-dispatch grant and refusal Co-Authored-By: codeaf --- .../1095-redispatch-room-and-refusal.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 docs/changes/unreleased/1095-redispatch-room-and-refusal.md diff --git a/docs/changes/unreleased/1095-redispatch-room-and-refusal.md b/docs/changes/unreleased/1095-redispatch-room-and-refusal.md new file mode 100644 index 0000000000..ae759f0ed6 --- /dev/null +++ b/docs/changes/unreleased/1095-redispatch-room-and-refusal.md @@ -0,0 +1,15 @@ +--- +kind: changed +title: A re-dispatch after running out is granted more room, and one that banks nothing new is refused +pr: 1095 +surface: [resident, docs] +invalidates: + - "A leaf that ran out of its token budget was re-dispatched with the identical budget and the identical brief, up to the round cap, and bought the same truncated ending each time. The dispatch's grant is attempt-aware now: `regrantAfterRunningOut` in `cmd/codeaf/subharness.go` grows the token grant by three halves per re-dispatch, never above `overrunGrantCeiling` (four flat leaf grants; a fan-in that measured more keeps every token it measured), and the wall the leaf is given follows the larger grant." + - "A leaf that ran out was requeued whenever anything at all was banked, for as many rounds as the cap allowed. A re-dispatch that banks nothing the attempt before it had not banked is failed now, with the refusal named in the node's error; the count it is measured against is the one the hand-on release journaled, read back through `store.ReleasedTurnsFor`." + - "The `--json` envelope had no field for in-place re-dispatches. `redispatches` counts them across the run's nodes off the journal (`store.NodeRedispatches`), and is absent rather than zero when there were none." +--- + +The two halves are one mechanism: the grant moves with the attempt, and the +record says whether moving it moved anything. A re-dispatch is worth buying only +when the room grows and the work follows; either half alone is the old loop with +a bigger bill. From eafafb1602d3b05cb2046af41158e0e422895c2d Mon Sep 17 00:00:00 2001 From: santoshkumarradha Date: Thu, 17 Sep 2026 05:56:46 -0400 Subject: [PATCH 071/173] docs: the change note carries this change's pull request number --- ...-room-and-refusal.md => 1098-redispatch-room-and-refusal.md} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename docs/changes/unreleased/{1095-redispatch-room-and-refusal.md => 1098-redispatch-room-and-refusal.md} (99%) diff --git a/docs/changes/unreleased/1095-redispatch-room-and-refusal.md b/docs/changes/unreleased/1098-redispatch-room-and-refusal.md similarity index 99% rename from docs/changes/unreleased/1095-redispatch-room-and-refusal.md rename to docs/changes/unreleased/1098-redispatch-room-and-refusal.md index ae759f0ed6..89f6a7fe32 100644 --- a/docs/changes/unreleased/1095-redispatch-room-and-refusal.md +++ b/docs/changes/unreleased/1098-redispatch-room-and-refusal.md @@ -1,7 +1,7 @@ --- kind: changed title: A re-dispatch after running out is granted more room, and one that banks nothing new is refused -pr: 1095 +pr: 1098 surface: [resident, docs] invalidates: - "A leaf that ran out of its token budget was re-dispatched with the identical budget and the identical brief, up to the round cap, and bought the same truncated ending each time. The dispatch's grant is attempt-aware now: `regrantAfterRunningOut` in `cmd/codeaf/subharness.go` grows the token grant by three halves per re-dispatch, never above `overrunGrantCeiling` (four flat leaf grants; a fan-in that measured more keeps every token it measured), and the wall the leaf is given follows the larger grant." From 6442917de600b84dfeb1eb4cc7a6fabf423135b2 Mon Sep 17 00:00:00 2001 From: santoshkumarradha Date: Thu, 17 Sep 2026 06:04:25 -0400 Subject: [PATCH 072/173] session: a git status with no directory is refused where it is built git -C "" is a no-op, so worktreeDirtIn and UnsavedEditsNote built their status command by hand and ran it in the process's own directory when the directory was empty, past the refusal in gitWith. Both go through gitWith now, and gitWith is a thin wrapper of a new gitContext that carries the caller's own context; the empty-directory refusal and the pinned environment live in that one place. Co-Authored-By: codeaf --- internal/session/task_run.go | 19 ++++++++++++----- internal/session/taskgitdir_test.go | 32 +++++++++++++++++++++++++++++ internal/session/taskpreflight.go | 5 ++--- 3 files changed, 48 insertions(+), 8 deletions(-) diff --git a/internal/session/task_run.go b/internal/session/task_run.go index f3ef6300ef..533f28b916 100644 --- a/internal/session/task_run.go +++ b/internal/session/task_run.go @@ -6596,15 +6596,15 @@ func worktreeDirt(dir string) string { return worktreeDirtIn(context.Background( // worktreeDirtIn is [worktreeDirt] under a context, for a reading somebody may // have to stop ([treeWatch.close]). func worktreeDirtIn(ctx context.Context, dir string) string { - args := []string{"-C", dir, "--no-optional-locks", "status", "--porcelain", "--untracked-files=all", "--", "."} + args := []string{"--no-optional-locks", "status", "--porcelain", "--untracked-files=all", "--", "."} for _, dropping := range taskDroppingNames() { args = append(args, ":(exclude)"+dropping) } - out, err := exec.CommandContext(ctx, "git", args...).Output() + out, err := gitContext(ctx, dir, nil, args...) if err != nil { return "" } - sum := sha256.Sum256(out) + sum := sha256.Sum256([]byte(out)) return string(sum[:8]) } @@ -8959,13 +8959,22 @@ func git(dir string, args ...string) (string, error) { return gitWith(dir, nil, args...) } -// gitWith is [git] with something extra in its environment, and it exists for +// gitWith is [gitContext] with [context.Background] in place of a caller's own +// context, kept as its own name because nearly every call here is that. // exactly one caller: the ground ladder stages a parent's tree into AN INDEX OF // ITS OWN so that handing out a child never moves the parent's index // ([sealGroundWork]). GIT_INDEX_FILE is the only way to say that to git, and a // second copy of the pager and editor settings beside it would be the drift the // one-source-of-truth law forbids. func gitWith(dir string, environment []string, args ...string) (string, error) { + return gitContext(context.Background(), dir, environment, args...) +} + +// gitContext is [gitWith] under a context, for a command somebody may have to +// stop before git answers ([worktreeDirtIn]'s watch). The refusal of an empty +// directory and the pinned environment live here, so there is one copy of +// each; every other spelling of a git call in this package is a wrapper of it. +func gitContext(ctx context.Context, dir string, environment []string, args ...string) (string, error) { // A COMMAND WITH NO DIRECTORY RUNS WHEREVER THE PROCESS HAPPENS TO BE. // // exec.Cmd reads an empty Dir as "the calling process's working directory", @@ -8984,7 +8993,7 @@ func gitWith(dir string, environment []string, args ...string) (string, error) { if strings.TrimSpace(dir) == "" { return "", errors.New("git: no directory to run in") } - command := exec.Command("git", args...) + command := exec.CommandContext(ctx, "git", args...) command.Dir = dir // A pager or an editor in the middle of a merge would hang a node forever on // a terminal it does not have. diff --git a/internal/session/taskgitdir_test.go b/internal/session/taskgitdir_test.go index 3ed374555a..96607cae52 100644 --- a/internal/session/taskgitdir_test.go +++ b/internal/session/taskgitdir_test.go @@ -1,6 +1,7 @@ package session import ( + "context" "os" "path/filepath" "strings" @@ -17,6 +18,37 @@ import ( // wrote "task: Rewrite", "task: Measure" and "task: Paint" onto the branch of // the worktree it was launched from. Every call site names a directory, so one // that answers "" is a defect, and it fails here rather than somewhere else. +// TestStatusReadsWithNoDirectoryNeverRuns is the same proof for the two call +// sites that built `git -C ` themselves instead of asking [gitWith]: +// [worktreeDirtIn]'s dirty fingerprint and [UnsavedEditsNote]'s status reading. +// `git -C ""` is a no-op — git runs where the process is standing — so a direct +// exec.Command with an empty directory slipped past the refusal in gitWith. +func TestStatusReadsWithNoDirectoryNeverRuns(t *testing.T) { + repo := t.TempDir() + for _, args := range [][]string{{"init", "-q", "-b", "main"}, {"config", "user.name", "codeaf"}} { + if _, err := git(repo, args...); err != nil { + t.Skipf("no usable git here: %v", err) + } + } + writeFile(t, filepath.Join(repo, "note.md"), "work\n") + t.Chdir(repo) + + if got := worktreeDirtIn(context.Background(), ""); got != "" { + t.Errorf("worktreeDirtIn with no directory returned %q, so a git command ran", got) + } + if got := worktreeDirt(""); got != "" { + t.Errorf("worktreeDirt with no directory returned %q, so a git command ran", got) + } + if got := UnsavedEditsNote(""); got != "" { + t.Errorf("UnsavedEditsNote with no directory returned %q, so a git command ran", got) + } + if staged, problem := stagedPaths(repo); problem != "" { + t.Fatalf("read the index: %s", problem) + } else if len(staged) > 0 { + t.Errorf("a refused command staged %v in the repository the process was standing in", staged) + } +} + func TestAGitCommandWithNoDirectoryNeverRuns(t *testing.T) { // The proof that it never RAN, rather than merely never committed: this is // a real repository holding a real change, and a `git add` that reached it diff --git a/internal/session/taskpreflight.go b/internal/session/taskpreflight.go index 54a1098c74..0a500ea356 100644 --- a/internal/session/taskpreflight.go +++ b/internal/session/taskpreflight.go @@ -43,7 +43,6 @@ package session // an overlap worth a line ([hotFiles]). import ( - "os/exec" "path" "path/filepath" "strconv" @@ -153,8 +152,8 @@ func UnsavedEditsNote(root string) string { if strings.TrimSpace(root) == "" { return "" } - out, err := exec.Command("git", "-C", root, "status", "--porcelain", "--untracked-files=no").Output() - if err != nil || strings.TrimSpace(string(out)) == "" { + out, err := git(root, "status", "--porcelain", "--untracked-files=no") + if err != nil || strings.TrimSpace(out) == "" { return "" } return unsavedEditsWord From 4146dbfc347044735a35471ac4dd3fae49f067ce Mon Sep 17 00:00:00 2001 From: santoshkumarradha Date: Thu, 17 Sep 2026 06:06:14 -0400 Subject: [PATCH 073/173] session: releaseLanded refuses a tree with no ground and takes the root lock The mirror road releaseKept already closes: a tree whose root or directory never resolved has nowhere a worktree is registered and no repository to ask, and its removal and branch deletion would have run wherever the process happened to be, unlocked. Co-Authored-By: codeaf --- internal/session/groundladder.go | 12 ++++ internal/session/task_release_landed_test.go | 69 ++++++++++++++++++++ 2 files changed, 81 insertions(+) create mode 100644 internal/session/task_release_landed_test.go diff --git a/internal/session/groundladder.go b/internal/session/groundladder.go index cf16c1a929..e60b8d8f79 100644 --- a/internal/session/groundladder.go +++ b/internal/session/groundladder.go @@ -1147,6 +1147,18 @@ func (t taskTree) carryBranchHomeLocked() (string, error) { // session's own and goes with the work it carried; what furrow is told is that // the record may be forgotten ([taskTree.dropUniverse]). func (t taskTree) releaseLanded() { + // THE SAME DOOR [taskTree.releaseKept] closes: a tree whose ground was never + // made has nowhere a worktree could be registered and no repository to ask, + // and a command with no directory runs in the process's own directory — so + // the removal below would take a working copy out of somebody else's + // checkout and the branch deletion would name a branch in it. + if strings.TrimSpace(t.root) == "" || strings.TrimSpace(t.dir) == "" { + return + } + // THE REMOVAL AND THE DELETION ARE ONE SPAN OF GIT COMMANDS against the + // ground repository, so they take its lock the way every landing does + // ([taskTree.releaseKept] and [taskTree.carryBranchHome]). + defer lockGitRoot(t.place, t.root)() if t.ownRepository() { _ = os.RemoveAll(t.dir) } else if _, err := git(t.root, "worktree", "remove", t.dir); err != nil { diff --git a/internal/session/task_release_landed_test.go b/internal/session/task_release_landed_test.go new file mode 100644 index 0000000000..4a815758b6 --- /dev/null +++ b/internal/session/task_release_landed_test.go @@ -0,0 +1,69 @@ +package session + +import ( + "os" + "strings" + "testing" +) + +// A TREE WITH NO GROUND IS LEFT ALONE BY ITS RELEASE. +// +// [taskTree.releaseKept] refuses a tree with an empty root or directory, for +// the reason gitWith does: a command with no directory runs wherever the +// process happens to be, and a removal and a branch deletion pointed there +// take apart the checkout the process is standing in. [taskTree.releaseLanded] +// is the mirror image of releaseKept and was missing both the refusal and the +// root lock; these tests hold the refusal in place. + +// TestReleaseLandedLeavesAnEmptyTreeAlone proves the empty case never runs git +// at all: the tree is released while the process stands inside a real +// repository, and neither that repository's worktrees nor its branches move. +func TestReleaseLandedLeavesAnEmptyTreeAlone(t *testing.T) { + repo := newTestRepo(t) + for _, tree := range []taskTree{ + {}, // nothing was ever grounded + {dir: repo}, // a directory with no root + {root: repo}, // a root with no directory + {root: " ", dir: " ", branch: "task/x"}, // whitespace is emptiness too + } { + before := gitOut(t, repo, "worktree", "list", "--porcelain") + branches := gitOut(t, repo, "branch", "--list", "--format=%(refname:short)") + + t.Chdir(repo) + tree.releaseLanded() + + if after := gitOut(t, repo, "worktree", "list", "--porcelain"); after != before { + t.Errorf("releasing %+v changed the registered worktrees:\n%s", tree, after) + } + if after := gitOut(t, repo, "branch", "--list", "--format=%(refname:short)"); after != branches { + t.Errorf("releasing %+v changed the branches:\n%s", tree, after) + } + if _, err := os.Stat(repo); err != nil { + t.Fatalf("releasing %+v disturbed the repository: %v", tree, err) + } + } +} + +// TestReleaseLandedRemovesItsWorktreeAndBranch is the ordinary case beside it: +// a released landing still unregisters its worktree and deletes its branch, +// so the guard above cannot be read as the release having gone quiet. +func TestReleaseLandedRemovesItsWorktreeAndBranch(t *testing.T) { + repo := newTestRepo(t) + place := Place{Dir: t.TempDir(), Workspace: repo} + tree, err := prepareTaskTree(place, repo, "abcd1234abcd9876", 3, "paint the fence") + if err != nil { + t.Fatalf("prepareTaskTree: %v", err) + } + if !strings.Contains(gitOut(t, repo, "worktree", "list"), tree.dir) { + t.Fatalf("git does not know the worktree %s", tree.dir) + } + + tree.releaseLanded() + + if list := gitOut(t, repo, "worktree", "list"); strings.Contains(list, tree.dir) { + t.Errorf("the worktree is still registered:\n%s", list) + } + if branchIsThere(repo, tree.branch) { + t.Errorf("the branch %s is still there", tree.branch) + } +} From f1f9f4c6d235499b80b9b9cf34a1d787a30c2c34 Mon Sep 17 00:00:00 2001 From: santoshkumarradha Date: Thu, 17 Sep 2026 06:15:32 -0400 Subject: [PATCH 074/173] session: the checkout guard reads a linked worktree's own share of the clone Branches and worktree registrations belong to the clone's common directory, so from a linked worktree every sibling's task/* churn read as this run's damage. The guard now answers rev-parse --git-dir and --git-common-dir, skips the branch reading and narrows the worktree reading to the checkout itself when the two differ, and says so in the explanation it prints. The live-codeaf probe moves from /proc, which exists on Linux only, to the process table, and asks whether any other process's command line names a worktree of the same clone. Co-Authored-By: codeaf --- internal/session/hermetic_checkout_test.go | 240 +++++++++++++++------ 1 file changed, 173 insertions(+), 67 deletions(-) diff --git a/internal/session/hermetic_checkout_test.go b/internal/session/hermetic_checkout_test.go index 3388179be8..b3f10ca26e 100644 --- a/internal/session/hermetic_checkout_test.go +++ b/internal/session/hermetic_checkout_test.go @@ -4,7 +4,9 @@ import ( "fmt" "os" "os/exec" + "path/filepath" "sort" + "strconv" "strings" "testing" ) @@ -66,6 +68,15 @@ import ( // do not care what anything is called and still catch the commit and the files, // which is the shape that actually cost somebody an afternoon. // +// AND A LINKED WORKTREE OF A SHARED CLONE READS LESS STILL. Branches belong to +// the clone's common directory, not to the working copy they were read from, so +// from a linked worktree every sibling's task/* churn — a branch cut for their +// own run, a branch deleted when their run landed — would look exactly like +// this run's damage, and the branch reading is skipped outright there. The +// worktree reading narrows instead of skipping: registrations under this root +// can still be this suite's, and a sibling's registration elsewhere in the +// clone cannot. +// // It cannot see a content change to a file that was already dirty before the // run — hashing the whole checkout each run costs more than it is worth — and // the incident's shape, new commits, new files, new branches and new worktrees, @@ -73,9 +84,12 @@ import ( // checkout is what the tree looked like at a moment: the commit it stood on, // the paths git considered dirty or unknown, the branches it had, and the -// worktrees registered against it. +// worktrees registered against it. When the tree is a linked worktree of a +// shared clone, branches is nil — the reading is skipped, not empty — and the +// worktrees are only the ones registered under the tree itself. type checkout struct { root string + linked bool head string dirty map[string]bool branches map[string]bool @@ -99,12 +113,38 @@ func watchTheCheckout() checkout { if watched.head, err = gitLine(root, "rev-parse", "HEAD"); err != nil { return checkout{} } + watched.linked = isLinkedWorktree(root) + if !watched.linked { + watched.branches = branchNames(root) + } watched.dirty = porcelain(root) - watched.branches = branchNames(root) - watched.worktrees = worktreePaths(root) + watched.worktrees = worktreePaths(root, watched.linked) return watched } +// isLinkedWorktree says whether the working copy at root is one worktree among +// several of one shared clone, which `rev-parse` answers by naming two +// different directories: the repository this copy carries its state in, and +// the common one every sibling of the same clone points at. A plain checkout +// answers the same directory twice. +func isLinkedWorktree(root string) bool { + gitDir, err := gitLine(root, "rev-parse", "--git-dir") + if err != nil || gitDir == "" { + return false + } + commonDir, err := gitLine(root, "rev-parse", "--git-common-dir") + if err != nil || commonDir == "" { + return false + } + if !filepath.IsAbs(gitDir) { // a plain checkout answers relative to root + gitDir = filepath.Join(root, gitDir) + } + if !filepath.IsAbs(commonDir) { + commonDir = filepath.Join(root, commonDir) + } + return filepath.Clean(gitDir) != filepath.Clean(commonDir) +} + // moved says how the checkout differs from what it was, and "" when it does // not. A watch that was never taken never reports. func (c checkout) moved() string { @@ -127,33 +167,40 @@ func (c checkout) moved() string { if moved := differing(c.dirty, porcelain(c.root)); len(moved) > 0 { said = append(said, "these paths are not what they were before the run:\n\t"+strings.Join(moved, "\n\t")) } - // A BRANCH IS A WRITE THAT MOVES NOTHING. `task/do-the-thing-` cut in - // the person's repository leaves HEAD exactly where it was and leaves the - // porcelain empty, and it is still the suite committing into a tree it does - // not own — the merge that would have moved HEAD is the only part that did - // not happen yet. - // - // AND A LIVE codeaf CREATES THE SAME BRANCHES. When another codeaf is using - // this checkout and TMPDIR is outside it, branch and worktree churn is - // ambient the way journal growth is (hermetic_test.go) — report the head - // and the porcelain, which a neighbour's ordinary work does not move, and - // leave the shared refs alone. The #578 TempDir-inside-checkout case still - // reports everything, because that is when the suite itself can mint them. - ignoreAmbientRefs := liveCodeafUsingCheckout(c.root) && !tempDirInsideCheckout(c.root) - if !ignoreAmbientRefs { - if moved := differing(c.branches, branchNames(c.root)); len(moved) > 0 { - said = append(said, "these branches are not what they were before the run:\n\t"+strings.Join(moved, "\n\t")) - } - // AND A WORKTREE IS A WRITE GIT IS TOLD TO IGNORE. The ground ladder puts a - // task's tree under `.codeaf/`, which .gitignore covers, so the porcelain - // above stays silent about a whole second checkout sitting in the person's - // tree. The registration is not ignorable: git keeps it, so it is asked for. - if moved := differing(c.worktrees, worktreePaths(c.root)); len(moved) > 0 { + // BRANCHES AND WORKTREES BELONG TO THE WHOLE SHARED CLONE the checkout was + // read from, and a LINKED worktree reads even less than the ambient case: + // every sibling of the same clone churns the same task/* refs this suite + // could not have written, so from a linked worktree the branch reading is + // skipped outright and the worktree reading keeps only the registrations + // under the checkout itself. The head and the porcelain above still watch + // everything, whatever the checkout is. + if c.linked { + if moved := differing(c.worktrees, worktreePaths(c.root, true)); len(moved) > 0 { said = append(said, "these worktrees are not what they were before the run:\n\t"+strings.Join(moved, "\n\t")) } - } else if moved := differing(c.branches, branchNames(c.root)); len(moved) > 0 || - len(differing(c.worktrees, worktreePaths(c.root))) > 0 { - fmt.Fprintf(os.Stderr, "checkout guard: another codeaf is using %s; ignoring task branch/worktree churn (head and porcelain still watched)\n", c.root) + } else { + // AND A LIVE codeaf CREATES THE SAME BRANCHES. When another codeaf is using + // this checkout and TMPDIR is outside it, branch and worktree churn is + // ambient the way journal growth is (hermetic_test.go) — report the head + // and the porcelain, which a neighbour's ordinary work does not move, and + // leave the shared refs alone. The #578 TempDir-inside-checkout case still + // reports everything, because that is when the suite itself can mint them. + ignoreAmbientRefs := liveCodeafUsingCheckout(c.root) && !tempDirInsideCheckout(c.root) + if !ignoreAmbientRefs { + if moved := differing(c.branches, branchNames(c.root)); len(moved) > 0 { + said = append(said, "these branches are not what they were before the run:\n\t"+strings.Join(moved, "\n\t")) + } + // AND A WORKTREE IS A WRITE GIT IS TOLD TO IGNORE. The ground ladder puts a + // task's tree under `.codeaf/`, which .gitignore covers, so the porcelain + // above stays silent about a whole second checkout sitting in the person's + // tree. The registration is not ignorable: git keeps it, so it is asked for. + if moved := differing(c.worktrees, worktreePaths(c.root, false)); len(moved) > 0 { + said = append(said, "these worktrees are not what they were before the run:\n\t"+strings.Join(moved, "\n\t")) + } + } else if moved := differing(c.branches, branchNames(c.root)); len(moved) > 0 || + len(differing(c.worktrees, worktreePaths(c.root, false))) > 0 { + fmt.Fprintf(os.Stderr, "checkout guard: another codeaf is using %s; ignoring task branch/worktree churn (head and porcelain still watched)\n", c.root) + } } if len(said) == 0 { return "" @@ -167,7 +214,7 @@ func (c checkout) moved() string { "`rev-parse --show-toplevel` answers with this checkout (task_run.go's repositoryRoot, " + "#578). Which one it was, this guard cannot tell you; if the run had GOTMPDIR or TMPDIR " + "pointing inside a checkout, suspect the second. Undo the above before pushing.\n" + - "(a second checkout running this same suite beside you cannot cause this; a test that names no directory can, and so can a temporary directory that is not outside your tree. A live codeaf using this checkout is ignored for branch/worktree churn when TMPDIR is outside the tree.)" + "(a second checkout running this same suite beside you cannot cause this; a test that names no directory can, and so can a temporary directory that is not outside your tree. A live codeaf using this checkout is ignored for branch/worktree churn when TMPDIR is outside the tree. When this checkout is a linked worktree of a shared clone, branch churn is not reported at all and the worktree reading covers only registrations under the checkout, because both belong to the clone the siblings share — a leak that only cut a task/ branch from inside a linked worktree slips past here, and the head and the porcelain above are what catch it.)" } // differing names every line that is in one listing and not the other, marked @@ -249,9 +296,12 @@ func harnessBranch(name string) bool { return strings.HasPrefix(name, harnessBranchPrefix) } -// worktreePaths is the set of worktree directories registered against root that -// THIS SUITE could have registered — including one it hid under an ignored -// folder, which is the whole reason this reading exists. +// worktreePaths is the set of worktree directories registered against root +// that THIS SUITE could have registered — including one it hid under an ignored +// folder, which is the whole reason this reading exists. For a linked worktree +// it is asked with underRoot, because the listing answers for the whole shared +// clone and only the registrations under the checkout itself can be the +// suite's. // // ONLY THE PATHS ARE KEPT. The plain listing carries each worktree's head // beside its path, so keeping the whole line would report a head move a second @@ -259,12 +309,22 @@ func harnessBranch(name string) bool { // question. The porcelain form is asked for so the path and the branch can be // read separately, and an unreadable tree is an empty set as everywhere else // here. -func worktreePaths(root string) map[string]bool { +func worktreePaths(root string, underRoot bool) map[string]bool { out, err := gitLines(root, "worktree", "list", "--porcelain") if err != nil { return map[string]bool{} } - return harnessWorktrees(root, out) + found := harnessWorktrees(root, out) + if !underRoot { + return found + } + under := strings.TrimSuffix(root, "/") + "/" + for path := range found { + if !strings.HasPrefix(path, under) { + delete(found, path) + } + } + return found } // harnessWorktrees reads `git worktree list --porcelain` — one record per @@ -440,58 +500,104 @@ func TestTheCheckoutGuardReportsOnlyWhatThisRunCouldHaveWritten(t *testing.T) { } } +// TestTheCheckoutGuardDoesNotReportASiblingWorktreesBranch is the shared-clone +// case, run for real: a linked worktree stands beside a sibling in one clone, +// the sibling cuts and takes away task/ branches and registers a worktree of +// its own while "the run" is watched, and none of it may read as this run's +// damage — branches belong to the clone's common directory, and the sibling's +// registrations stand outside the watched tree. The second half proves the +// churn is real: watched from the main checkout, where the refs are this +// checkout's own, the same branch deletion is reported. +func TestTheCheckoutGuardDoesNotReportASiblingWorktreesBranch(t *testing.T) { + shared := newTestRepo(t) + linked := filepath.Join(shared, "linked") + sibling := filepath.Join(shared, "sibling") + mustGit(t, shared, "worktree", "add", "-b", "task/this-run-0a1b2c", linked) + mustGit(t, shared, "worktree", "add", "-b", "task/sibling-run-9f8e7d", sibling) + // A branch the sibling will take away during the run, seen only through + // the clone's shared refs. + mustGit(t, shared, "branch", "task/sibling-gone-554433") + + t.Chdir(linked) + watched := watchTheCheckout() + if !watched.linked { + t.Fatal("the linked worktree was not read as linked, so this test is not standing where it means to") + } + // THE SAME REFS READ FROM THE MAIN CHECKOUT, before the churn, are this + // checkout's own — that watch is the contrast the narrowing has to hold + // against. + head, _ := gitLine(shared, "rev-parse", "HEAD") + watchedMain := checkout{root: shared, head: head, + dirty: porcelain(shared), branches: branchNames(shared), worktrees: worktreePaths(shared, false)} + + mustGit(t, sibling, "branch", "task/sibling-cuts-778899") + mustGit(t, sibling, "branch", "-D", "task/sibling-gone-554433") + mustGit(t, shared, "worktree", "add", "-b", "task/sibling-tree-112233", filepath.Join(shared, "sibling-tree")) + + if said := watched.moved(); said != "" { + t.Fatalf("a sibling worktree's churn was reported as this run's damage:\n%s", said) + } + + // AND THE SAME CHURN IS REPORTED FROM THE MAIN CHECKOUT — the narrowing + // above is tied to the linked worktree, not to the branch having been + // someone else's. + if said := watchedMain.moved(); !strings.Contains(said, "gone: task/sibling-gone-554433") { + t.Fatalf("from the main checkout the sibling's deleted branch was not reported:\n%s", said) + } +} + // liveCodeafUsingCheckout reports whether another codeaf process looks like it // is working in this repository right now — a chat, a tick, a hosted session — // whose task/ branches and `.codeaf/` worktrees would otherwise look exactly // like the suite's own leak. // -// IT IS BEST-EFFORT AND FAILS OPEN TO "NO". A machine without /proc, a binary +// THE QUESTION IS ASKED OF THE PROCESS TABLE, which exists wherever this +// program does: any other process whose command line names a worktree of the +// repository root belongs to is taken for a live codeaf. The worktrees come +// from `git worktree list --porcelain` — the checkout itself, and every tree +// registered against the same clone. A /proc walk was tried first and +// abandoned: it exists on Linux only, so on darwin the answer was always +// "no", and the ambient reading never engaged on the machine this suite is +// written on. +// +// IT IS BEST-EFFORT AND FAILS OPEN TO "NO". A machine without ps, a binary // renamed something else, or a sandbox that hides other processes simply keeps // the stricter reading; a false negative is a red that a person can re-run with // codeaf closed, and a false positive would hide a real suite write. func liveCodeafUsingCheckout(root string) bool { - under := strings.TrimSuffix(root, "/") + "/" - entries, err := os.ReadDir("/proc") + lines, err := gitLines(root, "worktree", "list", "--porcelain") if err != nil { return false } - self := os.Getpid() - for _, entry := range entries { - if !entry.IsDir() { - continue - } - name := entry.Name() - if len(name) == 0 || name[0] < '0' || name[0] > '9' { - continue - } - var pid int - for i := 0; i < len(name); i++ { - if name[i] < '0' || name[i] > '9' { - pid = -1 - break - } - pid = pid*10 + int(name[i]-'0') + places := map[string]bool{root: true} + for _, line := range lines { + if rest, ok := strings.CutPrefix(line, "worktree "); ok { + places[strings.TrimSpace(rest)] = true } - if pid <= 0 || pid == self { + } + out, err := exec.Command("ps", "-axo", "pid=,command=").Output() + if err != nil { + return false + } + self := os.Getpid() + for _, line := range strings.Split(string(out), "\n") { + pid, command, found := strings.Cut(strings.TrimSpace(line), " ") + if !found { continue } - cmdline, err := os.ReadFile("/proc/" + name + "/cmdline") - if err != nil { + // The suite's own test binary stands IN a worktree but does not name one + // on its command line, so it never reads as a neighbour here. + own, err := strconv.Atoi(strings.TrimSpace(pid)) + if err != nil || own <= 0 || own == self { continue } - joined := strings.ReplaceAll(string(cmdline), "\x00", " ") - if !strings.Contains(joined, "codeaf") { + if !strings.Contains(command, "codeaf") { continue } - // The suite's own test binary and helpers name codeaf too (`go test`, - // `codeaf-session-test-home`). A live chat is the binary itself, or a - // tick/host child, with the checkout in its cwd or its arguments. - cwd, err := os.Readlink("/proc/" + name + "/cwd") - if err == nil && (cwd == root || strings.HasPrefix(cwd, under)) { - return true - } - if strings.Contains(joined, root) { - return true + for place := range places { + if strings.Contains(command, place) { + return true + } } } return false From 9e0fd5a12946a12bd6bde58d424ff9cf277dc170 Mon Sep 17 00:00:00 2001 From: santoshkumarradha Date: Thu, 17 Sep 2026 06:19:37 -0400 Subject: [PATCH 075/173] session: the no-directory status test stands in a repository whose tracked file has an unsaved edit Co-Authored-By: codeaf --- internal/session/taskgitdir_test.go | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/internal/session/taskgitdir_test.go b/internal/session/taskgitdir_test.go index 96607cae52..296421cbcf 100644 --- a/internal/session/taskgitdir_test.go +++ b/internal/session/taskgitdir_test.go @@ -23,6 +23,9 @@ import ( // [worktreeDirtIn]'s dirty fingerprint and [UnsavedEditsNote]'s status reading. // `git -C ""` is a no-op — git runs where the process is standing — so a direct // exec.Command with an empty directory slipped past the refusal in gitWith. +// The repository the process stands in holds a TRACKED file with an unsaved +// edit, because [UnsavedEditsNote] reads with --untracked-files=no and an +// untracked file alone would leave it silent whether or not git ran. func TestStatusReadsWithNoDirectoryNeverRuns(t *testing.T) { repo := t.TempDir() for _, args := range [][]string{{"init", "-q", "-b", "main"}, {"config", "user.name", "codeaf"}} { @@ -31,6 +34,19 @@ func TestStatusReadsWithNoDirectoryNeverRuns(t *testing.T) { } } writeFile(t, filepath.Join(repo, "note.md"), "work\n") + mustGit(t, repo, "add", "-A", "--", ".") + mustGit(t, repo, "-c", "user.name=t", "-c", "user.email=t@t", "commit", "-q", "-m", "the note") + writeFile(t, filepath.Join(repo, "note.md"), "more work\n") + + // Both readings see the edit when they are told where to look; that is what + // makes their silence below a refusal rather than an empty status. + if got := UnsavedEditsNote(repo); got != unsavedEditsWord { + t.Fatalf("UnsavedEditsNote(repo) = %q, so the edit is not visible to a status read", got) + } + if got := worktreeDirtIn(context.Background(), repo); got == "" { + t.Fatal("worktreeDirtIn(repo) answered \"\", so the edit is not visible to a status read") + } + t.Chdir(repo) if got := worktreeDirtIn(context.Background(), ""); got != "" { @@ -42,11 +58,6 @@ func TestStatusReadsWithNoDirectoryNeverRuns(t *testing.T) { if got := UnsavedEditsNote(""); got != "" { t.Errorf("UnsavedEditsNote with no directory returned %q, so a git command ran", got) } - if staged, problem := stagedPaths(repo); problem != "" { - t.Fatalf("read the index: %s", problem) - } else if len(staged) > 0 { - t.Errorf("a refused command staged %v in the repository the process was standing in", staged) - } } func TestAGitCommandWithNoDirectoryNeverRuns(t *testing.T) { From 951af3202175318d54e898762bae4f6a9bddf9aa Mon Sep 17 00:00:00 2001 From: santoshkumarradha Date: Thu, 17 Sep 2026 06:26:06 -0400 Subject: [PATCH 076/173] session, docs: the gitWith comment reads whole, and the change note for the directory refusal --- .../1099-no-git-command-without-a-directory.md | 14 ++++++++++++++ internal/session/task_run.go | 8 ++++---- 2 files changed, 18 insertions(+), 4 deletions(-) create mode 100644 docs/changes/unreleased/1099-no-git-command-without-a-directory.md diff --git a/docs/changes/unreleased/1099-no-git-command-without-a-directory.md b/docs/changes/unreleased/1099-no-git-command-without-a-directory.md new file mode 100644 index 0000000000..d6bdf92d81 --- /dev/null +++ b/docs/changes/unreleased/1099-no-git-command-without-a-directory.md @@ -0,0 +1,14 @@ +--- +kind: fixed +title: No git command runs without a directory, and the checkout guard reads a linked worktree's own share +pr: 1099 +surface: [engine] +invalidates: + - "`worktreeDirtIn` and `UnsavedEditsNote` built `git -C ` themselves, and `git -C \"\"` is a no-op, so an empty directory ran the command where the process stood. Both go through `gitWith` now (`gitContext` is the same call under a caller's context), where the refusal of an empty directory and the pinned environment live once." + - "`taskTree.releaseLanded` removed a worktree and deleted its branch with no empty-root guard and without the root lock. It refuses a tree with no root or directory and takes the root lock, as `releaseKept` does." + - "The session package's checkout guard read branches and worktree registrations for the whole shared clone, so from a linked worktree a sibling's task/* churn was reported as the run's damage, and its live-codeaf probe walked `/proc`, which does not exist on darwin. From a linked worktree the branch reading is skipped and the worktree reading keeps only registrations under the checkout; the probe reads the process table." +--- + +A command with no directory runs wherever the process happens to be. Every +spelling of a git call in this package is a wrapper of one function that +refuses that, and the two that had gone round it are wrappers of it now. diff --git a/internal/session/task_run.go b/internal/session/task_run.go index 533f28b916..a9b75fcd6d 100644 --- a/internal/session/task_run.go +++ b/internal/session/task_run.go @@ -8960,10 +8960,10 @@ func git(dir string, args ...string) (string, error) { } // gitWith is [gitContext] with [context.Background] in place of a caller's own -// context, kept as its own name because nearly every call here is that. -// exactly one caller: the ground ladder stages a parent's tree into AN INDEX OF -// ITS OWN so that handing out a child never moves the parent's index -// ([sealGroundWork]). GIT_INDEX_FILE is the only way to say that to git, and a +// context, kept as its own name because nearly every call here is that. The +// environment it takes exists for exactly one caller: the ground ladder stages +// a parent's tree into AN INDEX OF ITS OWN so that handing out a child never +// moves the parent's index ([sealGroundWork]). GIT_INDEX_FILE is the only way to say that to git, and a // second copy of the pager and editor settings beside it would be the drift the // one-source-of-truth law forbids. func gitWith(dir string, environment []string, args ...string) (string, error) { From 32a1ca44de43a3d93d24e9312b390ed3d350e972 Mon Sep 17 00:00:00 2001 From: santoshkumarradha Date: Thu, 17 Sep 2026 06:47:46 -0400 Subject: [PATCH 077/173] session: releaseLanded leaves the root lock to the landing that holds it The lock is not re-entrant and every road to releaseLanded already holds it, so taking it again waited on itself. --- internal/session/groundladder.go | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/internal/session/groundladder.go b/internal/session/groundladder.go index e60b8d8f79..c3cb24dc4f 100644 --- a/internal/session/groundladder.go +++ b/internal/session/groundladder.go @@ -1155,10 +1155,11 @@ func (t taskTree) releaseLanded() { if strings.TrimSpace(t.root) == "" || strings.TrimSpace(t.dir) == "" { return } - // THE REMOVAL AND THE DELETION ARE ONE SPAN OF GIT COMMANDS against the - // ground repository, so they take its lock the way every landing does - // ([taskTree.releaseKept] and [taskTree.carryBranchHome]). - defer lockGitRoot(t.place, t.root)() + // THE ROOT LOCK IS THE CALLER'S. Every road that reaches here is a landing + // that already holds it ([taskTree.carryBranchHome]'s span, task_run.go), + // the same way [taskTree.releaseKeptLocked] is reached, and the lock is not + // re-entrant: taking it again here waited on itself for the whole of a + // suite's timeout. if t.ownRepository() { _ = os.RemoveAll(t.dir) } else if _, err := git(t.root, "worktree", "remove", t.dir); err != nil { From f08813956c7d866a5bd42e642950dd206b77eb6c Mon Sep 17 00:00:00 2001 From: santoshkumarradha Date: Thu, 17 Sep 2026 06:34:20 -0400 Subject: [PATCH 078/173] store: a delivery gate anchor names when a job was first found done --- internal/store/gate.go | 69 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) diff --git a/internal/store/gate.go b/internal/store/gate.go index 1ef12e2310..323c129e2e 100644 --- a/internal/store/gate.go +++ b/internal/store/gate.go @@ -6,6 +6,7 @@ import ( "errors" "fmt" "strings" + "time" ) // DeliveryGate is the final judge's evidence about one job. Pass is the first @@ -441,6 +442,21 @@ func (g DeliveryGate) Whole() bool { return g.Pass || g.PolishClosed || g.Overturned } +// Done is whether this gate's own row reports the requested work as done: the +// judgement passed, or its only shortfall was behaviours the request states +// that no check exercises or asserts — the work is there, the checks for it are +// not. +// +// It is the positive fact a delivery gate had no way to state before, and two +// readers turn on it rather than on their own reading of the fields. The +// envelope's `core_done_seconds` is the moment it first became true for a run, +// and the share-of-spend rail measures growth from the same moment — one +// definition, because a rail and a receipt that disagreed about when the work +// was done would be two answers to one question. +func (g DeliveryGate) Done() bool { + return g.Pass || len(g.Unexercised) > 0 || len(g.Unasserted) > 0 +} + // Cited is the gate's citations however they were written down. A row recorded // before the list existed carries only the joined line, and reading it as one // citation is the honest reading of it: that is exactly what it was when it was @@ -657,6 +673,59 @@ func (s *Store) DeliveryGateLineage(baseID string) ([]DeliveryGate, error) { return gates, nil } +// DeliveryGateAnchor returns where in the journal — and when — a job's +// requested work was FIRST found done: the oldest gate recorded for the node +// and everything spliced beneath its id whose verdict passed or left only a +// coverage finding. ok is false while no such gate has been recorded, which is +// a job whose core work has not been found done. +// +// The SEQUENCE is the point and not the payload. A spend rail measured from the +// moment the work was done reads the journal by sequence (SpendSinceSeq), and a +// caller given the payload alone would have to walk the whole event log to get +// back to the row it came from. +func (s *Store) DeliveryGateAnchor(baseID string) (seq int64, at time.Time, ok bool, err error) { + baseID = strings.TrimSpace(baseID) + if baseID == "" { + return 0, time.Time{}, false, nil + } + namespace := baseID + SplitNamespace + ceiling, ordered := idPrefixCeiling(namespace) + if !ordered { + return 0, time.Time{}, false, fmt.Errorf("read delivery gate anchor %q: %w: prefix has no ordered ceiling", baseID, ErrInvalid) + } + rows, err := s.db.Query(` + SELECT seq, ts, payload FROM events + WHERE kind = ? AND (node_id = ? OR (node_id >= ? AND node_id < ?)) + ORDER BY seq`, EventDeliveryGate, baseID, namespace, ceiling) + if err != nil { + return 0, time.Time{}, false, fmt.Errorf("read delivery gate anchor %q: %w", baseID, err) + } + defer rows.Close() + for rows.Next() { + var rowSeq int64 + var stamp, payload string + if err := rows.Scan(&rowSeq, &stamp, &payload); err != nil { + return 0, time.Time{}, false, fmt.Errorf("read delivery gate anchor %q: %w", baseID, err) + } + var gate DeliveryGate + if err := json.Unmarshal([]byte(payload), &gate); err != nil { + return 0, time.Time{}, false, fmt.Errorf("read delivery gate anchor %q: %w", baseID, err) + } + if !gate.Done() { + continue + } + when, err := parseTime(stamp) + if err != nil { + return 0, time.Time{}, false, fmt.Errorf("read delivery gate anchor %q: %w", baseID, err) + } + return rowSeq, when, true, nil + } + if err := rows.Err(); err != nil { + return 0, time.Time{}, false, fmt.Errorf("read delivery gate anchor %q: %w", baseID, err) + } + return 0, time.Time{}, false, nil +} + // EventAcceptance is the acceptance checklist journaled against the piece of // work it will be used to judge: the behaviours the person's own request states, // read from the request before any work began. From ab32d99af6daddfd0a2d36c293439dcc92e02858 Mon Sep 17 00:00:00 2001 From: santoshkumarradha Date: Thu, 17 Sep 2026 06:35:02 -0400 Subject: [PATCH 079/173] revision: a second unexercised-only finding buys no coverage round --- internal/revision/judge.go | 74 +++++++++++++++++ internal/revision/unexercisedbound_test.go | 97 ++++++++++++++++++++++ 2 files changed, 171 insertions(+) create mode 100644 internal/revision/unexercisedbound_test.go diff --git a/internal/revision/judge.go b/internal/revision/judge.go index 2eaa6343d9..7064504e96 100644 --- a/internal/revision/judge.go +++ b/internal/revision/judge.go @@ -2127,6 +2127,12 @@ func GapHandover(gaps string, revised bool, refused string) string { return handover } +// maxUnexercisedRounds bounds how many rounds one unexercised-only finding may +// buy. One is enough: the round exists to add checks for the behaviours the +// request states, and a second round aimed at the same finding measures nothing +// the first did not. +const maxUnexercisedRounds = 1 + // ExtendForGap is the authority the delivery gate never had. // // The judgement at the job root was already the right one and its maximum power @@ -2231,6 +2237,22 @@ func ExtendForGap(ctx context.Context, graph *store.Store, node store.Node, part if settled, met := metExtension(ctx, extension, unmet); met { return settled } + // ONE COVERAGE ROUND, AND NO MORE. An unexercised-only finding names + // behaviours the request states that no check exercises or asserts, and the + // round it buys exists to write those checks. That it STANDS until a + // measurement closes it is right — a behaviour an earlier round proved + // nothing exercises does not become exercised because this round took no + // reading — but it is also what let the same finding raise the same round at + // every later gate with nothing to bound it: a run whose named fix was + // committed minutes in spent the rest of its wall and most of its bill + // writing more tests, because a coverage round could be bought again and + // again. One is enough. See maxUnexercisedRounds. + if unexercisedOnly(unmet) && coverageRoundAlreadyBought(graph, base) { + extension.Refused = "the coverage round was already bought once, and the behaviours " + + "with no check that exercises them are left to the person" + extension.Unclosed = true + return extension + } if refusal := outOfWall(ctx, node); refusal != "" { extension.Refused, extension.Unclosed = refusal, true return extension @@ -2280,6 +2302,58 @@ func ExtendForGap(ctx context.Context, graph *store.Store, node store.Node, part return extension } +// unexercisedOnly says this finding is the coverage measurement and nothing +// else: behaviours the request states that no check exercises or asserts, with +// no other gap a round could be aimed at. +// +// A MODEL JUDGE'S OWN PROSE IS NOT ONE OF THESE. A judgement that still carries +// one is unsourced — the citation it names has not been weighed — so it is not +// unexercised-only however many behaviours ride on it; the round it buys is for +// the prose, and the coverage finding travels along. measuredHalf is what turns +// such a judgement into an unexercised-only one, and it is the same door every +// other measured finding goes through. +func unexercisedOnly(unmet Judgment) bool { + if len(unmet.Unexercised) == 0 && len(unmet.Unasserted) == 0 { + return false + } + if !unmet.Sourced { + return false + } + return !unmet.Mechanical && len(unmet.Constraint) == 0 && len(unmet.Consumers) == 0 && + len(unmet.Unbound) == 0 && len(unmet.OwnFailing) == 0 +} + +// coverageRoundAlreadyBought answers whether one of this job's earlier gates +// already grew work for an unexercised-only finding. The gate row carries the +// finding in Unexercised and Unasserted, and Extended says a round actually +// landed for it — the round counter is derived from these rows, so the ledger +// that bounds the next round is the one every other reader already replays. +// A read that fails answers no, which is the fail-safe direction: it buys the +// round rather than refusing one on evidence it could not see. +func coverageRoundAlreadyBought(graph *store.Store, base string) bool { + if graph == nil { + return false + } + gates, err := graph.DeliveryGateLineage(base) + if err != nil { + log.Printf("note: could not read the coverage ledger for %s: %v", base, err) + return false + } + bought := 0 + for _, gate := range gates { + if !gate.Extended || gate.Mechanical { + continue + } + if len(gate.Consumers) > 0 || len(gate.Unbound) > 0 || len(gate.OwnFailing) > 0 || len(gate.Constraint) > 0 { + continue + } + if len(gate.Unexercised) > 0 || len(gate.Unasserted) > 0 { + bought++ + } + } + return bought >= maxUnexercisedRounds +} + // coverageRefused reads the growth journal for the refusal that has just // happened and answers whether it was the coverage question, so the delivery // can say which governor declined in that governor's own terms. diff --git a/internal/revision/unexercisedbound_test.go b/internal/revision/unexercisedbound_test.go new file mode 100644 index 0000000000..42aad6b3ec --- /dev/null +++ b/internal/revision/unexercisedbound_test.go @@ -0,0 +1,97 @@ +package revision + +// One coverage round, and no more. + +import ( + "context" + "path/filepath" + "strings" + "testing" + + "github.com/Agent-Field/codeaf/internal/store" +) + +// coverageRoundJob splices one job into a private store so a gate can be read +// back off its ledger. +func coverageRoundJob(t *testing.T, session string) *store.Store { + t.Helper() + graph, err := store.Open(filepath.Join(t.TempDir(), "coverage-round.db")) + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { graph.Close() }) + if err := graph.Splice(store.RootID, store.Subtree{Nodes: []store.NodeSpec{{ + ID: "task-1", Brief: "add the flag", + }}}, store.Provenance{Origin: store.OriginUser, Intent: "add the flag", SessionID: session}); err != nil { + t.Fatal(err) + } + return graph +} + +// anUnexercisedFinding is the finding the acceptance gate raises on its own: +// behaviours the request states that no check exercises, sourced because nobody +// has to ask for them to be checked. +func anUnexercisedFinding() Judgment { + return Judgment{Pass: false, Checked: true, Sourced: true, + Gaps: "1 the request states has no check that exercises it.", + Unexercised: []string{"the flag turns on"}} +} + +// A SECOND UNEXERCISED-ONLY FINDING BUYS NO ROUND. The finding stands until a +// measurement closes it, so without this bound it raises the same round at every +// later gate — the run whose named fix was committed minutes in and spent the +// rest of its wall writing more tests. One round is bought for it and no more, +// and the finding is recorded unclosed so the delivery says why. +func TestASecondUnexercisedFindingBuysNoRound(t *testing.T) { + graph := coverageRoundJob(t, "s1") + // The earlier round: the request's behaviours had no check, a round was + // bought for it, and the row says so. + if err := graph.RecordDeliveryGate("task-1", store.DeliveryGate{ + Gap: "2 the request states have no check that exercises them.", + Unexercised: []string{"the flag turns on", "the flag turns off"}, + Round: 1, Extended: true, + }); err != nil { + t.Fatal(err) + } + + node := store.Node{ID: "task-1", Provenance: store.Provenance{Intent: "add the flag"}} + planned := 0 + extension := ExtendForGap(context.Background(), graph, node, "the flag is added", + anUnexercisedFinding(), nil, 10, + func(context.Context, string, string) (store.Subtree, error) { + planned++ + return store.Subtree{}, nil + }) + + if planned != 0 { + t.Fatalf("a second coverage round was planned: %d calls", planned) + } + if extension.Spliced != 0 || !extension.Unclosed { + t.Fatalf("the finding did not stay open: %+v", extension) + } + if !strings.Contains(extension.Refused, "coverage round was already bought once") { + t.Fatalf("the refusal does not say what it refused: %q", extension.Refused) + } +} + +// THE BOUND IS THE SECOND ROUND. A job that has never bought a coverage round +// buys its first, which is the whole point of the round existing; the bound may +// not refuse work the finding has not already paid for. +func TestAFirstUnexercisedFindingStillBuysItsRound(t *testing.T) { + graph := coverageRoundJob(t, "s2") + node := store.Node{ID: "task-1", Provenance: store.Provenance{Intent: "add the flag"}} + planned := 0 + extension := ExtendForGap(context.Background(), graph, node, "the flag is added", + anUnexercisedFinding(), nil, 10, + func(context.Context, string, string) (store.Subtree, error) { + planned++ + return store.Subtree{}, nil + }) + + if planned != 1 { + t.Fatalf("the first coverage round was not even planned: %d calls", planned) + } + if strings.Contains(extension.Refused, "coverage round was already bought once") { + t.Fatalf("the bound refused a round the finding had not paid for: %q", extension.Refused) + } +} From 3dd68b873e54d1d114b5c6e879d75ced65152e6b Mon Sep 17 00:00:00 2001 From: santoshkumarradha Date: Thu, 17 Sep 2026 06:35:52 -0400 Subject: [PATCH 080/173] resident: growth is refused once its spend outweighs the requested work --- internal/resident/grow.go | 72 +++++++++++++++++++++++++++++ internal/resident/growshare_test.go | 67 +++++++++++++++++++++++++++ internal/resident/overrun.go | 2 + 3 files changed, 141 insertions(+) create mode 100644 internal/resident/growshare_test.go diff --git a/internal/resident/grow.go b/internal/resident/grow.go index 16274281b8..a3e7f4c2bb 100644 --- a/internal/resident/grow.go +++ b/internal/resident/grow.go @@ -113,6 +113,14 @@ const ( // is not a cap and not a count: it is this job's OWN measured pace read // against the clock it is actually running under. CauseOutOfWall = "out-of-wall" + + // CauseSpendShare is the round refused because growth has already spent more + // than the requested work itself cost, measured from the moment a gate first + // found that work done. It is a bound against the bill rather than against a + // count of rounds, and it exists because a count could not stop a run whose + // named fix landed at minute eleven from spending the rest of its wall and + // most of its bill on growth. + CauseSpendShare = "spend-share" ) // The refusals in the words a person reads. They are constants because two of @@ -142,8 +150,19 @@ const ( // it was going to end with: a judgement on what it did. So the job stops // growing while there is still time to finish and be judged. RefusedOutOfWall = "there is not enough time left on this run to finish another round of work, so this is handed over while there is still time to check it" + + // The refusal that bounds growth against the bill. Once the requested work + // is done, a round that would take the total past twice what that work cost + // is refused and the job is handed over: the work the person asked for is + // finished, and what is left is growth they did not ask for. + RefusedSpendShare = "the requested work is done and growth has already doubled what it cost — handing over what's done" ) +// growthSpendShare bounds how much a job may spend on growth after its +// requested work is first found done: one means growth may at most double the +// bill that got the work done. +const growthSpendShare = 1.0 + // GrowthStopped reports whether a refusal cause is one of the two the governor // reaches by READING THE WORLD rather than by counting — the round before this // one changed nothing and neither did the work before that, or the remaining @@ -652,6 +671,20 @@ func growJob(ctx context.Context, graph *store.Store, ask Satisfier, req GrowReq } } + // 3b. Share of spend. Once the requested work is first found done, growth is + // bounded against the bill: a round is refused when more than + // [growthSpendShare] times the spend that got the work done has gone on + // growth since. The moment is the delivery gate's own first "done" — a + // pass or a coverage finding — and not this round's, so the rail holds + // across every later round. A job whose gate has never found its core + // work done, and a job with no session to bill, have no anchor and the + // rail does not apply: it refuses nothing it cannot measure. + if reach, err := growthShareReached(graph, req); err != nil { + return GrowVerdict{Round: round}, err + } else if reach { + return refuse(CauseSpendShare, RefusedSpendShare) + } + // 4. The only question that can say "there is nothing left to do". // // IT DOES NOT GET TO REFUSE THE FIRST ROUND AFTER A REVIEW FOUND @@ -715,6 +748,45 @@ func growJob(ctx context.Context, graph *store.Store, ask Satisfier, req GrowReq return GrowVerdict{Allow: true, Round: round, CoveredDespite: covered, Spent: req.spent}, nil } +// growthShareReached answers whether a job has already spent more on growth +// than [growthSpendShare] times what the requested work cost, measured from the +// moment a gate first found that work done. The read is the one the daily rail +// uses — SpendSinceSeq over the errand's session — so the two rails bill the +// same rows and a run cannot read cheap here and dear there. A job with no gate +// that found its work done, or with no session to bill, has no anchor and the +// rail does not apply; a read that fails is returned rather than guessed at. +func growthShareReached(graph *store.Store, req GrowRequest) (bool, error) { + if graph == nil { + return false, nil + } + jobRoot := strings.TrimSpace(req.JobRoot) + if jobRoot == "" { + jobRoot = req.Node.ID + } + session := strings.TrimSpace(req.Node.Provenance.SessionID) + if session == "" { + return false, nil + } + anchor, _, ok, err := graph.DeliveryGateAnchor(jobRoot) + if err != nil || !ok { + return false, err + } + // The share is read against the spend UP TO the anchor: the total billed so + // far minus what has been billed since the gate. One query per window, both + // off the rail's own read, so the two halves cannot come from two different + // readings of the same journal. + total, err := graph.SpendSinceSeq(session, 0) + if err != nil { + return false, err + } + after, err := graph.SpendSinceSeq(session, anchor) + if err != nil { + return false, err + } + before := total.Cost() - after.Cost() + return after.Cost() > growthSpendShare*before, nil +} + // admitGrowth journals a round that actually landed. It is called after the // splice rather than at the verdict, because the round counter is derived from // these events and an admission that never spliced is a round nobody spent — diff --git a/internal/resident/growshare_test.go b/internal/resident/growshare_test.go new file mode 100644 index 0000000000..ec0764724b --- /dev/null +++ b/internal/resident/growshare_test.go @@ -0,0 +1,67 @@ +package resident + +import ( + "context" + "testing" + + "github.com/Agent-Field/codeaf/internal/store" +) + +// shareRequest is the ordinary overrun round every cap is tested against: one +// node's worth of work, measured, that the free checks would otherwise admit. +func shareRequest(t *testing.T, graph *store.Store) GrowRequest { + t.Helper() + node := jobNode(t, graph, "job") + node.Provenance.SessionID = "s1" + return GrowRequest{JobRoot: "job", Node: node, Reason: GrowOverrun, Adding: 1, Measured: true, Produced: 1} +} + +// GROWTH IS REFUSED ONCE IT HAS SPENT MORE THAN THE REQUESTED WORK COST. The +// rail is measured from the moment a gate first found the work done, and it is +// a share of the bill rather than a count of rounds — the bound a count could +// not give a run whose named fix landed early and whose growth came after. +func TestGrowthIsRefusedWhenSpendAfterTheGateDoublesTheBill(t *testing.T) { + graph := crowdedJob(t, "s1", 3) + // The spend that got the requested work done, and the gate that found it + // done. + if err := graph.RecordUsage(store.NodeUsage{NodeID: "job", Cost: 1.0}); err != nil { + t.Fatal(err) + } + if err := graph.RecordDeliveryGate("job", store.DeliveryGate{Pass: true}); err != nil { + t.Fatal(err) + } + // And the growth since, which has already spent twice the work cost. + if err := graph.RecordUsage(store.NodeUsage{NodeID: "job", Cost: 2.0}); err != nil { + t.Fatal(err) + } + + verdict, err := growJob(context.Background(), graph, nil, shareRequest(t, graph)) + if err != nil { + t.Fatal(err) + } + if verdict.Allow || verdict.Cause != CauseSpendShare { + t.Fatalf("growth past the share was admitted: %+v", verdict) + } + if verdict.Refused != RefusedSpendShare { + t.Fatalf("the refusal is not the share rail's own words: %q", verdict.Refused) + } +} + +// THE RAIL ONLY APPLIES WHERE IT CAN BE MEASURED. A job whose gate has never +// found its core work done has no anchor, so growth is bounded by the caps and +// the rail refuses nothing — the fail-safe direction, because a rail that +// guessed at an anchor would stop work it cannot account for. +func TestAJobWithNoGateIsNotRefusedByTheShareRail(t *testing.T) { + graph := crowdedJob(t, "s2", 3) + if err := graph.RecordUsage(store.NodeUsage{NodeID: "job", Cost: 5.0}); err != nil { + t.Fatal(err) + } + + verdict, err := growJob(context.Background(), graph, nil, shareRequest(t, graph)) + if err != nil { + t.Fatal(err) + } + if !verdict.Allow || verdict.Cause == CauseSpendShare { + t.Fatalf("a job with no gate was refused by the share rail: %+v", verdict) + } +} diff --git a/internal/resident/overrun.go b/internal/resident/overrun.go index fd4fbb6475..ab5763f73d 100644 --- a/internal/resident/overrun.go +++ b/internal/resident/overrun.go @@ -543,6 +543,8 @@ func governorPhrase(cause string) string { return "no more room" case CauseCovered: return "already covered" + case CauseSpendShare: + return "the bill has doubled" } return "handing over" } From 996f6fbf645704adad547ca4f518d1b984f70f81 Mon Sep 17 00:00:00 2001 From: santoshkumarradha Date: Thu, 17 Sep 2026 06:36:44 -0400 Subject: [PATCH 081/173] do: the envelope says when the requested work was first found done --- cmd/codeaf/coredone_test.go | 51 ++++++++++++ cmd/codeaf/do.go | 80 +++++++++++++++---- cmd/codeaf/envelope.go | 50 +++++++----- .../manual/chat/running-from-the-terminal.md | 1 + 4 files changed, 148 insertions(+), 34 deletions(-) create mode 100644 cmd/codeaf/coredone_test.go diff --git a/cmd/codeaf/coredone_test.go b/cmd/codeaf/coredone_test.go new file mode 100644 index 0000000000..32e3a251f4 --- /dev/null +++ b/cmd/codeaf/coredone_test.go @@ -0,0 +1,51 @@ +package main + +import ( + "path/filepath" + "testing" + "time" + + "github.com/Agent-Field/codeaf/internal/store" +) + +// THE ENVELOPE SAYS WHEN THE REQUESTED WORK WAS FIRST FOUND DONE, in seconds +// from the run's start, off the journal's own gate event — and leaves the key +// out when no gate ever said so, because a fabricated instant is worse than an +// absent one. +func TestTheEnvelopeSaysWhenTheRequestedWorkWasDone(t *testing.T) { + graph, err := store.Open(filepath.Join(t.TempDir(), "coredone.db")) + if err != nil { + t.Fatal(err) + } + defer graph.Close() + + const gated, ungated = "session-gated", "session-ungated" + spliceForErrand(t, graph, gated, []store.NodeSpec{{ID: "task-1", Brief: "add the flag"}}) + spliceForErrand(t, graph, ungated, []store.NodeSpec{{ID: "task-2", Brief: "add the other flag"}}) + if err := graph.RecordDeliveryGate("task-1", store.DeliveryGate{Pass: true}); err != nil { + t.Fatal(err) + } + _, at, ok, err := graph.DeliveryGateAnchor("task-1") + if err != nil || !ok { + t.Fatalf("the passing gate has no anchor: ok=%t err=%v", ok, err) + } + started := at.Add(-7 * time.Second) + + gatedOutcome := headlessOutcome{started: started} + priceErrand(graph, gated, 0, &gatedOutcome) + fields := envelopeFields(t, errandEnvelope(gatedOutcome)) + got, present := fields["core_done_seconds"] + if !present { + t.Fatal("a gate found the work done and the envelope says nothing about when") + } + if seconds, ok := got.(float64); !ok || seconds < 6.9 || seconds > 7.1 { + t.Fatalf("core_done_seconds = %v, want about 7", got) + } + + // And absent when no gate ever said the work was done. + ungatedOutcome := headlessOutcome{started: started} + priceErrand(graph, ungated, 0, &ungatedOutcome) + if _, present := envelopeFields(t, errandEnvelope(ungatedOutcome))["core_done_seconds"]; present { + t.Fatal("a run whose gate never found the work done reported when it was") + } +} diff --git a/cmd/codeaf/do.go b/cmd/codeaf/do.go index 9ce388f972..9c377e9fa2 100644 --- a/cmd/codeaf/do.go +++ b/cmd/codeaf/do.go @@ -110,7 +110,14 @@ type headlessOutcome struct { Nodes int `json:"nodes"` Seconds float64 `json:"seconds"` - Settled bool `json:"settled"` + // started is when this invocation opened, and coreDoneSeconds is how long + // it took to finish the requested work: the first moment a delivery gate + // found that work done. Both are unexported — they reach a caller only + // through the envelope's `core_done_seconds`, which is the one spelling + // every reader shares. + started time.Time + coreDoneSeconds float64 + Settled bool `json:"settled"` // unfinishedTree is the finished-tree reading's own sentence on the one run // that cannot be called settled: its checks failed to collect. It stays // unexported because the sentence leaves through Deliverable, while Settled @@ -616,6 +623,7 @@ func errandRun(request doRequest, seats config.Seats, started time.Time) (outcom return headlessOutcome{}, err } outcome.Seconds = time.Since(started).Seconds() + outcome.started = started // The wall is the case that made this necessary. A leaf cancelled by the // timeout journals its usage row on the way down, which is after the // watcher has returned and — until this line moved the shutdown ahead of @@ -656,6 +664,47 @@ func priceErrand(graph *store.Store, session string, openedAt int64, outcome *he // work is read from what was written down, for the reason the bill is: a // counter in this process could not see a round a resident spliced. outcome.rounds = errandRounds(graph, session) + // AND WHEN THE REQUESTED WORK WAS FIRST FOUND DONE, off the same journal. + outcome.coreDoneSeconds = errandCoreDoneSeconds(graph, session, outcome.started) +} + +// errandCoreDoneSeconds is how long it took this errand to finish the requested +// work, measured from the run's start to the first delivery gate on one of its +// jobs that found the work done — a pass, or a coverage finding. It is the +// EARLIEST such moment across the errand's jobs, so a run that finished one part +// early and another late reports the first; a run whose gate never said so +// reports zero, which the envelope carries as an absent key rather than a +// fabricated instant. +func errandCoreDoneSeconds(graph *store.Store, session string, started time.Time) float64 { + if graph == nil || started.IsZero() { + return 0 + } + nodes, err := graph.SessionMemberNodes(session) + if err != nil { + return 0 + } + var earliest int64 + var at time.Time + for _, node := range nodes { + if node.Parent != store.RootID { + continue + } + seq, when, ok, err := graph.DeliveryGateAnchor(node.ID) + if err != nil || !ok { + continue + } + if earliest == 0 || seq < earliest { + earliest, at = seq, when + } + } + if earliest == 0 { + return 0 + } + seconds := at.Sub(started).Seconds() + if seconds < 0 { + return 0 + } + return seconds } // errandRounds is how many times this errand bought MORE WORK: every growth @@ -2970,20 +3019,21 @@ func reportErrand(request doRequest, outcome headlessOutcome) error { // `exec` and `run` from publishing three different objects again. func errandEnvelope(outcome headlessOutcome) resultEnvelope { return buildResultEnvelope(runResult{ - Stop: outcome.resolvedStop(), - Answer: outcome.Deliverable, - Files: outcome.Artifacts, - Error: outcome.Error, - SpendUSD: outcome.Spend, - TokensIn: outcome.tokensIn, - TokensOut: outcome.tokensOut, - Seconds: outcome.Seconds, - Model: outcome.Model, - Steps: outcome.Nodes, - Run: outcome.run, - Calls: outcome.calls, - Rounds: outcome.rounds, - Extra: legacyErrandFields(outcome), + Stop: outcome.resolvedStop(), + Answer: outcome.Deliverable, + Files: outcome.Artifacts, + Error: outcome.Error, + SpendUSD: outcome.Spend, + TokensIn: outcome.tokensIn, + TokensOut: outcome.tokensOut, + Seconds: outcome.Seconds, + CoreDoneSeconds: outcome.coreDoneSeconds, + Model: outcome.Model, + Steps: outcome.Nodes, + Run: outcome.run, + Calls: outcome.calls, + Rounds: outcome.rounds, + Extra: legacyErrandFields(outcome), }) } diff --git a/cmd/codeaf/envelope.go b/cmd/codeaf/envelope.go index f191944547..147ac5be32 100644 --- a/cmd/codeaf/envelope.go +++ b/cmd/codeaf/envelope.go @@ -294,6 +294,13 @@ type resultEnvelope struct { Tokens envelopeTokens `json:"tokens"` // Seconds is how long it took, wall clock. Seconds float64 `json:"seconds"` + // CoreDoneSeconds is when the requested work was FIRST found done, in + // seconds from the run's start: the first delivery gate that passed or left + // only a coverage finding — the fix committed, the checks for it still to + // come. It is omitted when no gate ever said so, and it is the figure a + // person reads to see how much of the run was the work and how much came + // after it. + CoreDoneSeconds float64 `json:"core_done_seconds,omitempty"` // Model is the model the work ran on, as the seat ladder resolved it. Model string `json:"model"` // Steps is how many pieces of work ran: `do`'s nodes, `exec`'s turns. A @@ -392,11 +399,15 @@ type runResult struct { TokensIn int TokensOut int Seconds float64 - Model string - Steps int - Run string - Calls int - Rounds int + // CoreDoneSeconds is `do`'s fact: when its gate first found the requested + // work done, read off the journal. `exec` and `run` have no delivery gate + // and leave it zero, which omits the key. + CoreDoneSeconds float64 + Model string + Steps int + Run string + Calls int + Rounds int // Extra is this verb's own fields: its old spellings, and whatever it knows // that the contract has no room for. Nil for a verb with neither. Extra map[string]any @@ -421,20 +432,21 @@ func buildResultEnvelope(result runResult) resultEnvelope { files = []string{} } return resultEnvelope{ - OK: exitFor(stop) == exitDone, - Stop: stop, - Answer: result.Answer, - Files: files, - Error: result.Error, - SpendUSD: result.SpendUSD, - Tokens: envelopeTokens{In: result.TokensIn, Out: result.TokensOut}, - Seconds: result.Seconds, - Model: result.Model, - Steps: result.Steps, - Run: result.Run, - Calls: result.Calls, - Rounds: result.Rounds, - extra: result.Extra, + OK: exitFor(stop) == exitDone, + Stop: stop, + Answer: result.Answer, + Files: files, + Error: result.Error, + SpendUSD: result.SpendUSD, + Tokens: envelopeTokens{In: result.TokensIn, Out: result.TokensOut}, + Seconds: result.Seconds, + CoreDoneSeconds: result.CoreDoneSeconds, + Model: result.Model, + Steps: result.Steps, + Run: result.Run, + Calls: result.Calls, + Rounds: result.Rounds, + extra: result.Extra, } } diff --git a/internal/manual/chat/running-from-the-terminal.md b/internal/manual/chat/running-from-the-terminal.md index cb6bc70c71..58f40ddaca 100644 --- a/internal/manual/chat/running-from-the-terminal.md +++ b/internal/manual/chat/running-from-the-terminal.md @@ -186,6 +186,7 @@ stdout, always parseable, printed even when the run failed**: | `spend_usd` | what it cost, whole, in dollars | | `tokens` | `{"in": …, "out": …}` | | `seconds` | wall clock | +| `core_done_seconds` | when the requested work was first found done, in seconds from the start; absent when the gate never said so | | `model` | the model the work ran on | | `steps` | how many pieces of work ran — `do`'s nodes, `exec`'s turns. A saved program does not measure it: the key is still there, holding `0`, and that `0` is a measurement nobody took rather than a count of none | | `run` | this invocation's id. It names the folder `--debug` writes into, and every row this run wrote into `~/.codeaf/logs/calls.jsonl` carries it too — so `codeaf logs --run ` is how you get from this object to the calls behind it. Empty on a verb that opened no run of its own | From 2c9fafa7c15304948f73f8d6b8afb101865cb787 Mon Sep 17 00:00:00 2001 From: santoshkumarradha Date: Thu, 17 Sep 2026 06:36:55 -0400 Subject: [PATCH 082/173] docs: the change note for the bounded coverage round and the core-done figure --- .../unreleased/1095-coverage-round-buys-once.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 docs/changes/unreleased/1095-coverage-round-buys-once.md diff --git a/docs/changes/unreleased/1095-coverage-round-buys-once.md b/docs/changes/unreleased/1095-coverage-round-buys-once.md new file mode 100644 index 0000000000..e04870fc24 --- /dev/null +++ b/docs/changes/unreleased/1095-coverage-round-buys-once.md @@ -0,0 +1,16 @@ +--- +kind: changed +title: the coverage round buys once, and the envelope says when the requested work was done +pr: 1095 +surface: [resident, docs] +invalidates: + - "An unexercised-only finding bought a repair round at every later delivery gate, because the finding stands until a measurement closes it and nothing bounded how many rounds `no check exercises this behaviour` could raise. A job buys at most one such round now (`maxUnexercisedRounds`), and a second finding is recorded unclosed rather than funded." + - "Growth was bounded only by the round counter, the job-size ceiling, the wall and the daily dollar rail. A share-of-spend rail (`growthSpendShare`) refuses a round once growth has spent more than the requested work itself cost, counted from the gate that first found that work done, and journaled as `CauseSpendShare`." + - "`codeaf do --json` carried no figure for when the requested work was finished. `core_done_seconds` now appears, in seconds from the start, when a delivery gate first passed or found only a coverage gap, and is absent when no gate said so." +--- + +An unexercised-only finding is a measurement, so it stands until a measurement +closes it — right, but unbounded, it raised the same test-writing round at every +gate. A run whose named fix was committed minutes in spent the rest of its wall +and most of its bill writing more tests. The round is bounded to one, and growth +past the point the work was done is bounded against the bill that got it done. From 489423252be59332f18d5f25ad2c8e369c4e832c Mon Sep 17 00:00:00 2001 From: santoshkumarradha Date: Thu, 17 Sep 2026 06:48:25 -0400 Subject: [PATCH 083/173] store: a gate is done only when the checks are all that is missing Done read any gate carrying a coverage finding as the work being done, including one that also named a failing check of the work's own, an unbound consumer, a constraint or a mechanical failure. Anchoring the spend rail and the core-done figure there measured from before the work was finished. A test holds each shape. --- internal/store/gate.go | 17 ++++++++++++++++- internal/store/gate_done_test.go | 28 ++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 1 deletion(-) create mode 100644 internal/store/gate_done_test.go diff --git a/internal/store/gate.go b/internal/store/gate.go index 323c129e2e..ba1adbee15 100644 --- a/internal/store/gate.go +++ b/internal/store/gate.go @@ -454,7 +454,22 @@ func (g DeliveryGate) Whole() bool { // definition, because a rail and a receipt that disagreed about when the work // was done would be two answers to one question. func (g DeliveryGate) Done() bool { - return g.Pass || len(g.Unexercised) > 0 || len(g.Unasserted) > 0 + if g.Pass { + return true + } + // A COVERAGE FINDING BESIDE ANOTHER SHORTFALL IS NOT DONE. The work is + // there only when the checks for it are the whole of what is missing; a + // gate that also names a consumer left unbound, a check of the work's own + // that fails, a constraint unmet or a mechanical failure has found the work + // itself short, and anchoring "done" there would measure growth from before + // the work was finished. + if g.Mechanical || g.Unreadable || g.Unjudged { + return false + } + if len(g.Consumers) > 0 || len(g.Unbound) > 0 || len(g.OwnFailing) > 0 || len(g.Constraint) > 0 { + return false + } + return len(g.Unexercised) > 0 || len(g.Unasserted) > 0 } // Cited is the gate's citations however they were written down. A row recorded diff --git a/internal/store/gate_done_test.go b/internal/store/gate_done_test.go new file mode 100644 index 0000000000..ea1b2826a8 --- /dev/null +++ b/internal/store/gate_done_test.go @@ -0,0 +1,28 @@ +package store + +import "testing" + +// A GATE IS DONE ON A PASS, OR WHEN THE CHECKS ARE ALL THAT IS MISSING. A +// coverage finding that stands beside any other shortfall has found the work +// itself short, and the moment it anchors would come before the work was +// finished. +func TestAGateIsDoneOnlyWhenTheChecksAreAllThatIsMissing(t *testing.T) { + for _, c := range []struct { + name string + gate DeliveryGate + want bool + }{ + {"a pass", DeliveryGate{Pass: true}, true}, + {"an unexercised finding alone", DeliveryGate{Unexercised: []string{"the flag turns on"}}, true}, + {"an unasserted finding alone", DeliveryGate{Unasserted: []string{"the flag turns off"}}, true}, + {"nothing found at all", DeliveryGate{}, false}, + {"a coverage finding beside a failing check of its own", DeliveryGate{Unexercised: []string{"a"}, OwnFailing: []string{"TestX"}}, false}, + {"a coverage finding beside an unbound consumer", DeliveryGate{Unexercised: []string{"a"}, Unbound: []string{"caller"}}, false}, + {"a coverage finding beside a constraint", DeliveryGate{Unasserted: []string{"a"}, Constraint: []string{"no new dependency"}}, false}, + {"a coverage finding on a mechanical failure", DeliveryGate{Unexercised: []string{"a"}, Mechanical: true}, false}, + } { + if got := c.gate.Done(); got != c.want { + t.Errorf("%s: Done() = %t, want %t", c.name, got, c.want) + } + } +} From 0710b5cd4ae4efea6174c8d1d25bef862456a02d Mon Sep 17 00:00:00 2001 From: santoshkumarradha Date: Thu, 17 Sep 2026 06:48:33 -0400 Subject: [PATCH 084/173] docs: the change note carries this change's pull request number --- ...rage-round-buys-once.md => 1100-coverage-round-buys-once.md} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename docs/changes/unreleased/{1095-coverage-round-buys-once.md => 1100-coverage-round-buys-once.md} (99%) diff --git a/docs/changes/unreleased/1095-coverage-round-buys-once.md b/docs/changes/unreleased/1100-coverage-round-buys-once.md similarity index 99% rename from docs/changes/unreleased/1095-coverage-round-buys-once.md rename to docs/changes/unreleased/1100-coverage-round-buys-once.md index e04870fc24..09a3ed0453 100644 --- a/docs/changes/unreleased/1095-coverage-round-buys-once.md +++ b/docs/changes/unreleased/1100-coverage-round-buys-once.md @@ -1,7 +1,7 @@ --- kind: changed title: the coverage round buys once, and the envelope says when the requested work was done -pr: 1095 +pr: 1100 surface: [resident, docs] invalidates: - "An unexercised-only finding bought a repair round at every later delivery gate, because the finding stands until a measurement closes it and nothing bounded how many rounds `no check exercises this behaviour` could raise. A job buys at most one such round now (`maxUnexercisedRounds`), and a second finding is recorded unclosed rather than funded." From 00b9dc89cf7327bbc725fb2fce18ef136f400944 Mon Sep 17 00:00:00 2001 From: santoshkumarradha Date: Thu, 17 Sep 2026 09:05:34 -0400 Subject: [PATCH 085/173] tui3: the model pool row has its place on the settings sheet It sits on the Providers tab beside the crew rows whose models it chooses among. Co-Authored-By: Claude Fable 5.1 --- internal/tui3/settings.go | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/internal/tui3/settings.go b/internal/tui3/settings.go index 6ea57f29c2..b31c29f669 100644 --- a/internal/tui3/settings.go +++ b/internal/tui3/settings.go @@ -651,6 +651,15 @@ var settingUI = map[string]settingMeta{ about: "Rich icons normally; plain symbols when your terminal needs them.", widget: widgetCycle, }, + // The pool row sits with the models it chooses among: the word decides + // whether this machine reads the shared measurements and whether its own + // runs are sent back, and nothing about the code leaves either way. + config.KeyModelPool: { + tab: tabProviders, label: "model pool", widget: widgetCycle, + about: "codeaf picks your models from the public Model Pool, and your runs " + + "improve it. Nothing about your code leaves your machine. read uses " + + "the pool and sends nothing; off does neither.", + }, config.KeyVisionModel: { tab: tabProviders, label: "looking", widget: widgetSelect, about: "the model that looks at images. Blank picks one that can see.", From 32834732b5fd8b2bdbcfc0057850b31bdf0d7e67 Mon Sep 17 00:00:00 2001 From: santoshkumarradha Date: Thu, 17 Sep 2026 09:19:25 -0400 Subject: [PATCH 086/173] pool/index: the binary carries a seed measurement document --- internal/pool/index/seed.go | 33 ++++++++ internal/pool/index/seed.json | 140 +++++++++++++++++++++++++++++++ internal/pool/index/seed_test.go | 39 +++++++++ 3 files changed, 212 insertions(+) create mode 100644 internal/pool/index/seed.go create mode 100644 internal/pool/index/seed.json create mode 100644 internal/pool/index/seed_test.go diff --git a/internal/pool/index/seed.go b/internal/pool/index/seed.go new file mode 100644 index 0000000000..126c92737a --- /dev/null +++ b/internal/pool/index/seed.go @@ -0,0 +1,33 @@ +package index + +import ( + _ "embed" +) + +// seedJSON is the index this build carries: the first measured pool, shipped +// inside the binary so a machine that has never fetched one still has numbers +// to pick a seat against on its first run. +// +// IT IS A FALLBACK AND NOT A SOURCE. Whoever reads an index reads the cache +// first and this beside it ([Fallback]): a fresh signed document wins, and +// this answers only when there is none, or when the one on disk is older or +// unreadable. seed.json is the same bytes as a file under version control, so +// what the binary carries is diffed and reviewed like any other figure here. +// +//go:embed seed.json +var seedJSON []byte + +// Seed answers with the embedded document's bytes, as a COPY: the index is +// read many times and by many goroutines, and a caller that changed the slice +// would change what every later [Seed] call hands back. +func Seed() []byte { + return append([]byte(nil), seedJSON...) +} + +// SeedIndex parses the embedded document — the index a machine with no cache +// starts from. The error is returned rather than panicking, though the seed is +// a file this build was shipped with and a test in this package parses it, so +// an error here is a bug and not a mode. +func SeedIndex() (*Index, error) { + return Parse(seedJSON) +} diff --git a/internal/pool/index/seed.json b/internal/pool/index/seed.json new file mode 100644 index 0000000000..d4e8d1fef8 --- /dev/null +++ b/internal/pool/index/seed.json @@ -0,0 +1,140 @@ +{ + "schema": 1, + "generated": "2026-09-17", + "min_installs": 1, + "judges": [ + "reviewer" + ], + "rubrics": { + "role_quality": 1 + }, + "aliases": { + "deepseek/deepseek-v4.1-flash": [ + "ds-v4.1-flash" + ] + }, + "metrics": { + "role_quality": { + "kind": "gaussian", + "unit": "score", + "dims": [ + "role", + "model" + ] + } + }, + "cells": [ + { + "metric": "role_quality", + "role": "high", + "model": "anthropic/claude-fable-5.1", + "mean": 83.3, + "sd": 14.1, + "n": 4 + }, + { + "metric": "role_quality", + "role": "high", + "model": "anthropic/claude-opus-5", + "mean": 80.0, + "sd": 16.3, + "n": 3 + }, + { + "metric": "role_quality", + "role": "high", + "model": "moonshotai/kimi-k3", + "mean": 80.0, + "sd": 12.1, + "n": 8 + }, + { + "metric": "role_quality", + "role": "high", + "model": "qwen/qwen3.8-max-0902", + "mean": 75.0, + "sd": 14.4, + "n": 6 + }, + { + "metric": "role_quality", + "role": "mastermind", + "model": "anthropic/claude-fable-5.1", + "mean": 77.5, + "sd": 6.5, + "n": 38 + }, + { + "metric": "role_quality", + "role": "mastermind", + "model": "anthropic/claude-opus-5", + "mean": 80.0, + "sd": 8.7, + "n": 18 + }, + { + "metric": "role_quality", + "role": "mastermind", + "model": "moonshotai/kimi-k3", + "mean": 57.1, + "sd": 10.6, + "n": 19 + }, + { + "metric": "role_quality", + "role": "mastermind", + "model": "qwen/qwen3.8-max-0902", + "mean": 50.0, + "sd": 16.7, + "n": 6 + }, + { + "metric": "role_quality", + "role": "mastermind", + "model": "z-ai/glm-5.3", + "mean": 62.5, + "sd": 16.1, + "n": 6 + }, + { + "metric": "role_quality", + "role": "worker", + "model": "deepseek/deepseek-v4.1-flash", + "mean": 66.7, + "sd": 13.1, + "n": 10 + }, + { + "metric": "role_quality", + "role": "worker", + "model": "meta/muse-spark-1.3", + "mean": 87.5, + "sd": 11.0, + "n": 6 + }, + { + "metric": "role_quality", + "role": "worker", + "model": "qwen/qwen3.8-max-0902", + "mean": 72.7, + "sd": 12.9, + "n": 9 + }, + { + "metric": "role_quality", + "role": "worker", + "model": "z-ai/glm-5.3", + "mean": 75.0, + "sd": 7.5, + "n": 30 + }, + { + "metric": "role_quality", + "role": "worker", + "model": "z-ai/glm-5.3-flash", + "mean": 65.6, + "sd": 5.9, + "n": 62 + } + ] +} diff --git a/internal/pool/index/seed_test.go b/internal/pool/index/seed_test.go new file mode 100644 index 0000000000..b6ae57e35f --- /dev/null +++ b/internal/pool/index/seed_test.go @@ -0,0 +1,39 @@ +package index + +import "testing" + +// The seed this build carries is a document a reader accepts: it parses, +// declares the pool's quality metric as gaussian, and carries at least one +// measurement for each of the three seats the picker resolves — so a machine +// with no cache has numbers on its first run rather than an empty index. +func TestSeedCarriesRoleQualityForEverySeat(t *testing.T) { + seed, err := SeedIndex() + if err != nil { + t.Fatalf("the embedded seed does not parse: %v", err) + } + if kind, ok := seed.Kind("role_quality"); !ok || kind != "gaussian" { + t.Fatalf("role_quality: kind %q, declared %v", kind, ok) + } + byRole := map[string]int{} + for _, cell := range seed.Cells("role_quality") { + byRole[cell.Role]++ + } + for _, role := range []string{"worker", "high", "mastermind"} { + if byRole[role] == 0 { + t.Errorf("the seed carries no role_quality cell for %q", role) + } + } +} + +// Seed hands back a copy: the document is shared by many readers, and one that +// mutated the slice it was given would change what every later call returns. +func TestSeedHandsBackACopy(t *testing.T) { + first := Seed() + if len(first) == 0 { + t.Fatal("the seed is empty") + } + first[0] ^= 0xff + if second := Seed(); second[0] == first[0] { + t.Fatal("mutating a Seed copy changed the next one") + } +} From 4ba832b73e5b21423e5b0bba7180093e729ba282 Mon Sep 17 00:00:00 2001 From: santoshkumarradha Date: Thu, 17 Sep 2026 09:20:31 -0400 Subject: [PATCH 087/173] cmd/codeaf: the crew picker reads a seed index with a background refresh --- cmd/codeaf/chat.go | 1 + cmd/codeaf/chatv3_host.go | 3 + cmd/codeaf/chatv3_process.go | 1 + cmd/codeaf/pool.go | 42 ++++++++---- cmd/codeaf/pool_test.go | 123 ++++++++++++++++++++++++++++++++++- cmd/codeaf/poolindex.go | 114 ++++++++++++++++++++++++++++++++ cmd/codeaf/subharness.go | 5 ++ 7 files changed, 277 insertions(+), 12 deletions(-) create mode 100644 cmd/codeaf/poolindex.go diff --git a/cmd/codeaf/chat.go b/cmd/codeaf/chat.go index 0a24e53827..8c488911a0 100644 --- a/cmd/codeaf/chat.go +++ b/cmd/codeaf/chat.go @@ -174,6 +174,7 @@ func buildBrain(w *chatWindow, session string, opts brainOptions) (*chatBrain, e BaseURL: settings.BaseURL, APIKey: settings.APIKey, Dir: settings.ProfileDir, }) config.AutoModels = modelCatalog.ModelsNow + wirePoolIndex(settings.ProfileDir) if opts.seats != nil { applySeats(&settings, *opts.seats) } else { diff --git a/cmd/codeaf/chatv3_host.go b/cmd/codeaf/chatv3_host.go index c2b1e48635..265c27b10d 100644 --- a/cmd/codeaf/chatv3_host.go +++ b/cmd/codeaf/chatv3_host.go @@ -612,6 +612,9 @@ func hostOptions(fleet *engineFleet, welcome remote.Welcome, pick bool) (tui3.Op // A tier row that says auto is answered from this catalog (config.AutoModels): // the same non-blocking read, never a fetch, and set once at start-up. config.AutoModels = models.ModelsNow + // The pool's index is seated beside it, read once here and refreshed in the + // background, against the same profile the catalog was read from. + wirePoolIndex(profileDir) // The refresh key in /model asks the same router THIS machine's list came // from, and refills the same shelf — the list is this laptop's list of // names on both doors (chatv3_modelshelf.go). diff --git a/cmd/codeaf/chatv3_process.go b/cmd/codeaf/chatv3_process.go index 2ed2a357df..7f61bb6931 100644 --- a/cmd/codeaf/chatv3_process.go +++ b/cmd/codeaf/chatv3_process.go @@ -180,6 +180,7 @@ func openV3ProcessWith(door string, askKey bool) (*v3Process, error) { // A tier row that says auto is answered from this catalog (config.AutoModels): // the same non-blocking read, never a fetch, and set once at start-up. config.AutoModels = models.ModelsNow + wirePoolIndex(settings.ProfileDir) shelf := newV3ModelShelf(models, discovery) shelf.setSources(settings.Sources) return &v3Process{ diff --git a/cmd/codeaf/pool.go b/cmd/codeaf/pool.go index 404fddedf4..9b8f1217a8 100644 --- a/cmd/codeaf/pool.go +++ b/cmd/codeaf/pool.go @@ -122,18 +122,18 @@ func poolReading(name string, args []string, output io.Writer, poolDir string, c // ci — then the cached index with its age, then, for status, the outbox and // the two doors the mode opens. func printPool(output io.Writer, poolDir string, cfg poolcfg.Config, now time.Time, asJSON, withStatus bool) error { - var held *index.Index + var cached *index.Index // The cache is read the way show reads everything else, as an answer and // not as an argument: a document that does not parse is not there yet, // and the line below says so. The signature and the puller's version mark // are verify's business; show reports what a person has. if doc, err := os.ReadFile(filepath.Join(poolDir, "doc.json")); err == nil { if parsed, err := index.Parse(doc); err == nil { - held = parsed + cached = parsed } } if asJSON { - return printPoolJSON(output, poolDir, cfg, held, now, withStatus) + return printPoolJSON(output, poolDir, cfg, cached, now, withStatus) } for _, line := range []string{ fmt.Sprintf("mode %s · %s", cfg.Mode, cfg.Source.Mode), @@ -145,19 +145,26 @@ func printPool(output io.Writer, poolDir string, cfg poolcfg.Config, now time.Ti return err } } - if held == nil { + if cached == nil { // A nothing is said in a sentence, the way an empty cache is: - // silence and a bare header both read as a command that broke. - if _, err := fmt.Fprintln(output, "no index cached yet"); err != nil { + // silence and a bare header both read as a command that broke. And the + // index the build carries is named beside it, so a person knows there + // are numbers before any fetch: the seed is what a pick reads until a + // fresher signed one is cached. + line := "no index cached yet" + if seed, err := index.SeedIndex(); err == nil { + line = fmt.Sprintf("no index cached yet · built-in seed of %s", seed.Generated().Format("2006-01-02")) + } + if _, err := fmt.Fprintln(output, line); err != nil { return err } } else { - generated := held.Generated() + generated := cached.Generated() if _, err := fmt.Fprintf(output, "index · generated %s · %s old · schema %d · %s · %s · min installs %d\n", generated.Format("2006-01-02"), reltime.Elapsed(now.Sub(generated)), - held.Schema(), countWord(len(held.Metrics()), "metric", "metrics"), - countWord(len(held.Judges()), "judge", "judges"), held.MinInstalls()); err != nil { + cached.Schema(), countWord(len(cached.Metrics()), "metric", "metrics"), + countWord(len(cached.Judges()), "judge", "judges"), cached.MinInstalls()); err != nil { return err } } @@ -187,7 +194,9 @@ type poolAnswer struct { // indexSummary is the cached index as the reading forms carry it: the // document's own day, its age against the asking clock, and the counts a -// person checks before trusting it. +// person checks before trusting it. Source says where the document came from — +// "cache" for the one under the profile, "seed" for the one the build carries +// when there is no cache. type indexSummary struct { Generated string `json:"generated"` AgeSeconds int `json:"age_seconds"` @@ -195,9 +204,10 @@ type indexSummary struct { Metrics int `json:"metrics"` Judges int `json:"judges"` MinInstalls int `json:"min_installs"` + Source string `json:"source"` } -func printPoolJSON(output io.Writer, poolDir string, cfg poolcfg.Config, held *index.Index, now time.Time, withStatus bool) error { +func printPoolJSON(output io.Writer, poolDir string, cfg poolcfg.Config, cached *index.Index, now time.Time, withStatus bool) error { answer := poolAnswer{ Mode: cfg.Mode.String(), ModeSource: cfg.Source.Mode, @@ -205,6 +215,15 @@ func printPoolJSON(output io.Writer, poolDir string, cfg poolcfg.Config, held *i SubmitURL: cfg.SubmitURL, TTLSeconds: int(cfg.TTL / time.Second), } + // A cached document is reported as itself; with no cache the build's seed + // stands in, so a script reading `index` sees the index a pick would read + // and the source field says which one it was. + held, source := cached, "cache" + if held == nil { + if seed, err := index.SeedIndex(); err == nil { + held, source = seed, "seed" + } + } if held != nil { generated := held.Generated() answer.Index = &indexSummary{ @@ -214,6 +233,7 @@ func printPoolJSON(output io.Writer, poolDir string, cfg poolcfg.Config, held *i Metrics: len(held.Metrics()), Judges: len(held.Judges()), MinInstalls: held.MinInstalls(), + Source: source, } } if withStatus { diff --git a/cmd/codeaf/pool_test.go b/cmd/codeaf/pool_test.go index 8aa447112d..9c47adbee8 100644 --- a/cmd/codeaf/pool_test.go +++ b/cmd/codeaf/pool_test.go @@ -1,6 +1,7 @@ package main import ( + "context" "crypto/ed25519" "crypto/rand" "encoding/base64" @@ -14,6 +15,7 @@ import ( "github.com/Agent-Field/codeaf/internal/home" "github.com/Agent-Field/codeaf/internal/pool/outbox" + "github.com/Agent-Field/codeaf/internal/pool/poolcfg" ) // poolClock is a stopped now, so an index's age is a fact rather than a race @@ -111,7 +113,7 @@ func TestPoolShowPrintsTheConfigAndSaysWhenNoIndexIsCached(t *testing.T) { "index https://pool.invalid/index.json · default", "submit https://pool.invalid/submit · default", "ttl 1d · default", - "no index cached yet", + "no index cached yet · built-in seed of 2026-09-17", } { if !strings.Contains(body, want) { t.Errorf("show is missing %q:\n%s", want, body) @@ -154,6 +156,7 @@ func TestPoolShowJSONWithASeededIndexReportsTheDocument(t *testing.T) { Metrics int `json:"metrics"` Judges int `json:"judges"` MinInstalls int `json:"min_installs"` + Source string `json:"source"` } `json:"index"` } if err := json.Unmarshal([]byte(out.String()), &answer); err != nil { @@ -179,6 +182,9 @@ func TestPoolShowJSONWithASeededIndexReportsTheDocument(t *testing.T) { if held.Schema != 1 || held.Metrics != 1 || held.Judges != 1 || held.MinInstalls != 1 { t.Fatalf("the document's counts moved: %+v", held) } + if held.Source != "cache" { + t.Fatalf("a cached index did not name its source: %q", held.Source) + } } // status is show plus the outbox and the two doors the mode opens, said in @@ -368,3 +374,118 @@ func TestPoolVerifyRefusesADocumentItsKeyDoesNotTrust(t *testing.T) { t.Fatal("a refused document was cached") } } + +// ── THE SEATED INDEX ──────────────────────────────────────────────────────── + +// writePoolDoc puts a document where poolIndexFor reads the cache: doc.json +// under the profile's pool directory. +func writePoolDoc(t *testing.T, dir, doc string) { + t.Helper() + poolDir := filepath.Join(dir, "pool") + if err := os.MkdirAll(poolDir, 0o755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(poolDir, "doc.json"), []byte(doc), 0o644); err != nil { + t.Fatal(err) + } +} + +// With no cache the reader answers the seed this build carries, and a worker +// cell is in it — so `learn` has numbers on day one. +func TestPoolIndexForAnswersTheSeedWithNoCache(t *testing.T) { + held := poolIndexFor(t.TempDir(), poolcfg.Resolve("", noEnv), poolClock(t))() + if held == nil { + t.Fatal("no cache and no seed: the reader answered nothing") + } + worker := false + for _, cell := range held.Cells("role_quality") { + if cell.Role == "worker" { + worker = true + } + } + if !worker { + t.Fatal("the seed answered no worker cell") + } +} + +// A cached document whose generated day is newer than the seed's wins: the +// reader hands back the cached numbers rather than the embedded ones. +func TestPoolIndexForKeepsANewerCache(t *testing.T) { + dir := t.TempDir() + writePoolDoc(t, dir, `{ + "schema": 1, + "generated": "2026-09-20", + "min_installs": 1, + "metrics": {"role_quality": {"kind": "gaussian", "dims": ["role", "model"]}}, + "cells": [{"metric": "role_quality", "role": "worker", "model": "z-ai/glm-5.3", "mean": 75, "sd": 7, "n": 30}] + }`) + held := poolIndexFor(dir, poolcfg.Resolve("", noEnv), poolClock(t))() + if held == nil || held.Generated().Format("2006-01-02") != "2026-09-20" { + t.Fatalf("a newer cache did not win: %v", held) + } +} + +// A cache that does not parse is not a cache: the seed stands in its place. +func TestPoolIndexForIgnoresAnUnparsableCache(t *testing.T) { + dir := t.TempDir() + writePoolDoc(t, dir, "{ this is not a document") + held := poolIndexFor(dir, poolcfg.Resolve("", noEnv), poolClock(t))() + if held == nil || held.Generated().Format("2006-01-02") != "2026-09-17" { + t.Fatalf("an unparsable cache did not fall back to the seed: %v", held) + } +} + +// A mode that forbids reading answers no index at all. +func TestPoolIndexForAnswersNothingWhenTheModeIsOff(t *testing.T) { + if held := poolIndexFor(t.TempDir(), poolcfg.Resolve("off", noEnv), poolClock(t))(); held != nil { + t.Fatal("a mode that forbids reading answered an index") + } +} + +// No key in the build means no fetch: the refresh starts no goroutine, which +// is every run on a build with no key compiled in. A key starts the one fetch, +// and the mode still has to allow reading. +func TestPoolRefreshStartsNoGoroutineWithoutAKey(t *testing.T) { + started := 0 + prev := poolRefreshGo + poolRefreshGo = func(scope string, fn func()) { started++ } + t.Cleanup(func() { poolRefreshGo = prev }) + + key := []ed25519.PublicKey{make(ed25519.PublicKey, ed25519.PublicKeySize)} + on := poolcfg.Resolve("", noEnv) + off := poolcfg.Resolve("off", noEnv) + + startPoolIndexRefresh(context.Background(), t.TempDir(), on, nil) + if started != 0 { + t.Fatalf("no key, yet %d goroutine(s) started", started) + } + startPoolIndexRefresh(context.Background(), t.TempDir(), off, key) + if started != 0 { + t.Fatal("a mode that forbids reading started a goroutine") + } + startPoolIndexRefresh(context.Background(), t.TempDir(), on, key) + if started != 1 { + t.Fatalf("with a key, %d goroutine(s) started, want 1", started) + } +} + +// With no cache, the --json shape reports the seed the build carries and says +// so under source, so a script sees the index a pick would read. +func TestPoolShowJSONWithNoCacheReportsTheSeed(t *testing.T) { + var out strings.Builder + if err := runPoolWith([]string{"show", "--json"}, &out, t.TempDir(), poolClock(t), noEnv); err != nil { + t.Fatal(err) + } + var answer struct { + Index *struct { + Generated string `json:"generated"` + Source string `json:"source"` + } `json:"index"` + } + if err := json.Unmarshal([]byte(out.String()), &answer); err != nil { + t.Fatalf("--json did not parse: %v\n%s", err, out.String()) + } + if answer.Index == nil || answer.Index.Source != "seed" || answer.Index.Generated != "2026-09-17" { + t.Fatalf("the seed was not reported: %+v", answer.Index) + } +} diff --git a/cmd/codeaf/poolindex.go b/cmd/codeaf/poolindex.go new file mode 100644 index 0000000000..2bd8587fd2 --- /dev/null +++ b/cmd/codeaf/poolindex.go @@ -0,0 +1,114 @@ +// The Model Pool's index, seated at start-up. +// +// The index is a measurement document the crew picker reads a seat's quality +// from (config.PoolQualityMetric, through config.AutoIndex). A machine that +// has fetched one keeps it under the profile's pool directory; a machine that +// has never fetched one still has the index the binary carries +// (internal/pool/index's Seed), so a tier row that says `auto` has numbers on +// its first run rather than none. +// +// READ ONCE, NEVER ON THE RUN'S PATH. [poolIndexFor] parses the cache beside +// the seed exactly once and hands back a function answering the value it +// already has, so a pick asks no disk and no lock. The one fetch this file +// makes runs in a goroutine started where the loader is seated, and it never +// answers a pick: it writes the puller's own cache and the change is read at +// the NEXT start. A run's picks must not move under it. +package main + +import ( + "context" + "crypto/ed25519" + "log" + "os" + "path/filepath" + "time" + + "github.com/Agent-Field/codeaf/internal/config" + "github.com/Agent-Field/codeaf/internal/guard" + "github.com/Agent-Field/codeaf/internal/pool/index" + "github.com/Agent-Field/codeaf/internal/pool/poolcfg" + "github.com/Agent-Field/codeaf/internal/pool/pull" + "github.com/Agent-Field/codeaf/internal/trace" +) + +// poolIndexFor builds this process's one index reader. The read is +// start-up work and nothing on a run's path: the document is parsed here, +// once, and the returned function answers the value already in hand, so a +// pick over many tiers does no disk and no decode per call. +// +// THE CACHE WINS ONLY WHEN IT IS FRESHER. A cached doc.json is handed to +// index.Fallback beside the embedded seed, which trusts whichever declares +// the newer generated day — a young cache keeps its numbers, an old or +// unparsable one leaves the seed in place. A mode that forbids reading +// (poolcfg.Off) answers no index at all, which is the same nothing the seam +// read before this file existed. +// +// THE ONE ERROR IT CANNOT RECOVER FROM is an embedded seed that does not +// parse; that answers nil rather than panicking, though a test in the index +// package parses the very bytes this build carries. +func poolIndexFor(profileDir string, cfg poolcfg.Config, now func() time.Time) func() *index.Index { + if !cfg.CanRead() { + return func() *index.Index { return nil } + } + var cached []byte + if doc, err := os.ReadFile(filepath.Join(config.ProfilePath(profileDir, "pool"), "doc.json")); err == nil { + cached = doc + } + held, err := index.Fallback(cached, index.Seed()) + if err != nil { + seed, seedErr := index.SeedIndex() + if seedErr != nil { + return func() *index.Index { return nil } + } + held = seed + } + return func() *index.Index { return held } +} + +// poolRefreshGo is [guard.Go] behind a variable so a test can prove that no +// goroutine is started when the build carries no key. +var poolRefreshGo = func(scope string, fn func()) { guard.Go(scope, fn) } + +// startPoolIndexRefresh is the loader's own tail: it starts the background +// fetch that keeps the cache fresh, and starts NOTHING when the mode forbids +// reading or the build carries no public key to check a fetched document under +// — which, on a build with no key compiled in, is every run. +func startPoolIndexRefresh(ctx context.Context, profileDir string, cfg poolcfg.Config, keys []ed25519.PublicKey) { + if !cfg.CanRead() || len(keys) == 0 { + return + } + poolRefreshGo("pool/index", func() { refreshPoolIndex(ctx, profileDir, cfg, keys) }) +} + +// refreshPoolIndex fetches a fresh index and lets the puller's own cache keep +// it for the NEXT start. A changed document is NOT swapped into the running +// process: a run's picks must not move under it, so a pick here always reads +// the index this process was seated with, and the fresh one is read at the +// next start. +// +// It is quiet by design. A dead host, a timeout or a signature that does not +// check is an ordinary state for a fetch nothing waited for, so the error is +// said only under the debug record's switch and never on the surface. +func refreshPoolIndex(ctx context.Context, profileDir string, cfg poolcfg.Config, keys []ed25519.PublicKey) { + puller := &pull.Puller{ + URL: cfg.IndexURL, + Keys: keys, + CacheDir: config.ProfilePath(profileDir, "pool"), + TTL: cfg.TTL, + Budget: pull.DefaultBudget, + } + if _, err := puller.Pull(ctx); err != nil && trace.Enabled() { + log.Printf("model pool: index refresh: %v", err) + } +} + +// wirePoolIndex seats this process's index beside its catalog: it hands the +// reader to seat resolution (config.AutoIndex) and starts the refresh beside +// it. It is called once at start-up, from the same places config.AutoModels is +// set, so a tier row that says `auto` resolves against an index on every door +// that resolves a seat. +func wirePoolIndex(profileDir string) { + cfg := config.ModelPoolAt(profileDir) + config.AutoIndex = poolIndexFor(profileDir, cfg, time.Now) + startPoolIndexRefresh(context.Background(), profileDir, cfg, poolPublicKeys) +} diff --git a/cmd/codeaf/subharness.go b/cmd/codeaf/subharness.go index 10a8750899..3ac6bcebb4 100644 --- a/cmd/codeaf/subharness.go +++ b/cmd/codeaf/subharness.go @@ -380,6 +380,11 @@ var sharedCatalog = func() func(config.Config) *catalog.Catalog { // the same non-blocking read, never a fetch, and set once at start-up so every // headless door resolves the word against the list it already holds. config.AutoModels = resolved.ModelsNow + // AND THE POOL'S INDEX BESIDE IT, in the same one-time manner: a tier + // row that says auto is answered from the index this process was seated + // with, the seed when no cache is fresher, and the one fetch it makes + // runs in the background and never blocks this read. + wirePoolIndex(settings.ProfileDir) }) return resolved } From 1a864b13ef35ff81257eccd7bfffcc70c9f750bf Mon Sep 17 00:00:00 2001 From: santoshkumarradha Date: Thu, 17 Sep 2026 09:20:53 -0400 Subject: [PATCH 088/173] pool/judge: a finished run is scored seat by seat by a model outside the crew Co-Authored-By: codeaf --- internal/pool/judge/judge.go | 305 +++++++++++++++++++++++++++++++++++ 1 file changed, 305 insertions(+) create mode 100644 internal/pool/judge/judge.go diff --git a/internal/pool/judge/judge.go b/internal/pool/judge/judge.go new file mode 100644 index 0000000000..5f66c81996 --- /dev/null +++ b/internal/pool/judge/judge.go @@ -0,0 +1,305 @@ +// Package judge scores a finished run seat by seat, independently of the crew +// that produced it. +// +// THE MECHANISM. A run's record names which model held each judged seat — +// worker, high, mastermind — and what the run delivered. Every seat the record +// carries is put to one model that was not in the crew: the caller supplies +// [Ask], which sends a system and a user prompt and returns the answer. One +// question is asked per seat, in role order, and each answer is read as one +// JSON object carrying a score from 0 to 100 and a one-sentence reason. That is +// the same scale crewpick reads seat quality on and a pool records its +// role_quality metric in, so a run's seats land on the scale the picker already +// uses. +// +// THE CALLER OWNS THE CALL. This package touches no disk and no network and +// holds no credentials: the model, the transport and the bill are the caller's, +// and the caller bills the judge's calls to its own seat. [Pick] chooses that +// model from a catalog, and the chosen id rides in the record ([Record.Judge]) +// so a seat already holding it is skipped rather than scoring its own work. +package judge + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "math" + "strconv" + "strings" + + "github.com/Agent-Field/codeaf/internal/catalog" +) + +// recordClip is how many runes of the brief and of the deliverable the user +// prompt carries. They are the two fields that grow with the size of the work +// rather than with its shape, and a judge scoring a seat does not need the whole +// of either to say whether the seat did its job. The report, the claim and the +// ending travel whole: they are the run's own account of itself, and cutting +// them would score a seat on a sentence it did not finish. +const recordClip = 6000 + +// DefaultFloor is the published coding index a judge must reach to be picked. +// A judge below the worker's own class rubber-stamps the work rather than +// scoring it: it has less of the capability the seat is scored on than the seat +// it is reading. +const DefaultFloor = 60 + +// Role is one of a crew's judged seats. +type Role string + +const ( + // RoleWorker is the seat that carried the work. + RoleWorker Role = "worker" + // RoleHigh is the seat that checked the work. + RoleHigh Role = "high" + // RoleMastermind is the seat that planned the work. + RoleMastermind Role = "mastermind" +) + +// seatOrder is the order seats are asked in: the work, then what checked it, +// then the plan it was cut under. +var seatOrder = []Role{RoleWorker, RoleHigh, RoleMastermind} + +// Record is one finished run as the judge reads it: the spec it ran under, its +// own account of itself, what it touched, and who held each seat. It is the +// judge's own copy, assembled by the caller from wherever the run is recorded, +// so this package imports neither the session store nor anything else that +// owns the run. +type Record struct { + // Brief is what the run was asked to do. + Brief string + // Deliverable is what had to exist when it was over. + Deliverable string + // Report is the account the run landed with. + Report string + // Claim is the run's own summary of its answer, kept beside the report. + Claim string + // Ending is why a run that stopped early stopped where it did. + Ending string + // Files is every path the run wrote. + Files []string + // Changed is how many paths it changed. + Changed int + // Checks is the repeatable verification the run ran. + Checks []string + // Seats is which model held each judged seat. The chat door asks no planner + // call, so a record may carry only the worker and the high seats. + Seats map[Role]string + // Judge is the model that will answer the seat questions, the id [Pick] + // returned. A seat already holding it is skipped, so a model never scores + // its own work; an empty Judge names no model and skips nothing. + Judge string + // CostUSD is what the run spent, in dollars. + CostUSD float64 +} + +// Score is one seat's reading: the role, the model that held it, the score on +// the 0-100 scale and the judge's one-sentence reason. +type Score struct { + Role Role + Model string + Score float64 + Reason string +} + +// Ask puts one question to the judge model and returns its answer. The caller +// owns the model, the credentials and the transport, and bills the call to the +// judge's own seat; this package writes both prompts and reads the answer. +type Ask func(ctx context.Context, system, user string) (answer string, err error) + +// Judge asks one question per seat present in rec.Seats, in role order, and +// answers with a Score for each seat that was read. A seat held by the judge's +// own model ([Record.Judge]) is skipped and named in the returned error; a seat +// whose call fails, or whose answer is not one JSON object carrying an integer +// score from 0 to 100, is an error for that seat alone and never stops the +// others. The scores obtained are returned beside a joined error naming every +// seat that failed, and a record with no seats returns no scores and no error. +func Judge(ctx context.Context, ask Ask, rec Record) ([]Score, error) { + if len(rec.Seats) == 0 { + return nil, nil + } + if ask == nil { + return nil, errors.New("judge: no ask function") + } + scores := make([]Score, 0, len(seatOrder)) + var failures []error + for _, role := range seatOrder { + model, present := rec.Seats[role] + if !present { + continue + } + if rec.Judge != "" && model == rec.Judge { + failures = append(failures, fmt.Errorf("judge: the %s seat is held by the judge's own model and was skipped", role)) + continue + } + answer, err := ask(ctx, seatSystem(role), seatUser(role, model, rec)) + if err != nil { + failures = append(failures, fmt.Errorf("judge: the %s seat: %w", role, err)) + continue + } + score, reason, err := readAnswer(answer) + if err != nil { + failures = append(failures, fmt.Errorf("judge: the %s seat: %w", role, err)) + continue + } + scores = append(scores, Score{Role: role, Model: model, Score: score, Reason: reason}) + } + if len(failures) > 0 { + return scores, errors.Join(failures...) + } + return scores, nil +} + +// Pick returns the cheapest catalog row that can judge a crew: its published +// coding index reaches floor, it carries "tools", no crew seat holds it, and +// its vendor differs from the worker's. Rows the provider published no price +// for are out of the running, and so is a crew with nothing left to draw from — +// in which case ok is false. Ties in cost break to the lower id, so the answer +// is a property of the catalog and never of its order. +func Pick(models []catalog.Model, crew map[Role]string, floor float64) (judgeID string, ok bool) { + workerVendor := "" + if worker, held := crew[RoleWorker]; held && worker != "" { + workerVendor = vendor(worker) + } + held := make(map[string]bool, len(crew)) + for _, model := range crew { + if model != "" { + held[model] = true + } + } + bestCost := math.Inf(1) + for _, model := range models { + if model.PriceUnknown || model.CodingIndex < floor || !hasTools(model.Parameters) || held[model.ID] { + continue + } + if workerVendor != "" && vendor(model.ID) == workerVendor { + continue + } + cost := model.PromptPrice + model.CompletionPrice + if judgeID == "" || cost < bestCost || (cost == bestCost && model.ID < judgeID) { + judgeID, bestCost = model.ID, cost + } + } + return judgeID, judgeID != "" +} + +// seatJobs is what each seat is responsible for, and therefore what its work is +// scored against. One paragraph per seat, read as the opening of the system +// prompt. +var seatJobs = map[Role]string{ + RoleWorker: "You are scoring the WORKER seat of a finished run. The worker seat carried the work: judge whether the delivered work did what the brief asked, coherently, and with the checks the record says were run.", + RoleHigh: "You are scoring the HIGH seat of a finished run. The high seat checked the work: judge whether the check read the work itself rather than the worker's claim about it, and whether it caught what a careful reader of the work would catch.", + RoleMastermind: "You are scoring the MASTERMIND seat of a finished run. The mastermind seat planned the work: judge whether the work was cut into steps the brief supports.", +} + +// answerShape is the one object every seat's answer is read as, and nothing +// else. It is stated in both prompts because a model that answers with prose +// around the object is still read, but a model that answers with no object at +// all fails its seat. +const answerShape = `Answer with ONE JSON object and nothing else: {"score": <0-100 integer>, "reason": ""}` + +// seatSystem is the system prompt for one seat: the seat's job followed by the +// shape of the one answer asked for. +func seatSystem(role Role) string { + return seatJobs[role] + " " + answerShape +} + +// seatUser is the user prompt for one seat: the record's fields, the brief and +// the deliverable clipped to recordClip runes each, and a line naming which +// model held the seat. The model id is named and nothing is said about its +// maker — the seat is scored on the work, not on the name behind it. +func seatUser(role Role, model string, rec Record) string { + var b strings.Builder + fmt.Fprintf(&b, "Seat: %s\n", role) + fmt.Fprintf(&b, "Model holding this seat: %s\n\n", model) + fmt.Fprintf(&b, "Brief:\n%s\n\n", clip(rec.Brief)) + fmt.Fprintf(&b, "Deliverable:\n%s\n\n", clip(rec.Deliverable)) + fmt.Fprintf(&b, "Report:\n%s\n\n", rec.Report) + fmt.Fprintf(&b, "Claim:\n%s\n\n", rec.Claim) + fmt.Fprintf(&b, "Ending:\n%s\n\n", rec.Ending) + fmt.Fprintf(&b, "Checks:\n%s\n\n", strings.Join(rec.Checks, "\n")) + fmt.Fprintf(&b, "Files:\n%s\n\n", strings.Join(rec.Files, "\n")) + fmt.Fprintf(&b, "Changed: %d\n", rec.Changed) + fmt.Fprintf(&b, "Cost (USD): %g\n", rec.CostUSD) + return b.String() +} + +// clip returns s cut to at most recordClip runes, so neither the brief nor the +// deliverable can push the prompt past the budget whatever the run was. +func clip(s string) string { + if len(s) <= recordClip { + return s + } + runes := []rune(s) + if len(runes) <= recordClip { + return s + } + return string(runes[:recordClip]) +} + +// readAnswer reads the first JSON object in the answer into a score and a +// reason. A score that is missing, not a number, not an integer, or outside 0 +// to 100 is an error, and so is an answer carrying no JSON object at all. The +// reason may be absent and reads as empty. +func readAnswer(answer string) (float64, string, error) { + raw, found := firstObject(answer) + if !found { + return 0, "", errors.New("the answer carries no JSON object") + } + var got struct { + Score *json.Number `json:"score"` + Reason string `json:"reason"` + } + if err := json.Unmarshal(raw, &got); err != nil { + return 0, "", fmt.Errorf("the answer is not a JSON object: %w", err) + } + if got.Score == nil { + return 0, "", errors.New("the answer carries no score") + } + score, err := strconv.ParseFloat(got.Score.String(), 64) + if err != nil || math.IsNaN(score) || math.IsInf(score, 0) { + return 0, "", fmt.Errorf("the score %q is not a number", got.Score.String()) + } + if score != math.Trunc(score) { + return 0, "", fmt.Errorf("the score %v is not an integer", score) + } + if score < 0 || score > 100 { + return 0, "", fmt.Errorf("the score %v is outside 0-100", score) + } + return score, got.Reason, nil +} + +// firstObject reads the first JSON value that opens at the answer's first "{", +// so prose before the object costs nothing and prose after it is ignored. An +// answer with no "{" and an object that does not parse both answer with false. +func firstObject(answer string) (json.RawMessage, bool) { + open := strings.IndexByte(answer, '{') + if open < 0 { + return nil, false + } + var raw json.RawMessage + if err := json.NewDecoder(strings.NewReader(answer[open:])).Decode(&raw); err != nil { + return nil, false + } + return raw, true +} + +// hasTools reports whether the provider published "tools" among the fields a +// model accepts. +func hasTools(parameters []string) bool { + for _, parameter := range parameters { + if strings.EqualFold(strings.TrimSpace(parameter), "tools") { + return true + } + } + return false +} + +// vendor is the part of a model id before the first slash — "z-ai" in +// "z-ai/glm-5.3" — and the whole id when there is no slash. +func vendor(id string) string { + if slash := strings.IndexByte(id, '/'); slash > 0 { + return id[:slash] + } + return id +} From 7b13e1dfdbbb0a00ad51f4c5e7f7ebe733f1635a Mon Sep 17 00:00:00 2001 From: santoshkumarradha Date: Thu, 17 Sep 2026 09:20:57 -0400 Subject: [PATCH 089/173] manual, docs: the pool section names the seed index the binary carries --- .../unreleased/1103-pool-seed-index.md | 25 +++++++++++++++++++ .../manual/chat/running-from-the-terminal.md | 4 ++- 2 files changed, 28 insertions(+), 1 deletion(-) create mode 100644 docs/changes/unreleased/1103-pool-seed-index.md diff --git a/docs/changes/unreleased/1103-pool-seed-index.md b/docs/changes/unreleased/1103-pool-seed-index.md new file mode 100644 index 0000000000..e850c386fd --- /dev/null +++ b/docs/changes/unreleased/1103-pool-seed-index.md @@ -0,0 +1,25 @@ +--- +kind: added +title: The Model Pool's index is seated at start-up from a seed in the binary +pr: 1103 +surface: [chat, engine] +invalidates: + - "`config.AutoIndex` was a seam nothing in the binary set, so a tier row that says `auto` picked on the catalog's published figures alone. `cmd/codeaf` seats it at start-up from the seed the binary carries or a fresher cached document, so the pool's measured quality reaches the picker." + - "The binary carried no index of its own, and `codeaf pool show` said only `no index cached yet`. `internal/pool/index` embeds a seed document, and show names it — `no index cached yet · built-in seed of `." +--- + +`internal/pool/index` embeds a seed measurement document (`Seed`, `SeedIndex`) +so a machine that has never fetched an index still has numbers to pick a seat +against. `cmd/codeaf` reads the cached `doc.json` beside that seed through +`index.Fallback` once at start-up — the newer `generated` day wins — and hands +the result to `config.AutoIndex` where `config.AutoModels` is already seated, on +every door that resolves a seat (`sharedCatalog`, and the three chat doors). +The read is start-up work and never on a run's path. + +A background goroutine, started where the index is seated, refreshes the cache +through `pull` only when the mode allows reading and the build carries a public +key; with no key compiled in today it starts nothing. A fresh document lands in +the puller's own cache and is read at the next start — the running process keeps +the index it was seated with. `codeaf pool show` names the seed when no cache is +present, and its `--json` shape carries the index it read with a `source` field +naming `cache` or `seed`. diff --git a/internal/manual/chat/running-from-the-terminal.md b/internal/manual/chat/running-from-the-terminal.md index 2bd41e767c..1a6f92d3bd 100644 --- a/internal/manual/chat/running-from-the-terminal.md +++ b/internal/manual/chat/running-from-the-terminal.md @@ -528,7 +528,9 @@ codeaf pool [show|status|verify] [--json] [--key key] `show` — also what bare `codeaf pool` prints — is the reading form: the mode and the addresses in force with the word saying where each came from (`default`, `setting`, `env` or `ci`), then what index is cached and how old -it is, or `no index cached yet`. `status` adds what is waiting to be sent +it is, or `no index cached yet · built-in seed of `. The binary +carries a seed index of our own measured runs, read until a fresher signed +one is cached. `status` adds what is waiting to be sent and whether the mode allows sending and reading. `--json` prints the same answer as one object. Neither form touches the network. From 0cf5186574f5d902c87dcdf62a0f5a657671d1e1 Mon Sep 17 00:00:00 2001 From: santoshkumarradha Date: Thu, 17 Sep 2026 09:21:12 -0400 Subject: [PATCH 090/173] pool/judge: the seat-by-seat reading and the judge pick are pinned by tests Co-Authored-By: codeaf --- internal/pool/judge/judge_test.go | 208 ++++++++++++++++++++++++++++++ 1 file changed, 208 insertions(+) create mode 100644 internal/pool/judge/judge_test.go diff --git a/internal/pool/judge/judge_test.go b/internal/pool/judge/judge_test.go new file mode 100644 index 0000000000..6b8aafab87 --- /dev/null +++ b/internal/pool/judge/judge_test.go @@ -0,0 +1,208 @@ +package judge + +import ( + "context" + "errors" + "strings" + "testing" + + "github.com/Agent-Field/codeaf/internal/catalog" +) + +// fakeAsk records every prompt it is handed and answers with the replies it was +// given, in order, or with an error when one was set. +type fakeAsk struct { + systems []string + users []string + replies []string + err error +} + +func (f *fakeAsk) ask(_ context.Context, system, user string) (string, error) { + f.systems = append(f.systems, system) + f.users = append(f.users, user) + if f.err != nil { + return "", f.err + } + if len(f.replies) == 0 { + return `{"score": 1, "reason": "no reply was set"}`, nil + } + reply := f.replies[0] + f.replies = f.replies[1:] + return reply, nil +} + +func TestAScoreComesBackForEverySeatInRoleOrder(t *testing.T) { + rec := Record{ + Brief: "do the thing", + Seats: map[Role]string{RoleMastermind: "v/mind", RoleHigh: "v/high", RoleWorker: "v/worker"}, + } + ask := &fakeAsk{replies: []string{ + `{"score": 80, "reason": "the worker did what the brief asked"}`, + `{"score": 70, "reason": "the check read the work"}`, + `{"score": 60, "reason": "the plan held the brief"}`, + }} + scores, err := Judge(context.Background(), ask.ask, rec) + if err != nil { + t.Fatalf("judge: %v", err) + } + want := []struct { + role Role + model string + score float64 + }{ + {RoleWorker, "v/worker", 80}, + {RoleHigh, "v/high", 70}, + {RoleMastermind, "v/mind", 60}, + } + if len(scores) != len(want) { + t.Fatalf("got %d scores, want %d", len(scores), len(want)) + } + for i, w := range want { + if scores[i].Role != w.role || scores[i].Model != w.model || scores[i].Score != w.score { + t.Errorf("score %d is %+v, want %s %s %v", i, scores[i], w.role, w.model, w.score) + } + if scores[i].Reason == "" { + t.Errorf("score %d carries no reason", i) + } + } +} + +func TestASeatTheJudgeHoldsIsSkippedAndNamed(t *testing.T) { + rec := Record{ + Seats: map[Role]string{RoleWorker: "v/worker", RoleHigh: "v/judge", RoleMastermind: "v/mind"}, + Judge: "v/judge", + } + ask := &fakeAsk{replies: []string{ + `{"score": 80, "reason": "ok"}`, + `{"score": 60, "reason": "ok"}`, + }} + scores, err := Judge(context.Background(), ask.ask, rec) + if err == nil { + t.Fatal("expected an error naming the skipped seat") + } + if !strings.Contains(err.Error(), "high") { + t.Errorf("the error %q does not name the high seat", err) + } + if len(scores) != 2 { + t.Fatalf("got %d scores, want 2", len(scores)) + } + for _, score := range scores { + if score.Role == RoleHigh { + t.Error("the high seat was scored though the judge holds it") + } + } + if len(ask.systems) != 2 { + t.Errorf("the judge was asked %d questions, want 2", len(ask.systems)) + } +} + +func TestAMalformedAnswerFailsOnlyItsSeat(t *testing.T) { + rec := Record{Seats: map[Role]string{RoleWorker: "v/worker", RoleHigh: "v/high"}} + ask := &fakeAsk{replies: []string{"I would rather not say.", `{"score": 70, "reason": "fine"}`}} + scores, err := Judge(context.Background(), ask.ask, rec) + if err == nil { + t.Fatal("expected an error naming the worker seat") + } + if !strings.Contains(err.Error(), "worker") { + t.Errorf("the error %q does not name the worker seat", err) + } + if len(scores) != 1 || scores[0].Role != RoleHigh { + t.Fatalf("got %+v, want only the high seat scored", scores) + } +} + +func TestAScoreOutsideTheScaleFailsItsSeat(t *testing.T) { + rec := Record{Seats: map[Role]string{RoleWorker: "v/worker", RoleHigh: "v/high"}} + ask := &fakeAsk{replies: []string{`{"score": 140, "reason": "off the scale"}`, `{"score": 70, "reason": "fine"}`}} + scores, err := Judge(context.Background(), ask.ask, rec) + if err == nil { + t.Fatal("expected an error for the out-of-range score") + } + if len(scores) != 1 || scores[0].Role != RoleHigh { + t.Fatalf("got %+v, want only the high seat scored", scores) + } +} + +func TestPromptsStayInsideTheClipBudget(t *testing.T) { + full := strings.Repeat("b", recordClip) + rec := Record{ + Brief: full + "BRIEF-TAIL", + Deliverable: full + "DELIVERABLE-TAIL", + Report: strings.Repeat("r", recordClip+500) + "REPORT-TAIL", + Seats: map[Role]string{RoleWorker: "v/worker"}, + } + ask := &fakeAsk{replies: []string{`{"score": 50, "reason": "ok"}`}} + if _, err := Judge(context.Background(), ask.ask, rec); err != nil { + t.Fatalf("judge: %v", err) + } + user := ask.users[0] + if strings.Contains(user, "BRIEF-TAIL") { + t.Error("the brief was carried past the clip budget") + } + if strings.Contains(user, "DELIVERABLE-TAIL") { + t.Error("the deliverable was carried past the clip budget") + } + if !strings.Contains(user, "REPORT-TAIL") { + t.Error("the report was clipped though it travels whole") + } +} + +func TestARecordWithNoSeatsScoresNothingAndFailsNothing(t *testing.T) { + ask := &fakeAsk{err: errors.New("the judge should not have been asked")} + scores, err := Judge(context.Background(), ask.ask, Record{Brief: "nothing ran"}) + if err != nil { + t.Fatalf("judge: %v", err) + } + if len(scores) != 0 { + t.Errorf("got %d scores, want none", len(scores)) + } + if len(ask.systems) != 0 { + t.Errorf("the judge was asked %d questions with no seats to read", len(ask.systems)) + } +} + +func TestPickExcludesCrewModelsAndTheWorkerVendor(t *testing.T) { + models := []catalog.Model{ + {ID: "worker-vendor/cheap", PromptPrice: 0.1, CompletionPrice: 0.1, CodingIndex: 90, Parameters: []string{"tools"}}, + {ID: "other/crew-held", PromptPrice: 0.2, CompletionPrice: 0.2, CodingIndex: 90, Parameters: []string{"tools"}}, + {ID: "judge-vendor/fine", PromptPrice: 1, CompletionPrice: 1, CodingIndex: 80, Parameters: []string{"tools"}}, + } + crew := map[Role]string{RoleWorker: "worker-vendor/w", RoleHigh: "other/crew-held"} + got, ok := Pick(models, crew, DefaultFloor) + if !ok { + t.Fatal("Pick found no judge though one qualifies") + } + if got != "judge-vendor/fine" { + t.Errorf("the judge is %q, want judge-vendor/fine", got) + } +} + +func TestPickHonoursTheFloorAndSkipsUnpricedRows(t *testing.T) { + models := []catalog.Model{ + {ID: "a/below-floor", PromptPrice: 0.01, CompletionPrice: 0.01, CodingIndex: 40, Parameters: []string{"tools"}}, + {ID: "b/unpriced", PromptPrice: 0, CompletionPrice: 0, PriceUnknown: true, CodingIndex: 99, Parameters: []string{"tools"}}, + {ID: "c/no-tools", PromptPrice: 0.02, CompletionPrice: 0.02, CodingIndex: 90}, + {ID: "d/qualifies", PromptPrice: 0.5, CompletionPrice: 0.5, CodingIndex: 80, Parameters: []string{"tools"}}, + } + got, ok := Pick(models, nil, DefaultFloor) + if !ok || got != "d/qualifies" { + t.Errorf("the judge is %q, ok %v, want d/qualifies", got, ok) + } + if _, ok := Pick(nil, nil, DefaultFloor); ok { + t.Error("Pick found a judge in an empty catalog") + } +} + +func TestPickIsDeterministicUnderShuffledInput(t *testing.T) { + first := catalog.Model{ID: "a/one", PromptPrice: 1, CompletionPrice: 1, CodingIndex: 80, Parameters: []string{"tools"}} + second := catalog.Model{ID: "b/two", PromptPrice: 1, CompletionPrice: 1, CodingIndex: 80, Parameters: []string{"tools"}} + forward, _ := Pick([]catalog.Model{first, second}, nil, DefaultFloor) + backward, _ := Pick([]catalog.Model{second, first}, nil, DefaultFloor) + if forward != backward { + t.Errorf("Pick depends on order: %q then %q", forward, backward) + } + if forward != "a/one" { + t.Errorf("a tie picked %q, want the lower id a/one", forward) + } +} From 50a68680cb216b551f6c804a63d9804b39362733 Mon Sep 17 00:00:00 2001 From: santoshkumarradha Date: Thu, 17 Sep 2026 09:21:19 -0400 Subject: [PATCH 091/173] docs: the change entry for the seat-by-seat judge package Co-Authored-By: codeaf --- docs/changes/unreleased/1103-pool-judge.md | 24 ++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 docs/changes/unreleased/1103-pool-judge.md diff --git a/docs/changes/unreleased/1103-pool-judge.md b/docs/changes/unreleased/1103-pool-judge.md new file mode 100644 index 0000000000..9f84aadc78 --- /dev/null +++ b/docs/changes/unreleased/1103-pool-judge.md @@ -0,0 +1,24 @@ +--- +kind: added +title: A pure package that scores a finished run seat by seat, by a judge outside the crew +pr: 1103 +surface: [engine] +invalidates: [] +--- + +Standard library and `internal/catalog` only, no disk, no network, and nothing +on a surface wired to it yet. + +- `internal/pool/judge` reads a run's record — its brief and deliverable, its + report, claim and ending, the files it wrote and the checks it ran, and which + model held each of the worker, high and mastermind seats — and asks one + question per seat, in role order, through a caller-supplied `Ask`. Each answer + is read as one JSON object carrying a 0-100 score and a one-sentence reason, + on the same scale crewpick reads seat quality on and a pool records its + `role_quality` metric in. A seat whose model is the judge's own is skipped; a + seat whose answer cannot be read fails alone, and the scores obtained come + back beside an error naming it. The caller owns the model, the transport and + the bill, and bills the judge's calls to its own seat. +- `judge.Pick` chooses that judge from a catalog: the cheapest row whose + published coding index reaches `judge.DefaultFloor`, that carries tools, that + no crew seat holds and whose vendor is not the worker's, ties broken by id. From a35b1c24e7c6b1182bdd4f1f761e00cf3a777bac Mon Sep 17 00:00:00 2001 From: santoshkumarradha Date: Thu, 17 Sep 2026 09:31:24 -0400 Subject: [PATCH 092/173] docs: the judge change entry carries the pull request it ships in Co-Authored-By: Claude Fable 5.1 --- .../unreleased/{1103-pool-judge.md => 1093-pool-judge.md} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename docs/changes/unreleased/{1103-pool-judge.md => 1093-pool-judge.md} (99%) diff --git a/docs/changes/unreleased/1103-pool-judge.md b/docs/changes/unreleased/1093-pool-judge.md similarity index 99% rename from docs/changes/unreleased/1103-pool-judge.md rename to docs/changes/unreleased/1093-pool-judge.md index 9f84aadc78..c9efbea63c 100644 --- a/docs/changes/unreleased/1103-pool-judge.md +++ b/docs/changes/unreleased/1093-pool-judge.md @@ -1,7 +1,7 @@ --- kind: added title: A pure package that scores a finished run seat by seat, by a judge outside the crew -pr: 1103 +pr: 1093 surface: [engine] invalidates: [] --- From 83b3920ece69b6df9c5a81b1d995f1a08bb5a636 Mon Sep 17 00:00:00 2001 From: santoshkumarradha Date: Thu, 17 Sep 2026 09:35:41 -0400 Subject: [PATCH 093/173] cmd/codeaf: the seed index change entry carries its pull request and the wiring comment keeps the file's voice Co-Authored-By: Claude Fable 5.1 --- cmd/codeaf/subharness.go | 2 +- .../{1103-pool-seed-index.md => 1093-pool-seed-index.md} | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename docs/changes/unreleased/{1103-pool-seed-index.md => 1093-pool-seed-index.md} (99%) diff --git a/cmd/codeaf/subharness.go b/cmd/codeaf/subharness.go index 3ac6bcebb4..c09b3b45ae 100644 --- a/cmd/codeaf/subharness.go +++ b/cmd/codeaf/subharness.go @@ -380,7 +380,7 @@ var sharedCatalog = func() func(config.Config) *catalog.Catalog { // the same non-blocking read, never a fetch, and set once at start-up so every // headless door resolves the word against the list it already holds. config.AutoModels = resolved.ModelsNow - // AND THE POOL'S INDEX BESIDE IT, in the same one-time manner: a tier + // and the pool's index beside it, in the same one-time manner: a tier // row that says auto is answered from the index this process was seated // with, the seed when no cache is fresher, and the one fetch it makes // runs in the background and never blocks this read. diff --git a/docs/changes/unreleased/1103-pool-seed-index.md b/docs/changes/unreleased/1093-pool-seed-index.md similarity index 99% rename from docs/changes/unreleased/1103-pool-seed-index.md rename to docs/changes/unreleased/1093-pool-seed-index.md index e850c386fd..bc17fb9839 100644 --- a/docs/changes/unreleased/1103-pool-seed-index.md +++ b/docs/changes/unreleased/1093-pool-seed-index.md @@ -1,7 +1,7 @@ --- kind: added title: The Model Pool's index is seated at start-up from a seed in the binary -pr: 1103 +pr: 1093 surface: [chat, engine] invalidates: - "`config.AutoIndex` was a seam nothing in the binary set, so a tier row that says `auto` picked on the catalog's published figures alone. `cmd/codeaf` seats it at start-up from the seed the binary carries or a fresher cached document, so the pool's measured quality reaches the picker." From 2d1f02b8d47d80b32b4f431a852aa3b60a244912 Mon Sep 17 00:00:00 2001 From: santoshkumarradha Date: Thu, 17 Sep 2026 09:49:40 -0400 Subject: [PATCH 094/173] docs: the Model Pool paper, its figures and the script and data that draw them Co-Authored-By: Claude Fable 5.1 --- docs/design/model-pool/Makefile | 17 + .../design/model-pool/data/measured-crews.csv | 57 ++ docs/design/model-pool/data/seed-cells.csv | 15 + docs/design/model-pool/fig-cells.pdf | Bin 0 -> 19197 bytes docs/design/model-pool/fig-front.pdf | Bin 0 -> 24502 bytes docs/design/model-pool/fig-regret.pdf | Bin 0 -> 19785 bytes docs/design/model-pool/fig-semibandit.pdf | Bin 0 -> 17562 bytes docs/design/model-pool/fig-shrink.pdf | Bin 0 -> 21230 bytes docs/design/model-pool/figures.py | 285 ++++++++++ docs/design/model-pool/learn.pdf | Bin 0 -> 340120 bytes docs/design/model-pool/learn.tex | 512 ++++++++++++++++++ docs/design/model-pool/tab-regret.tex | 7 + docs/design/model-pool/tab-semibandit.tex | 6 + 13 files changed, 899 insertions(+) create mode 100644 docs/design/model-pool/Makefile create mode 100644 docs/design/model-pool/data/measured-crews.csv create mode 100644 docs/design/model-pool/data/seed-cells.csv create mode 100644 docs/design/model-pool/fig-cells.pdf create mode 100644 docs/design/model-pool/fig-front.pdf create mode 100644 docs/design/model-pool/fig-regret.pdf create mode 100644 docs/design/model-pool/fig-semibandit.pdf create mode 100644 docs/design/model-pool/fig-shrink.pdf create mode 100644 docs/design/model-pool/figures.py create mode 100644 docs/design/model-pool/learn.pdf create mode 100644 docs/design/model-pool/learn.tex create mode 100644 docs/design/model-pool/tab-regret.tex create mode 100644 docs/design/model-pool/tab-semibandit.tex diff --git a/docs/design/model-pool/Makefile b/docs/design/model-pool/Makefile new file mode 100644 index 0000000000..b9815e3d3b --- /dev/null +++ b/docs/design/model-pool/Makefile @@ -0,0 +1,17 @@ +# Builds learn.pdf: the figures from data/*.csv, then the paper. +# make figures + pdf +# make clean remove build products but keep the pdf + +all: learn.pdf + +fig-front.pdf fig-cells.pdf fig-shrink.pdf fig-regret.pdf tab-regret.tex: figures.py data/measured-crews.csv data/seed-cells.csv + python3 figures.py + +learn.pdf: learn.tex fig-front.pdf fig-cells.pdf fig-shrink.pdf fig-regret.pdf tab-regret.tex + latexmk -pdf -interaction=nonstopmode -halt-on-error learn.tex + +clean: + latexmk -c learn.tex + rm -f learn.bbl + +.PHONY: all clean diff --git a/docs/design/model-pool/data/measured-crews.csv b/docs/design/model-pool/data/measured-crews.csv new file mode 100644 index 0000000000..75e0f98120 --- /dev/null +++ b/docs/design/model-pool/data/measured-crews.csv @@ -0,0 +1,57 @@ +door,worker,planner,checker,delivered,clean,usd,minutes,runs +task,ds-v4.1-flash,-,qwen3.8-max,0.75,0.69,0.13,0,5 +task,glm-5.3-flash,-,qwen3.8-max,0.95,0.78,0.27,4,17 +task,ds-v4.1-flash,-,kimi-k3,0.75,0.69,0.24,2,7 +task,glm-5.3,-,qwen3.8-max,0.91,0.85,0.53,1,11 +task,ds-v4.1-flash,-,fable-5.1,0.75,0.87,0.45,0,8 +task,glm-5.3-flash,-,kimi-k3,0.95,0.79,0.50,11,21 +do,ds-v4.1-flash,opus-5,-,0.67,0.9,0.46,1,12 +do,ds-v4.1-flash,glm-5.3-flash,-,0.67,0.84,0.36,0,16 +task,ds-v4.1-flash,-,opus-5,0.75,0.64,0.40,0,7 +do,ds-v4.1-flash,qwen3.8-max,-,0.67,0.56,0.22,0,11 +do,ds-v4.1-flash,glm-5.3,-,0.67,0.51,0.23,0,9 +task,muse-spark-1.3,-,qwen3.8-max,0.5,0.95,0.41,0,16 +task,glm-5.3-flash,-,fable-5.1,0.95,0.95,0.95,1,25 +task,glm-5.3,-,kimi-k3,0.91,0.85,0.98,6,14 +task,glm-5.3-flash,-,opus-5,0.95,0.74,0.84,4,20 +do,muse-spark-1.3,qwen3.8-max,-,0.75,0.82,0.72,0,32 +do,ds-v4.1-flash,kimi-k3,-,0.67,0.39,0.18,0,11 +do,muse-spark-1.3,glm-5.3,-,0.75,0.77,0.75,0,26 +do,glm-5.3,qwen3.8-max,-,0.92,0.72,0.93,2,22 +task,muse-spark-1.3,-,kimi-k3,0.5,0.95,0.76,0,19 +do,muse-spark-1.3,kimi-k3,-,0.75,0.65,0.59,0,32 +do,glm-5.3-flash,opus-5,-,0.64,0.95,0.96,1,37 +do,glm-5.3-flash,qwen3.8-max,-,0.64,0.66,0.47,2,34 +do,glm-5.3-flash,glm-5.3,-,0.64,0.6,0.49,1,27 +do,glm-5.3,kimi-k3,-,0.92,0.55,0.76,1,23 +do,ds-v4.1-flash,fable-5.1,-,0.67,0.71,0.89,0,13 +do,muse-spark-1.3,glm-5.3-flash,-,0.75,0.95,1.16,0,48 +do,glm-5.3,glm-5.3-flash,-,0.92,0.95,1.50,1,33 +do,muse-spark-1.3,opus-5,-,0.75,0.95,1.45,0,35 +task,glm-5.3,-,fable-5.1,0.91,0.95,1.87,1,16 +do,glm-5.3-flash,kimi-k3,-,0.64,0.49,0.39,3,34 +do,glm-5.3,opus-5,-,0.92,0.95,1.88,2,24 +task,glm-5.3,-,opus-5,0.91,0.8,1.65,1,13 +task,muse-spark-1.3,-,opus-5,0.5,0.9,1.28,0,19 +task,qwen3.8-max,-,kimi-k3,0.83,0.69,1.31,2,22 +task,muse-spark-1.3,-,fable-5.1,0.5,0.95,1.44,0,24 +do,qwen3.8-max,glm-5.3-flash,-,0.86,0.84,2.01,1,55 +do,glm-5.3-flash,fable-5.1,-,0.64,0.81,1.87,5,41 +task,qwen3.8-max,-,fable-5.1,0.83,0.87,2.50,2,27 +do,qwen3.8-max,glm-5.3,-,0.86,0.5,1.30,1,29 +task,ds-v4-pro-0813,-,qwen3.8-max,0.5,0.35,0.47,0,7 +do,qwen3.8-max,opus-5,-,0.86,0.9,2.53,2,40 +do,muse-spark-1.3,fable-5.1,-,0.75,0.95,2.84,2,39 +do,ds-v4-pro-0813,glm-5.3-flash,-,0.67,0.5,1.32,0,20 +do,ds-v4-pro-0813,opus-5,-,0.67,0.56,1.66,0,14 +do,qwen3.8-max,kimi-k3,-,0.86,0.39,1.02,0,37 +task,qwen3.8-max,-,opus-5,0.83,0.64,2.22,0,22 +task,ds-v4-pro-0813,-,fable-5.1,0.5,0.54,1.64,0,10 +task,ds-v4-pro-0813,-,kimi-k3,0.5,0.36,0.86,0,8 +do,glm-5.3,fable-5.1,-,0.92,0.87,3.68,4,27 +do,ds-v4-pro-0813,qwen3.8-max,-,0.67,0.23,0.82,0,13 +task,ds-v4-pro-0813,-,opus-5,0.5,0.3,1.46,0,8 +do,ds-v4-pro-0813,glm-5.3,-,0.67,0.17,0.85,0,11 +do,qwen3.8-max,fable-5.1,-,0.86,0.71,4.94,1,44 +do,ds-v4-pro-0813,fable-5.1,-,0.67,0.38,3.24,1,16 +do,ds-v4-pro-0813,kimi-k3,-,0.67,0.06,0.67,0,13 diff --git a/docs/design/model-pool/data/seed-cells.csv b/docs/design/model-pool/data/seed-cells.csv new file mode 100644 index 0000000000..a1cb43b02d --- /dev/null +++ b/docs/design/model-pool/data/seed-cells.csv @@ -0,0 +1,15 @@ +role,model,mean,sd,n +high,anthropic/claude-fable-5.1,83.3,14.1,4 +high,anthropic/claude-opus-5,80.0,16.3,3 +high,moonshotai/kimi-k3,80.0,12.1,8 +high,qwen/qwen3.8-max-0902,75.0,14.4,6 +mastermind,anthropic/claude-fable-5.1,77.5,6.5,38 +mastermind,anthropic/claude-opus-5,80.0,8.7,18 +mastermind,moonshotai/kimi-k3,57.1,10.6,19 +mastermind,qwen/qwen3.8-max-0902,50.0,16.7,6 +mastermind,z-ai/glm-5.3,62.5,16.1,6 +worker,deepseek/deepseek-v4.1-flash,66.7,13.1,10 +worker,meta/muse-spark-1.3,87.5,11.0,6 +worker,qwen/qwen3.8-max-0902,72.7,12.9,9 +worker,z-ai/glm-5.3,75.0,7.5,30 +worker,z-ai/glm-5.3-flash,65.6,5.9,62 diff --git a/docs/design/model-pool/fig-cells.pdf b/docs/design/model-pool/fig-cells.pdf new file mode 100644 index 0000000000000000000000000000000000000000..aadd12cde4a5b3899298e4bd757235594e61aea2 GIT binary patch literal 19197 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 literal 0 HcmV?d00001 diff --git a/docs/design/model-pool/fig-front.pdf b/docs/design/model-pool/fig-front.pdf new file mode 100644 index 0000000000000000000000000000000000000000..d9fada050038214021062d587598ae321d2cd4a4 GIT binary patch literal 24502 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 literal 0 HcmV?d00001 diff --git a/docs/design/model-pool/fig-regret.pdf b/docs/design/model-pool/fig-regret.pdf new file mode 100644 index 0000000000000000000000000000000000000000..010cef0e1bf39d036c4c2cbe018de9d323691c7d GIT binary patch literal 19785 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 literal 0 HcmV?d00001 diff --git a/docs/design/model-pool/fig-semibandit.pdf b/docs/design/model-pool/fig-semibandit.pdf new file mode 100644 index 0000000000000000000000000000000000000000..199fc415071e6e803d4fcd4a8f5ea38313f3a5b9 GIT binary patch literal 17562 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 Wfur-KS%mWPLLnFo4C0Cs82=v~Y{k?7 literal 0 HcmV?d00001 diff --git a/docs/design/model-pool/figures.py b/docs/design/model-pool/figures.py new file mode 100644 index 0000000000..a02e1f3d13 --- /dev/null +++ b/docs/design/model-pool/figures.py @@ -0,0 +1,285 @@ +#!/usr/bin/env python3 +"""Draws the figures in learn.tex from data/*.csv. Standard library, numpy and matplotlib only. + + python3 figures.py # writes fig-*.pdf and tab-*.tex beside this file + +The simulations are the ones the paper describes. Ground truth is the table of measured crews fitted +from our own scored runs (data/measured-crews.csv). A learner picks a crew per run, sees the run's +outcome and its dollars, and pays the all-in cost E = c + lambda*t + H*(1 - clean). Regret is E minus +the oracle crew's, averaged over replicates. Every replicate draws its own noisy catalog prior. +""" +import csv, os, random +import numpy as np +import matplotlib +matplotlib.use("Agg") +import matplotlib.pyplot as plt + +HERE = os.path.dirname(os.path.abspath(__file__)) +H, LAM, K = 10.0, 0.01, 30 +REPS = 200 +plt.rcParams.update({"font.size": 9, "font.family": "serif", "axes.spines.top": False, "axes.spines.right": False}) + + +def crews(): + rows = list(csv.DictReader(open(os.path.join(HERE, "data", "measured-crews.csv")))) + for r in rows: + for k in ("delivered", "clean", "usd", "minutes"): + r[k] = float(r[k]) + r["runs"] = int(r["runs"]) + r["name"] = r["worker"] + ("+" + r["checker"] if r["checker"] != "-" else "") + ("/" + r["planner"] if r["planner"] != "-" else "") + r["run"] = r["usd"] + LAM * r["minutes"] + r["p"] = r["delivered"] * r["clean"] + return rows + + +def allin(r, h=H): + return r["run"] + h * (1 - r["p"]) + + +def fig_front(): + rows = crews() + fig, ax = plt.subplots(figsize=(5.4, 3.5)) + for door, mark, label in (("task", "o", "chat door (worker + checker)"), ("do", "s", "headless door (worker + planner)")): + pts = [r for r in rows if r["door"] == door] + ax.scatter([r["run"] for r in pts], [100 * r["p"] for r in pts], s=18, marker=mark, alpha=.75, label=label) + front, best = [], -1 + for r in sorted(rows, key=lambda r: r["run"]): + if r["p"] > best: + front.append(r) + best = r["p"] + ax.plot([r["run"] for r in front], [100 * r["p"] for r in front], "k-", lw=.8, label="pareto front") + for r in front: + ax.annotate(r["name"], (r["run"], 100 * r["p"]), fontsize=6, xytext=(4, -9), textcoords="offset points") + xs = np.linspace(0.05, 4, 100) + for h, e, x in ((1, 1.2, 1.0), (10, 3.0, 2.4), (50, 12.0, 3.2)): + ax.plot(xs, 100 * (1 - (e - xs) / h), color="#999", lw=.6, ls="--") + ax.text(x, 100 * (1 - (e - x) / h) + 1.5, f"E = \\${e:.0f} at H = \\${h}", fontsize=6, color="#777") + ax.set_xlim(0, 4) + ax.set_ylim(0, 100) + ax.set_xlabel("cost of one run, $ (dollars + λ · minutes)") + ax.set_ylabel("P(delivered and clean), %") + ax.legend(fontsize=7, loc="lower left") + fig.tight_layout() + fig.savefig(os.path.join(HERE, "fig-front.pdf")) + + +def fig_cells(): + rows = list(csv.DictReader(open(os.path.join(HERE, "data", "seed-cells.csv")))) + fig, axes = plt.subplots(1, 3, figsize=(6.2, 2.6)) + for ax, role in zip(axes, ("worker", "high", "mastermind")): + cells = sorted([r for r in rows if r["role"] == role], key=lambda r: -float(r["mean"])) + names = [r["model"].split("/")[-1] for r in cells] + ax.barh(names, [float(r["mean"]) for r in cells], xerr=[float(r["sd"]) for r in cells], color="#8a8fb0", ecolor="#333", capsize=2) + for i, r in enumerate(cells): + ax.text(2, i, f"n={r['n']}", va="center", fontsize=6, color="white") + ax.set_title(role, fontsize=9) + ax.set_xlim(0, 100) + ax.invert_yaxis() + ax.tick_params(axis="y", labelsize=7) + axes[0].set_xlabel("role_quality, posterior mean ± sd") + fig.tight_layout() + fig.savefig(os.path.join(HERE, "fig-cells.pdf")) + + +def fig_shrink(): + q_cat, s_bar, sigma = 80.0, 65.0, 15.0 + n = np.arange(0, 121) + mean = (K * q_cat + n * s_bar) / (K + n) + sd = sigma / np.sqrt(K + n) + fig, ax = plt.subplots(figsize=(5.2, 2.8)) + ax.fill_between(n, mean - 1.96 * sd, mean + 1.96 * sd, color="#8a8fb0", alpha=.3, label="95% posterior band") + ax.plot(n, mean, "k-", label="posterior mean") + ax.axhline(q_cat, ls=":", color="#555", lw=.8) + ax.axhline(s_bar, ls="--", color="#555", lw=.8) + ax.text(2, q_cat + 0.8, "catalog prior", fontsize=7) + ax.text(100, s_bar + 0.8, "measured mean", fontsize=7) + ax.axvline(K, color="#a55", lw=.8) + ax.text(K + 1.5, 62, "n = k = 30: half weight each", fontsize=7, color="#a55") + ax.set_xlabel("effective observations n") + ax.set_ylabel("role quality") + ax.set_xlim(0, 120) + ax.set_ylim(60, 84) + ax.legend(fontsize=7, loc="upper right") + fig.tight_layout() + fig.savefig(os.path.join(HERE, "fig-shrink.pdf")) + + +def simulate(strategy, N, seed, rows, oracle): + """Crew-level learners: every crew is one arm with one Beta posterior on P(clean).""" + rnd = random.Random(seed) + truth = {r["name"]: r["p"] for r in rows} + prior = {n: min(.99, max(.01, p + rnd.gauss(0, .15))) for n, p in truth.items()} + a = {n: 1 + K * prior[n] for n in truth} + b = {n: 1 + K * (1 - prior[n]) for n in truth} + fixed = {r["name"]: r["run"] for r in rows} + total, explored = 0.0, 0 + for t in range(N): + mean_best = min(truth, key=lambda n: fixed[n] + H * (1 - a[n] / (a[n] + b[n]))) + if strategy == "table": + pick = rows[1]["name"] + elif strategy == "catalog": + pick = min(truth, key=lambda n: fixed[n] + H * (1 - prior[n])) + elif strategy == "greedy": + pick = mean_best if rnd.random() >= 0.1 else rnd.choice(list(truth)) + else: + pick = min(truth, key=lambda n: fixed[n] + H * (1 - rnd.betavariate(a[n], b[n]))) + explored += pick != mean_best + clean = rnd.random() < truth[pick] + total += fixed[pick] + H * (1 - clean) - oracle + if strategy in ("greedy", "thompson"): + a[pick] += clean + b[pick] += not clean + return total / N, explored / N + + +def fig_regret(): + rows = [r for r in crews() if r["door"] == "task"][:12] + oracle = min(allin(r) for r in rows) + Ns = [10, 20, 40, 70, 100, 150, 200, 300, 400] + fig, (ax, ax2) = plt.subplots(1, 2, figsize=(6.2, 2.8)) + table = {} + for s, style in (("table", "k:"), ("catalog", "k--"), ("greedy", "C1-"), ("thompson", "C0-")): + reg, exp = [], [] + for N in Ns: + rs = [simulate(s, N, seed, rows, oracle) for seed in range(REPS)] + reg.append(np.mean([r[0] for r in rs])) + exp.append(np.mean([r[1] for r in rs])) + table[s] = reg + ax.plot(Ns, reg, style, label={"table": "fixed table row", "catalog": "catalog only", "greedy": "ε-greedy, ε = 0.1", "thompson": "Thompson"}[s]) + if s in ("greedy", "thompson"): + ax2.plot(Ns, [100 * e for e in exp], style, label=s) + ax.set_xlabel("runs N") + ax.set_ylabel("regret vs oracle, $ per task") + ax.set_ylim(0, None) + ax.legend(fontsize=7) + ax2.set_xlabel("runs N") + ax2.set_ylabel("runs off the posterior-mean pick, %") + ax2.set_ylim(0, None) + ax2.legend(fontsize=7) + fig.tight_layout() + fig.savefig(os.path.join(HERE, "fig-regret.pdf")) + with open(os.path.join(HERE, "tab-regret.tex"), "w") as f: + f.write("\\begin{tabular}{l" + "r" * len(Ns) + "}\\toprule\n$N$ & " + " & ".join(str(n) for n in Ns) + "\\\\\\midrule\n") + for s in ("table", "catalog", "greedy", "thompson"): + f.write(s + " & " + " & ".join(f"{v:.2f}" for v in table[s]) + "\\\\\n") + f.write("\\bottomrule\\end{tabular}\n") + + +VENDOR = {"ds-v4.1-flash": "deepseek", "ds-v4-pro-0813": "deepseek", "ds-v4-flash-0731": "deepseek", "glm-5.3-flash": "z-ai", + "glm-5.3": "z-ai", "muse-spark-1.3": "meta", "qwen3.8-max": "qwen", "kimi-k3": "moonshot", "fable-5.1": "anthropic", + "opus-5": "anthropic"} + + +def role_truth(): + """A three-role ground truth read off the measured rows under the linear rule of the paper: a role quality + q_r(m) in [0, 100] per (role, model) and a dollar surcharge per (role, model); a crew's P(clean) is the mean of + its role qualities over 100 and its cost the sum of its surcharges. Workers and checkers come from the chat + rows (delivered is the worker's, clean the checker's), planners from the headless rows.""" + rows = crews() + chat = [r for r in rows if r["door"] == "task"] + do = [r for r in rows if r["door"] == "do"] + mean = lambda xs: float(np.mean(xs)) + q = {"worker": {}, "checker": {}, "planner": {}} + cost = {"worker": {}, "checker": {}, "planner": {}} + for w in sorted({r["worker"] for r in chat}): + mine = [r for r in chat if r["worker"] == w] + q["worker"][w] = 100 * mean([r["delivered"] for r in mine]) + cost["worker"][w] = mean([r["run"] for r in mine]) + base = mean([r["run"] for r in chat]) + for c in sorted({r["checker"] for r in chat}): + mine = [r for r in chat if r["checker"] == c] + q["checker"][c] = 100 * mean([r["clean"] for r in mine]) + cost["checker"][c] = mean([r["run"] for r in mine]) - base + dobase = mean([r["run"] for r in do]) + for p in sorted({r["planner"] for r in do}): + mine = [r for r in do if r["planner"] == p] + q["planner"][p] = 100 * mean([r["clean"] for r in mine]) + cost["planner"][p] = max(0.0, mean([r["run"] for r in mine]) - dobase) + grid = {} + for w in q["worker"]: + for c in q["checker"]: + if VENDOR.get(w) == VENDOR.get(c): + continue + for p in q["planner"]: + k = (w, c, p) + grid[k] = dict(p=(q["worker"][w] + q["checker"][c] + q["planner"][p]) / 300, + run=cost["worker"][w] + cost["checker"][c] + cost["planner"][p]) + return q, grid + + +def simulate_roles(level, N, seed, q, grid, sigma=15.0): + """Thompson sampling under three kinds of feedback on the same truth: one Beta arm per crew fed the run's + clean/defect bit; one Gaussian arm per crew fed the mean of the judge's role scores; one Gaussian posterior per + (role, model) fed each role's own judge score (semi-bandit). Judge scores are the role quality plus N(0, sigma).""" + rnd = random.Random(seed) + arms = list(grid) + roles = ("worker", "checker", "planner") + oracle = min(grid[a]["run"] + H * (1 - grid[a]["p"]) for a in arms) + noise = lambda x: min(99.0, max(1.0, x + rnd.gauss(0, 15))) + prior_q = {r: {m: noise(q[r][m]) for m in q[r]} for r in roles} + prior_p = {a: sum(prior_q[r][a[i]] for i, r in enumerate(roles)) / 300 for a in arms} + if level == "crew-bit": + a_ = {a: 1 + K * prior_p[a] for a in arms} + b_ = {a: 1 + K * (1 - prior_p[a]) for a in arms} + elif level == "crew-score": + n_ = {a: float(K) for a in arms} + s_ = {a: K * 100 * prior_p[a] for a in arms} + else: + n_ = {(r, m): float(K) for r in roles for m in q[r]} + s_ = {(r, m): K * prior_q[r][m] for r in roles for m in q[r]} + total = 0.0 + for t in range(N): + if level == "crew-bit": + pick = min(arms, key=lambda a: grid[a]["run"] + H * (1 - rnd.betavariate(a_[a], b_[a]))) + elif level == "crew-score": + pick = min(arms, key=lambda a: grid[a]["run"] + H * (1 - rnd.gauss(s_[a] / n_[a], sigma / n_[a] ** .5) / 100)) + else: + draw = {k: rnd.gauss(s_[k] / n_[k], sigma / n_[k] ** .5) for k in n_} + pick = min(arms, key=lambda a: grid[a]["run"] + H * (1 - sum(draw[(r, a[i])] for i, r in enumerate(roles)) / 300)) + g = grid[pick] + clean = rnd.random() < g["p"] + total += g["run"] + H * (1 - clean) - oracle + scores = [q[r][pick[i]] + rnd.gauss(0, sigma) for i, r in enumerate(roles)] + if level == "crew-bit": + a_[pick] += clean; b_[pick] += not clean + elif level == "crew-score": + n_[pick] += 1; s_[pick] += float(np.mean(scores)) + else: + for i, r in enumerate(roles): + n_[(r, pick[i])] += 1; s_[(r, pick[i])] += scores[i] + return total / N + + +def fig_semibandit(): + q, grid = role_truth() + Ns = [10, 20, 40, 70, 100, 150, 200, 300, 400] + fig, ax = plt.subplots(figsize=(4.8, 2.9)) + table = {} + for level, style, label in (("crew-bit", "C0:", "one arm per crew, clean/defect bit"), + ("crew-score", "C0--", "one arm per crew, mean judge score"), + ("role", "C2-", "one posterior per (role, model), role scores")): + reg = [np.mean([simulate_roles(level, N, seed, q, grid) for seed in range(REPS)]) for N in Ns] + table[level] = reg + ax.plot(Ns, reg, style, label=label) + d = sum(len(q[r]) for r in q) + ax.set_title(f"{len(grid)} crews from {len(q['worker'])} workers × {len(q['checker'])} checkers × {len(q['planner'])} planners; {d} unknowns", fontsize=8) + ax.set_xlabel("runs N") + ax.set_ylabel("regret vs oracle, $ per task") + ax.set_ylim(0, None) + ax.legend(fontsize=7) + fig.tight_layout() + fig.savefig(os.path.join(HERE, "fig-semibandit.pdf")) + with open(os.path.join(HERE, "tab-semibandit.tex"), "w") as f: + f.write("\\begin{tabular}{l" + "r" * len(Ns) + "}\\toprule\n$N$ & " + " & ".join(str(n) for n in Ns) + "\\\\\\midrule\n") + for level, name in (("crew-bit", "crew, clean bit"), ("crew-score", "crew, mean score"), ("role", "role, semi-bandit")): + f.write(name + " & " + " & ".join(f"{v:.2f}" for v in table[level]) + "\\\\\n") + f.write("\\bottomrule\\end{tabular}\n") + + +if __name__ == "__main__": + fig_front() + fig_cells() + fig_shrink() + fig_regret() + fig_semibandit() + print("figures written") diff --git a/docs/design/model-pool/learn.pdf b/docs/design/model-pool/learn.pdf new file mode 100644 index 0000000000000000000000000000000000000000..f0c88de96c32f309617afe52a5bdc4c0c8af6294 GIT binary patch literal 340120 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

5Ecz4LylmwLW}Sn9#^{I!|Z-%^A}f@(L7+s>Ke z^o}I})WxqjNG}yoyk#M%w_KSAY|q?->edvPAJSu6CX-*nR6d8uoxD&7#lZ4MTa*DQT%Tma(O|w`=-#0|L(O;1jjA zW*)i4BbQv*5Imp`5MGYgTvrSU`{e9tNI0Z!83U?vVAV`zOr^v4zOS%mk2!x#9Efi1 z+lCFGn*U?3cPcyA@Nm+s2Dod(LvKEy5LUdlC45FvlW$?1PsVrGiwzXAC~f}1+nIe> zBwCP=3=os6af;)rGft(`QZve4v_eIp@MV%AhgBy-9yZetpKby73;|3kn;t=_dTTiy z^taTh3i=vYmwjZf*Mp>ZJq`97D#}32=l@JxF@241`M>v#{=18-yrHGC^H*D1IRZw8 z|GI4B=;Tbm!SPkj`~NSlb1?pEVr%$^jU6^S(npP6zun3%oi}z5nqUBl?L5#Q9mSJ% z-5B4mfeO_lDJmA(MUUrKm~xLMju5U6#g5oa~!c(DKbcMquwj5^-qri0R6&&hu zjD8492Z~02j@vd^5v^0_pU<*>O$51KN@$H{#7vxahi1SazA-hs%=}^J&@31CEr@ z^pPDu%}LUG2`c#N`Zjj{Hf!z=hw(BuKRT@KTS;ahdQ%(buWmq!Ciw|3^_tW4DM_ZP z-r64bK29vSy%~N;;9U;6-NxB+GXfgD2F*7}$`EA+{l1);(~Nt94m~mn)!geudAl)- zAHxP(BRjHYSK?<&-X4oIlR_`-_c1SO-FyrteMJVn;4_w|c?X1Mr)ztw6u30>ShULF zeO{Vr4fKl}&zt* z4vcip7(Uqu=Jepz)PF)g^dBH_SC{|F^0cwV#c1_n!z)6P3^4DfM{mSf14*`#UsaJf zc7wDf7eT(e9}+|S*+D2szax8`gVpJ*zqAuhQqpQ5dq;=Rh0B{32-HG$vSQza3Cd-Lp7ut-1hpQoIA-8SaFCq zAe6*5vG^=-Yzc_{JtcsTwlG367zqh~b=F}!M=aYuh_Ec2AH>=DcwBG(&KeTIje1wc z#}{Y`g!V3I*DRM#7z2#`C9j~Xusqa;cVje4^f|2zG?`x4(JCM)Kon$SrC|zcOxvs*K37H8t4vO!DM?cuxu9x;5qT0Gi4dKrxNb$8E5IkF!QEzT zAD<(O`gTK$dHm!hj)a}4DmJo}*9nw@NC5QDTp_$0T<=MXJn06RdxcBICP z*V!64b!W`T#QCdwT@cqei>}(p%)+E)u&3hQ7LLGTX>jP2NP$feBB>{sKoAV~Xx{#m zD{=GjSRER7wG~%+1tthbOmyVS_KGGpBNs+)+5JSyn;M9_;E#$Bo5DCrIrb z`23bs2{o383;{qV9#VSINc75)kr?Z4-|(lo$v`EB3LCTD736U|_jld~4SQNHt+xa!^d;#>VjLVhJLbSA9^9( zr@Pz2JO%R0Kl;Er0=OWWkVog`m}cd-t!!8jr}(8%Al#yE=P8DQD9@Ct)4qQdmjnL~ z>Fjn{=)#`t$b-}I;lC55yi>vKgRz2bf`_75WeFYjrNarxh*|Z=muU+*GqdQ_aNHum z^GHC3wY?-@ky2xNui=`yRccO8LrvLw;7+}7p1^UpRuV+_yB;!VM6G{-Czjmhjl;QK zy@xr)Y+R_{~qGfR^nD)5&lr4 z+?1&8e8!L0?acV-_|K#J`hOm|(G0fH7oDn|;TIi~nriPOqwGt`OpE(-6K2~J>6_ul z16Kz&yV;VkePEo@gk${uCL8oh`k!3AZ?ZF~+iDXy}TR79OsZcxL@7VcsH!nl8&7Fy$+mO2QH=6hbQMG-krR+HyOE2 z-Bk}Ma>OOhIdi2nm-&ofI+?`!wo{eG0?by1pY;R<3_LZu)sDJq5w7p8nfSk&)($P{ zuGH##EF2twz4Sx5`0JFbnrSQ2Vwq!N>?OX~HD*;lS1JA(ZMT$9l`V|T4?c5$$ycmA zC2zLmV_{yBk(bkA%>;4);?5>)R>L`*5s9^|f?E>7rSz_lTP)9T>@VN0WOk`BwX8uq zgKt)$UaEk;7xXdCso;+qDa$Pt|3hh4meJSw+-s(6o!4JkW7Vk~Z|CRxy6~~B6S%l? zSZ0@CxK28(Rf?w(`P^7swlr-0a{O}{`{JN+i}pcL^BV0z$03i{Eo>$yMqX_g%k%Nw zQS}b8oC?CWqu)sya08dVhjL;N>FEgStpzt%mYm*<+zb}}apU-n9IOZ5k17*s?VR<- z=%U1x01j~ZwhK}Zu}oi4a(;;_7C_C%P;Bwiua)JL>R!%EQXLbAxVK8icxiNO5wQxe zu;T-AN9cE-@{9zxJyc!IyRQ9JtAEd^+ic+NZ^aY!t{QjKdW0+l?oyKhyGzb2uml<%$$;Xi} zQVox*y!=3@WQ5}r>dqP{e0!&!1Xf%^aJzq5xtdbHzM2}}&w%5bYfLo`zA%P5KAq8| zouJ2kra~T{LZ4L7FPIW7pR7LCI@Q<{CNq-2C8g!f-6may%DJv8(}vZXN#x|Uwn}8CBMUJXU)VFs zH}3o32PJRc7GcFE*}}HElXvKtmSh40GM|J~`OIO|H6Ug_=ZA)U`{e}rUOo;0HWare)1gCnm`L~r zHUo~)WVuayP(THNx_ zLF=7e&UQ)PXtG=>x8^Oz2@VLr6|vrr&g5IwcRu?t;v$Qtm@v043$nyLG)XJ~hnN7a2m%GGny<8m;uxDJBVgh5bD)5=o?_nQ%QH|Mp=A zj`-dV50}5G4c1U3sYy|wP$F5OQKXV#OciGfPp}C;{cf>Z$FeMjhdJ4L!t(d@hq1?- zTX*KR?S=i^W2O}c>u<ZIbVp+E*t-aZ0 z^X!0S2Z!}n^b6`v<^FIATX|R5wo@GL_cnvNK|HC05_~qMs9N}rg4RuoO@XTI%hy2% z5Hx|Oa8gRWa+zfN1kehYr^K5TqYZe0gC9jq+vk>plTXqMSlAv+NmRa@?A0R%Asc5O z>|1+N%Nxnj5o4<|&UNVzvSCSD@#aGog>1K#mDPLR@{MH}jjd;vSyuNFEYw_9tR`et zx+G^IKgH;qj21^N%gB6$SLikE-)BHfW|9iD%MbWZV8|5mr*1|1_xI9XgeY02%)k|e?M}xunY@8nCd-Q`3^3hSV=jczMH_n9snkJEP=wFYd8I?CMdV`I` z-r}3=dRw~U@I4#OI;y^i`5`HyU=7%{F2SirN4yG;ASiYWWhr8Y>LQh+C~sUIt%DYJ zg9wQ0`J|t>UZR}v(7l4jw--}^@wcJrI{h<@Y3iOICBDFu?=;HjQ^G%Y+t#iaKkW|jJOvfW+LaQL8Puhm&o^XKhL?1#f;ggv zL#A$%A}S}72lvl>f%Cg9xuOp_Bsl{{-xX{mdvS>DHq-{g;gc)cWK~5h&4~6x#zqAJ zM$~|^?tcrI+X93kObVyP6P#-!46So5i>q{^5l?wR?lW2~K2x#kOZP<`R;litKYPYanvh7=)K`Z?o5~O#X3Z8x@GPQ5z+#f1#j2NZ>T|YY=hg^Z z1)`fU&KD;#_&d#F(155fVLUufEF5IUQhj!Iy(Na^j$!hHg1^kVG6%{lf-PSSPYQS~TW&^VZ_#;xLkJ50#^?_86JFby(4Xh2snyDCNx zpIEdk?VVG!jWM%+WP>e&+i#WkRUkZ-zy3M(pF=#BHJDo|tscg@u9CfYHe|>}XK1o3 zG$=#2HVF9t*n7*Uy80|%7k76LB)H4Q-Q8V-ySux)2NK*Z1lQp1lHkGJ-GZIGb-Ko_ z>e}~S`n0{LE1!@t7_eC|*Ke->oX;cITVRmjFD35Y`$H#AM zOXn5P^oMvU#yNVnmZTn3$`Vvv*E3|qij2fyZoTe_PTi>aSw%%b8pt?|kbOb)WjDqu zAbE}kNsXF*diEqn7Dyn%Izk*~xGZdU(M(7VWY8RY{=J*`7?1}$_MdTVCS|pM40T=n zT1S!Dd_RHCB<-fEEy$$Vad4LV^JkgU61rzu{V>LnL0oQZ)$~vb{;cPq*g*ctLE|nn z0_o9PKc(S@Aho|Gp;2F3bnLQ%$DXHcM^VV-Sr1ppEZ z;6sjJSCO0?A@45K;1v z%0I_p8ei1`osIShSoPT?m+3grw@bX(X;EPoe=BWf(Sj$vp&E-@kB_+3!r}Va zjqPLBA}rCuScc5r2G_y+PJXQ;XCr(BGX@l+&5dV3>i({1(pT?#dP%bAV%oh~4Wh%` zZm?4_LCF?_!hROsx9=n^sd zj)jg>@qxSjO30OC_xJVO6FsD#sq#fDP0cFojC!Q$pmrpE zbfl@4VP_zOozd-kxeGq!iE9LCn4~FWyzr4%voJ3LDMBo5&Ar$YowT6Kw0ompqaU8n zwOM1B(3>}UHF4D{S51W}v#KiG6Jkm~%5AdaB!J8}pAW{1E#)M|8Dy|Rs@fef$cVY> zu)AflHgDwv74sI2{!M>=554W2biS(U#$xg}4Y_`C2!wRTr}E2z{!l}f(0WY456s)v)aFHFL+Z}uijI&n~;(<#CB^7^TG zTPEIIBL+JSUw<3C1agsZx#7V3!}Zd4TN7Uop_g(qGqKY@Nq6r$(6alOPhvr+wh*ev z?MSD(Q2(!|JiZLCu(`|#uHdfK$5aP-i z{JY)QbVcqD_vMsn8e815(W*2zD>Pdgnr9@|JkHGO6_YGZ$q^W?P@6ebl8}t@Uf4bd z3V=uKnbtSiT`3@zY?^A<)7ub!U~Ni_kdmGV32E1a@!|c#gDsgV)iDDt^fD}bD%2JD zkKG^>lQi9lh*SglWC;*-lUbOeEe~vY1C&`~qV`cfGMP1yjw(^4Cn!8(OtD zh~wUaR31v5kq{`od!n?u1R;BSY&|M$_v|FJ>O(ZtNr#QC)bJr<5X z7MlLfpa%f3y~?1+!tuwF&0h?9EF6C)dCF|#tiVn23FYs8kY z8CACC=*!w$P}0u4!40p3J`QAXJ&3btf7)nT@*1(5=%a{%r1Py)r$`?=w}h|qBld3v zQco1w56|LhXM5)S+8?&Zj+f^qem$}AVbT4PZObpC<`Z}7W%B*@?zyDGGxYRfXzkn5 z&_fBYuh;yeSpPw|yy^Y%HNb~m*Dpk!Z>`x3o#;DX602x)1-b!Bji$0(-#y|LG0CUe z@viOU@|5Ed#IH<3wFzD*5Sp2S*yOnB-m~NBXd8yA#DeJfnOTNKZ;Eg4=1ORS6ul?; z7UDm%@KIfGsc+Ilx;(ggda~%g%#`KXwwF8p`1(<=4Bn~qIoCEz-mf`bPbQ_eSH-7_ zp0*#dX3m}MqakyS`M$`~O=7W)E}L{e!sN#D>ZOX{vZ1p5#_v+XGj@9YxuaIwGwNhx8GOc zz=qzCUC@*#ZXtFQp}!7M!+y^M*hd)y6M}%ArEdQ9d8|nQhAwR%e`q0p@3hh^NrH4` zF4bB-4;`dU^)01#-0qt$0Fg1CKtx7Nu1QPxKspp7Yt|Y|OhlZ)M`Y$cOmMP0U#qmr zGlwG1H3?_}^0=s*XzkrIMNj_fLYjtOGj#DWfpACAR3hqU!<_AVl|ON7hWrJ_MRk9u z_x9A95P8heDBliGJRv~6zlH}dXE%Cb)2$yo8;?r~mnI`SMZkDd>g~l@AN0f3R#}(l zjN38ZpCBbSDQt z=C>h&E0NG@KNm<>vtbg%?Xen$N#Ig|8kzERb(uzTVS%HbId|z$Lb)nfNenQ$I#M1X zGGY?Ay}fWb)1gJe-n0rgLElM7E9$+&o1eF4eY>WzxlWeWnVI{Up?QUARfggFrYJm@ z%1o>!7VO7+6s@thMVK)?2>4@YH$pHgO=>JZ63C3ynrCP)+|zQ70a0@A%Ffu_J~I7M zJk4A5-AlUd*=&}SwLD;TKQ?fNh!R1$~bK~Ly zVLkPEX>T#I1s}ogv~?LU=PQTH6w?>bFI#KONKuhXWogFBZh0)>0u=JgC%v-JpRS{a zmm}I_ND5Cx!t&s|xmKg_G_dgxBU$q{qTimU$+uu&5(LR!mq;j58G4%~&S%laYF^B{ zE^>EoEk`i*b5X7KJI%ds=JfF$zvpe|SNrDFz<=M(bxE1UkuC_ebGD1&I4_h|TE2i_ z7Q|vq=u11H@8Kazwucg4wj?QVW@|!Z6#^y?^Ii&PP?4LhDn2bhyUSUOh~wKWkv4-Z zN_NB4Bv(x*HE47#G0O;a1Z|S$K$h8$L+6qDh~lM1h&eLTIi?jAols8`1VgcCv548I2{ zZ)(zKo8z;&&wMjj0%KN|;Lt@LF|fS5v8s7C9W|QD2%_z{0=%jIeq-FfnKM zaM^ca#;bD-rZdyIvK||p`qjkEl;2T)^*nLN$o`5^$jbFAc^lJ`0l z6I^~8St0BolACikUv;&U9YkJ|JvXp_vq^bc(EEwV|A;Kj##wO6_RAPqj$n!L60xX& zZ2Y?wzFHaqR&Db|Fto+@4We(i_=Jr>VB6$u%mjoUjz0wVKEPi=5xC;kw}HYQlZDUY zqfUR=L*(dnEK^cb_#z7@!RNSxVg4r0K3;qSO@N-Hkw?v) z9eG+Gmz{~f%K#5tJk^JD!i#`d4U8VApo6d)lTWTf;|JLacHR+E@({FP7-sG4x<$c7 zsR{Yn=671N6Ef^zA{gj#C-bnTbqGh<^4{2`$n4G-6s694$Y8Dqxc$}=!UVdtIgL0O zX`4RzoPJn<95klR#y$^&S2+Hz)LBB8+Yhxz2wPM)s08-D0`Ht}toHC;TKHRUp*`$5 zdR4*UX0sO-?qssA#8K1LKS9WA=7o>8lJ}j8nCra{pFb*!8Q;F-UIfV$`vSdU1aB%X zoQKFUU^_e?h)bJ2+2`Jq9!PK#+%PsnZ~!%;!^tktIhwBkGjk?4BFti}BR9kD=!v>= zHI`HT{z`bvyEjMP(*(Nog_vPU%S%UY4!bNynHDD8l8}A<34>#!%OX^kea->&!8#$< zp)>p3pmGz8Ug6zrpyhg&z~@acIeH}<@bGXhhx^H1EllTL$aAg!K0@>8p+dg4&_;hj zAKT9GG&DvN2s`hQC;=qjO`QWUaSfeZ4xSO3DWF^2Q}prZcUvw z3@ydxWLss@BrR3k;=vcivXqnoCX!$RE!}2h(|37QYU(El2Avw*s6ra}4&MpJQ}#Fr zsAEqf(Ql2zagOW0Uz+<6a6*jG!EeP97-OSEZB|R7OxeM2o#8BAv6{fz!fk;?+S12M z5892)cY|DL3_xL_vDnj;j!z29dM@kqXOToMSqmf33$kMmLDqdvlr~RKWMq{0o-C(J zo&&GK7?~(LP?TB19jPHAZ1_itkDXs9pK}mvoFtsuiCv8 zfem8qf#>O4nN3zL1v(wiY#X?3Pi~4Rd2Rs72vf6O5pHPhY5$SE!aY7$F(>`ONmmTr z4>PIKM>4MPwvLk4IAuB&AH;QhsIyuvvw?iaNrUEMavpZ@l8hvEv#@7hJ+-tO?DKXA z1;{0Qi=kMgqDhVLm<|C~jZHU|Fr*G6sFkP7O&5;O$4cIUQ_kLo)En4J+}W< z1BuXy1Cax;AduFji)zv!_%QeZLi#ksCXI#uJ6n;7<;glsqJ8`N!>IMb9(>`8)s~Wd zd&F?4RI+(o%YxF-T3nOU0jWrU_cHi!tHt*3vZVAUK7 z5b6RG<15gevg2cj{){kClU~L8py@qLPJmhiSPNm{9`1pO5SWO+R;b!tH z0K=NG&>s`1AA$YQK==oouejQ>_jQg>xwA<3K-$Cx;)O-Sy~SDspXJF>OubQf{H`};#-AJ(Y7y{Uc>gUB`UIIO z>n*UATh70oT>NWQc@6gPw^HXnQmMlU1itWZ^mP1I>iorCW@2mnM}cJlCO>2Tt<-s) zQil^r)?i|K1q6^amI+Ci2}amWZXI5v8wHG0zWfVP$L)LRGFQ+M4POp;xc4|$!RY~{ zS;ma}Lx91lUkf`6fjlxMd-NCyk&55R}N#eXJ)40XFS&CYCoDfof1&fpSN= zYPBH4^-5?OS3-$5PTw4yzD!+o3{dg_rpL()z2q!(7UxY}K~Q^fO68!E2^EXj?MG)`56&gbW7Bb_6@=*g;c(#cJf8oplyO zc7e3P9#XV*GKrTR{Mo6Aa&6#JJ4V$ZmdI~ARUQ!+Sh1DM17i*eTCG0`>M_0+5p?$m zYT%jzghd1ZDjWQ*F8a5qivWzkAZ@QfTK!fR{mIlt07f7!1^X*HivLt~5r7d$&%pc| zRm*Q(&p+k?z~3!q{Y!iEr`PoW7=d&HzjZx-VqFh_5eR(lm70tHak?G=BarTa^;LA( zhCjqFOlpkGH7blX07XVNMs*3MEoRW_Q4GgsVnb6nPW{w`YS4I-z+V4B6Z=9?5STh7 zUSR+CK>C3HxyB#>P~Y9Fn52IuV-SD|2>bWH)ffc$#h&|r7JL8Y{_)!w^q*-A0x$uI zWv?uy|Kp570488*@HLNT|7ol=045+$u>U+(nqQI>AQj}dmF7RfN&{d5lDd9dY5oUV zX#h+>A|(^^E71)pM|L^y}0Dubc zetTg4EFKsDGmrrOTe$MyFI)jI1L;Fv11SIh7p?%9f#vOg2H^?-nAGmKaOKY?Tmdix z3HZ#fdbsh=CR_n918Fw?xr8eKW*|MmpHjF2UWrx4i%nNHU^$J#)IXsOj|q0e0h%*SPstvp6wngMlr$q|rs$J6 zg1Y4gP%W?V0m=2aY3Vzti@}HI;Jqjpo}8t~;RrDfoje1RTCJt4 zY50W5!Zb=$*V}IM06l&|b;@vO$w3b!m;;lV{T9glnxz7;0Evaa1u_PI(MbO>Yz6?T zZexChw2n7%#0LDAK;~2%5?@2$Q6lfa0cC6L%5Uu59E`q^spvV2u&gXD8dW3clF4E+ zLz`=4Hd%*gcVkAphIq0$LDF2I(})T-eo_X?JFUAGT6Gq6a9N(aS=nBvP8FdcTzaM$ zOSyuy-Q?Tt-A5KrDwjlr_ZvxyMQVf;H5|ALEK4gfS44|4*faaud&Q0)7xq1l3pBF& z%do~)kfRU}_j-6fQ90p??d`-?y6W2x!?HVX2k}uWMe|ANL3PEe{>1#uMA_qasSSzX zFA?t4YCNsqKw-W>8)9nxw(bYNOZ(@A2ZM5$AOqOcmAgLL#@M(T?U7=koz(>mzA;m+ zPXpx9b;J4*iukD&W5mT3G&RNgO^Rx`BN`x%mdDX$=+Y4fRD@He@8aq$k{7m;zbe)@ zZG4UJ)MMGY9M+qgxPF3`Lh%6NvjngJ$+XOjuP9Bj3DRgi#DBO}yTv>GIOMr9>8Bs; zXh!Rt(c8MhiBS{2OM+)J7Dvk=+tN#s1g(rQN7*VUxW_|cl$%ZtIkP6^9LaIz(iXH| zTQ0Iox*qM3rf#==3||Ml>jJnZdq%Tr_M-baXTOm8GjdPna$Oa(R;58B?4NL*D$;S9v zo>HOus?!n&lJCU(+{S!ULprL2^r+ZbC#X|%jcS46lH-aKRyEuEgTz$hQ*&vQ`{A^-h4>!#raY?&@`;{)p>c-2A zEz>6SY-EP}$EV{X*=bTGO@y)^v$f;Fux7{x6&i2o4f__#tgJk_IvSlBPAf(M!CM>vTN?zsJ}3? z*})8nL?w2_@WM>XXLg^Z$?^6Q_7of2eVZj(S@F1@78bpwOqMqOW=jTyAyFbuH!XxvwFP6Mn{QG@JJ?3lb6LLH z==g&?@O&~z=uLpg?a3n^qpyp!$>5i&K|>c105czpwLr|699@PzG-i<)8OA^yvON+& zHb$ZSshc?Sjra)(7!hGN&1f>w?;BGvve`R{G*i?xeLn z7QCsp-ThX#StZBH&e)fw!cqZp^Aw?))0jpiMRb-yL)21IiouNaCnn4w{$b69S@s61 z&{U9mQPGQ)@2(Eu@hDswiD1tRQ$`=a&l1HdKkgI?aHb2(_2W4?Qlu$ZF^}YlP#9x= zYzH|s8av6fbMN?W-KvQdH-Wpd|W(ZOctjQ*{bq`_IUD!`&NH3=Zsl znbXzx(-AldQn_C@G2}}m3O(XQv`LG^6fRM*Rnt*9oxZcLPRW1+}JM8zG)=l>L>tZ3WT=}`ROM0m~Odr zN`@RtIg_x}K8ujduhUQzzZ6*~>eVpL4f}Px2{B3*2VbCUZ8e1lUmlS>6Hg`g5&CqV8|L1}+~tg9Rx( z#qg8$_HL>J=ze^kLOP6F(O#X{i=5Fr&U>q$QC)s)4cbaH8JMa^@bJkuI_$?Gr#vwk zifd(l^-{(nmO0_O<$z6i7+Hs=;#G-K6VKB+_$-ocVXXfV6dCr zu{g~G&wbYt5r(ng7B#Q&^1(HsJ>D%eoGux8@?T%@chefh)-4x_7#i*VUJg+WFTqI3t0WQ}&czZwC2S~wtHN5))NU|Z<9!e-OyPCY0F zJ#cXiaAwUB(n%l8zmeiTT90fwO5(fn7;+B(8J?I+&s3ImLYurgA3XzKIQn7-A+*yK_h-QK$Wn^}fXxCR}8F=5UM zS;T5;FTb(KpVwa2TMDVlIGJPqP5V<~XYoQ81b2~SoPi9m-Z1;`7|k7d;_8JJ4`86w zg-`?EAw!6h@6&QQVr3qNfZ+$AV&)DlEGz=}w)_H%lOB!INEDZev>wps8bn#j+)XP= z8fWyeH?PEk5X=B(O^T9L+QAcmBK(O8E&@hEWH7Ua<>kS&XXomG@DzxNr1{ruklJ3Pof_C8PlLUNoel-V1JJf}MuVRy8IS{8=ZoJ3;@ z4bD*(y~h?DI8W-2-4j11;XMi56U%e^8i)?3 z)|oIYvuZqPv5&$YWAVDg-f5_wHV2_oU*0tPJJL+0n z&v|BckWBkRw$Ir$VrkHi7}(0WH~AZdth(>8XJYx~j%c%#;$Zqw>y$24cGRM$0~82{ zQ+o_WSIdcy5LBNANu)nu+w%QvVd5XY92pt;Fs<`7erG|@+86o*InK)cOLimsBnmcaopcUd(r6l)+Ot>+*p=1zN`SR8)R43SRgVXaQ+g;O{d z%5SlF{MsyZBqxdh6B6M`3uLu5`;7dfoF`*5)}&brK>P) z6iJS5F)B=gCKki{YN8bEGHN)spabio62jpK6O$%O+;o=s!^+K&k`WC*LH<+7=YNilfXdtvr@rlB(OSpdxQK$j%=Cb4IB!Dai??ogAwxfeMB0y zCOfNgCEIV_n{&*oG|{b+9K(4Ci+Vp6p!npvYoJ;MGOHC!AXY;W zK;+9x(-!kkT?n{~(t24lk@SXYxbBVOSnCSY%4l-v>g|n%o7~$=(>DdveFBgn8w!@X z*ux52K8+aAuYVzHGAGwzM)3RqHyk-KimtIqF2NcIdCk`pDs6)rJ!PKdn9k$flRGX~ z8a%RAWZUPNBvy!Ug%> z)4cekM`fvX-*&6x99pyHwUEScPC3YCJz`b0RfsrffwiDzJ|hU=)CLfS3szOeBPGUEHP`q0f8>yuWSzDhP;(-q z0m(6=z*Vrd92!qoyj^n``Ns8raiycZj;`Lpw=!`65hTw_3+K28i&KSTSa(f#-}tk0 z68=JT9mZ*tQeC!A(83DE+-;rbp(f{h;kN!wPtO7vS==}M&h8kK4#HQ$cPX{aB-uQk zW?7+32*~(4Imbs)P;Xqm_d_xj&>Z%rA)&pG8zKKT9y+9lt@CL@jn8U7c@$|~xi)iJ zu210(32eXa?Sp;8KvuA%pL+=bm1P@CsIgH=hKeQ8L~Y`tg1FMem4#wd*TjArhm}XQ z$nZ$)ysh+v6z7FSc3_W)*;sUtADigjbwparZDFOGl;srLu0;Y9TLvNbwsieDVC)p%X#C99j3Q!$JBk(`uN-}(%!)%=t_A{zVAqx&{k z;(Z$^|6z z=M?)3of}+;?X}nvW#D}EB~ac65Eb{>6NZ}ny^k3+7WdssTeyXZ1dpJ{qV99W}WNTX_IUJ14V&jxJWSdze`z3)D=DGRtq}Qa^iU*R^LqS-H@syH| z3e|K&u8GH_OOg#q))ZvIOijWb#?H}MxwxTS$C9@4Auq*);#4tpHd0qJUIg)s9}Sqz$;IFs_g!l9sXhHb;6DyX0lRj<4u<~Z ztOfupuszq;OxpeiA^Zn|5CE*ej+*{$3*5g!2!9Jg0I&iX9r_JIAb%AI0q}Pn`(O75 z|1SU`09b*X_52%wB9EOQvpd_o${4&dFMf2T2#a8_5`$QxQ!LO>7i$I~7NM-rd`Hv_ z$(s*KF3`K6kqIdTtFNREhI`O|ECKC{J;;lYPZlSw#SC3I~SFWs(y zm-+8yO7}*Y{pyYGr0*;n4l^aUggl*z9FUr!dN)cec(%c;x=exwd2oEy(2ocxkk5#$VGCoFQ>*8qWp00R|eKPmP270t+f%5;Et7N=u0E z9b`&XnkUv3B$K~FW3jbKkf)QE#Oo~gyns96p9xDT>wh1D>ziF-Tv;*Bs_@5|ul<_o zan^v5|EY6UqnQv*w;ktNJd0Pf6+5Kf!X=<#NB!m;(7JY1cpY>ZqU(%m#zVDi?D(=} zokYhP#4;YV)zvmWnA4J?S!I^pB;98yC3qZDJ&zu(nU1zR-ZY8t_sutE*WDY0BT0G^ zNbkZ5q<8s^Um#b&-SL(Utqoq;H=C9TsPM-x4LS+h`LN>0f7{+(Iq}BbQ=bgTZ}Zno z!#}Lg{`v6>Y(RRD-}r?;Gk$>$NRRW%o#Fp^_yxAVJ!SlB&%(y~>qO7O+S$bM*MXk3 zfwPH-iIJVL2^=3EoRjlk`bGB)Q3XM?USgQmhe#el0mapCnVCe3FYnnE%)BOzCBkzo zL_;_kbisp;-i3^s^<^5nNT^10kr?9C*keqIIfcxRlJBQV^tpwA?rYR|ITusP7m!b5 z!ljG%t?4f0pOGi9K@w5PxE$Zh5o0c4$|WL=H+bF274)7VERPUu(^A91xkczoR+6Tq z@yExTDEdfVo=f;mH&0v_w9y`!-9Ll0-0+tGt1&=39hO(TTt#VHNG6!2392T6f;14x z@yoKZsQsr#Rv9tf^)se^+`Z|-V=7sfa3Pb}cm|z4T|<|9pGj9iw{#y^6z1w}Cqas( z)=mYZvy#DKo^;l%SQ|4jC>W>G*W(5z@qvid;P}<{Co^VX_%P z8?#WaNV1u*5wD5epSiY}QA11pwDuJC2d!V~*50rVL`}vd26nou<8)Oh59E0G3@=*b zNwwiB_XOcFi%zrA22{`faEj)vLGka1-{L02DsW+6`WY3S4KdzOoy61;jdAwlyA%n7 zCe?`X9Xh`{L7n5rvV>3inqW0WqlNvvS$o?n4tqt~t9}&gCO|;L05%|v4%4glE&fz6 z2{s@h|GyU|!3HEU|AtBYr@|!IfJ9r)SCm55m}aMqB~R>BI{atj6(YxJ0rtl zkY8ZqsRhLxrzCUH^>q$aEh>8`qH5C|v`wS7iTsZy-r*K&XCip`g$MA}j<;xqR)yZ< zmNosUbT=;qjw9D#BZP$Yz={}<*!jvE*#A7}1v`+`$o!f|@&7!~3w9t8^-l!7UC@d>Y)5xZ_VAKWsF;!y&2Hy8ajtU&?rtf(`kK|ma$#+_8uE~ zTWCv~LOr|R<(S&bQbz2roD8Fb8>v1X`P|yn%@7%QS*KJ9$5JomisPJQ8$aPav{`)} zID`imWr6@w2-$(8IVRRun6OyoC6U0HU|M%EpF1Ve5aPcUtIh~NyjYDzdrH`Y>7^~{ zQ}%R8$Z%03%ExXzUAHCBc|++|k>iB)fLFvcpW)a-xafFS`pj6Y5auQYWWjvGCS#tl zNVtl@8lxU8noourCT>+=Uf@7Xhp%+AZYVohEB%`Mv*qA;E}UixUF$PdhC0&cGzzUt z^N|*YKiF9@ACqf9j5>*Qp->=6HTOpMCgp3Zulr?$ z`83$A$rYCRvrf+mw=TEz55j~a*y?3&*(LfVFc#2r%W{Z|=n3a3?r{+_c$#X+B=NNp7PoEC1t{8gN(r zPH8AXgJm<0L+l^~tIW}IrVTW#iYcCLfmX6X``Kr}0jq|RV(Id{|8fVhiUR6L+p$@0 z{0l1CNXQ8D*lgUWl&LN~ak2W4?S0(%X^SOy!R_HElur|eDXQIDi8=j~44LwnXTsEo zr{7Wloo`wjA{xs(RQDV4-`FRXyIdp^Z-e$|C`OHHHdrJsdFhia*x|r9#}f9Jgjxg% zEk>WPKBPR#=v%J4kDt{|QkzNbcAJvxXZhJ7JS>ja7;Y*}d=vzA6tXAoBvyulq&7Yb z{n%QWb)y^E>C-ZuzOm4U_O6lU(Em$zan8yE^GcbGvAhYRu9v0?jjc9 z6x1WDy+=Z9NlCuj=3xu6I_DIAG^K&iO5HLlhv8AfOWYCoApmR&900w0|mIvUJLn>otlNP9{7y35!_G)RD;vY;^6?T0;GaNu}e^TTv_xnxCMX$g3kJw21Dhr0G`alOg574b@_c;hDIb z?vrHcD1`Pk)8iQd$otKAgBd@4zO6beb;3qavo%^RaZO`4hWbyQ5An zjMk)#(hgZAVh$(KDqZ;nT+p!o<9&nW??$fvovV*VCf3$YueHzvIR9bm@qb(BnK;<# zng74H9y$NA^~fN`ObqyoC-1*@0g{i8LBYWJ*NLs|AN!cUK2kO}Ft)HY`zHti(m#&HYB8;Sl27e~I0O`mW*g0iHXQkGl7Mt=|@z~>S99JA=EI%Rf&VwQV zp@K=*uvSA}%}3ekx){6TtLM!IgFI23-&dl_ono_7D3)jcjwQu7{ySGnVkx&c~_UTe(%kvnKFU)u%97%QR^?I$oYGbZX|0_q1ujxy#z`NpVhSiSSUoQE;AkSuXleMW zjBIRBXG(e_f>5_&NFPc#YRpm-r8aP-4Qd0d&J4ttVDM_;x{8FB+h`-Walf?J zC=a)awul_4pbnwKGAgG!t;skKqAk&K@QB^`@PF-lxxRz3aETuD9TMMzwX7MP-I|y< z2_0bXnVB<{*&G{5%(TlwUX8{6>G-(hemQow`)Pe&L2im-iSwn>k*y#X)d^tbl-|dN zQ9qMVl2Q0>Gz>ihhA36%V}_t+B!HK;cKGYbPgoM73&iL`MpCzF zh9Qzb_bj3d71G*cu6z3sEAl2(TsR!VR~EpE2m8Xfh*>TGn|y#-s+M9iX`ry0w`pd` z^Q{hHpGJ$$NAeD%&?xb~1ek8rERX%nS?S2s?ZAhP%;>0v#4)e0p)6uLZN2n;Li_W{ z0<(OOVbl!Ss@t;hSq{2Z!yA(yV{;kKkh;{lvPz}7Tnh%1O-d5OYiie1H5AWn-nqj{ z-}M#nbr=W9$$jW6+a}-_kh4jS6XRAN;#9_Vvc!sWvXzz?uo1EevHteWYrfJkuivC7 zr|iR|Ac>LHiJYPv8B!rVH~#~6ES7y&fkwgM?c_11(ui6kL~Gs?ipmAmE?(|qdQ*v) zaV%9lxHoF3qj3@v%}>9Ty$)qJar4ddDxol~^p3iNT|Q%cnonvkaSe-Y zKNsZ!-$A*+b{~i6)jlrYoUH6eq-57M>-r7~|J=Ivd0&2Xbf|FCv=)Lq?R@ z#hX&jkL|F-GDrliVvCM(VbAq4zu~QYv?GKK7lzphUT7Ocs<8!bmmzswAEm9uIm~52 zzj8=o=I1O)^6@9uP)doUIb(CP_!*o~2tkGEVX*ZeB~GACQo<#E{Ghu{If0?I3Q1mB zqYG9#heq9YNn)Wg-iUcr)$n&)51%|WCtUM$+hFeQ@yMr}4#pYzGfkoDB<}3Immj7W z$0N6)^|zKI5C*t{;-so%33UjtBNCn^c6STRO^2pE5f@;n-OMbkGyA$Y{ct@n zHD50J?(xM!#Wax8XBn9R=JJDp(1j2&6Kqt*7)=rLrtNSGgm*$c^x`shX*P1zo9uZW zl`O-a_-#F~sDT54u^MGvlhL(fzK}d7@I<-8TT;a+$vyQI=^5-Bf#)tXk3}}1FA8YY zN5+p?dK!#DyzfXlv?utodZS~Hdm-I2pJr?3WNDt)e|!@(1&v9W+Kc@hb3>{pP23B* zbEcSm(RYgQa{%2&i=kRIL|OkD78MOzQm;^B7{z&HA?P-r=u}#eglmTgW6$$??wH%h z%9r8rh|ZOv1AKBR9*5R3n8ikCK_|P9y0W1_S7MxD*4!8AF+}za3=u|ga(Fvp^c7ay zCf?Ks=M5b`u~itFpaA=H`hVx%(Snf`V(af}~{bpu`}v;mnP!N-{+u`2L9siSZaj>TgN&K|Q@ zrnw$*G#*R}M%n<;*`1O_Ml{g)@>}q(>U%E&ph}`*c5mx%{Ic7D?*8sL-97-p+y-5awSMl!WQZW8a4K?24St(;s|VW*>OqKis&B9j9zH`Sh_jN^)lgZl+RU&>}uMdk&c z@bJuC0r80BkVjnK{g4xeYgUf2jfx}Lz}UOwKuXYp`zPeEl+x+{KY-1C))b6&hoPxJUPjd}93ozE-QL z!l}p-<*7doKieOpRoVG&x2?U?jMe)_5wlP63>Sa6qJHP+Tf;5>tG*0Bu5U6v)z#Di z`MF#pJ6`BWZ(7qCW>Jtokj&f@e;7xBGZAOMOj%_#h7~PBpdSx0XTIOC4Q;2~{sO}L z_|;L7?acSR?{f{~l}n(SA2@MIO*9UD;+1JrNezPJ2#hDM4q! z@o@dE1ul~g>Clt?RJyiUznDgB`Z#mzd`@4uMFn%1m*cC}@OvQ%G`*$hXAyK*9&!#n4}oli9p5d7t@L0R1 z&;QsT062l2O}yqA_^*9ms~`cC(9`Eg-l8yl!;i}lB9!}m!u%!2rCT{CIKeH$mPpSJ z<40CXC2LAq0 z`xo;|P3j?38m*vrn2e6N?T)nsPX)0IN;RmAwO0V-J7@`w+~~0~CKtz-eh*f`kHk>o ztp0cch&fOS;3c5nK`cR7LA4^Z0|hiE93>kM;~x^YTNs3WuCs25t@SlPnt#qt`ACNO zqI~$;vHz{>%RgQ)R$%r-X69FnA?PYpTlO%bEMH@?7YQpQk|d!fJD*Ro^^U1<4Neci z7)oYrcl|H+?kYBtwoTBqnVF$n#xi4>nVFfH%goHo%*@PemzkNFnVETNy0>>{b}h}< ztJ&@U@BZ>2rBI%vQW2@p8}IepUYl{T%~O7E)~=U0(j?O7>e%jJE`=s+V_WXk{_^R= zcy}yMcn{#!8+(Ha)8TB8!v~frKO-%yWf@?HpyT8VL7A$U9-asQcsyeTZ*;xK@8{OD zT3HLQBQbuS`OE1{U$;7CaNPytmAR~{9e8Lpn!n1sQ$cRa9KLhLhM*PY!^Xk^hZ+qQ zWZP(n=~RC{1(i9KrT2q&U`7dB=6Qx*mXK16crCgc5$~AXlQPPS?gCdjiPNDfA8R|C z+l74fYkje;f&Q;-Y5H3i|8LaAj9=v-|D7uGZ(aO9>SD&PQk8$};=iVg8NaMX{uAh8 z#;*#G|IX{^Z(aO9Uq@fu|NKo-q5nN3mGP?z=HDbW2efmXuxm5ha4f~6=ZM|zxvwtG zD3VzufQQvY{Ld-|%P%cg$A2)A%J^jo^gp`I&G=OfljMT}(ql6f}^6IaDm4Vz*m#Tdy!LAk(0feJq1;{HXgmDREMf zji6u;j2(Ull{yg*kB9|>Nx*4C;laMOaBZ6>rhw2n)e<3iw)i@1U(G-^2pWkYoDS`i zu;1btBZ80_Np$$r^l?=X7uwgip9H8P!u^UzxyWSc0dImGq2Nu5FP@FzTUtgH6$&_u zD9iSzO=6n$whm)h7dtw-j2>dvFOs0v$nU@(jA@W(<+XQU>j6Dg$gY$dmV;bkC<%mK z$#a~ls*MkFfwzy!(!IqCdBs%Q_$j|`ADy9*z9uJCOCT_sPdIaZ-9euR<%mHeLyxv# zyynr!SJBCHULul+C>;Qo8cou#QlBsZRdv?AI+YPAtnNeLJGLBScQ5g=Y0I?ANA*yZ zIrH!ielFtiFG;HZW1xefrNMvS)6Do)=JIb&`#-I|e3h8|$KkY$Uu7Nt=Cq9eJ2)-l zSG~x8&ayb;S3$|YwlV$Rh8^gD(9+S$ptSs%bpY&$)aDZh_rAL$GNhoNiZZ9?{PWv9 z_=X>x>{+yHi~_V+TQFkY#Uv1mq+H3iQ=OV`$@^XZo3 zbAQ^1{+U#{qZ=InQv;b$!S2f}r*cTqc>A=R!4-xGB?y;4$b@T#7)BNN649i<4yM7b zYML2UD~-Xk;7jG$e1`q3?>N9$c%@kd;%_ch-1`)ns+tx~bhec4Np+p^n*|?{v8mLs zRwQKrJl4Ez+-=W7E79Qc72Xoi4Csp=_N#{Fe-V+(_*JFyZ;{LXk0Npzzl>zk|Ak>J zRC4T=Kj>giJ)ku4ygzCu>3OUB4?ZkJCe4JtspH1l3Wtd`xvPpwju;FzRb0i0JZsYN zMzFwiuSJ$;9T2XBHsw$Bgk5*9&DnigER)npJ1K1!RB{V3Y zMJlBu+}DA4Hcq``k*M#l#W+d+COFGQI?vppg@E~@RO0P^J+oYAx5%!T1+$Tj?2TWdL|J3H9kg0u z!>Vy3P7a3}1{h_$Okgj0|6>O1gjEustKgSJSv?|H9&& zWQ?R`KOT(#e>dB~dvr_5x4%nIa@#)}^+i^iAl&ud=p8vU6e_8cRMFu*3vYakAX*Yf zrRj~kVlNNy35k8Sy|ze)U4Ks_Zc8Uu{P2z)xO%O)eJhGP)wQx$SiDw%WCLPK{NWtx zK$suqqf>hGOC`XXHympeBoUl67gwXsQY6kyfF5_mc1p2U zA_Ac@H1fCF)HSFQuy6~l;?ymGVAQL{363J%#h5Kz<&Yy}SKC)L{szYX7cgf0m+s^LZ88EK<6qc_AsNjJ$Bz#o@R;dK2r-m>XgX_u?e=Mbcq@*a zfDUE=v^6J+2yY*oHyf9>(k)FNmrp@9F`unfjq32G2L&v*&0#f=5Y$DHJ>* zj4bPm2?_0^EY!O;QaE(xbtO!=rd@)zucN9Jymde&aMuP!ui-!O`Stc_`hhvYH(EDsUHo|=tpR~J zK>+eu!(Uh?c`&_NPdI&kDy_FRl(x2Ju?{3(MrXE@itHsE1%#W!@aQouMBed$5nFmw z9);Dmh7);*9Ph`3KxX>#Essqj-Ccq@Ln%hL`=vxMK6)hwFZtc!XfFwS45EwExzT$^ z$3c3hfC3oo8yW6+w&)t^8NClq4P;{D=^L4p!q!k4s44v;gAr zJ{V2l5qj6ON1*bk;^@5Pjss>}S%4rUDI(T^zl&3@Yr_Meffat*Ce+aVq-UakOG-pX zcSG3P3k7kadx2sBq%zGK6cM?-75E0Hj)i}(ju}|M! z#TcdLXYUiu>ZfTw*o&L8V01`aLC5ir`Z-vV(W9spXzKh%AT z8@9z;%C#o|nI4kVmQUMy=-Xj_(qm(H;Xu&3h3o+VwER9BAJVbdecA!4cCfv3H}&Ow z)1C!!ha59`y3srO?Ee%x;{nJU?%a@kWgZ#<)v5P5&X1Vx#H6S9o`!siNeH-yt@%yR z$lUi3%Cdp*O0nb4pI#+$1Z&eyuNR;WyL6Zo+#dj?zc5uuW@F;xbkw1w@8dVM4J zfo=1fAd&g5H0+*ActIJl{LBjCDRjfu;C)c`4z``R?}a$woDpp`O`&m7#oS76Sw~i9nN@(1R|UwcI^VY{x#dl$@oVygcZ*~eE#b)biidQsQ$2LIG)Soj<-0@# zl0gbr@!+^y)b<~J&rQ^Dg-!m=-c?7Eoana{uJV)pFQFUxwl(d!%q1FURYw)38sGlq z{WIA4bV{3~cT-}Idx`s}z^7l$@1HfFI$npvfbM0K2v5Jj_g>&1Qh~25RJ^@f)Y9la z4L_|Os;1q1#(KPx_BtmB`AOVfldR4Xn-CFBw`8!L=R8Pyngi{{V|`# zg^|UXJpU)b&fvaDW1L3~wJ=iPk8l=iEEinIs~My4jS~mL)OukDd7rX6T$kD*w=ZV% z^J$(A0yEj541RDG${}5nIXfysO9LKv#;GJwV=Z5^p(2{YcwENA%=Pii2P^|wUSEX$ z78OHZKA+FRyGp6ayZ`4%*&5Kdo|*5Sge_*KY9F3SK}`WyNCMh9|1 zzWI7mWiHe0zNf0gdd_1?U@U}z;zaZD0@i}?NpULejQz8*OAgb_r6988@78hA^C1Fq zdFjIEJvfVw;7w~dG$YB1M#rl{U1>AX;P`IT3K*?GT7cUM8@(z~cBz%@cI9D#F z-SINYcEPJULxlbAhR_?j+3i3NSA@gQS!ciX(C|ew+n?lHQdrPvo%uy^IHG269@eMLAg_u9 zq%;TZL#AE1Kb`HM)KBtCKg*U3Gvn=GiJ{32lK8=_0wp)NAYJ|TtC;K60T4oW=rFIJ z5V0p;fVYG-J9J76HS&z;Gxg?3;j9^MIUO{ao?BYop*^Bk0dc}-e_qZWxE!~?hn0@c z<i8@6%F-Ok(wBLFRUQ0Ppd`D84=DC6B2(*yRvIp&{M;7x5U zrq!rIBW>4c;oR^ooQ?EQRZY41Ww}%rJ;VxUI^qwU#+^))cc%dAT=WSPlHlEaA|l`Uj7IlX=KrZEAO9*hcZCLJ_8-J(w59n&4B1(6zPVE!0$%curm`LuXad&0D2$)hEIGyaW#|)er2z|5 z5|WA9+hM~XkYF`3k;45#2wNKDx>;wh4Ig|bn28aUYe~H=V3C_>cFpRhc7m+MI*SN3 zPUfHFs}wB!rW$mK7?lp0SFXmMZ5a`7D?R2r);ZuPM$I$6xT4f!&&CSbOrgh$E z!Uc2{j>3JM08) z$s|pp;+zXhvvo`AQGfUAzt?MYL~lcqA!%Yqr=!O(BZ_ ze{n3s2jPdUtwo?BtT;b+gTk~S^ybDDL6&|kIgAT3>l2UT*>iqwkurYlxWUEGa)Vcg zQzoZmvKLg)=;c18O=X2$clmyQHrCac;3*wuE zShOgNiFqrg;$RUbG9w>H*>uv2Dm_0O2uVE}y~C&tqI zRnNr;l_vb2xK3mW=Ko03Z-aMtxMtiedqKe*7TL z{Ouq@E-hC=Gr!NIuw=X5sS>__%pXwno4lul_Q+ItZZdg4!F^U>4yV?-lA^F5i{vJZ zI~`6!y-70o5Qf%F7}6#n>h z2q^W%$%gbrwos!(qZ-n_CyHB;_c>9bONJ8Q_bF$xbiF{UCvt0Uj<99(qw5gz#5sTn zqF$JQ&oIeqzLe4sf}_`TmXC#eA}Nrxm&O*D8OwzxQ?cdNmEbMMSnVl$rxAS!+ZIFr zP*U;rqJs_#{nnBgmxHx(MGQ@457bG0l$tDKEB1@;J8={n_e{+Fqz;rfY?I5GTKI`m zPh{@$4%A-8S__gI@uqQx_FLaTAU5Ty%^7)0(;hPDYJT`1np*5z(d1RY?Zj`4oOf39 zLamPu)b996B`m=KPkX_#K}2NI$=>T=>ur#0m2AJ3sIx1x&_NYGnBlFSnE4g(GJoF} z9LyGNl`VyT0mmlP6m=>T%;{Y*qT(Y^ujg#*s~`7B*5GSGO{p(pqf-M{bi=+P8L_w# z=+kn=_5y|OGpO`=Y4H0|XJBO%@xjfgz`hm3r={S{;|_J&!h+5b@!OITzX_$|_< zbXHxfb_U3dHn?CSL7JU~EsJ9{8&t!86Emo2__p@AYU61qw6$7NR>e{+!)_Pg+9dEE zI!92^?#>Q}i>08;@39rv*e`4#grImcY|V`}U(M)a!PH!KWdmM(%TKvM?fo6J;dZe` zS)B7TWjqLTwCLy(Mp#;+glCQr)_|LBBN*slTds^G;axXC1gLhc<@u1H-X*z*b9UdA zFXyH3K5824gnV*n;R85;%_L8rlN2W+BiF2CsZl}J1iVl^LZ45nsjnQi&hi?#0J-}Z zD0#dw5Hf@|LnqI{4yH4kMsyb=;Dme^TP54Um>YRT{RmFlz|KqlyRlkx<#V3+4pJG$ zv0yBpc8*?3*?1i;-s@neUac}~RO%vdjw`b{HPD>@?1;w9rL|lZ0JN+`rT`zIs~Y+R zbe@eX6lSxC7|}4bPtjuxeXIA3MR~)#4@o{pXOhOoP0}WQ-%xQ&KX1QMFCZT1*(iyT z{KH+Gx2MFnj=)RR(Ev3lGXS&5hF>-#5H#oAEZ-;3k;XrGmF0uX_*OK7rqijrHy*f0 zR-2tqwP)f89_$n&U0ifMf8ucUtK7?GN!>uIE>o*V2iW5TxOcy3nBSR3Y^AN6KJ&0f7Oy?5q74yG{n&if*ne+$2%_kRrVcTkRLq zmtq{&$-^vnB7dSD$(c=7d)$A}yJKvKI z3;iRd$T7PM^MCWTsf-oM^6Rs2D5atg9OEyVv_%!UQ~}4qhGaY_(`c-1?nVkG7j3)k zgE5P(b5PG+w<)^!r-UtjM6;>zbE{Yrjd}x9Y!S02TA^Fs7 z3)HSGST-vxW3nV8-X3@!`4mA+&Pv8@X)im}-F4LK>7q+3fij=|n> z>>n{%Ad4R?xVO23D12Vk4#e>y3B}T|rSq*i z?Ye=sSlPi*v(Dk(P9A%)5`{ckvkmsXH#S1pX@Myl!~MgjfcAlQ-u~48fo8uW7c$vdmb$32V8Hf0uRv& z$jeE-P}`8}#DeDVdPG!w4?GSDChH_K{aH)GQI@zwnsmlyE9~#$>WXwIm*~WBbdRu` zllIYnu{Z08w^GXvK0;v=(R%(glRu|fQLwQX09zS^*&a$PaDQ>;g6_D+O&V`tc%I$w zzcnTZU+F*nPmdQ%_?A5Do-Pt<`Fvya`?-Y<0OwQwS=e=UvLJ6VeZa%6@ z)Q{vob2*}46S%<1evp|?*g-xJh$aAzDbop={n=waSRU%)ej?6wjIyFp1%Djw6rZpD zZPQOBaLneG5Mm-Q4J~(%WQfE&)>n44w(FV`SCi&DIP9QQmnXs zV9bb>KkAj1?Rh4?OKYgu}I-5Ze2QMXzM*BL2XGakMpo&A*r3~d9}9)-u)X5mS=KLg(quU(^) z#LbAS1%)h8FJ5lx#kPBK>c?bHfAx|0qrgv))3=@6KBqW=G^rVSoX=O5vJcM=$s@jF z_{8s}`jGMoByVStht(y`3+9vgj z?duI~aU6q?5_cLvPy;2?wf=9#P?wDPcBMtdSYYHchr5j@$O1c8=>?M@I=Q1?{ES#h z36agS`S_|3u$=!lJ@GduHp}c|WA3(|#J{mte-AxSxgJ zPUp=_BPXgOLk~gnT}|o?f8Qof?Bop{)j||(5~Ab$k=!Z`#BXr%1GOFbqzPqpJj4rS zppcKV)xC)jy6UdCxja$kX?W?PSZEmhJy^iomrW0=3XKlU6T#cz{#HgNhd#4{IL1}i z*?wQxM>a+H0Hi<4xf2R%nfTP*L~926&+`z?T8GFeR)F?);f8m!A7blH_ST$jGitze zT+=VHreUcND$2FUw5jiV85?w#bKh1O)QSp_2+K+|J8GTg@mj>?gILe~ro-O%_Bf0U zbY0pmtr;gTci^rR%A4K@*<17V6B12j*- zbJdT1?`T$5Dg!nHQcdzC%A)m0sjxGu+)It@FqUn(4MU-tk~|F@oqSId6`C|6fBW;a zQCYM3RaK|$N1SY!$<3i-XPnylZPcx8%4;vhQPu3MEZiAX^druzQKd^Ctx>e@rT>aH z=h(ndc{DDPG~ntO%y^6TG~r zxO`%8@hs`WeT@l^*lMR1VlOJQAtIiL%+AF)z~ffQa<8R_n#kWmX$-(sxXf`6Sj?s% zaO8|5*a46;>|-4Uw4Ce`L)Fwj*8y?j)Mw~wGx=Fy8CpE~b7OWuPr*6fLL4>(Rjt|V zP0mjtvo>T8oBZt;5oMUNPQ34`tbmFsON`m$+uwWAtY3 zrailSGGc>nmhiSA+uYOZhSP_zaJ&~Gr)%IJo*v+(zw`_O$=z`Kgnl+fcEW)kIqSH3 z3Ymv%gkQz!hF4G9;=66gJ2YOd8)pD8s*(VQR_3hl)^tZ6t=@7!)+;VxzP0q2b(R<{ zKKB)5OagWLTQIJ)(78)$qy%sEOLLEF#zqQSAez5NrU0tjg36|fdYf-k1i5C$oq86i z;Nc0Nt-&G^pFGLOUDe@wI6WAP1z_&y!qZ-J;|js+!uBB^(IDt2PZkp57!>N}1{%hq zgot+Em53EaQ|o{sQVAC5V5&Jm^|REB-K{aFZb=Lqwkw36@UfESVGtZhLWWuq4NrJL zJ5yAA`isFlIkC=JA1?hk_fS&2Dj>OJrud!PliglfZUo_Q&74B8@D94juQp{}hvqhD%>d1r7dF@l13fgl8^sfg6 zq0skt3lVzFIYg%0HVR@OXZ4$-6XyNY{sem(LylI@m z9@E6%1{v?if~~0^Pkl5(*@`tx=??0j?+Gg!2Y*$M*uv9f03cK7{v4058SjVeuS?(M zbI~a-Nb8I4zQ_b>Inee!zOlW~$HaDdQS2_*YJPuHIOjIAM6SpWR=^G(&~l9_(Z8=e z;w$411)vRm**l#RK|EF+Hm{%BR&~6mUq_k~A+j%y6Zf4 zA`M*#p?Yzc+L6eLw7TpdZK=hY5$mgj^1av-4cU(@^fJ81MX{-mA^AOQ>GwpTZ%hWn zp1lx$M+gF!!|laIl?Ta7yO*P&?zf~NcziuChzEwtZ@#mSoO>~HgXVOP!`2=hgT#C; z=<{j&Cf?mco=(lEZz=w?gq-(oZ|6dN%$uukN<6Jm6-#&_ zJ`tnfRgL$MzXc7EF;}d@UQAnAZc!q%@SvJc98f8DvJV0lCV0QWP>#I)TKpMkq5GI| zOCMk95BMF=)0DA^oWx!4+rlm1fdxAF42k%1j{Uiy;z-#B*y5Ybj3x}STWk$3hKL?dQkcJ|^=`XC=R`($b>X$CngFR)8Q^4k{u*Qs&Bk|@^SRY%>C zj+{g<-?`L}qtIm6CV5jQb*L=VC_WAduPt(ct0$*E z3Phq)*#x!5d0e{Q=yA0L`M|_ep*c=eb$xbwS7H{9_k`R%tVkj>acj>Dt_Fr@*^g$c zvXBNo3^vJkiLL{sHOmJFdRU)TrK*`|Z0e3}0W>9^pXQOU(;&AIi1agioD3JeBQe_w zGGf9T!_>eu)3|`jZciomDOlVMp*5EZC!);Fu${=j)a1+-{m3;(3GeASfg~MGDv@sI zab)y%&KQ!yoH8i6eny_l+NM(-rHpwwFGicAOrwj3nsNSVs_z zBG;gbW?=!nzQ1-|31Vf*+D_vdL_a8>bCo3G^mDMqEP_OjsY0jRncqQ#$SYSkmcB~dpPkFpl0 z4}%}27N(&Q?hS)VA6>^V15`|ruyXD?xj?_^;?IN~CJFQ)I+!xDJA9#yxACxSFVoDq z;u?X_bc>9B0vG`6e^V&eIX|)06KB7d)@y;;$#^3}2(m)_GYh}<6(laNSYcn&B*}0q(82!MCagJv}I&rNAjWe9;RsPiA3@VV#4p*PXQ^$D5^Hcsnt}#Q~ zd^Z)tpq0*@2;y_z?J4M2|8Vdde}8wmP}HQTE4as4CAQfL7KvF@O$H8Yu)d8di@~t& zbv8{2uxuQ@vLfcz_fm+$Y%(GWhhq4oJAoW?V3)JSv5gH%kc0S*qR8h0A}wkwh!{VA zEvPUWq&KrUl%A%s^v(1}%JiUj4BHUkz?>Cd%RL)NS1=J^u3Y5&4#lxT?%1@_pCL{# zWg16QPY{;qWkKT`yPq!^@JCJwQ?C06)M}ysDaVo2=MFHzPN9O9| z*Xk0}cr-1JAQV~t3<1_9x!-)`z7w?6K zJ6i)4azkL+ewh9dbup4zwr-BJMtykfsg9dz=-hIGWOBxAKpL0yFs=;|6xi+~mKkn}yIHB_)_xr8RcJac7!;T_0n)gWdS6%wH*4FxXhlW@FRx>R z^vb)i>jD2k;`GhQAJ7?T->+ErmkQ0&^BL}_=mYezOx;823$a)#2(+e4G(0CaqAHDX zOui=p_MCWqGMv!Uo9-Fy5+epJgD zj9D5lZETHrMvm>n=N}N#Id)n8U^&})>Lnf70bnY(;Q7>&TSxW)Ej2|;Ulr9}fl|I8 zvYI-+s+_#)Dr>%#0f!wtbZ*eFI$9Y(ml{|;%ub^u!kc+<`U*q7yJtq|1?$^0pN7*h$&E$9ww4wYd*+GMZiMUal%F z1^r!pTFyVVoIAlyGbM%9VEoD!n`Batb=@6UP{#2d(&4V`g?s_CJ--R^_=6OR;X`a5 z96vv;zwDd63|6QX>F)sQjpvMO&+v;vP@)G`oC8tXSqNFlkh^Jk$6-)tpBn1Qefsq&s2!j zbR(8;NXh3}_};Y*m@{;2%kKd8;LPnCI;yZ7fDYVgZsU|O5b97}e?|CFB1G{IiE&C-280x}n`%YiQh|H$p6N*Yt zGc0{%XaX}?b)lvHHhZWwthUx*m=l>G>teP21qTdIPCVkyD`UXQO%y(ChD`=7Zv%QF zxWz}hWO68o5bVxbvBV4_c<9W2yjR44kmL<3?no#4lfVInuy-Cz3&s;B8^W5JxCT{t zhZ@n(pHk;F$wczGo_@Ogj(!(Oddm2jB7<;b4l9#G3jCrUG1$1fMlkj7ZIEc(VkCSN zT_Ox<8^KpmH`-ZMogr1Xy8vD_VA}1s==@w{UPI3Sypz2gsa%7n>nCdO>?~0^>gO&G zXvhi2Tk6Nct?Am`=%>o^?G)jwig4%HO$MsaK^fuk?q*HRPL@jrn*zhR(s9NF_=`Kq z7a5u6rSo#gC8nZJg1^&)PZZ1Np{zBr7(xt{oFVm-ALi#f*>5AY^HlN_>YuFW07d>R zuT#Acp#)G}5u9zIpu{{pfJ9)begpTFCdT7X7ni+?EBZ5Po9s9QG@TpQe&GX*u=qt4 z&qi`IFBi!r3p!>!vVEG#rTCY^xA3RCtz#H^rgBP_mx4z?J}AY-xx!5Vy9}$)Zt~c; z1Zx@*g-Pe;J7buq380VeLEqk-AIE6DYawA<$6L!WawGHbLVNo%F&^=4F_kOOb}*i} zWAoMSu{oL)iJEfjM)&8uDgp*{*?W#nNKjo-3T~W`lPNyI@q!(8LhPml4Bun%boaS) zToL(Sv*<-djSR6N$?S){vM*mXf8DpORFyH!{n|^rTsu=k>i*$W(CYRHH)8PJH?3Xl zTHi_R){krrV^oxgr1i(-A`H$9 z&|Z6N-k*)rPH5gaMoVZc5$a`GrOlSUm8c6DHC}DzZ6%@2Y>2HQ zKb);?qB7y%*@_B;ZSKGTf2B9ojnCsll)yR9*4}n9T9Nb2iU^ptj@r0nj8+`O5)DZR z6!Y7xtMj9h#A$u^dxWnGXuikisvX4;;%;RK!g%!vIKy_G`*!X>aWS(>4?r?4Aj&idALcR@TE#jNxvQF|bb0XZGt^a*<$vTt zYu(WYJHL2@Qd6<3=CeV2w{2_cqyViNB(xbYZam!Kmf7;=JD9&Ud0TVTs4wP3?ypGv zmgDG*@*U|Ojg*?S>;Uc8n9rh~)AWj+*na+QoC|eXQqPb+l5UT(II!qJ@{~UP!sVj} zw~kV{J=$~uhHI-iV_)p}+Cz59T@7Dl4iU7m2TZIZsE{BDba;+GkB3v)u_S>k;w|~9 zwhJBubTbhz%balQV?qPUda3ObE&qrTNFRms&A^c1W0t3xqe|9mG?fB(!tezVOJhW( zNu%+tyc!fnE2om11@gGC18bvW3I-ZL8w6~$26S!^*{1f5{4T|0aItSovo}RLV4b_e zDVzQg1k}M*>vqB&dh(fd&4v}ET;%=uj1AWXFHFy2CGD{FbK3UCsPVVMJ{nH=2N{2( zMlnKH`*D^fMFQza(4ZmP(OiiN>hdDrNp6!VO=QOOXW+>;WTi+7ft3sRGc5Rb;=}v0 zbE>g*KH=%}W$uEwRa!DQNZ(^rFn(<$JzIweTvmSa?DP}N8g)0J55mHgf=*Le7C@za zDV7iI-O=~Yn#U$JJltdU_#h3pqrL5L`8`!zXkvKktf16L=ii4P)nq|0`&Eyzfw}#} zPVA7CpCvNLQfcE!GxMzkeVtJ{2GLi5Y%R=MWn2T_6R%$q6k8wukYm`hzjVbHD z;X8A~W5DBD)!c$1lMEN33Y?EsdYAwmA66KJ;W!WccB(%R=iQ)^dLew+ia+Tu3K}Ca z{Rzlap3@c~3~aq`F73#s1l@e_BXGC2A< z%25eDfhO4_jLxdqnPkS>og5_})InCOQoD7T1<-CIx`6){+~Ab9T_WQ>+xmAHGJ{RW z(?nJSiNdV4(#xt!*XSoG9kuT27fyuf|0%=h{s0h`KOpAcoCrM=;~&!guSfqQ6JcQZ zGn?VBG7$xE1^MFz)Z-lJnSSUQw;m9^{9uM-L04%$2wo9df8uPEV>!@cL4<0&8AJpS z5K{tDO^}$4_)velVtj0hm_$DKa zT)kmJ(v{-v1TH}ZXdKHX4d7!3R1Y)80C|*U`y&e8JH7sFv-riCq?{h@ULHDxj|bQb z3{WAUc3?ju-UR<&PX}A0xKO7c>5g=8xyMz)M+-ULK+~1!^BlA}wqwH>9omv5VaA1du zRaI5eG!vg@H@JI~sJA@;y~tWN@w`z$-mn3{`1b)olQ|Hc9_54}&MxH)J|!0hSa*H6 ziP&9S-tbpn7XtxuJFS8-t{(NkoC(kbu@1cs>nkWRZoDwfDWDSsV=T8m*AKiP5MQdC z&Od8(f2L|#T5_)=yqf~^{G0)uA|_a?CIxtzL^LH>tFiR-66K!wFpdR%QPxIg7#M}+ zeTu?T{`j?2B~{82j*S%!#P8{&A}v`?voZ`JF3XoQXnU{$rH%}tt?Xof_stmry*AjOV z`C8*FK$jj@^Vp*yk_sqr6H`K5ohVS@kPoy+p0!ETGT*|2Mu}nelA3l4c4)~q+| zJbTU{bA3LBPxck*m2Eo8=~vEdhi(-XaWYtyNh_1#!D`q*cqkQ}N-KO>N$gU%H7J=S z!uB+g#?WSb<=l5^o^6{;e}PgQZjWLoof@V&cgGVoe_Gbp#BrQeg8M4fWa?2V*8+EY zmNrA7EPu@V)-*3P+HbHpfRMc3wT*7b0zeyT@nMST6$7Pv)BHr{zfn<$%u<4(?QrxO z9jT`8H|P%H{~hbFqxT{r82>r;PI11{fF7G%kf{9IzX6uqBQ*?5gG3usWyQ(HD6jr_ zBEi+paHrf_n=+-M*k7GGdQSz0F?T%8Lu2C z{pE^B+@;1SsXa5M4Yuw>tzR4Ox-SD#xNWT&WWwA8^OFbhb@X=-b#+N1J3WkL6?7>i z=AXY6ze86NKVh&Vsn=`0)|qZKPXc9B=ALo_u*|GC$E=)6$<(Vb_1$ZWS)SmYL}`z~w3gh*wV^*g^1A zK}gDM4dYTLrX=Cv5HNCCD|%rVkWTDmOZs31(y>6Jpbc(14Uy)4hqDa0EwrKU4xCFL z2e`RUVtf^L^z1XsC-J0-EL_Vkyj5;DV)&*t`yx?W`XZx3Na`A+9~z_$rDpZg-4S9Y zw!gO0YzB;AZahIxgwXK8M2ER@VR}QH2^TB-YaRo0v)`m+5?yh4t}^{cJlDDHCc>7MLDfGn3D=+La8oGW~kM~zMbLX7}@Wt zPOLohwmsWAZ?gAP;*}3^SWMRc&vs^ShKWj0A2k`f;EH?f->G7}O4B@;XXD2o2mV4O z&rNBKTOKsPC+NIW)RQe2Ip5ZiT^g~V;jwFx%2tC0^E>t8CxEBocRSbPOMA$U6YdsNUmvTEhB_2FhW+|ZrAX?F?NnI zo<-4?uj;aGcGQMs7R;#=r_K#%C27%13%9Wvl@jOvX1&{W#8op2!U zBdcmAqUWv#XlnUi06h&hU~x*L7-p_H(z8EUd1LdlaCC*mZ#!=3=0A&5CfVXuBXp9# zQp8V#O{&N4%mF*suB@^%3AdkDM$>}1tqF<^BdAqF+F!>QRAD^6SfAJA^D>6_mBN;~&?4RO9O%?q_pQaDZGm&{8M zm&T@e7=NyOnM_sF=S=Niq8M15)bC4QW=kTgJ}Q^tI6*!`bA;hk*ND!IcF5+Tsxgt` zu=t=drnR=I53%f$3YiFI49P5zamPz0U6Z38c^|9Ocb(M@&aU0a5oJI?I}HUx(~{U8o1kD*F+urpaJXcl6(Dem z-HCg)C&e{y`E_Q~sslmsOTSuACunWd6#JTD?&j2^fPaF0(5LY0@X&|q2N_428uM+O z#ECOODx#0&0<=P`7zYdg8Lck2uWDeZsU?V%Ws?t0qYT$hQRW3cH3Y0|cmkMgHAr`= zbz=#p|0yaMuBD^riYH>tNFW>h{qVSpY9-^$EeeVkw00-#04ld0HqTBuTY(l;(G7ac zUJO-{g|Br9pKEN~yFz>bqUyy$iQcVA-R8mPToz)^yyjg%01^)xN2t?cdrURd!N`If z!hJOd1lEiaT^pqPHd|}a1m6hhgZbVPwl!+UYoMB4EVf`|Hm~|7yDwjR+X(|+;k1fku_hNV;8 zwW(=Ji;K`oPaNaaqSB={&^U2 z$aK>_K`&{ptH>h}Y=YC>B`t8Os{9&f!c~|wupd9^>UTg*TZ&C*n7+!kmhs(-99UN? zuoq`m)Aq57f8K!W0h&aW5ds+Zh-KINE_W-D-NuzVMITW$>ASm?)p!|?0@^b(|= z<07=*OV92bR}gzVPQgW5E=|+|@Hq`oSCJVO9B)mp_ULjMsH3X1w-hkoz_yQo{p&JtjjosDW^lW|zXuK#}Ia zVW@edc3B(nnXj{BMC{JwD=*PIB%FoIijx?J{2oTwN1n2mnW?-?%2DoXqF?zP?+j7PrpRbWBtikp9gP+S8=7iFiNR zaIJm4PBzELiZcW1&x=J*Z^^@sl^5jA2wdOCC5C$8=*5>Um!^{I*t1QYdKc(%`Aqy( zJW!Eo8$wd>15VXHyDsl^PqO93Q6#Nvp2>%*b3m63THKe3SERi~M2Z)b=VLA>S>i(0plo+~xP67QKL9OEq2+&~ z$&T?B=s>KpyI6*`6&~%o)7A+qR)$6s75|XMe5Sy~_J;J2l;j4d8FFgBy#C7nEXTac z`XxE2aHc!$`?;sB-Pz$ubQia;QE;WrvtbgmE(hHuIe(~QwXm9aGI4>LxXpL%@hZ5;rXaF*yh3>|?Ap{f$?X^f`q*CV;LONY}}I5(1q z@zv`^8~$X6!{;Wo*t%K9oVL~}24o?br*NjKX{o!3l^Lx^L$=G7^wivmwKOdNm3(yK?dW8mqR@!s!tnqrcO(J6Q2 zgy{oihxT^pY{g&kayt%(2^*LV7Vy|~K2)os4_(5VB^lfDruIS5nany_(bmXxcFqsN zY*mR*3{HbxxZZB2%EVkh$Kc~OhVA|9+uY@5rPLwjcpj({v6Y=v%`64#S~mC`Ker%D z?k>ril8`XPE+<9G!pA~x|L6`?c|J^y>xytjPrS^y2`Ay^`kf^SPG2rcw|!$LIh1FV z30CL%6)*4+h7*#ye{i2{i}M0CISt4k*b91VNkL2vm?_yX53mX`icD$akWzWh^h{;}%*L$_ffWMk#{$H)2aDh@l# zKMCmnSH(#LQ&zU_crlNQx6xmK`%~f+HXofhzyR`RvX_Bn9!@bJh!AdGyyRB^(eGdX z6dWQ_iY*8NZ~UDX-W}Ivu8Yd9W3e9OFsaTLgREi?GK!ONr02RzfM-X04C^aUyYe)eFgl`f)#HeaVFOh==E7|0o|1zL0yb zx-;uP`u#zRyqQ@)=io28)diVz*WS>E46pCwPe*l9w(N34G9 zo*aP+KeQoxJ#3e%WTZh|%&3Tn=x<y}xB4OFOs;zb#=r04=^Lxgma9Xt4UePxP^^<>=6te}LMtO_1#E zd*v|wbVcsLbGKF7fn| z#6A>H)>K->l6~Wic?BMmHx^0=jpK{NTr>dwptrt;J8wR8(tf58WqG-Vi@nbQL4&qK z$Cfl%l+Da=BpVK|Fx}Z1H|fL0wKf-u_q~edrCp6?${qRxmzv&Lm`zyjx|3K~I97ii z>9Yof$^Mc3fx7P?eD%|FGgpy~(@tl)qP1kg715 zS{nVVNyT!xWEC$%hNiZ2ySunp_Ha&PdYOEUJ}-43US4y0HtJ;=szvscI%J(xe=+3f4KV|Q za{(&^Xu(gpyy{#w*<6lj-3*yHQccE332B8cADeF*gy^d>s@BdlAN957$+scRyQ$m3 zQoc2m>33ss+@T`pi(}L53^A@ueFxZ-1ulB66p$L6^aX$`n{Ypw-;U2(-40^@PC_X7 z+JM!EBcLO1yV*OAGrUrR2p**6P0y}xGaBBh^|0|nJFO^X6D2CPkMC=4;UFf_s=Cvm zKfwbiqa7L9HhULXY>*(8t>?$MrfuU3WJttKd#y|Dwj`Wx^sZQOT&c#?>Qrn_7L>&t zeFzbv8WiZdQZiknxhuCHyvzIYyji(mVrdG*D#gp1px7%Q=ES*x8dvY38jI+znzW(s z(|Q^7P3~xotww-SaJfS}AyUwH_eGTrp2WW`S+e^S+ z|FsPAk+kt3Aja;x%m~Q`1g}hBqmMxozmF&9$9m81{!TrP~2RTjE>v>MKkfk#aSt z_9)3TN`RE}BsiPP&O?_1eQ2dKF=*%73pv}NzbTg&!yPY;pb8XmqBa8}}8NShZa`z-q@%CuuNoU^|S4=I&^RPOiK6|An$n^+0)NZcXIssvEOTMqo4t9h7NDc9$oyR|x ze{9E7gV!;Y@u~M#ydk!N0$IvxOuxCJ)!hqzN{_huSvC&F@U7SHh1M0P+dFM+EVJ{f z4g?9_SAl70vEU_WmuF}E(7MgSUW=J+;+b>g;GnT^}i?*nKEUKCHO ze9pR&#IH=19gXh34gx>ir)pxC&mxT@yh2E#v{Hw7fF)u*m!|bt)XO?f?F7D!i&x?@ z%OH`nJh2|U2@#tsh}P_b%It;|r#lNPpEn_&lJ-rb9Cx=ZZXoOKp#bmPrTCf?qae^l zz%;dIbkN8>k#-j57b=YVfPj32R@o>+^%467RLhG657nznaa)nDVAgEyt&d%wo2XK& zhE7_{Y4fk6<>T8z`H+#zEp21i4UVVbq`?lE#Chqnj`H1;c!LTdm}b-~t?j=>HyLi0 zbhw#^Ri^bruRZeqIasJC6<(zjDU){Sszh)lAIS5Lx5c9tgCm_e@CeHGoR1>K)>oa1 zIhZ1*4v?|Rq&(}ARf^Nk1lW}-zV|`n;o;?{WZxEES0=(pcP^;am|F`OqsYgY+L@q^ zCqcB=c=QM3X<{eeEX4aoQ;uz3b*-q=g`)sl>!tPl>bxPYA3|+jC4LO(&omlR+6p=$ zi|)4a+^EO5VOgv18!z1khq^5*x+@H>;pfPuM(J#!pdP;fAxFed^RUh}2u`W6nIfe+Cm)Mw=ix}La^Q2;_?`l*E{m8PDi z<`pagY|PBScTD`ejK#g7HiKo+o@V?n`a$K1O`ieU_5;5?48*m#Dl_8I2jq}Yl?Ugp zQ_4|ib<}gN$b}*1`PG{EMp>Gkwc|2X-C@PNPc2auXqS^oyB*X!_W{iFSJAY$_=??i zOA#w4|bMrO|b!E)S<=!pcSU{o+n$7vy)gpb)LTjur3kQVcW(xF3>@S5-c9|nGHRP&h z2OhlA$I$ss2z8a-MYjmW{b=70VmC+UfpeG_b!^V#nI$AUnS);rt^@*2L)_hbk}RY= zFVoNx0DU*_r?vhua*I=FD?(|BbldQJ+s#^Jc(ml+$W18OF=1hhm8z%4&@gRvHZ$zT z3K~Y<*87ihp$U6JmZ4s(Z^6m{QLE*y3F#+10zOwk8ic0@qLNMzz9`Fdj=3A;P(i2* zJiF+5RplC)3jc5jOfn9}arB-!lYA6!Yuvy&50<}#_C4bVi&G@7KKyFAWb4vJ0 zf_UH>YRb}u83gP~TFg<83IlT4lFx`0NKw!v*;&jxKBt{6$b_)cJCo$mdfg^ismQ=B zH~FXWUlM^W0Jep49>t?3Z-}aHCzwkyJBbqkHhSkCzZu#niJG1wn<_ElzZjk})Qfh> zB-!H>i8nYq_&WPeWoducJy-MI$4xykJ)>JlOrs6}@14i$RLrh|^Zwc&OhU60;z0m%l)$iQDmL)N}j*56et#P1U=X_(d80Sc{D3K zdg7f{%(8r(4=FXC2t^cw{j)=*uo@GAeIE9umIB0=Bu9QL?VHxwM@RVF!eA81v@s)C z*=9ofKHQASE2OJTPo!toah$N=Br(UIw+bvoCnSLmB1ryJy9cxpXAId(C~k|b@b{%R zkG;*`!kUcHR--k=`{I^%B%4`R%+|(XiSr^yY^=BYEa+7kVn_+APMk9S@$vnGjbV&D z9KlkmI!o)*|88Zt~1DIGV%IIns(*4j&Bh&b% zTVeY`VHkklAG_kV$! zPB>Q z4{*F9ao*&~XM88T6=rfJ2u?2oZ`)zj{4E*hr4djncwl^hm~RIIwEty_sVOgS;>>nC zLJnb%be1VJ&}j^z;~%iTkd8%{&8pOWfA3?yjp1nxEjdsV9Fnz(2v+=i|cvJnRjROmj0^4Y08snZa@ot_~IQrn?sOm>Z z&HTD_{!cx%!h+K_|3(wuFBf2uZYU2qM;@m^%g1xgFms7MUII2rw1)Y2WwX=4nt z2x_JW2)wlQ2Uc0ii^*Rvb$*r}I%=r&eF_VW`D&=6fr9H*8Je&(pvJltitqsLHy@g^ zvS!{HliF}AHxz^FNp6pwB;6#s0K%iMdT;a6-QdUOc{0}5zTO1BWwNu|^xSx8-C#^N zD+u!Cd?-(}PK%VS>X4>=mUT4%_CA14n984cXd5#QzlXM_HrAnMrriu}?(_S2e{6Q% z$0lrUm6x0f$zvFh<;KLH!5{`WA{SN+%+sF*FJ zLVh7LNYmfN5yTuIvuF}6ZMnn93GMCcX&g(n#SMP+ahJgSUbIGiFpvn(LR~-!e%sw` ztD>n3BxxJwM&+qw6j7(%q(GiP+`~%o8$+S~R$d#6p##%M*|_AI9U#IRf70ApR8I!Q z5DO&bY3QF}yp<+jYwG4t!j2A%rW-iN5SkL}!^)^T%5XF?oy2&~1R!+R8glrCOB#S} zQDHhzFFDe7$(LOcI4|})c3jKQm}^xU_TW;kVUzmvb$pI?8ErDD33V4%C$?j5I9V+j z{xsSc-u6qq{t>8MQh}N!{b#5$=ttcx~8vKhr))Ar-6eTu`uGk zp3^54%NG;aAA)@l{;Nd1A0h<7ey#e(JUE7XR}?34M2YS^MyVdr6$RfL!o?{{jCsB7 zXiQ|o^?`fpv3#a z5E*J4XNO+3Nr;^>JPGc$lry^Clg2Jjv#^Z@#K%`K8)ZG0J7}%#5H|hrGCHYPnBz!K zzZ}qGL;KX^Nyn6BZHboJH*J0^NK8R~*3KgwGXb)iHr=H{pWrO7Apes}s|!C^ESl*DzDCw6gu$8dxHm3JUge63n&iozuBlfo5ysfp@h-vD&? zS0U84ay!jGVgbG-+?>IyD*y7qYDu~)gYxJkbbsr$x{A3989;1B$wxZ{(?%GfbG$g> znP3cxzXCs_Vtw<(Zf9*F9n9-a56gw+eB$Xd9b6%A_Jtj*eANNUtMjDi#KqAzu)EnF z@j%;U!Jm(i;YF|bd#B#7G;g)}eo_u@Zi39rY2qw#q!dpIJ8r8`=^Qf}URRKMY5bkc zF8oCSko!96^~7M45K=tIu+73dzNF6QIK2O4HQBNy9D8v=P}>keS#uAn^Ee}6QKG0= z{^t)m!APgGYjH0#anI;$F-+X1lY`!D0QB~S7kY55fr0W%qmlx#`(Dsky7bHwcAtv3>Ew_rhdvq&% z+f?{1AQOM3_b2xRMN_f&sHv5bfnb^g^{m|3m778xE!`YLP#w+>%8H5_xUzzj$`D7* z(nlScJzE_Z#Pch&Xy+rnYLIEpG_pU5lK7rVbIe7BE&$Q`v>O=8BT$I(9(VHtJ!6t_HZAkC zgAKzRWpRia1;z(GQZy#~p4-H?K1nW{NCsU8ZQphMX?C=^34G(oa^z$PFO$@9HIa0* zg<)Rg+grkjF}FC`3Q8OF$)k~I6@{C+6Qc&^Y6b?3#fus~+bbO0W=MWHByk^*u04p} zKOeC9G=B?<3?DMpii20fOUY;-ami})s#63~(yWg2H@rP2X!)}wpSu~ND}P=zOleS@ zX0mni)1K&3e%2v$I)AM#{cZ3>I8qlWAyk)OpiQFD^*BBz$jQc^s0pM!PYr$ZOzWVI zK!JF8cSk_!!rh#FUt47QG$^*&wJS)Z4m?tx|0_`_K}1WZ<^ zO1XHNpScxaAL!o9i8>@U?W1QeLM(Zl2%K;}%5(L^hk+7?f;JJiHs2*OziygZZ2QkN zS=L~G(tm?v5-jsU&A?ZRf2}c6^xPSXte(qEY;AeLP>K&Ba_1H*>a#Dcd2Kkza0SH~Tod{oCV`&{W_60h0 zv5xCBmGgw+M*fn4D%1Dq3{1>+Zp>&Iy%~%;>f8mnCzkmph-&_CjN#4z<-NOce9ay& zfAz6T%!vwkemrhe5WQ+<MMJnJ;mX90Luq-GTmUZJ^-F z3P!h^wqH)O!r@AmfePQWl@$q`P2UTPW9n)}d-E2YrG2bT=0yVwoMkSeQ|JO>$d5;}_osnu(6e9Y!Eo(;kt zM#l?X>9Z190sTNT922)husZ@7RX5*O$nBM_ePCs_QsYf@# ze|rc#O6SS?>D&L_Xh&BNF#Y?55bvMxujd8zcTZM0D-yvsT@X+hY4Trj27_*qW0>u| zu4C+5yT!6gpm4yGipWc4>9VV@m3R>cotorx?a=B4g1h{`N|ID@%4GKtC{yy`(zK9YPm~v;X7DE=L&R%5;#yc zuh~P|b`3n5F%9_Yw>jec2`gOM-98Rc@Ys|*4EY<7r_%VxlxT8pO%yl-HzX@#*A7KY zUg=l|S=(zq4^gCHRTM~KPSxevOx1s%z0akL=lv`-tyJSE_xOuI7eIxdeoX4czUeE? zw@$sWwwN|U^|2RfwqMflTd4N?LZ9GI#=^?fl#&$YXzYCdarw3;qxCkU_nW?45x|vA zA@Sjpz7*1MQ!$(be@kL)%~2v2v#*DKcRqA&#C<(pK5DDW@P=A(?yTjxKeT)18&IHU zJCrLe?~oX{5ZTHWH86kS$_e>IY^UfkR*PO1r4IUVRW%ju67@*wm<~J1lkt~Nvx|wT ztm4|B!4+Yc0Osb8*>TSz5?bd=da(wak}^VjnS21EBu^ACLBM864$LC z0ybv>WXRJ@ZR^elL6?yXd-}Lt?U{Hb{HnXasR}F}C83-BGLInlwqa zf7|^z<~<{LYbgamYcS$a!2A6=^fGC5Vl4~PAo8hL1kDRq%qSwko48K?PK_@urzKnG z$}FTToVKib-mE(&Cw_V!5rn9=ay|kH*%d|}UTx{6uc%t54pE5hX`T#gqmmmPY?Q9! zY<2WZ70zo!Mcq?$9qf@JGgBL@pV}>dx{n;qP7M+4!S^d&G$w3VMWc5Li+GdKafvg&8{IB9)*LR}to5};`}8dAu^1ZFY_Mv7Se z>|&VD#qC5D@JT$Y*Fzx3SWrc^b&lRpV5@g#Vx`YM83hvLqGDi*UGqjlZ2*8D{jde< zcc~8~vbYk!D9c>3A1fhTI99mezK&*(P#1d_AxuWb13Ig003eYtMYH|_4W8`>{wGXZgI9$t z0x5e_sO<>EHa+;T5RdvM+@k#X>G!$3xXF!y&y5(F7y6yNr2Fs1v4e{I3S~%hc9MLa zeM>+8@gZ5&5rd;(?uzCFxZv`hXQI-%awYrfRenjmu7qJj%NK0$Lu zmD#p`2mg@0*KB(Ip`hV!B!C%#e!5vlFxbL{oEOyX&I7Km+0Fr8^4->{8tfGUY(M3$ zvmkhXdBC9 zzWrSatWTA^=Z?J^&%p>%FIZ`}j>TxtEnmqj2TYz-j`CBzUJu2<2)N4!%R9p3H+q?~lf(_74;YmUQ(IJrP1e%EK7L*+sZc&MYqH2ob0TBVX0h zUsmI)7N}dIc}HMC>N=6Rhl|8AGvxS(bSoo3r0=J&H_$X- zZd>gz*Z0VpcZsloV(FJ*5{MWzIJlxQU1N5)$Z%m-dw0ry%bv9MoU6T7*zY}0S9zZq z*$*IGiM#D3Vn}i`wk!=OREmv(Iec*}cRiXH%{F+wP&!O5P{-I}L)%{C3k>6VEXTjg z_`FXHZ|uH3lh%B4&O`%rw9&FsQQ8cDf6N5dl9gqL)7=ym`RA>f<{_@Joc$w~67d3y z3QF-cz_~7O-%1;ICKDsM@)i;8fdSZOo*h@T5Jtt@YZzJqn$qinXIHfnPIQMB8XKl* zo__3BcdrWr&V69r`S8h@i-57qO1eV{{oxJ-VB=>v){V96{e*7I_25n8RA@H>H(&=7 zOIkcm$81B3NT-#$!H0_iEvj#=NwgaSkxKDhA=C>*zOKiRstwtGAf zG)WU5n+BJF=qs(QfHEAW1h2DtJY=76D7>VG&vL(~g`yQ0lLRqz4Go24rOwbCxMM0) zwi#B8wCVH>aC8KhqefEhzG!gLYY3N>%2czzzQ1G7D`Km5{Yy)d*<7HbAB_ouF@pHXp}Qtl)6<{@SGVJ6M9XA@N7UZRbP50^i_6s2JaIk z0l0LWEMv3@m%$m_<2Kz#-qx)rBO(`x-(XqUX|%-nbY+-l3eKm0Iuh0UEzu zI%U;mKZqK-8{&8?od%;vo)6UVhZuUl;d8WL?wlSgEA)8(pt|L6 zZY3`@?KYrX^}GZ6tuH=4`&qWGA3nxF{k`xU3MLk==ImTW_c783mj20_aVVehzurH1 z^$TXK>yHuYPnb6dql-Cn&N4YfVt5i_UI?m9@H%~3ESBu&bhhr=#f}BcuLY)K0C$Yd zRt*ugKxo^VOPYU(I^IBI93)l{(Y-Mq}JJ(~cai$q?J3O{qA^#iQ8U z1)U6Sdnp6!gxONrf8gA@yc}MiDs77cFvqT}Nw4s;E)`&2WRR0h!-MF}43h*e9#k4k zA|*;*N>tk~UtT4VQu z$2dvJXJzAUPlP`pSRMpAk8yP=1Psb8&m!H;Z{vgl-yafU)L$z_a~9p*eZ-6GOVr=b z#Qs(`X%;(`d6|gvKHr_xU`GvH>o#zjv(9~;2CTN4-ggQ7;!^!x#P)(PRjC;&n?%3G zV`)e7J>Hy2ggbOkv$Z1Dtz1yq%PK8_et==U+wMWC`-X#NRe@3}No5)T<}*5A3Nv!Z zSp#yHjXdD?dL8-c5v_p|5rFB@eJ!Xz#O%SkPOExPs8`rMULzzA@y6>&J z?a2ewiZm6*Vi^{}e8|h2gHx*A>S&KX8zwp~**0r)Z1fA6s4D8ya9{R8v1(3lg_^#D zLoXjQm!Fq3PPcvF1H-h?ix#AzEGpL(hSgoMbH)T4Q{Fmq49t7ks^P+}Y?&GkzJuN= zsz&Z_6wK}tLRMs-9h})jM`Y1q7;)mAdvC>N72cy;@N8-C7$ptG{E?IRF~Zb!Vx;L& zl{_!MElm1y!$b7HoE}@lFpojRej-Q9D|Wb+#p_eHbCgby+&RrwxhB0>4W+$oee#^- zp9P~%1EtXf+YbIHd}hHgWs9O;zLnly>j@H zaRuX}c{&?t@$cBR;S+ zqg`5giREX!)KMeZ3$g@&PhO-+#udVQ>bU(na3SSoX*PSS$0?^f3b`B;8K2-Q=|pG= z$u>%0Cht{&9&vI1z!IZ>(n@Sp>d6usY9#L*ZV{J!U!WV=haiBFiP19eDQ;a{a}(WC=W3uaOMg{6u-WnSN*7g;?)kZP zWE{dh_n@Dn&D{Ch=4%eQlkUUdNCJrhwbbpV_C8MTj*Kmp_sAgi;K-p75Z@~4iX|*P zZSRaL{hInBPa&mzoOn<-?0Wi;Nrvc?c6mm4^4ranyY;nkhI&e1uo;fjG3{bGCjB*K zQ?pPwdqpP0>G_OSRcgb5qi3OxTe$fOjVh53cS#g{n>hmgX7_?%^TKvxxuaWuliPPV zc~#-fVkDrDb!RQ|76Ss;)K)93BF*hww2YxPj%{WkOZs~e?d`J8*KVza3KCw`3aNG1 z>G)z&ZET{At*>^WENsFlN=gX~+n^esIg{A0liL6)!d$zv&lI8CrK5`jw4p|ki}qZ( z6D_BlS<_Vy1X-l>L-@EA41PEhk{Q0l5&Xr+dS%>`|L~MggHusm`@w7HaDmY{< zYfX^8Z78(k`uLdAKIDuBmfm`G!q9vFWLLg)Pzj*_<#1uR;9TMLak?f-`$Ct8 z)m-zij#f~CRtFf%k69ssc`kUvsgN>q6d@4Jt691?R>jTt9)YYC)32*86_eFvrpId6 z?2;Qxg@V_7qPzU+O~_uXE0G=8>>ypByDlBb+M~5X5Mu}ooFMYQz=NVwJvw~y)Hlz& zy7%R2j_5k|X#n20yAVs#VVe14^xL9!x{M{wU}cl)GMjweM8kYMzbGg;*Ogi~xJw&c zh#d1Ii+QE((VHB0+u%k-7&4KJ7xd^$@|U=UVVCmbX|Q0%CMfL=OLNiZ2dDO|!oAKe zNayZ@Uw5t3T0i0dlQxg&M$<-Og6;Tc-2ITw_qaq<(H`|x`4Rcr*4ojskUn=lcsoHO z0z_G2RK&oi*5b!z#p%EYO*tnejQQ9uc8$+xe>Zh?-HOxsdat9!<(}n4!$`ceoU}&J ziUuf{T{rHvsP37WP8a~)=khx$F1lPA)z_^+81GM;wHRgCy7lm8`XY;^IwoTY)os)l zj3lM@P7KoOt?w{7ef0lS60;LB5dPmGjEsaF9L)bw12YkFGBW(<>c3XRob0S@|G#!+ zZx>_bOqpiZ>b@Y zPq042slPrQB6;cUP9+O0tixAB3UJ#u<$Rqp2!($x&~Knv-PBOk)D(!`siCQt8ZPow z1_3k!bC_S;K#kUdwJ&#RwYR@M zjpe7IOhCS+5<_7|!9W={+S|J^jt?8Z|ET|n*te=V3iMTH()eB{25U%gb8-dbRnuV# z(D|Va&ZFr8H~R}Pa{R>4-+3T_0|Be(UHD!D1gjas$a_;G126tEt?2Ibs22NPYH|wV zb$*v)xWqrs&3((=)_~im2?!M#*VH1w- zh_-&8)DkVOL*3F3sMy^67_Wcbc%}Y;4*q(T`^`$O8b*rQ`?-Vl?V6RD5jen~$^5f$ z|Fb6;g9uUcqZ`ckT4Mv83g}ZoWFPr))cNr35uHOj1gc96TIcx031}S^scQ@k(AEO-`YG&{ulQjiglTfB z4-l{h+VuJaLWBjs@85l2Ft*|2^Fu%cdMh}D0`mNB%SZq@KYTxd-0jM!AlRL`)%!$a z>}c>`$`{H?_!)cF(g+V<^l@A#ws8T=IrBE^n*IWziI;O=|*WoZld zmH>=-3DrEPrejx+3e{8rW8G5~S9q(pljFrbQCpKU-hbTydtuixA}V82HVwHQ^pz}) ze>SAOk!1W$S|{{%=s3Ubhkf^yV6qW-Q{W!3{OE%-JkP%0R~CRW_6|(tBsf6JGa+}MGej$L z(p^ylRn>*6k^L1To5QikXJ+42u1lVpwASD?Yxo}Z9n;!=WNXT^M)lOL!zp1d0V_`N z_jf-*j>%&j*L$UK{i%{-u;=RX0xRgn#Nw1RNz)N3f<*EfC>bC=Lr#<0?he}hMu^wy zit#oH#6^SOXT@e^JLoxBrXvQmTiNL~9#UP5K~3dSG^9xx4P=4H!zLex=v~c z>2btmiKNexJ1rVb3P6ey*5Nose2+CESm+w5QNymeQ|t10JWyZ*J6=0ZeljSNAg@IS z5SVc@E%++%-5QX3edxdxZqAAys1qWs-U${vgXlml&iFoo&_bFJ2=#9 zHQhn@9A4<^{TGX;*etLZOYFma>alIPxR}$FqJE^&^#asmhnpK|+$lnQA;l?#a=X+v z>d#)XnJ~%>qE}^`p6W}ziGw;^dg8A%bz0(m)tPC9zHX`Fioy`jlh#6wqqEbAzI-m@ z@8=$)KD7XR3u~XG`8Qz=w$8l`Ik9bZjL}ga00{b>#hWLZDitd^;G84z83a9j1 z<3qi=Mh`OmX?;YVPAN3t}U-3F0yxI}b3*r_xx!lz1dwoJt&e0C8yRWn&qt27P2NiCEuWSXYYyQ(==2iyS%v}_<8 zD2W>p(DJD_-(G;x$$SYxk`r_Ffz|xJIRW&uz5zllxd~gI8NuCUa7e{^F^cWd4Y(hs z`ulObsgnX_#B&eB9y!Y&y*6xV576 zPq+E7himYMA{o+WApsu~_|ExJ3>TayagzD#U0hp+4p=64eN$=6%;;idx3F)q;YwTy z&5g9Tf$c(9k}Qe%nm8|wyq7ky2+gmqb)A{NVdtUSL~b&{>G4Ls&X^vic5Hs?2L_4&PN!{A(6M5D%Lqtl8bf+S_R9XAK z5n&hi=PvEKi@g`uvY@DdwF4>i3O6z6Ry5me}UOt7);z#YBls9P%I(S>jG(h$H?dBQMEF;KfzCk!e zFUqmv>eeG~2}MjpX45<%fVd2|HYkZ+LE~9i!#YV7$ENIfShKylatn`u%gFSz(gVJV zwLQ;r*Jg!GP8;w*H0K=iN`f?l6nzq5gcL4a=0Vk)sjY{}Lz?i~=R9DBE%`&KsUTm* z@VZRbr@cHGw7JZwSbj8La0DiLcBg9d8${jQPV~Z!S@N@|^o)i{Hi-ejVr@J3$qgnc zZ8YNc6YRGOdSnZYvr4p5NwE8Q!c}|hv6qGB>y77_nrosxVllaMwdw?pE9LTlp4?Za zJLf;EY&@2;z*>?J>q>(cDe*JOhu?0Rp*UmGT2!Nc)4UxBo*iYn>%+PW+AH()z(ePNZ8 z2Ifphs6t2@M|1zcbtU`Cfp@2pFbVm(6nEQ;vsh|qpcoEwyE(^X6#^l_Lq!v}b#3HV>8Y z&JW+IPXqEze;_VP3Z9rD@~MdlrLu%`G-U^Yu~`j!veya);a#m~>9cwh+46U8Dh=P* z=-R0L9*$O<>$kaLRwEisi=JpnW(tMq2j24w6vF7V3GvW5Cbr!?z- z(V~4y*%Hj$U8xAAs&qv>`q4-#3t9DK;&JnFiUei*-8yO;--X*dtkmjj% z67^fxcmH4-pzf-O_0%6h%Ur9JLCMJ?yH?HT#~2*$+M%J(zfi&xcP3H9l38x((zjqS zi?AK7L9tauWfWr_y<|xe#p)A>E3I+u(1xopr;$5mJ9PFzbP7X4;mxCQb-feTEKFNa zQxXH168DJ-8h;q;iTe=sbh+8>@QSHD7{`}~+{h5NvXnDFlH@<(zpul~*yax?1pE}V5ZsMao z4)C5`3ts8>GjxprEfw274M0pO!PDrOBuj-ZNvuvNi2B4c_Pxg>Hct17yUTj4s|{!C zV=B29V=R~ERx3z9(*zzviF@$oJyXiWcX{#HeKh9O{*0o#w_GN!f4bFhiH+B?=3$nf zrKF_HZ$Kw4G*0Wv6*JF&c=r5}ZNPuu#juDN5;)B9KistC?7lA1Bu%r>8Ktg-CDW?R zVZ=JDvD{Vyg`@y=^hu##HZs?sf~GMtO%a)hJZ#+ljv9p`D_%eh;($23BGZ!HF3&0R zem4*PGP2H61D}z(QnvSbd7GgX=b`-=Akwg4_|0xVXL1%OXRi=vuoiS-kLI}4Vs8jV zCfKpdALcdl=7wjp22FSI?PP|mP4G@-^Ir}fsk)x#I363)`e`Cp2aM)AiZI{e5O2vb z76Cx}-Z3)cN3k)FhnCz%56!;F^H*TNCQ6C*cE=SW~ql6mYQo-!D`6`z7owykA zpl|UfLFX*1UB+yLzHe&Jkw)O0q17#xZRJBPU*#X^rRW63B6T7w&Ntst2gP(iQ5zb} zlzL|_l-r4At_g|Qq5f?WKUZGOBKD9A3tea|pS9Yq*xh%yBIetI@ZW+IzV@mNI&H}c zQByrDcmbAJPl%a51hgg3K?yP1n9!g(ny3gi67G z4dNA8;s9aIm0MM{yMItIv@Xf1VEoK0D5{1{iWKi4(LNbAS^~<&FAW<VUO{z*aq3)(a>@TN3}9lL6Hm46u=n)49>j8oXJQ=awL zV>Ex5na2C~gcoa0K?|(>nux6KEcq*{^+-!d;p0zh*aplR%uYBY##OV4XKgCo0+I*P zU%CB6FbmS)SgKc;rYYOLt_Sz(Z}meOl;m@X+4lYau-F zg`*8Al)kCJ1C)e;gvivvgj8jO{sSq_+qZ!*p|BEgO=F08!(J}u*?q2Xw%o_+0alP5 zw^be@p38IHDx4;BQY^bXCPT0oE6+Qr`&n)}N)6z(m-Hy@6qLF{*^Qm zd-r7>PvtqS8ev_T4}9sbrHH?h(EBN1K*6og48@%W^x_Q4C+j2N<=Wb_=x-A<7k#zx z{w{vUz?3qhgmZW}90ST|wE##N^$_%$=+&=1#g4Q?@l&%UShUm;1L;#v2*aMhd`P5^ zT=iPYuCv43vdLGyTJb zL(|oh46NZ1fFot0h9zF77z_6MC+5|%mqANdp9v$|Vqn*smo zq=6N10+M9h>*znE*_A4SNM{~?UsGOuB%3swSTqD`=P$sjIJM=%Z;K{&z(}=%*|F=R z7M5oBLJr4+x2XYd#WmI&d8gvF&9 z-%gJ&NN0d>+f3$;5MXj*)rkA3Zux^Hckp5xfO5h>sP+z)V{-T;5wWKpak$A}i>lJX5+Hh~n4o$;wjEly{?hx@|z zn#0uPHRX#-2M-heOb-(kG>`R!kB>_g-b=Ia;Y3+YodH!#5%S@p2yMmKbf{nVq@;fx zc+aY?MM|J!|9L2B>=kG}NP*IK1H;<@lYKTV_reanQZ2p}R|UY6}rHF|?QX z=56aI!}bV(EClB}if_146Tucm@pE=}cA{YiVY#&@t4}P#sbJqwr*zVPVL-ckRCgkM zQUbbJYyyz7pCDUXKsREN1^ihPTfkJj5d*Y}M_pEadNC`@%(PH8!v#{NWofT~It6RW zTY<*?ytfZ>uOE^An({6=(Os~CIl{8gLuWK1YeK>J?iusObuTB_w2zDS zt+{~&bsDI(UvGR(eN6W@6v@ye3rOxN6NndUar$EYH84FDLq`*{^yFf^vY!Ta z*IA~}F1nJiK-M*Yi3CgFQ@moiGub^9P5vogi;B_1fA7;awTo*4B9|CDG)<`$)FJ?8 zDdC#lfjS-)py3^V0^0m7dJR_dgt@8MyG7&Kb2h_y(s@-$+0p%N?-RuCI9IS-5+ZdI z%9q~2&%Poggz;!tpvS%#GD>-A_-SKR7&bTekkesJYJP?*N`yi%~YylUct z#ISU`p)AN@zM1Z7D)4O>qJ5!oe*0L==(xuB$Cq@%hA*M1$g1vFf3)qdKIp0mBC4&J zFUR<*l2~$y*S|tee&PG?D5KYyve#mLMiDV2j}y`|XQs(yC{a+M@>8@|OCLIsAcy5P=H|1*d^obCwm7l? zf>eZ!f5+x)$=T1B7&)IJu^f{Ke##~hpCzkmYu)Z#K1%=iZy0fqzdAN#Hwv*!VKtDU z@`>hH&VC#jvpv{0X-{ecBr<02|I@5pLmC>L9H4qHy0pZs%$Gj~(Zn35Wu{*dO}ZkW zrzC)fy{y(G!>wwXXvaE201>l0o;;g>R7)02cwV+nFDlYj2BDqL(e9w2DUGk1NAel; z_s27OXvzsAJe&4HE*E3k9i}^Y(+)**VX0Ic*QL`4p2}KAn3pu0vLaG^d3M~k$=)i( zPRu!uYq8akuCj5s_~1s7l(g5xm!-{MnhmQW2HLd_TE#Lb3E9ED@nX74Zpu~w{xj_5 zL=vm$)GpCib2KDq0%n@nD$~15@{{PGZ}`|D-k7M!hXZ;gT6(@^w1|sex+=u)vDmpT zCPQ-Or$nnYG_uLa!5jCUobs(BJElS)T`}az+KOj0kp}soZu0qm$=FIM*YEognzQtT z7m=ZxyiQC0!kEkXCt`0LyB;9l=^qQHpj_0xvpO(SB`j{f`}gDTb_%-jwRYIqJ{SAH z`9~HzSXfFRGSo%31{B%E(lD3GfUY{k&Cy-ebpjU_mct`FEIQvwA@JmZo{FheOjvR zYo8Hh<6e}{>8!lLQJfDrwl_VFFET3Z_3)@@&AT;vU$)M1KY+SuW{RixJal0U;@BdG zoR2_&{|0pj53wPgrJ8b5h;9!sv85=IoUS6`{_Fpp&&O(k~qb;27QAdG+> zm|v*VVqPzLzOBIf+1MPMOtbdCh(>r66j{ECP_2h}|DJW!`Pba?@vfM%Ah6>DZjy-+7x3n=p;oYZH z+Ne&t&!QW<1%#*$OUgvo+7s*E3RpKhJ2qyNQ9OQ)E_L0N0(ng7kpidAFE5T#$_`K~ z@7#{cL(arZ3k(fifB7%z_g-L{0Q)9rs>nFck8G44+uIw6st{UWaQi+DtuqkSlw25? z`Qy{SfYKCIpzP$xTs4-9W z4)TnlZ_bYLZ-H%=9IyY8ZbZG1k{x^0f*64xz>MzViBUimVfBYYO@<8~Pl)mP$ct{b zKqCZkpF0g%#VMp`^=lAWCX)N(0)!yQpN_SY$k9Vn*Vq}n_Or+hEs;pYW z-6mRB5IcSg+&!;BHaLdDuwkVA5xng(xeqlYVuOFF z?UrV|N1~VL8{Y-Cc8YP#SLutg68pa&dNib)AP@raU_o7q>4+574p+zR z?Q5|Y%O0+j<3q((Aj@1JuhQMjx#fy@Urv$C4!#5D`2`owDn18a3ILKJ8PVVpY1%z> z1U_i|c!q+;1)*k7_xCKZFCg3Bb25LPqJn*|$060j$Wv1J82%kfSaALxM;vP3a08FDKqSeLv+v#N1s7t5XDoO07q8?OyQ=?yErWW&jK=R@vXcL{Q=)fKFUk;T!L+NK{i|InQOh!)&yun@(m*Gyf~(ybdpi~sBNjkYz6jr zP8kI-P-qd*3Fp$zS{J(!K$X8KUid`>OaqH+zJ7EeBuqo6o0Dy6UJfv^C&6x@9@~h} zK4T}+=Kw8>Gj_&T9Nq*AIN=j{Hq;;e!{F5QhYqaSpUPD@CsjZT z!Vrae#PWXY>w4AG$!&WFITKtt5JjW<%9I_Azr=+O=SP*O>-zJmPEZ=^DSZKm5xXAP zi3nhFL-Ekhis(yAa^!HOcpp=+bjqAF%d4X@t1uL3)^=* zL}p8KaPBW`)t=HJx3QV=szLG+@gI`!HTls=gw4gs5V2~?!^ z4Ag0OD!FQolw(?KD5N`Pd=}uk#77Ky@41}=k;S+naPB}%qt{oYS=0vs6i`%OgM?Uy zSHb-(15uI}l3v7F=Siob@hh(jjTV1Cdod_ebGn&{2nLpw@@s>f+jq(#FUzGY-0>)Gk|y}?Z@%kGOyZD z?W1u7tq^_4en%1Hy%MoQLI(T(9TAokj27hUS8{1ma~GdjXY0~s<>LTh%hGX%R=fNN| z?Ht9H-SMVna9~SHX1;A1z452-B!{j0^G^q%t=vJYMgt-R?Zg8sW#Z;?+-zs>E9IEv zjd&xoi&M!id_*mFkz96WM`_u!4hwbeBjLcDf8x6QQ*7xL9b-*9G)0dtt+|7&vIXsj zRiWu#zn6Qr2f5qK{%kFjZ%ecQbnE3Raz@CZJw!Gcj48ht|M*8Z#y1~8nOWIGquXX15ds=nrFTFx%Bh#0MasWL&##_?AcKxFyE)flCluWn z+e86gfW_)c#r#Qm#SF@2SI0y#W)P6;z55-7TjQG8O`uLZJvHt?^qHXp(ss?x=#uRt@c{};l;vf*Ry~DJy$lfgEsrvx9+bAE#UK*0JcG$V)|IA z4Fc;YJY9#%M>=4`*jF)25@%@?C%4LI*`S+Rwwi!_D;*gMO-mf0=n4t>SMhe{HtFD- znw5RXZW$Tk_rAWNx91S!eC4=L?2Avi=Xc zNrfu8Q4I#@hsoPe$Bte*J=bBw4B|U??7v$xhBE@HtGc%rW_+0D;Ui!ym+*fGq~SOf zW^Vkbw(Nny&_xW!@#q@HdZN&fBc+uA?t=y@u1(n(1A{G7=~zNZt%Yj|PZVMe?<$kY ziT)ArxB7v@_<2m$`21Pu$L1_Ymb%jeeZw$XfcJ$Xm9pL%=_ z>4~94AM227KH#muROwCWapks`E1}$J71}P+i6gGzczS3gjVE;cW zG?!hCm9(}SX!^IcvCOeCSK+q^+9U&*=RmA6Tk-n?JGnq0Z)pAZ`U%+m0RRJ#vP2zv z9A`dfIuF0wURQBEqj*%RD>5oAYScSLfz6kj*@ar-96 zgJD4|g!EkYdRylFJM_x_49yE1(UmeITY-5t7*FZRGHBGm0*I?x0Ex=~$X#&}OXZ+@(WU}N#8(^eBZ+|}7n7R0Mw2y6Ntg*uW z^j(w-`UMwua|{7qJ(LnN`K1RQ=O%OXL0y>rHhtk74Zzm9HU+7<3u0IOf&n)I0#{vg zXIp6jz=N}ia(@bqCr2l51ebL7szUL8E3#XaAa^viL|_%%hR2ePak+tQ!p{0e~itp^;#>1Lhr z`*E)ULVHiNeBr!)njB&0uk7*J(EfV>jWEzB#6UpbH!Byd|7=hZi-6F;_1yI|mwFV} zX0~uXgh1Jum*skY))eDc;K7~dY{{)>O9$HkQ}il**D6ft$%laThK9$VgQlK8-&ZG& z%E2W?!MSFALwlJU!GZZ595OmSDy)9vCVIUT6k>1zY+-$L0KjAM`TRmdRUz<+`(W=#BR}y9-eOn$e}}sOep*|J@$=$W{}f{Oy1w|iUgX&vC<>WVo*O!R^47#ke{ ze+_j<_seZN$xN1UwDEiSya8ZsXls7&c(enDGszyb0lt3i{Bt4RJOLYEQDv9->sm2fT7`P$PaknL&a8@z-}X zeW?Wq0M3qksR?ia#2$4~+345+jt;IYz(9Z3KQ%E>Yvj%rRS#x|c$VK}H=!PcLC~)N zTljUe9Gl=_TB>1edMi?j{x!GbqGf{FToyOjxzP!7Y1!7vC#Y3000<^l6!@5?DQZ1yH^ExW{(L8cP-uIBsI@R)Qok+)R|P?_3IFfG14>JWe&5h@Z#l zV=I+ay^Z3sGohs}zHuPMGE3N7o%MjP9uC!Y=1y|V*W&_jmw(q## z)l$YN`{KEaNfw^np5av#o6>5Y(^iU~qAWQKCqoY1kgzTvIkvG0WtNE0!NZwVrVorJ z!euKfDudkM4?g)ZV?a5iW?mJNuss8^RVzE@fAXIeJYnm;T!RK{fNm?@vuSH0s%V+_ z-u!{DKpLGh4&vh(QFcEd>*Tu-%kSSko2k1lW6Q%W^vNcQK@1}I7Q){n3GpOJw@{7r zz%TZ;E-(F_mLcZ}Bx>rHE4kCd-5)ikc2M(ua*)xq>(?}yF87&I!HkYKyX8}qLofbLQL^h({g`9~uQR*yxM zK4#rpqR#fWx$r!!jffq2DFHN@#jkZ^_@_`w@swow7atJSPGjYwF?CQRIj}l;10GAs zr@K4uGPz4DqdYdkI4#e%?jq;f+${^-Tb4L#Cnl!9W6W)j>gZN;6gN-iZ#a{hceQF> z8j(;cjfiHLWT5(>_Epni&jzk3iq0h-{N0Tfo|G)Xc)C!vxwhn<-HIm(t8w*(AWG@n z9|A6VPS(FJ3<oMeYAg~c^|B1yX#c5r5abR@-h)M_>-T3m&y%bD>GlsO)>J4k;rfZn`{VNWsVCY z2gS94&4q>~ZB4{nOGDLV%*J0L&CV1zHdf$jq~QBhDRWQ!w0RbSjdcHukLmIFv^XY> z#GH}mHxdPvx;1U?a*@8f#P4e+Jla^61z?Gd3)!BB#)s;=(^iOS+JQH6E8{#JP1#4> zmR5*s_u#jvLXnA8mSq}_CmG3-;#kkq0Utn>3Z2INEZdJ+%oxuUsd&4!>CyV2Q-RK| za@LV6h+t?GRB7ATt#OfMpN2j!esHHK2XaXNPa^jFR5vPQtAv3@p>+bBlCXt)yUbJ? z9d|=LgDT>%ON`AYuN8?`b(J$aA?daqC_~9Df+kCuh3)XD(guD|Itf-acJFO}s;?GC zT8Lf4_%(lF0uqs?8-_Z>V?bnI*tGW*YVl2!DEYMg!r*^K>_XuF?O5U>vko-i+tWs z{GmT~yQXbPU16_OeVt=sLNO2EF{uL=ur9|Lgs<)_euvT2S<2ny^mz?YaM&y#GLh}I z(V)QAF-HA1b=GkOE*-9}j%gi>gvE^f*!qP|T_Nq(kiTpev63f`8#IU^J(F;<4uSk*Wm0r3vxwX@-Mv?(EaJ#0obQJ0xd6o!KynJTm(Rhu~ zBZWmeI*Gl(m`)Dx+Sp)|_!0{GVqBwj=`zm#w;PLNKSh$+)}>fs4di2O?ub{4Q1dNb zLzq9-xQENHUMSAKoH8=f+Wj`QK{?#`CSbi!S_+Q3$YkK83on7EfZQa(#F4tGjT4Wr z-H4Iuf)0#tj)luYE=x&=>~E=g^Kh1}KC=8Z7n@Uc634UTFjG8zVRfq(qBsl(X%`c! zV)K`(ZkqX8DI;mFSqjzUsuQ1VfHK84y20#H51D7lfp1vitj?5WyxvHu&_(by!7?}* zyq@?dMQk9ROz(-W-`N@XJ0(yB_5y&p3KFS0-$9AQY>-(IoCQazmTZo*v|(+xa^hni z4~rvr4Rd|S7~h}i?joD*cmzAuN>$3HUpf6>>)4{3Acov#-M4)S8{zefe9L4*t7Z)4 z(A*`t3MG6ejv7WQffS(n4~Tc;tyH#-lk=9f{2zFw3bQ2FHI%7XKFjnxpvi)iVf)RPwUB8V}PkCd(d@Hjx0)%v=`JreJl-g6{baYmeA!{1O zQh*>lzk>o3jB)fww#V2papZ`8PSS9PqH-5=-gFgw&NOOYJ*sa2dfQJ)ffvz!I4%yt zzGF6{P30^suN`2kuB?bq&Wnot7Kj(d01M7aC;d|@`>H8>WVB*>ZRo-a2;dp|ex*#o^b23c;bD zcYZqhIWOqGpckAp^Mk>tR&pdNjZ-lwO4L6G0DKm48wrAbS(vl5Qg#X>BPGg}eLdLH zJ40lvX1P>UF?=@a6t4mm?e)=HnSw!Qf`2Y?s}Ar;erkrbyfcMz2wRNcl4nT7P({Rv zql#pqYNEP@jV#6(Z6Z@l6@ytd&z~8xNR=?%a2yGZh0qphWx0g;{#TYu-4I&nvb%M0 z9HfYae7%dan`O}~Bp5TNs;X>G>4dtGbaHcV zKM}?9@0kAIi;>{{NY9M5{<6aSwstoIj-vB$`R#l4`zXRd@Wd55!idiYSycXk1@w<@ z`-V)i=$voZ@X$av{evYsNfPmhqi2^v7!u;K>%-yMouCZh*4H*)buXAiZUIUp5|%n* z?Q62f(IPkshTN-y+Zu^aO-mz~XGdGoYo4W$Dlwf18a*mP3s@H@VevF)?Gi1d1<^hxa@LqQar^pbcv2!kHKCkIbmOY12rv zoz`swWP{>r_e@1kfvh2>x~+WMO{)Dr9DKXWT)?1ehSfuMmkF8f zTRLDISHZ-PwzBdq#^~m8!bQS$(M{4V`!N@~$Evo`-&-l>`P0Pir&PBWhA|2-rm;z) z3Z{G;Ki5D?RNI^^CTkP>hXjR1e}eZYBKVWG2vQP()7kqjgQ zY*tZqTMqwF5g@z|GZbGoMD=t#X&v}GuIgx-ZmIkG>^$$UU?%vtByYyo{7!K#Ry}Ik zv_uXV3B+_Xk@2hl84vLbZhXs|C{7|Kftpj~Ngl_c<~?+eZfZJ4HD7?c8dOAzF4$BY z9@$fbxGR)WHteQa$`X`?^-O3`P0AXM)uxwiTq>0~zrQRoZJ$SXTod0Rg4#<=OiqKW zeXZ(@wBVV()ZOZ34>it3(%m$$UZq!Mx=t)6)+L>o31;PxaWq53KeZ#IOjb$jRBv1jBSJtWgy58NZ*V z%*1O1BL6Wq;N&F-)##9)WCEa&BnrN2T@J(~b`SYy2Ybv(pAm-;$*nrZ^L@fA?do8R zwFg(n=~xmo?FIU~LhNI+$N5-b03j!tAfY7s5#{$**(LJ$++@qpJywk&5{@?mkDU!# zW9P=RgVl^MrK;Oafsg>rU-$Z?fv(-<;8sS~!E{LHPx!*# zT0l~cbW9Axr_B8{!%dxhjA(CVHwsi0+!n zPQjOgy0Y~`1>zspIVYh(vB|Az`xS?hc8vqW$*p&28-y;uaI@4sv#?5oaaP9hs8h6u zz6)!-CD~Tp;=_@wq1qYbl;}=W-9xdAM~@Yzrn6b^(yEJ@r)#I~aM(N%N_mUQN`|fw zCVZW!83x#(ni)uYd)}xKk%Ex1s9aZ6=e;Emf@7GxlA$fs4WFsKTp7@_G(5JG3-q(G zJMB)?m!<>ln_qsbIb#!|`>-UtK^vytI5S@5d=ON5IB`~XiLohseRc_VUr&x#!gW7M zhRiyL%~7&OKHqZApfc0&VH2l@Jb(z^9tsleCcIR&3th`p95lhf%3)hcMVuz;8u$h& z430F>Z3VGQXb(42MgJPpNJ&vI zB+!1#oG=+x=79&~L+|aDd7HC*A9tKV6&wSG6a)X+otXtK_ejq-6_3l1Odwf z;CZDUwF;`IyU6zrCDcTRr%qFI5q2i4rgAcSbsiILPqe_X;FGMNOyYAKJc}L7I$nty zU5@^mcNg4bU!~1xKO3>T95cMs2-d5U4LS(BNSy|>=QR5#s(Dfs`_@r*q;p4U?xA8< z-S=mxM-kLs@v85Ie$l-hYL@2OfBMKSQOo1#TWP?dl_#s5&Q)a)Q-F_3bx?&0h)pc> zKbX=$-IgSAtHQFH`;oRk#<$jn&SJ!Sj9y~gN0j$7c-~=(>%*lgkpm!i<0!R(=>^HI z$mgK6QP`;G#wdr@NMgMJ*ioF}nY`U_X0*-OO@f<4iddMIaD1nEys9Y^F`(jk^+UjG z{Ky0tivk#AGmdTV-4GOk&_Se7T^GB<&~lMbzPQwb7zY96e002cwF~Abh~CczNn+Z> zx06}szNs(q@ip~(E8r3~HJfoBtohk5U-rek7zK#`U5s zI<%x@F$|F$%hp6_9u*!qT@YgeJr?v;j<(eV>CI+mYLUh^A1fN-)YW-YQC1O z*%S2*WQ(vZ=m!KrG?pi+AQ*HkdN*0W360Q2V z-&g6J?lju}d&rXX(^2}HeHC$oP$ePMxHlL4S2>yrY`uJd3TQ!I{tMX`i07JHRB%4= zTotk2wDcepPy1mr8_)Fa#jTaX6^{jiVsw{^t474M%6A((*;Bq}e>J98t8zq@4nI2< zA!&CoD@*n-IRK$ZiKW1MdJzP>ny|l79U}YXWQO`nrIr2lS;%=(JQ$b^)dj`?rIX&8 zl4|&2%5d=rY}5&sZRR0jHtsg~prNH(=w?JwKuQ4fpW}LMfye&xTn{HyM)O+WY*K3U zkZK=}56%jeJ3HEA`9aAfcXSgN(w#{@H{~Y1%aGI_7jz@YK6X=gG9^K@3XiY9YEK!- zv3tpmXRx1#n4PFV4qG5t<;U4j0UXE5Y_~*N`I6#T=>-=Wv%}xiX*pVDwgau|?(Q$% zi<|ul87Nj#k|c-~r@M?{Sz$}THMx$76gq1;Lq##^#U0SF40`h$3o#s>k+w5-21=Yx zsCv@(Csa65@CL9Al{?=5lD2!bC~$f?Hb6FFysM-KI~Yb(Z&)l4bl^fHfgu!l%>dWO zY3!4$!D}M-(t75A(}jO6I{`e;10Bhjbh}Vi0IupFi72Z}HnI*or{3#-GjvB>Mn6Xo z6p;&csZO)}@RAc!&m1^fDoT{ZL?Uu61#+d4H=KsC04k`!TpvB9Dp6}?_?GnG(NPC; zx6Fn_CB%GKSK6>$erAvqlZp$mvj9v6C1|hEb3Lqm;-~Wd$;SfgyEX$sh=Eg5vR0zs zahWCY!yW`K9GiR+sJ1+>@g?m^Jr53N%*| z%5=ZQy&=QQy3nYbUd~(a6Jp-r9EiG73R9=vJ!1s9jxZ5cA4FF6MFd4}{*(=?6(qRz zYt7(6Mk15G3US-K-e9X4a8mq}`Pzqihz@f9kjCBcJbu1W?VU25)SKILcnrFzPS}nW z2zjiO2p1t4l!2BWf`s;++sw1o~A2P||ff=INbjL7Jc;Udggfvy64Gp9YIxXbTI18!lV)7c*?rB5z}$Z0yYf^mw?V`c4<)Zu3N)cadA7k3+9w?CUrSK&yh zf+dNO%#!Hvs#7)X?&a^>{#@OHg)u=qY06Yt|Im+9YcuEJ0N&pol23^ki`;P}tU$a> zIUIW#hknCfd^c_j=}Z?F(v9KF0OYz_FZK5{WuZ{Y*_@Y+C#e%!i|+(3^sSCfPYOat zenHq*>B=d~;PhId3?FlwI?P?3?`1)ias%-G`v2$fkS&J_yq1u5u&q+Hq3v zN<`#Ys&D~WLel;h>2WgE5$q&)t^2H_Yi0wb9v}f*o96WD`3%(>d;=V{kYyh7n%KKa zCe!;R>D1r|2Aur$POm~kdvVXIr}b}h7H1CHSMs3!mi%gU!@rbXuP)2x%3kN?MvglMht#N1`gT@9TH*d z(;J7^KgPuA9G)B(L}rfzL$1Ew0vT}tVTZxuSoDZhsbYMhxf2xGEFA0$O2R&&Vvs*2 zoUq%3`9s|NAQU#k|b@r5B2$^}}Yn4Dsn(;}RO zb$K@(4F_6`S-gF9m$wt8>lx}+12Ppf})j1P8_+fRXA>u7AEH-7#Sn@f3j_AhJI=J6luG8`^Y-ev- zmC;a=eKwRKq`u#IJerGb@pja{>f8Nw)47F0w9C?HLvQ6#jpaG6DM;*;4<8mg$)CII z-^GH1!lV!J+jXGuZIRIOSNdtY5sR%MlBiquQ$kaNa_(<4Zd5`^Wfisa?MRZMRfF{+s31Zu8fH+=#}qRe^y9A(`vpBvkX{eOh?Yz+Sy(zDV3e{l>0BQx{=%k2NR zIfj9WnThHDSaj+AFQwNuUtj@oh3lugxw-k5((~KV{u^%M_GIi_U0e75jb2YuvaYjT zmfn{?9L<#$syv_NoTl0eg6KwsnE;4CowS~{31$oQ~LKpWn6SU^wO zfWA*;Cf26VUx7*K{_*|@{Q3ubdV?aewom(vAA|pTpoL9Ao$^Y*$|x+1$*ru-A)6fP zLXC|8&@(XE0iOIvN>u8eeB~%QHvj;m0EF`9d#y2ez_}FB{78Xu2EmyI@p^l6ns_(Z znpzs{zPYqiW)UIsROOHq<&}$mTJ?P2gMzBG0dAV8uK+Y;P*DMpMnmDTr?3HLe@o(5 znZ6rgLV6uZs-mg@f6Z6t=1=%&xW3C_heNrO{C-IjnqBBw0RC-aRQrFUksO=q-c)yy ze=XjA+n%YVX|(>O+7^9$Hc7s%o`RUx#MV=GM&W?=C zY=D|wmYLq8%?!NxZ?3CzVO?l`S$_RK&^P{!8eUsHvrRmaSpYJBh*Cf1p8d+zeL?zi z{|0;WzrgZyDSA3e@_;Ay+WiG%1?cX{+2-?$Rrql60PFju93JcYBKz8HM(0@jjm`wd z`Qh>pJfs`-E%n;M`v!ly^jId_>Ps!0`O3zB2uJM1Yhkffyp)`O5*anS1w} z^#S}1Kd>>kcL@MEY z_M<-Nw+n6s&i|U`>FT$qG1k4rwTaQ#0NJSUb1k43MbF6SGj()hV&~=d%-7Jji|+^d zvbSZVZ~o@c4ffY#+biySl$G&&O%s0()C{1L-PF1dM^6Q3?n90u>`*xo6LRyC6m;zWomlvADrZC)#lazvd}dC!GGSW4p@vQiV!` zjaST_t>iOH&@hk3kHXcSx3ime?~RRhz*twqu{}Oll#!75SMINa7D)`U)*K;F(lN=Q*5Bjojs}P=YEK(k%60Z+ zbu0L->OhqUc)C7E{%T6{rWiR|5nh0xFBJ4a9560Di7|X$!Pk6$$rTZh*yU+IP4${$ z_E)UicwVP+FZGd0s20iL=F^q|F*Hu##{lA4OD>5?@edCSAISC#KaV8IcF$E=JPbPvC+3d|H}-bCP60<)d=RERTUjD5*Rov+y;7LyuD_Px)}U z*YVaFhC|fd_4OT*oOQMJTn4PL0x~z!?AyEs6@`6WKayDaDERo&%3RIaCU|6=Ht8(+ z{=+uS&8*99^}1{l#jv$xDM{c$yj5*CLfa+w`S<@ zR4Yp0fwXh?Ov1^oK;BvY{=Wp7#r15FtWGC!fC^u*g?YTMs&arPcH0{rp{er2WFhC* z3#%>C?8Y=>kJt^A>B}@Vu4I_H7m+3)V|Q0eQIRIkP-4AR#zfRvD;Trj<8|UIP?}0T z{bpy9e-l?ez zk=)%40oi<*>HhfFcATPnn|%TEfzRib6-JC}z&(o^bAX06NP{C5SgyBRd-S-&Vp&t@ z5(P^Qv%Us+V*J~ffKA)1`9K8!r(@{=!dbqb9|>v_^M~LTi9~Q?P2(xe=1p3|R+Wy; z5h)C`Tw>+X8Hm??%kXIF7A8G<{SMUh0!df)xNwE;p8+GU_ah)Dkrv{D839jpWs+^?^`UlCaS49vi5SRye1xh;D9M?r3Z@B1i)|f7lU`%L@D(CXRa%|3Zx& z^ki_8Ap7={){>a@>e>LZ)$unK`XA~o{qo5z0urmzCRS^-j7!GH*Pz`KX_M+LmtZe& z9f!u{r;x^h3>rvN{*O*zQcNi^HJnigtW0i zI$|I7+jX6q^8v6$X>$PvvaBLK5;r_vK3jHs{cIJt9dt|(XZcfB0_{fd8X-KjKS&i` z7oxkrG;e5g2bUek<4d3>=QpIKWU;kW-}M&>h%LdM%j!xG!OA67cnisRLF+UR2d?2_PJo7>g^ zpP&89kC2Ke&*iz^pDo7}I|3Xm&&N$%mLN1KgCh!wUCCRXOpg?E$QeFbK0Sfe`VBn! z`w;k46}%MV??$FqYLdhv81*upMEIPSBejfCY0?-sEy5Nwmhh0~e<~yqwZ+bt7-JRg z1oI@?4qPVJ4yn@*OT=tTW@FcxW?SD1FI$}xiR1R3MYFEDX7kIA3~IdmC^ROOW^f?L zHDXP|0RT3v?LcIOONBY59l~amq?j-aog!Yb^Hx$P6cW`6E059fM=Sa0?w}?fXXile zJ*LRYihoxgb;l1aOyy5U{!$9x=@-|GuZTIvsI6@O6kNxV9QqidOWg?#-MO2Haq*#| zS9#%YnsWlF8@oA{-6q{I4pKFEjBk_RZGrux<`^JW0cB=p$ypQ=S2&b%^=$JpB?+{Q zd{g1hP)^%BkMgMs#>(Wn9rl+wSqx=Y1$NcT7uU+Lmfbt%ca4&IUCSCWv3Vp=r<{~- z8Y5x#*_xcGymtvA-Hi6WY>|ufwEVytCvAHfcHqZ9<|aYTAGrd z+a&AQaBTB5$`&j0&Y+Uy%0cmQG0kDZkIRGz5fLT?;j#5hn-vY2NUDLBe=Kb~BZ(*M z?mlYkRgkQ>{9=}6EO|XKLoS|3-zdk>MaIp{$OTiMEXS(Vqs$f-2~m9rqB^ zmWdrmY!-i4nu8P*Aad(cGH-L+D5^hdwA*3cdgD>pM5`vdAq79fAUtciO!Q z9Q{f_{Pt~s{n&`0J)(M<5W!Z&#G$b~k0y!5$&UND;0j>V{|B@muVy9*0R$;P#f}w(XN) zlXH#aCE!A^QNDo=!W$`~!czF#v73+1k>*fxG_3fSB<7QUZ%JRZzC@Cai$*UYuFSuH zjU6$Ct|2xBf4X~(_2z6<4aw;s$OrcIWXUQ4=@yJzL*b?k54HmB1?VJL?`XfUPP?#( zu}G)$b^J3?40J>S1k^6qHo>EUJAp=TWv&$)<%Zd56bu7ThX*K3<6QiB#1*VA^r!8E zS7xGp+IL78O>LSAAm1VRWFbK|jOg!kDyh9fZ0hp}?zP|0h+#qZgsUN)f9R=}KvGX< zhR-~Gj0k19BEdG82GXX7Xeu!zHB10-XZGN_3{1dMn$C@4k(4-tYg1(K+lE{WKN+!d z@i}D4YH$iSLN*DfUxf2v^`_Y3--bOkdzWcA2=l8Ac(^fCDx@ZMIrF?v-|&2QZ)R{* z+QlIPd!!-nEf#T?r)ykIk>NqxBEW%->c|XLQh-w+hWIkMi)0aRE>#A!LG(GKT9%S8 z4bu_-Vl7iS)8pNM`zNLow??e$V&VNBtQG_An) zhs{T)3{JjMX9B6XQ@hUiv!Xd7+Dm{o2OQ z1?fyQTy5O#oxK|p9Ci`w0vQB+*uH*M^`{$AQ=q}8ke)G~kUhA?@0B%^Ti_or!2%JL5yQ3y?#YQ2X|1|`%%;vTM8o43HaLP8@o7 z2+d6Q6Vc*zkXjZ&dnj7}d zlqk0oEXSN85q1bG>yHHSo@~~=nWenp!y(|Qx1@ouKZ^=Mu^2nsuD~iAuHaVQm^{UT z;c}`QdS~h@nFi*tSAZ|k=ikLHYz@Dc5GBHb@w?KAG@wqdEp2T_c;bA#Z#_O zK6kN<>>xKq{B?=$L8!`{OT;PuVUQF%++bP}>Mx$PHND5hY`Erj7_2~gvUl>A6-+I_ z>{}9hw7?y`#pb_p@dS|}RAVghy89U`p&jQ@aYGovqb2~Egk%O5R0PxH9P4kkZyhuC zd6}oNt7>H^>HEG$^bsiR&4LysRPOpxyDQ$E1I_Uy4s~GPsRl9kOovu@>9vK`Kf8 zHcQl%-dwD2W_42r&YQ+msD~({?uqqIPaF4(@?ttL1#;Gx_WGtdpxB+YRl6<^VF?l9 z;>72*8Ew>SW$rKe$(UYkdZtuym!xAC05nPh2HT|JclttU;nf=7;W%X}<)s3=7{@Q^ zJtB?9e`q{B`w4LKN(Gc=|4{3Ajp!{ML$orVcgo^44E`TyZu`FV|FwsL34wB>segI<0<^vI-yP(mD@ zlNE7iM}*Q`_IeH^g#^;ElM-|8I_?wOc9V$KFHJrq4zJ6pJrw(V z)=5aD+-iaTM<vYo8{bE%B#AIa;X-om$1F8k8F}S}oG@6WKyBFD zsX-LkxU<$xBpX|sH`sFrp374m@M)%UGCg#+DA#)(UNADFE1l$!DVef&$|0y`1Adx= zCR(gYa5jU6=H?8^0DsE;9)K5S2;V%+9OzN7beqDylmJGvJ%_;lq^=_rFKQh&E*Xrf zO9%(&AW#dXyvrJP)e=Eq>rrSp^=lLP*Esf^VP{O9+S9>5aZ!gZ@bHLb$kF!qQ&f zobti#ivb7UASL@O@GP8FQtM<-Af<3o5l%z@OCw=gw&6YHVn4|y>`nKA%vij5HqrIwU>nE$M0F)-O z^&MNZf`?i86A=C3trc2F7+kc_jJbtGk&Ef-i+Xjs-Ez=zxOQ9Fwf6w4c^*PBh)sP8UW76Ln}!y2{c@N%E&af{ZWqalGpF&~BF{i1cg#VCD#GgEm)VM;0qt#LO z{vNve&EG0&5RdD!Roz;h1t9?*vf2>de^%sEQG1^GID1Boi)6nL#C&e_iv`l0P4kKe z8Uh)96w%q{mYy&VGWVit4=78BA4IO<9@d5Ysx^#x=Evphb*3$eYUS-M?`tEP(ap2ZwHdol2@m92$_PgYAYGHjdK`l!6Z~RAg|%L@p~!R!_@^AnIPzD zH?(}J++^S3-avCmZMPm@v&&7xQb6>4Dr!LBVqR?wn1oX->>07~@kGx4Kq+W7@I!GD zDwYww)6=m?Ko5vj4;nd6T7~hQ!4)JT8OTsFo8{Q!C?f-v4u0C?H4qheH1RCDb+8Gd zPClcLp++%RQpCst4kzPxv*l@?csJGcDhUssfWysJIccUPmIG3}z})iWyP&VSF-3$< z+s^Zb4FHweg>KB^kw@9;^X!sC`(fI@`STO=`GXUl0+6luC?w%sB#Rmdgvm$#&na;) zV{C}7vTiokxj{bba<0i_K59sfJ@9`r?{Kmq=hsO|Byz1$`z96b6dcfxhZW z?*Xu|Hw=%=M!Y~0_bvp6$Tk93=#ZGK?;+>Gt8%+`0=C`)Ye)^ATO$I}D3Pk(X6XoI zFTRQG60I}6##$^LMZ-ommYYQ4clT9lTSkPwUE3DsxG>0o5Ch^uwc+}&Q7!lldWsp- z(Tut^OVo_l5v!K6gga%fAniZt? zq@|7m2X?KF)=0)b$F-ft79QVF{EMIFbk{NH*eQC(m!>LPlD1HtEw9QTRX^ZS6+;MK zU@-P#n2@RbbqB99alN+}bS~c5l-UL~mZW1RB-+bWAa!-2N~ff5ICZ)Yh`0PRRk1#MAR{=2&ghIY9Y(^7CvtDBM^E8B7O?-0&c3dE4w@#=7_XIAgR7aQ z44r?Fh>Sn#UFS$`q_mw=#RIC}8*uftN}vuZC;KbB5x<-1-(j=(i5((4Y99B{91X}h zRSW(1Y-COp1vPGTcvatzDCpHmcLdZ!*BF_lLk`Tm>#u ze);VFnqcMxC^K^*TSdfa!|M9n3zHYd%g+e~PGp~G`TSrHrSDZu`-Z(_?>+Wa#^K-s zliXTDW3ySRhOtCJD?!|K&e<5E8*pcprlvX_UMs^%I{{aL}*IK3<% z59pzs-oap<=+Mx-qQVSnDn5BVDoUwL+>j1~>ANj9GfShrnl_~vOnWNZbvk{a;}@wp zr>6lbj(4`Tit8L^jJ~E#t0nGM*UbWZEo>8HFDVTSC<9OWw3u@X44_$tB(|cMr{&F1 zPk2pbH^h9L{IZu{oONQ=66tuiI}Ly6zDVGK57xaM`Dde|sr(RS1Vvx}D21BmO9y-x z)(5pX%mL{spNa@z^SGKZk8(pxzva^$f*%NuDaQ8Hzr|QRk)l?SLIup|NY+mgNa9@H z4-x4jR38eidY+T;^C{P!EyMUOHU2V-m+Cw8+JT+h-1mYu$o6N;IthDg0M(;h%PNz@ z>b>rxw?!@AS~x$|Jc!jYods?zLELa(fYrhIH~#Bz<8+a^MJmHR|zYki};BMFG$I_nuM<{JR{^JU@xPoi{`NP+k`W-9vEylrOVpqUyYF|5^LPPco}8|xb)JW~v?GZ^GiEn>Kg>yRTJl)b^>M+RO`&>S4B+CTF#!!vKExSe5+#aRbIY4VFz+hOfqJ(b2TJsM9oTm*L54?Kk`N2{6wz7lqW>B z%oDA*D>9|Q%Impg4M^y*K#_b~Mbx#LURBI+C=bKGBR!<_`A(`6CPdbBFr!~wg;9<* z$||M3oyxt57ZqhYvwNuu3Pimr{T$hAL1r`hCekC={c(?Isn16$DP+O8l_b%INFzMk zFc6g`%cf`K!hAq^vovAR{xdG7-m?3~5zps9>AgeVx4lZ(q2t49t%+i+{9GQ({w!Wa zj>>~`Ai0(xfi)G#&o}h=)H!wD=0W`szNeVBUWftmvV%bLGunqoZ@T#`vfPztB}^u! zYX6?$sEA^MkET9W4x)bWLvm_gKccDf1OZl*urYJo9MYUy)t+!u(=wT868H3EJorD1CvE?HL%npD4}W?xf3fu zte-6er|FmIQnzf{RCyandvVq|g0+>MRV6?nG*^D3?4vMcg(QbV6%Z{n3e}L1ebThz zWlvlkQA=*$IP}2c9rQ0p0cQN-lxk7o?=*40CWSVWj4^g{NbI#z&VC9}V~?>thN8HC z#w=FevYY)4I2@I$Ac*)>5`ujwwi(ba6HIXplZ|6wHTgD*Kz#R6%!U*@SMGZ}TFs8# ziqz+V_r%_D2*j0urQi*bmAO9806*&FiSh9|#(UE1;QjkbYDp}!13;nWO&;bP7(=9j zqa|QH%=A@HE-m`zL~S@hH8i41wYM(J(lq{e^UJNR_L>IqvE`osdvlYCnk5ygs7VGq zBJd6}cr}+{O`b)D9=*07O3UI~Q)dM3pn!m!iE-K*aBd7IPzm9H$ zQSVB7va^EGh=8C4fV%3$;ufbZFR~m#y{qm!H8ZeZQF}4TaCmHGaK?<)G*f8A2D3A zP(9jKbEB!-E~L&Ud2@_F?(2lu&V9+nTsrT(d)P}XQA*D|BfJBScBIL&+rnofX&V)k zQuBO_(!LkKgEu$7wc~+#2g}Y+QLz++vE3_6Sut4=k@d=-@GGfP0o-%V7yHDDjSwoD zI;7CaRPXHCkuz!ss+oxEiEab;weg}7`nax902`M0dq@AI5cw;``V)JwhJ?&heyB_L?CnG51oQKUv=cIPzwpBx}3wF+-BUJe=-Y zy_{oH#^KRqni+cjz16ztYD7mTn4OayR7;7=V^3^LiBZDCcRS*Cod}E*DBjxl-_3^s zd1Xg6#RMqk)7K^ll{v-eZX=B^O&;uWnL7CJF#qZ9biyde5B2J$91A0nK@ix^FyOc3 zEh2xDT<@Yy6(BqT2N0M0XYHOEBn$g6eO0+D_oG)C z=&}`DS18t zo_adBVVCn;$Jt7O!KgilljBD~ZCT8B$<8a@;Ix@l|Om)hF(?jbn%yi*0r!`=6# zQZm={5I0%|ZwmpTpDI}%pw$d0s0{Vz0 zL@`A9yFSq4_nOpnPUsde_&WJED#f$>N77pD2RKZZFFoIz07g-M?4Q{PE0TP%W>S9o zir=Zw=P%b%ymYi)&CNnk~A_|gU z^Y3t7Q%2h50KH9{`^g)5lLy%)6|P9b6*187wLrqh0n{z#UkG)jRavQ{wA0xqV7xlr zsN>^j3K$*lF0~VwjZ2+DoNxsgkJ~@rTspWN2?cpgynEM!esys5&Qulv|I6g!3-TAT<8y`F1QII8eJpse#I(G|AL z;47#CA0BMllg#$he$naJ-Di}u*ZpO?8b+8C9=mXIz28%rkLhfjmcD+rR4uKam9Z@c_ zB{fxZ4fU&>bL#nv4re>P_iNeqSH&T8_=!K-JD7VmqExH=`%=hH` zCP+4=^uZxI%l!6BpM{1p)&0Bhc2@RU@v3P1g-aLdW-=@5GKDNT?H>V8;=D>5<$i+u zURn~gEx0c$cg1LCYeSbXYex8n_63n+oWoV07 zLt(~69CP{~?IhmcImt;9+g92fjOT*V`YXP3!*4KerHM2Qno2-^j6<4yd8-zakUv)(czeTdcp`9eV8ianDPvv2I-lN7m*mXWomH1jd!|x}bN_jj3)=jlD49sz} zR=tel%4m1ccvgec^7fA_288e1QK^JTHYiG;H&R$1_YhZ86u2zayY-*L+X$L4vr{CJ zjSvBH$L3uHi_lFT{c9abUCX*!QB6@U?WZe!N!ogaVB}POI%sm%lB@7vdVNz7SBGpHgBHau8fN-^AEV6z8%`YC2a0;N1Bw`a2Ul);NmxzecB=sfnwpd8AWEtspk|}9SYFg4hZuK#;h0D z*m~0<%d|k16dMl(`aLULK;tCdO+`=XE9};GYfd8R6Jckhtb!YwtN;ZckS0F63XY{g z!cF_At3pk%9mHSN=j|0dQh5ulC7~y^z82a)Q+ngPs(Gg_5j-W(RFY_TV?TEq4g^6l zm2j&R?*r!=aVX*0Vf$|krHTE~%hWh0asWC-J7I)89eeO+oToVI)f9!Z<<{Uf+8~)` z!jVs$#9tNA+~t>?(8GCOd;pKEu}`~E)!4Icy?-W#!i<3=UK`?K=XthJjZo`(gcf_9 zb&|3@bzg{zughX#0F6MwzQebQ>0)+xS`r~f)US)N z*f47+b+IHnk1T2NFMj|9&6YH@fB#m2y6;4BUMdjd`YjWowzM~_AYlrAYW1$ie_w%9 zo6E`E(~&;pPL~3R0VAGMD7Wa_vq-PTqg&Z03as{X(c3fl_cshZ*}*ER z*(@-R?#ZGQETo@eYliwsJU@Ma1&_Tj0Q*>zfL=r0uWrS-fVp^p)B81Q?|NS6oS8EO zs`%_zu?oZy67GgPPa!xoSQP_Y*BuA+gqP>13fwa~rV0Ug)=?j(0b&*%5Yk4`-%x15 zB8oDExOllXZ$nDW-AFzOuJc9xaX~z9DP~7dIvr`D>hn-q@!!-rqG7O#wa&ZMB6$ov zHfKH!=um_yufPa-IA)f6h4^aE6(0BCIUIasZ4kCW2F^_$L64N1vaq*$C(2s-yiDgP z5hHX^Pi1tDR9Y(0ncj>|+h7L>gsnc>TPt@*4T^HdZe!r^3?aaOs+2ptww999NBxw1 z{T5Y=QMBWvC_{#ayEc6t1H`D{yo@DRa$aMozm;UWVFx%^orcN{g;Cfrdcvk6ME}rEXtc z3I2=`ZZAoL_gs|2s&Ui>Fptm!qJ?pqj5tM6+h&fl*{dj472nurAn6{Lry5n`cS83sxrCnNFY` zZ|7CQ^)%)Ldn%8OdDr<9>-lEUSENf`tUb1p++|FfCHsxk5MKM zxoEWCtz>`$)=1DDicNvCh5F!jp)$Jb1Bo?~aUFR0xIJtX(&t97JNP75yM?^O)-^tB zzYn5OaQ!#8wDc}cTx5@PeB$Mjrg*1MFq80FXn`r^Pc0<)+s^aNGCwa4-c-Qo+rQJ8-1F&C@FZbA%G9VeXv$)ibP@P6GkXd`HAYNJGy*SlysyZ@|zJHQ+0- zvb%&p(P{}Go1U1Sbl2sP;9X$xqHqhk{H~J7)-%Z_Xb>?#36v*|Xg!3B-VOCPn{EQd zVkvZWd*ey)b&N2KsP$uGpMyz(k@w5G#(ADqo_UlzjIhR}k?_6{%{~BSpS`^^3k9Yz zS1LLNuvx3s&^c)^jyX3t;PvZJ0cwsvyYaGmv47A`*Mrv5O3Z@Ghyty`XSU))7_|nC z6|D|5n05iu>I+2q{);D8$-P|Lt0v}Bk^tl2oeMD;uldY=LDm^y*b^23wT0M5OHG^} zt|{>f>7`{qg{j%bs4SBXwQA6@7!Aeu+7>Gp4nMofcP6WjC#EkFrNiqRowRe}=zQOEdq#pOqAz|rf(uvia&mb^nXDvo(pz_Plf+lyVVv$F{s zzR`>AO-yO;&?nhg;#A8`aygzR0@)*No`DuzR;uI?`vf7=Rc0+}WhR+TiG;c4C6)2y zEluDw?9kvT8BU-&JUOlH0%fFYrz|`jtGqS9JXSMhitJ{Z%G7C6uFiXUtp%EcZ?;J+Qz2XpYVwT4yjtv%nV*%Efq)R7_Z%;<6|wVkc~LA*oI_jgae zxam)*8h>Qzh7e00cTKvr@7_ymWf5wEjsjwgvauCbg|X$WVy<~_1U&=sw0XaAx&2 zc$=o*xrmQ0+>{}yIrqSvA;!$gNVU(f-L?fO8UtWNN$o7@nuk>7qnH}fNkBZ5*b?*k z9Uam|d5%GpwK|>~Zs>#n5f8?aC!7k$n*%w1Y@*ukN*{WoRFfD-*5igDnuXo_%rk^M z&HOsc?lHH=jr!fQUU{cIzwX639)SUX-)v0!bqsAe1sh{kV@=8!vUfC#<@ZN@E37Qn zb9FF-?5~J9-o1XvPEb_nwtBMC$jD+wO;B432DP0M-1?-y$$??I|eC$lc`=r0h9an2v z``U1H{LjaNHn0~82s4N>)MT&kjM~{Y<=dV(73(uuYF2C(LDY9rmA?|LFR=_){PIBh z47=m2F)J@;Fkj!f=e$4!t{!ECZ8uNZ1x5r8>l6%c1bPQIcnu?WR=1^!U1EHrv~d!aD9Fk z@Z&IIWvZu5fv$6A0|N4^dSvfbS+UjPrJ|)k+`F^G@J+~O{^YU94E784=CM{G59ls} zTqYecG@(-+uFj8bOmwI5i!glk)@h0hEPFUJACt3+j|To5&$BX?aMFa+D9QJ!VbR$p zi9X^Nf^IhQHbRo{-^Q#cmd+5W6I${KibE%}BP9G&NL6Mw;BS+{F5_f}p>=w{olW?s zAqB-Fs1eWxJ^K3Kk!N8KCFI?#FJ#R;Hgi7Y)w83)O#Lp?+0#JsUhq>lGtjdDm=5FM zI(p6~9YB!UAQxfXT^$<~3%;sRTYTHof!a;T&9q`U^XU&{U+sjB*UHe?*|dRiK1`q3 z@E)sTu8!Td>4S}rPIJ?F?jRCb*MKgaFEpfBUy}l%>JZg|c^L;0fL)Q%fe#n{GW^-n zCs`FZzu5v%pq)h4X}@5^EQTovuL$OHd-Aj zlwiG?9qf6aTNi9#(=!vedPAq-6W}|=1877r}mJBKCjWoZ!^+CJjpGdFp) zX@)lq=VZJPo$Q(hR44jvLD}4JiNG{&aydrc96!SG1{N=0);ZBQntU-A{^skB5w`-x z#h;$qW0SJJBw{zY%tl%v3~jfZHqJ-x+`tv`y{BmB3A2|fV7ok3PQsgpY@0Zkj*`-SNgQ)raocZjGOQeA<4#z-YAeLko~QH!N-m?WB=3Q>EDFuf0(iv=vg`b|Kf>>frIt`J^lB=6B9ct z^Z!+q?ULldz5MkyW-_C^rc2;$oMvHqZS@*?Z6(NJI*Ks`(`0^)IDu6$JmYFZ!Q4tN z)tE{md_2jSD(o6$-B)qjqqq0v^Zn<}Z+Gq=t<*>N&S_RlaCs1`I%>EhfA*+enEskt z85ch>LYV)68ikmkAWm!`0t06iKvuW{nBD_I8j%n_L=ccYLH6=FCWL}#dNx_uzsOIR z{{$7#BRvxfg%}<*9s>l?UZ8+M9pQNFMO0l42SDl9L@crU@9Rh5J%}d~e`c;SSbmxC zVSTPI0MdyhpV;FwbC6d7zy)68?-=ykp!?^~Utcd2up3d3Vi^T-n2D=BT_VD5SH zLLLQx=QKzm_Beu~cr*;cL0J4liE=$tEo!8Yw_$w+Gy`x8XrR^_>-gfb;=EX>(xm$Di8- zXAK>MKN0)0gkgZR9o*;Rz$z*L5Z>XJ=oEqR!~M?10P)9xA%OAAfyv?dwc+!D00i*( zMF0+60a5@UP5~B|JaPC{01iC>E*QIigXleVw*iaKUVg#<8R>!tjqJgL2hSC~;qY~! zegPIs5a95CfDQqRsfcj-I~n(Y#noJo_{9GLAv^*WL%)1Q{N3S&W(SjMet7ydgYbKP zA{+8;U_p#~;@>ce7j6q8D77d2Bjg&K8?q<`|I_fh%klTl`~2cy98jO+bGwofKr-^t zKeEtq-Y`zC`zZzsL<&!?}@X<4|efCijeGOQE1Kp3oA&(?(W$|n!4io=h1)fy!=Hyc6u3Pp zS08AWq|#*7G0YFqk&>PX%fq)Nm_|X{y33xxKvaYws;^rru^p|TfwC?=4Xo`hmPt%_ z)>7x)=t97_Wg)U~?q1~f=YF2pW(3=kXIGn7t418I-9Ep4l`aTCf2|*qkml0TM#dx( zr5l@}NNvaq+6ExfGv~drwvBTMDDr50G9;Q}y)jAbTZbUAyFZ|nIj;~!Rv$Xi`;|GxJiHUWT9R~Q1f!yR{ zZ@4!~(XK0-vs^Gz70nzzHaP?UP&^in0h5kZIt9Kz8x6B6S=?MtzFQ?K5V%Q!a}X2f zsfasVLa#Jj)4+cs|M99C-`C4L*df2W!BU_cqNHT*R8teoStMA+r&!@8xmex%JiAqR9T^{Q&)(kyFV|roqMAkBoM{u7_f%RFG^mC!*l#2q0Rgj7Eoj5`*%e(>^wt!FZJ;|n97#3+GNma(QG z49uKf1qRQ76>9kczeN0|qO1DX<$0*dW|$PUZf`q(CL-0E-fU?p4#bV_AmW;xc`udT6#y2R=(g+8Tx~o?8C+bOY`iVQ)S-ol0itjFMc!a>7ZdQ;o^3q{q%9k}B z^=nZtSgPoI)1ASxAJ#9|=4WuUZ5k{yml^rZ=q!`LlqA z0|kqt>Erbr$7<>{8snimLlx$UK$53Tg$fD~hlm*Z#%O01;oA}>V-T(Agm#OcLzn0| z3XNW~%J$qrdHJs)v!D|9tk3}6{7LpkaIgl%Sd6Omx`$4I@VlREP;p6KauPx-UlTF3 zC6MG+irYduMTfwM?)l-kb=hJdU5&S6*vP@IqB8v?7a8t1vmP(*Ips&bV_Qhzn}#(s zL!K`Snf|zU{i7rXIi3Z5)h*M0tm&i}FBBCKWFcb!9dxT6dqt^BYZRNga8QfW#YI}Z zFp>Ejtv327`>Ar6aQjoW8?)x6sRhmrkExq`B#!VN?)RE7;f*>nil+d;`mmSua)AC& zIlW-QO5O|Fk=C|v8Jq&yzCadrixF?A?xsm(qMHDRmNqWkzU<5LSH_mUj?SLL7}iN~ zGUh4dWYZg_k^O;G!aVyhd&E^SEZ#h&_t?zpS-HC$?fBVHfugGXsu;`wn4|~fR`U}kz7DTYE#&P8FPu0XE#e^0LravN00Md3jc=X+hjTWzBOWCY!91ShS*puVk zCOBXFbLg47`MlaRT_(u>%D4w&k7-*}Xb9$-YWyih z!JMLrR7@@W1e?G3ntzGiILvRADrfRnD|G*C)SRP}!N~O$Gxf*?v6lwVG8MB`9PG4=P!T50ChEu&6e zwu|><5R#c|-kJLmyT>7|6z3BFL22Q4Pb;M_U|hb>kNnPaoWr$@zE2J*e@y`O>&SaH$W%;y*)4dw+e~2-yC=GFDQA<-A)&=)A4+@g66b0}? zt9)eh^4-^`a?_&bi%${;epnxmT%P*uWM)&B97q zpsuk6Zi9{U8=MYs?G1mEy1ABAY?GW-#w9@QP2vt|UOjyA8$ZQ+-no14H@SF(hg>7| zamI39qA_FA9y+xJ81r2tpjfKRK8c?yLerr&J562Q+tiilDKzk*lo}_4k&*;&FyQWK z3tC>KxLz33*kPhJ)1wvjj%wBzc=hO{)nkx-AVk+yFOMrQ?So~7F`j02B#$sLfhr>L zhGu$%shG#EaH8nD!5%kFhmbowM4InNJ?CpwI598v2&dZ%xL=U6$f0QxID5MK>W<>d z9-N*;gfJ~~#6Vtp++2l=MJh(etC^cILZWwkbNs3*Mv!t7Sah8tt4s1reac4F+TC;q zp~}+Y*tsqnI;^M%IJ&l7KvX|2p^oVuyui6R}x($xAKQIaboLk)B3Gn5R` zG-$p=4vaILrnUDTgSLGAY`+}Bu$anzk%l|#E`>&M*o6!FxL$s9iAsdO-1GzoiE!I- z_kuTi`A%MXyHcqhQ<|#ow0GNRtPQ?oYrMPf*u~C|zQXWX{LZAd(1UXV@4FNNhouYt$JHoOPWLB4RJdT%PJrHil4Lq(iydM zl*)1UQu%Y*%s2>l^qHNM=|w|tbduIBY*m~07I8)8AgoQS^Gcp01gd>{|0^H-)?;Ra zjo*hC$x@{xW5f_{m|f<}XUjuJUJT^Lj9wpwtu&PzPy6F(;os58)Kk5D-0^TaE527r zj<%t1i=F4&gy87eWlvv%M6IL05{j~f?G<~VPJu&6SH(G|yxVz~SWYV)odi$QH`fkP zC&8&)Lir7DcfDeO-fi73tb*;SDBhjQCvU)#6ms&-k4S^Wt;ISZgiFyOG-8yJCP+Jj zYKOP9dgsFaxUO`7K+a7j9W#|(W9Y=aWM!_H+qK^u;gwzi7NZ>DjkuS3>A<$u(5H?6 z>Xo2o2NE4O>Too)i!46;7qAsAUFJXKahCs-#~E1u8-e6s?*3l_IR^_P`~Q(ZV%-kx zY@$<7gM*#pJdNyZy>-6MDm#r#hP%~jv!y!Lx*{-CrGbzR+RCR2r_ zY8Auk+KxDq@_DfIb(9~{i_6T@6pZwbKc&3VA~1+DXrOg~Z00lJD{CnL8W<@lD~PRs zMM(rxKZd!jMOgoPTu65U4MIOTB}HKkvBXy&W+u?gFEE9PnJN6c9FP)v*k3{i(7!laTVKUC|30<%cH1 zX7caZ=)<9v^*aJc54zUD$#$;51gJzyl?7n)if7rs!v<*v#Nq&y0i<;;0D_2D zR#E`{PD0~4$^>QD^7kx~FI4>`G@^51PiD`4!B@u=xc(8h($%>cIR3`0Q_ER!3Eh{rKFf;%i*T_`I0JH(1 zL=(ja$KUyjhCaVAvbZu1_AaL%`CkA!K*hg`vVi~e%a%>U9jS$h7?`KW0vJ z03%b=zw1se4nUCQ-vn%c&d&d6_>bCVmgX)Xpc4Z?)WXQl{4dJE-pSJW-{Sn$+tl9q zFU;896$r4T16a`kEdROf&F$@g*8iGY{zVuAZS39tiOSZ<*~9{%rmO*Q00B*0Ku-1` zCk9>W|Jf-LE;cs*HY3%4??S*oUHJb48`)agc>Hhh|B2K9{#y#F|Bc1cNy5?{XsT%W zPk?5BB?A7(yp*%iU)hE2%x!=GrvI2y`=>xQmUckJ{}cj1&;CD`{f!Kz-Tu>{|Bjje-&oGb8D!}W z&|&(!-$fTHNr)+x; z2b=?)djOev#dDmNf2(NLuRp94vhzsuQBxGhMxVj&cYeY1yikU}Lbsh5X$hFh4j9QB zMh^C?a=0~%ODZq==S{TH`M|`^wvZDIm}SX-Si7E$%4&7+CoeLX?^Ca>#;K)tvg1F= z6(N70jaR@{;*?ID*@%tVFD*T#W?3ToXv!|m_Nt%7wanS(r*MlRc@ugUQ2b8yWk&Zci(u|+j{P7)|CzK3Ob!h9 zM6rBE4u?WjRxqvGB}uxfqhqSEswWZ)msGg_yw}w&8foT$cN$=fk`@B)Gq14GF=PF$ zW>d#qftvM8>n+d5?2)hM!)F~bYjK#ecD5WPM~o2Vl|D}>ac%~<6Y`xswcXf~y#apI z>Ksz^9XW?$8ZcroZBV{r8L9mz4%9eZ2oCwVax0hTfcUFLHb{nZ6p=;1MlrrTDlZbt zIb0fII4Cev-|D%Q6W>{&3s#{kWYi;xLimm zazfBe(IiL#x?_v8@p1f1u(_Vk){o?mZP4cBC zs5W1D0|sWm@au)Ac30q+d{ExJq_#>rQALgC=Ls!2SzXn{q19K6DzL^EKC*@$o!ZuC z_ZL?}8xSeio*X7Q<}#MWlac=vn4GEzLsu;Y>6WT35tvym5%rA=r#zD-FW@W**|qX)FoZw_sy*c%vF=YU``z z&Tru&;&w~(DR=d8%9K(M%N)#smAj=IM+)vX_TLF|30D^FfD3|=K7!8CF_yL))X^i{?ddzCYa8 zT>s@3--JStYh1RHu-iT3&0L9P9v+vt82+OU#x*so&?9rqc-bk8Fwun$ z`tB9&mT>Wnghjj_V%m^(MqS3Ar(GZ%Qk_0$3EOc=L-N3^>AYr`=%cyP4iQWd^Fuf} z-Ts0L=ZzdSq*n*wkEnhU_+e;xp_KhTdr|tbFXFhGJ48F?WG`dT zTV4jukr`J0{sJhBc|P5NT@|goY7m)@uPcyBN&_T$3;HU2Q*~zbU%WA5_v+=stlep? z<~mXq7Mk#{LBO|sQWmm!Dosvye)N}V^eLs^5jg}k_6e$(p+m~E{rA(gcJGzqxF)}ScRy*MX>G>5gP&?OM7WO7BwnZH3fi5;= zW1*jy=^c}ebcK4Q#;<8Vzeo2pqUlbrZQ5@|wut)VC^!)oljL6OklV7ypMvQ9HqLbj z?MKSW)e@>9%;jU1xRu~vLPEOu#D4)I%(QAz3Gl4cA^spZ{`xgRN?I%4 z+$Uzh=A9r@BF|R|O2mO3LLnuhLe0d-fMzt$BzN)WOp)vhOHEoz70K@r}0z}Yc( znvY{!W7Qf7B{gJ6(8H}Ql!g)?8A&Y-7Hc6=4G zr#B(EeR$#a!uzJg{9C7U4?H?KDDNJyZQ(cJ2@*N2i& z8d+EaS5=i=xwmTC;ecXXLercpFSFao@HZ+Se~AzBJ$iG>qs;|f63O=G?N2EYLC27j z_-Ng>El-HWEuO)3-Ka@i6CrI1LEf5W(2vvke5q=~!Cj9^F(lsLr6nZwWp{iWye0Ns zgpdHqdZU4T@i##QJ4!<2HLZ^uF`n&S$po;+BU(#coX10aK1mtA+r&*t&c-N zF0;lsQtE18$PNt&AV4P#CJdN1Ig=9i=I{h!4f?$;X~6vpa{M_~jC65WvlGu-Eoy>C=>zyz-uLZ%F&W-vl(bN_%=x zYR19!#4mD3D8Wj{1dfhm0Xb%iH_-nrQO0)fmtZs8ssXA~YenSd`jt^uhua1l^RKk% zeD&MMkAg$w%`+Esnv@R|WtP>P3H#}DLK}lroa@9s`j!>^{+a!Ur;&p=ZA|2hh8nXUwk~^OiaP55L_B=n_N5*tmM|YRa z%uq#fQ!4^`u?6|tkm@0TN?*PBPn{9N=_qAqH#4+CuGA{LBPVqfR37VFhr54HN#WFe zCIkq6L`;5TP(6fotKVXAt7P=VPv~F4pnQ?E*<-w!&BTb=8dA^R#c-zslVND8ZSMD1 z1vg1ZgTi~Y`Bsm>rYWM ztfKcpqMs!i*Xx>>z>%rq>6kC#9_U*I5voS3vAGk3U)DWw_3F!COEB79!8U?JyKjR} zNjvT*zJ8dYw;a+QW_s(xgcn}r#nhq}lUe>CCJATLoqL`<^~6pg-C$~D|0%P$!&^1{ zn_$#n=cZ}jMsp^FqATbTDRszHq!ijMLXv~WiHDXSg|KEY(BiCL;mG@w3e{54&U4z! zcbn_d52;`C3BQOuR>)L>Rp9J5kD%NgFu%U7qQh({aioPGLuh{PxBqY0kG23TY+w|n z94K83sy6~$ENi2)zwk&p>75r-*Jhj~z}XZQ6*nhUy6?KgFHqGV#lm}(Krr?|zr6SkO>-t}evjHr(JP_~-G*bz70v!Z z?XVSbEnF`yTBz7)@7UiO#&<^&io_j~2Pq5nvpa~R5e|if;Iqz#i}x}6FMB~E&MlKf zM|E};-Nnz(V*UpxCNy~ye;u=WTf>r2$aM3IRB&rN*%Nl}>i1m$_jdUjW)t_-gRnhV zYUage^l#?=8zjRqri@SV#_NL(k;drl&&cp_pLcw0YzyvjWO+79zRX_u!C_Y_-}Nh` zja4;N2PBF)Dc8xH9=<;mlj+?f$imPxhe$)?!^%J?WxUd-&J}zbnhq~T718DcqRu{T z)GUnj0)ANXUDKRtG}L_uzdXp8!GS3&3c8k@#lf9;enKW4 zRw&ALHDvCq^0Wz*f;iekzbX*5`nUl@;)T`0hfDHf!YZWatHJjIgE{tsYWsC$w<7&Eo(^z1*DmAGpI~;gUSRp`M z6W3oxcIP&-SaO*l6>~b2drX3BD@C#**n5HT5ma_$SS?T6b&igiT~eL>tH{h8#Vl|K z;}BO-Yh{sr7dWGZUcMz^_A;J?1E)4Z1~K(6j@Dw+8p@$0e+d9HF8mbzag@KOrD!Hu z=?q&e^%Is_!!HR$;!yIc--EzXW62IZ1lfv{_0EbFLVLf2lyH?&Te?z;4+?N~v@J3YA&4(+W-haUiy_=7!y^>H>tBh^uN?o{iZKvZzg*@Fh?t;ha)2rpDr)CfXGWd_F|kyBE#GjkgH!N&9G5o zU==1u9LZO_HkFzpu?;TTSdXQ=ya*VNAm5+e=0M8$;1;^eM?LGQyXg(mkt5_wql=+j zAATvXYfh}C5gFq>-G=-PEQ_`3h_ntPaCgJ&*M}L#NWP-?A|?Emc(zJA@_ytZ#VuRF zdToKFaXlhlI4IkVh>UAC zLB4h=0pCoUPErn8_j}>)6Z!c8I;!OfLJB|Vik!Y7ZJUzGPJG31yh6KmhApyvMYZdf z)}~j1>^7sC@C+&I@iRP}c&*>Z5|nTguHqQdD0%I%(#pVf_0%<-^Cibc>7RY>6wT= zJWz_t5DeMgBY8#44CcQbOu2n@TPt30C{xgaxH&Q-r6eQcEr%XzI6Ae1{MBRGBXG@r z&gXIPcVzOQp?ESLb$5Cj^+YYJTdG%SOon$`{JFPn1WR-1adB@l+!k(c>+1OUD|C?1 z*()jf=dUD+>iA4nQ5in2AkYEn>)`21gdC>tO$vqbIx+vBSZz{w02s2t5bVTtZ|qdA zpc0Y8jYP_`cC1GTN+06fZA^(2sp?Ymsy*Ryq{#BalvDEB&A7(dwACu8DV{0`&F}lu zoiOyW*gkjZc;u>Esl0x>!XKVpp?lq@li#XoYe9ZbE3)j7kf|}Z5$Sd``AR6nma;in z%{2)uU4`9J{v%i7mAnKg+70vHSxT!$M`O-CTZF*+N^VOR>8G^b*28R7pF(tC%i(mkD6xSVgRyKTtzi%#$DZc$oxvNX6kJa zq)MeYilKyU8wJUO`24Vr;f?^uN7tG%rcm0yuC+~d5chUw1TG44WY#;oyL|~&qPV<+ z^~@?LXwrgh@~Qn5f)=~gepvIz#qfT$p@U#j>-rM@_3q-5Q`Ddu8i$2V24-l?h|@a7 z+Lz?O1GlJmlA=KeW;l1N16AVSPXx$eOkxTO$Z6mq?2!-G`{19jfL@G?Nfsu`O7S}J zZigoz14R=o3;Tr?E0NK-_s`+|oMvSj-mn|S_}NLkICEgh!EqoHj-Uws<=dFG0DG&#Mq7)<)xzwC?*n_nKw;Tj1qBgJK> zUR*59;KWUM1g)bKl^B4gmp@i%R`@T}pZj@3f-;liu*4=68tD_wPq}zeSs9tCz0~}` zM*7SuXpWe`Hd=?AHHyGO!GNy zjdxD<@;(&(k=-6ZH2(glm*SY5O*~U%S_Q>xw=D_s=UGgnmTSIu>*)KzMmmqF2RK}* zgiBhNDWd$O;AG(ue){((oEkwct~(kANMhm-+mAuKkDEM#9+f@c6+-iSAM+PVOj01>Ei`AIpB`kE3)T5XH?!?F zw2B`XTy9tZ@DsRiqlD6H3I#SKAKJ_EXG4=0ALtRmvG6DD7ri*E zbV4iA6Me9YNpq{$wC&kWaZx_2&P4ps5uSsVFwMxEx3GEiN%h5cwJ!(FzY4}*f zRx*_<(^P0V2@fSZ2GNiGC^&@W`Q@>uT^H2m6lpw6|+-S8Ahf)j_DT;olyaZw#1)!NNeNL^R$*F(~4OVS( z#*944RHX1I7ZYh$WY3(BoKfhyD!PMw^%U<$d6g$V>(5%T()lDKpQP$+4t7UOoLI-F zSZ&WXF=I_`;(n(lo3_$tDQm7@wn`!JP>)c%JN56S`YkiBseXERt-v2u?3w8A;+YrA zxvV7D&9cGSE-j4>2U79wJH?2}BV<+Hx5MLCC2`Ax+hlPl zKwv2d-+1o?cT!&cZf4M4IlSFR`s0=ZQF@MuD#Kp$>yLk1#I|(vydwmBSMLyNF@(+; zHJyFH(}1G+-b+b@{I zKLT{J_C9;&Z^m;($d+b_i4e3Njomz--b}gUdSwY^H_;@GPL1wsFJDAQPM_ z`SW!r>8+~?5!ut;etmtpv{;>Ex0`2bV$7%}KG*^hDRx$|oU7 zG2gBUE`tNA#lu{fQx(R7y3tn`Xc4=3IWdp70n zI$p-PnjZI!e5flWKLm>NeVL2JcF4JQUr6d)=SoAW;ME^(DW)%{#gB0BOQ0YF$3UtZ zc^=$dTsZr{EB50JbPEsr6|OWbiuA@!bax3Vi}G@1TZdU)LP!r6*%Y%bG)L}D1B}V?&7+%)LhSR9{-J zViRJ0<^6k3@`d3D;}R1%lX$wIg@TLnFy|ckz0r;-g2MMu`*O1B9mz11JluYjGuKf* z?gT8y&6z)9D89Dxzlo?a2St+Ct?wa%OtK7Ij`WnS4M_1(b22YM;76O{Os~$7NN8M( z+K{5H2u1kp6GrN*m=z|D+UscFnol2^)1pE+m@;FU3@{p5s%yqMem+mWDQ#THJ4>6_ z+%@Z9paLb^X?|aJl4J*PrkIU$GFd6toNdn17!6^RISW%NqX2@Oq39<7Xa{MLD-}t| z7~+0XT=+G*nTvrrqm9g3=~eQgY}7z~{;no%r$RRSK{@WyImH(k1g zvAqewR0cQas1UWpF4f*yvIGIsO)soj-mghd(NTFR;e`${0)G|`S_j}Tm4Kn}M7tXN z>)nCe*oqh2?#xF6&uSKt=q6KC*xdrICWt~MSzy`AoE&Cq0l<|5V>>pVH;kySoR6Ej zuW&oqYj0eIw{8{wSRY0SqI^PH zL-}snpeS>)dx`e1>&dsaC=Ka3Dx{i6eznTHEmJlUS0~#26lg@RvNr5O6k#0p3Dw)N zt&naKZ%tS-?eLKlB6GLF|cLPD78lipx71sD<21cTLLwZuo`!|~AY zeD9Cc9$5DW6}-xxPCcAPTFHlezDL5Zi8W?TMd2`!K;0TGWeQCbg_etIQ1K`aFV9X| z=ZiHV%Y_p9W>ibBpHLM+ozx^tq;Zp4kmTAGD)@0?da47x_W|cj)vkS)91BZ$FZ@f+ z9l{BB&}GN)MEBs`gUXy#VK+o5^G(i<;U>S61&2jGS$4>yS619KBmOKho)YC&;~|;S zOQ%#ZP%d;vwi@wKm{oRvdG1-;%{VVps=)Jf!95V|TSBCqaxIUioZ;q*g$K>Cjxy{S zvMzc&_-}9S9~BILsyDdYzmTq`B(8(3oS?ZfYsPHRaADCE*qKSSX4b=XY^q$KmvGX+ zF03M%D*YdQS5Herk^AeWn|-R}qFv;^hc{hrhiOv&q}YB}r_8c&bEDA9Di6XUqb5AJ z#mAfEqc_8IAd>hH>=p=NBz`AaTgbrQdE!&+_JSv*L`J3ui{3wTb^4kKaeHh;(&uKJdfR(4{RRj)ZGxK|PH=I5W z!LU!jiFrarUuGyz?BnePVPGzsx;+r#SKROZNhglU>V`x>oqc^xD)cJkxlycmU$dP) z>|LAyMv3IlqM#QyX86i@Ic_2ER|(-CR_1i4mVLsj(I-rXq)Hu%QE}$RxL#q0`aldO zUaBrq&ucWXHKjpqt`G?|Rp>}_nFPq7yw2x1KwYM^HThmCBMD9q$3=_3HfQEEoU!HM3 zRY&L7zvw~xJ@MVefbAKjI)Zj_Aj{+MMsag(FJf*nsM}|0*N+)jKqK1qN=Y39ypryA zH-3xY+^@N)%HP2+>se07p>M-^!vr@K@#))ZR9NSiZ{kI0)Xi5#55qaqGLWDBfPAe} zRhx@W&n4**$zMEE($f7M}p5r*{h4eOD^MBI)cbCO2G^t~o+p zoBW=}VGu&|vjG%e;SN)nHP!D*5|8g8>y;^OE#TiuGyq71j1dPSUMU6VP**Q$nf`Zk zl-b01F*!0i1!Bh3^@>4;$heA1^eGCq5t@`~40KiGjCB`r%-FA_V;`D4a6Y-sR;sv^ zn{?PC8Cjd})!PT#{t$*_<|a0wDY+e*&vou(AMg~8u?sG4#vfe{v6R#cOD=3g7A4S9 z=;Zjfm}Hbsz}=(`_QsVYIZ{`l-u|dc$4O`?zWF%kRjQ>iuJ0Q^>w_g9f^7sB7#d*N zqogVq3^1D@c&pATa_RuUrwKGAoIJUZ4=30tgLv{K?360uZN4g zv%ZTuQ`&n9?3IJb@rjJcF02MUzp{u#*s}D!E5(G32tpuxRYrTh#$$X}-9}!ev{pLR zud0ZPG!1Qlej94o1_o=2EmL6YhIC^+j^(NRy1x1&9glDD`#V* zGt_f`l4e{H6W!`NZTzs1h$X!@QfYX|}X#b$<liV1#4xTO|9xvXZP_yvR`pukio>pxzD4&P_sj4Ltn$5FV@Al{`SHf> z67u7Fz#^3Qy?hz!@19A{^VPnw&}+#`Ntp;?%vNvkO?+x9_FN+u6*ZdKcQ?q3C=smcDbuZ82?x?9od4n1<5(SdpzK=w6Mij_k~!+Dr+U_eM|vY+}zA>2WP>Bb@3tJ z!Z?LwJHH_k3iofwMSG<@xCAtR|1iHcMPPS{ls ziGtAFfga4`sBg38Y{gS}nV&(lll|rE7#~go57EYOdPQC)nfY|H_L@h~)w#faq$s9P z&}8Sdm)={qasVYX*yxCVpx*)e>#V+*4|PPly?T}Yx)HZzd9p(FW7;4b@?yUu`Gt){ zz*(4L)$niCe6zfqPdH)u-FS3i-#jfXH6~;Sn2xWIM^Ht@1;kkSaE>EaD9N!-t=@Q}z8j_miaqI`b@F$3WW9~qHQiK-ENeIpVF<%8mHa7C7|v1#XhX6o=hNzO+MxdWyGOHTi?McOcb^Xd2wAi z^18s{1-lRkmG4(RD0;>jY$Vb~Z+|v{qvrXVJc|f#J6s@(EM1{Zn5f^T$8(@&#!$cb zYWLf#=KPpZsW&dpGm%eC<7K@i#wQ2|n6J#$#slpC)cjL5u=ix# zUViC(#1zpGbf(VlrB+;ELw0yYa7T+7+s0Z%{FChG-t3>K@5(}y149nu4yJ7NB+REY z_WXLMH@oo(-(fq#%CtuyNw$85)d^r{gAX=BjszRpt~qJ83NL#+IKQQr7`C{Yu@gzr za-Lo6)D_sGp0wm_zM!2CagUNX*4+dXygR6!RuUi7djwv%8)DX`pCqPfCb`NN^#7_K9nEWEXzg$8qZGX6XD9 z60IWY`iHW)w-5dulo=h%01(+Pe|AFMCF*!wKtg{H#49mg^yTR7=*uckA=7@L@#kEJ zU-qIiMG(N<`YI>XiB@df>{0lTg4sz`r=Zx#Ly$mq_&(AxR#-4GPq0uz-%0yZ%R)_3 zEEodl0rmU}!h0NRB)UIIu=<%jSlix5nyHf6(mo+Yj(HeZdDso^^;K@pU|6RynfT_m zdYUnVuA!xh#AK-9QSz!l$Cf4qI5xld;{uz*4~fsW2u%W-dEF-b#P6oz(>|G(=5Fcu zF{*Ki-gOO}GT^`eWyn3+ z?&(hLRFH1L1>upb@BLu*kB$bNQw2{gM`LeXwa zG1)=dcJjt#rJ@e#o!ED5c_og`yRsQ#cOOMQTk(qPPjLuRK%2l$6!42q<7D>V==`C9 zO{@!=%XdFV|5YL1qAQl;6q;i51rf$X^iYqgvOahy+4|+lK5+&xU`(dObzV8>3?^tb zkAH!@UJiDSmXS!BrXN;*Xe{Cx5<$=EJB1#dCs?=H(-_%Yn2j~CD3HD;m9DeKM#i^v zc7V4oM(3%e<}EKjA1AYyTeVW=WwjLl+~9HDlG}YTg|0yn7wvhAxyJoj>y}-CGyBZ> zGM+!pdMV>LT=Kb*X)%4m^NmJhymlsT7;Wy5!;w%n?*Xc&i`h!BN~<6BrEj=?VTuH` z<=QW`Z>{bb$s<{0B;n;KpS$E;+jKdOFLR&j@Vq;O%Xpq@Us(nlzZ);G*v2RkW_%;& zLLPb_udCpo!5C@vwP_eURxf%Bc4RSs>?9MRCvC^7KrvYE9r#;PWy$diAKv^(R9$8{ z?Puyw6i%6z`3#=cVhKDqr6j47gtSqW3NJST!^3H&Cle2+Pyeh>Whd%nabprm8F?BEIqOa=*)uz`cCP^66N#{ zADa$>F`8_CqGvTS*46D}JYo*&d+0?XuZ=16zw>Gq8{O1<{buchStovS10RkmcgOv# zLj8cq=V+L68G3YkOPNRE=yDhNP4Rak5dsk^11dTg#)s=-1p*$$pj3())@0!yvqNT<^R@Ovk~B-@z%EXGZn2h+mx{$J1kc+ zgPvUyk+n4Yv!WoOzv^ z7iD4E>0!mr`_O3hDhJcWTqQba1FP$}hRn7t!=ZH74_H?jP1=sjtSC^HF^Hb;x{9!3bEX?pri zGYb*~!$X-YUG ziT1EU0$`H~G%Ihum}kupKCDFY!ptu|Zm&+a3Rd99OOM%d4ev)64{p#5 zTdZ`jln#G6WVS0cNOgT*!7Yr%eqZUDS~~0ZgpFQpM}R^&$I=wK@+^U`?Vsx8^ZCK3 zwv{xfK7~#pXW z$;XJ-ZzSF>6Tn91bWWNV+@EXQ3}40BGvz$oZgXZ!>#f<`cZqMi=f6sE2DU`+wCsy=bu-C#3>%Xxpk*31Of%SiWb)s@-cBa)FYoO zDu>jH^?e{78-*X}IWU(lEDRFNY}MhWPU+p(qDuBNE?6<*+f2M)Ps zd1_8Ua(0T_W8Y^jT{W?@5)&$uC*6=H*{qZH{D{rKN8BMfN(Ne^whla3z(py< zgG{v;QWA9RY@$z6DdJD0iFaX%W7!9S2mjNG?TnzvSP$O)#{MZSMEx*D;g`}$pTf#ykeo{%T@O=t14b^$twrMZ+>0||$!!ePFR#D%ooBxOHK zG`w=?(QV&v1*M_2C@#lJ?1J8q8-qVxAIc_4W3AHLzm>XVCXu37Ze5&WBkB1*(s@#< z>;RSoc~{Br2HX`G7Qh-A2!$xohaXQDtDr`tZJLQE>!GJhn5IbF;yRpd0WRim7w3%K zbLjS($W3A;-Jf2N8=teoFl(unixNDu&k^d2mBeR0+5v;S5Hxb=10UAzQKhN4dg^$bST_S3^w4$eE(3kvv! z0THLv#Bxn1@*lrovX_fxqUH5LQ4uZ7@24~14USxmo{2^XDR6?R+#YDhh;M=wu?rVeVQLi(zA&4fyqmjx8{Bq$19EMn7haX;z zTO1>nk=Ucl#Hqoc4k9PJ5dSKv&YWAo0O}hi?F;E!UD*)5AEYYZL-S!a{`#!l!E@W2 zg;F@#(7K7XtdU**CH?h2sGoz(34%Dq>hY|gQ&z4*aZSN;oG^QJ4}w@}hFFKz@?F0m z(KS7n+b;@R0D<7^^DnqVzcF)uRo4-d)*M38APo!k!}cFBoWh#ECV|{bjQzF;Lm|6l zIxxguG6cP`JYEfmaR~N2Y>8j%TelwS&_&~FhT`j(hENDV9%vsu#8^sU%lpV6K}3u` zV{dPVSdoFkJITCPj!CNOGK+IawIiByKLO9uZ*Y~I{qz~4gvZXgcYXM#gy9a<$C4fo zi+|8&aL0T`NFj5-|7jYbm(GD>fQS6i?A>!yZBgmn4>OHoLWs4}Xpl!Fhp|`K&wv>% zS~r!ve^4qJiM+bU!hD~O?i0Jv<@fi%m>p$onw5F+oz#tn{F+|X)i9&99Rv#!b)KQ( zN4@0eyyuH#R>|;@LYw$`fY-Ej&&?3|<;wl+S$3YJoB7e7B!yx+@!-c`VY})QCpNJ& zcaw92@Jv*igOzzn#>yBLK<7Ix@B#=QDgH>NT^Q%n*KT>{`u&M#ecNYPkGeye zbb}SW#T~25IwiGdID89l$4WM$ zFtMqiSKIryAu1GzVyeclKlHO#T24*rP-*;FoL}C z%1KK5X)d?n0h|}UEH78|E#PgBqO=-EdWzs}2Tb}Q{GxD5BYExtIhU>rLe_@E~kSLf)Una@Z16#Zpwa`P1^`4mQ(f**20Dkvva>yD*!r@p??t zes483uQbXy@Kk)l)kbB=p{kja)9_MbKPyZt1#~q#Q({E;rJYXO4%Ox5Dw}~Lc$pm$ z{pCC9BC7p%I z=y39mt9o+b%0&$^_1`2orG4@Z^~tMmOSTflu~>TtRhn(CAy2n4l1!nSi#{>jZx5|e z-Fp-*ltJ)aqilBM`&H^U@7MOd`mZ7L0Op+Hzx02WlcZ0w z5#0BN57!Hk*w)-Q=KhU@St7Oa(j}u-gH9?f51$Iop`4q$q!ij3|MI4(ut)D%SmI zT~s>?No)pxj7X?5m*&Y^KwNc{0~G^>t3C~BKT_D1LR3$0#|#J}IfqZ9U!$+gVLmg> zl*k~NF4D@GG-~{~+*mK<1?pu9aGNRvCz1-8^ATnVkM=HNs+*wsml%KX984BWL9T*) z<2eM00bdKp$jpk6R2(;kQ8Znzmbb0)XV4n3iU+(+B%o$Ws?u9;&{GV|#pp;adwRH6 zGB{w2zPB3-phu1`6-uKr!W&5sk&FdtFlP;&_tH;9*Jv0fR8=K3PDg6E53rKt&xlcl zRNy9J#l;)6;;Iv>mZOxdE2JicZ5rd8;)wI?B#cDH2KZ^}JcQZ`oPs%vy&QV(>10Oe zTqWbO3BbS*>j~vd%!fSC4*mA-lofU_$kY-rH$-2F{(YWD1Os93kfxxNdA?#(1GGbd~h2nZ9$bd?NwY=BAvMuYE$oXn!4z}UU z*9HSlwSy$tXgx)?`VuWz!rDrCD*vYJfzN8}oGQdAa8$HkL>Qzd;`NLYU5Tmdat8d2 zt1rX)mR^(D7={$>0**hKpy$Z^7#&C(qgvMrSNb2cF!O5^%5Lqsr?lcZ8R`h8_4kBs zva*DiTuH)zbvqBN#==H)nYd5VwEpV$rJ)nQ&8?mhuWU4XQjm&zEQX^aOu8uZ;z{8q zEyw}u31q80wxo70n1wf)|1&Aiox|ISD}n;IE2qYVULyV-gb|>~ucyEVyd9H=1YlB_ zZV)qTHP>&~m||rULbPt|HDjDDjp67uL2ICFo+()KJ@R-I`iwnPC79e)&*9T{+Up}2 zN(N00Dvs@|-S;G%w7I8#AX(cWX${<^)(nVB---RWnc8{$k zznxe+V^;#B-g{g~v02{WJr`j7M18ei1AmyPfT*%H};y(eJ+ zUvTfty|Aeg*+~hH=butqp{xX2HRFovGs!YZ7)uNH-Pk$3^dUA5wuaX^7f_D5YrFWfJfb6p}hq+)W@)XcAroRmVr}2j<~x_b9uVK zFwaC?zvk`%6CH(o)>eN=bCu-jU4Dw=t6;HL%uog$Hm-}6LLatNB6?ynjW>k{;Ck*k zYTSSuAdztjJ zTJR#fh+fJSt``%QCVf)j)&nfTFe#xgq}Ke8x*fl|dp{i$i0Uch42)(5sJBQ7(PNX4JH|gBz|}qM zKQ5vQ3sgurPT5Z3ju64rIIrMlD^S$WIl|wdX9JFQ4b)5bOSpG?Fk~z|^`N@an^; zxD^L7Go@#zrvEBz?;Wj<4j*os}v>_c!!f}T$!Na8=RlhRxoLwRtPD4Bj zI%P8FguqSpbcW0EGrP2`-;Z=xp-Xz;qU47LYTYMC({~+k5lKU;^Wk6U_W!eBkGySQ zQM)a8EdJpWksUm@x8rXyBwuXCDy&-gXIlapkVnjW+TS`)cmPd6vcLVa_O7i{Etfwq zV^maH;-ZDCEk~82M97>Sb|+S9CtMR%xvtzl&XI{DBOu)1&#HrR{Eyj@X;&#@w%Q&E zHB47{!(a7C|D@*lE*tz36Qu+iJ&`q%AE(cCWobVMIH9F!(d=Bln@iGKm8`V+4Nhzo6kwQ+$R&TC7L2BCNyj;5uYvJO`HomMDEp z=+#*tcT$TTh@yAga2d^^fH_sCs0H4cjnqMi&sjK588g<*pb9Y!=ej~cmR>BHfF7W% zA2)F7b&Tk4_klFy4*Pc}y=^M2V8%n->2&^91kl!0(Wu*Wa@H!dpd|AfKmmT{!~~kg zLREAX4iCC?k1rE(24q$VQYO-s`Z*eSlgyJgh(Yzt%rN^sCZCC2{#WpO^vpy|Ni+DnB=n8ag1Cu4a#(+DyT;^}PFw@al;Y^GXnz(}iFn z$av15V@~pb6im33llR-$3Qu3$9;ptCn+UGwBmYBl@+){JYMW<#a$))|RXMfV0847g z_j00$-IOt?rTM@k5x#G%l0Kjyex330q*P{S#$2#jG~ZP&qa7B}eZr@-HCUBDV5wm6 z;!gY>`TOD>NR;KyHXZ{afJjb_Nh%X$29nZMqg$hLc1;w?SqwIT*BPf48spW7PhLco zm6(WOI*wI`KRh5ZU=s(tkT`(y)9X}N;L6H&D3r-4VXB>qkA1BS=BJ5h_xlQY&1iuc z%?UT)#{BSfB-jrfn_C{K7&(wV@L9_$lqD%bR_HUiseSm*(@Q$h**n?rIi3oFcx?J> z(3-VFl#O$+9Vtf=^BJ`}PFR&UM3Ajz5<|cf=-xwUUcYL@LpqHxLKOfKh&U;v5=k@y zC(L+~O@D*ZEMa<#YxUBD#nVVV=yBt%NsnR$kNK_rDZ~m0AEV_oh50(m>@4J zdgl}R_!h-4j_RT~Mj@>H9CFhRR4% zd4SOJ?YVsN8q#@thPpjT=iTfi)TC{Hl3*ptLAM95SmIDh_0Vvi&+C_Zs^it~l+4;k z79Da#LdZ=q*>y}fT13P>6yIVg0qVy)-}5@nwfG-hJTpX%n!1>9Cp&S(ne&(!M-2{a zW&|hH%-7I0o!yG(LI0$On=Z=Q%J|In-Ffr%Y4=)0 znJvUjd_Q(xuPAu8RK(^TNE3kITSsB3D*414ui4Q!&pe0ejO0$(bYERrYZ(VQ`RC-K z72jh>rHItFyS?5Aamu)r{ZH3ukzWVVV_$Qqhce#f=ui@TH8%Y`al+j*+cQg9)F|vB zX56-y_APj@0LM=_jCLao{Ku+@`2)I0Kk}B^e4J}w?(DOfWnB*<2h~YE5K9b*KLpeM z0hBNxre!a06TM&+_~|Hzv7h4=WjpSpiTwSiwIyIt;m1iL_;|u0F3Cb_T$oaL;fdds zyDO*L^r)0LwO@eTaL(+D7sS4__?iq~(O?Q?Q__2=oy$_BVfD{hyosjZ#){lq^Vpz5 z0wDq)TURLASR~}VGuFRjYS1tD>=d@^fXB-WMk<|*{j5cUNbQJ0x(lxo*cXG!$K^ej ziU(Dphth|Itjqe<297N_>fak2&2dlX=is4^$$L=_X0X|LH>RzqabEp_2^mfW-bOp7 zt&G~lMm((YD}Ixscp6LghJ1=&Br0Hst$V^TgY$sI^JM0)SVKIzGPkNj8*CK*4;nHd z558zF*qkCjN2|qCAfX;{F@fYBGSpuIeVK@6lbf@xaZDCyx!6C7cVKn(ydA5{xa-VAn5{d5AEEZR zLb2)P`xv|WO!X2>2DO!TO^u8G%`gbx18VLl>#=>$CWmk}y$i>ReaD}v$j@gh{!P8y z9O2VNu*3$ZT@SrwsVkbW;f-xn1lE*mMGo_b{iHu4e6H-?+6_YM9{(axwd#Ll)t9Sq z5|P@i5N#@?%YbAACrCVPuZ_%m|Frl_sYpj0ujyyXrhJgC+uM*(to5DKYZRb&nqQ+` z)g}RFf5_lFrJ{I||7%{MI@sc6;b;$CL5rVOh|51CUbblcWp9D`A}AyCLCMO@YUBHAaWhG6zr6GVW6lu z#41w$nUbYcILB6UDdwj8F->p7B5ah`llK4!i<#-v{W zYbwF6<)FIBRN@Kj4>{%X(NQKn00NfQtNa0yS3jqne`f`qYTvdYH)!v4K@A1?V%Ys7 zpB2WZ;_3a6`jPJIfWmqigv2Oo}y5URU{& ze_r~GfE^p-Wyi36t!$$ve+K&g{tJ5mczLioL?iq#j3y-nvw>rLJOvW$($QhoXM*BQ z0Ja*`^4KrSJ(USkAA9C{9dhluJBsP!y;r68Hx_|)(}hM(y5Xk5g#wnK!8g~b5i6g3W6pK|i+1_Y=- zoE+}chiE>}gl;84$EqK~LXpL;Rdhh6?1EL*U8l;^Oaj_ z`v!$EAu1`5{rwQd4%j7uw$_|9)nDR>F$8uo} zAjY-<6|`#6p^g@dSabwKbSNyH6Y)fE15fCNTU!`0?a`wBZ1RITjpqcvNi15NRx#vy zB~YoK_#L6fukmVQX*fz7_l=dY#kf=(Icy;WVDhcCbq^+(k2@v{PcsXCZg<%&`}D{m zx@^^3K1#o7?;%PEI`F8nvY&&f;{EP+Os?eTy6IryQWuw=0zW8|DQ0h9I0;3FbgGOu ztV;>qY-3YZQ=vpO7AZaXg9y*TK8hRmZ%?qlsqV;U?6V9t9Jn`J)&wh3wJbe*{3#&F zNMLf@(*D>N()+S>7Lnciy8H`1P`CPRkgtlTSy=P{0n!GJjm+gegEe0I}eGw+#f{D7kC+T zCT`Uafx>fhzSMX~0AZ9Krnpg0^tjXLM^uwyyD*@9LFDMBTiCS>@lH}MA&Suk!!TgNvz3&12g1DPu_3uvMgERd){ zK@!$@piw|5NK$T}5e`9Wz8V_8d28fy+8f%EX}GH}N`6nhT28ycxjbOR>ZA7PxZcwL z;_>V}rmTafa9UeW(=4wJU|?qnktas@r}Hd89cR_({~YVj8RY-JGhrb2!` zk6E6uU0;cFA8$2{sP$m+@i&~%t%+qdQ|>AYXLWQ+5Xmy-RI)sVytw&!85pSg1D+18 zy)c6P!v9|~?HsF(vYPhGGDRgX?#a#NBTdy))P8TP8Wfzy5$-@e#N(5QUU)AQRWCB!Yo{QekUmeuUKi;)Mw#;$*o+SD*tS` z{!r6fUdzJp#P&(Xeu?vylmt2OWB)Lbx4;K6tT`(SM4ajlLaBIL;9pk#*&=laxU_d4s-s$O`G>Cm>!dW~EaQE(!Zcf>*S+7{P% z9GQ^47zH|#-F&=hr-yGONvE+g1Cd^5@eiPE5p5?CD1Fj(N*PNx^|=n(JeX%lmoHcQ zeQC>g`?=nZ<2e#@8heLwRT3h<-Rc^np8c}c8U~F;p;lU5C1)sBO=J+ubL&M`=W`p` z10_`23B6`PvfVlk9709Ijosn4w&@(OVm@s{xT@{|DFwb?|9vyU1Rcjn!fH4E4YEuL zqkoFPFN??wG$X zri1Sed3jC228ha|bx2L0USn@~5ZOpsvpi26W(2`+_5(@xr&6n*G6lrIa@Qe=znQGd zWG42u28ea=YC?-~&jMs=z$j+W?C%GJ@ww-`7it^iG1yH%lXh%E)5NoMSg#VhVeu{E z=E4YkJN3re-#M|<^ea~GL8@0W*(w4L=tXxCSF6NOAJGHEi!CK%Y>lbDXAR;Hh3^}{ z_5D^*l7=ZAfw~{KDh5WO-v9Jbe(D2V@t2SP61)CEE-HV@UL3!^2&-om`jDdIZaf zmVA9n!wT0t07rUIDS3)2W*L4Iy?`DDFo5QR7(3(oSPjFJ^&Tcoe#|pYfJx9RHsJn< zFqI!hY2ye9wYDB*#obtmyTXNa@}pSBmlyhViY|Hrf)_8=Yqf}vl&-s_P(bM_73iVw zZzosk$cnWf=rK?f#E%o7y07co=g7kA`nwInsScXJ!97?(4_!a)rxLR4n9}wdr_3(v z<$z{M#|$Z-^Em>GY97#I+>S-T?JCsgd<|qb)Aj~hJnHBIF$wA?;&B|By90A< zf+-`0I*|77r5|>sadyB42!+%7N0XMO2G{y@h(SV9?h-uvbFo%Yu)MFA)UJzK78jK4 zVG87>XCp6V7M%T5-qfX2>jHl>bm))sZw1zS`HDc)WmZ(VB9;9q4my5qai7Y3VMi!k zyt*K39NRh=>9JBnP0jHjk~wAx3#4WBkdz_?EGSa;O1l`fyd;06sJYpWkk-l+dA zSGBt8v}<^2Jo|gFkTQmQo{Y!X^H?(76G1#I>JS7^hVBUkFwtNbgx(vf*!_E&Ywub1 zjw7X&kikg0G!)my&KegN6Nm{28kw28{83o>V+Fut0oMkitbf6-*@qnf{Kq4X@`b_0 z?^>%T&~mkLhdPVj3QF&N6JJPn^F1eIqEu{B-|+)!sl4%~<$cDBAv%|TJm=kM=gtZ0 zom^L6&H-)$!nP#(du#<{tnH@URn?mX17>HiFd@Cy}gM8rUCVe;MZ9uegW> zkH-bna_fA<*kVqNy{pcVWjH*Nr+f!~Ndg z<~(GS*1I}qyT;f*bgA$s><*1{T>u3XnH|wg#hbzOObI@>-f2ERO82UBMqGKqnbh<; z@{y%%EwCdqM1Y7~UX4ygB34e%j#&%Db10Qo z=J>mPMdrdTB^W{$$W(f9_mZgmJ6O!xnS#arIouTE@egHX)>FvT7> zFaxPi@I1|eBgN4 zG|lR1z_NjnAX?I(^L!pXXz2OOtHZ)IK;WQ+?e)*SU^)qLsdx-uWph?2+X7x@cSDCL zbCAiwTL&}Mlb&HdnU*5=;3$T+S(pUPwjZChY^ey014$>a`Q{-5LLmH1amwmHp#8~n z#&E)_YvnsGpoK@`WnS$FgevaAqw0lHxL1e?Arh`fDIzUu2K*dMn13iZs zsdbXGA<$wpZ0(XztC+C!%~L*M4$n@c@jVUS(;4~sN$H?WlRxQpsdUsDcTDB>fCfG; zpCZeDf@E}ROCO21MPA67302AV?*@s1R|l}Z)$X=&%rmIE-78EsV&D+iG_I7=s%zjY{B=6)?rl?iCRihT?dm=1^odtp|r~qbJ0@#oLLeN|q=;b@Be*(s`c;!pdLUpm) zOH8S{{}@KNU-`XqSW+&1x6;mZ!};J5tENm5y^TiZJ7G78`rFA8AVL;F&RBMq${4(| z!{znz>Rq7Kr-bDSCkYMBXy4Gp;yI4r*p%voqxSh#*SE2)^^cTl@>b<9e!iEs{7e^x z+)0z$x{^)Ay)=5Ohe}YX`=h5vt9<*#+?f=m`RDZ~ko7mviAm=;%^dmz;#Bs5-}Mu^ zTf~35+=veolicZZ$*d%P6?8Ciz_j!6^Mo1LlQD&V7rgMeT3@Qc?eXG2Ap{8K{W)01 zfu$id0*eckWMg)=9K?Nd%;zz3?AkTlJJ;*kBC&4ZdbI{47)Ldzqve;fJFs}_Doy2- zhP%At-h)WwS@)nCR%cTR5{vvi#E5i5P`*7QZN@uZjmVS7@)K6^#cNogWf-0>0>6~b zd#ToeWsd7Vn+Fwv!bn&Z={S#)9EQqXIc2zrlCO_mBop>;&CU)miw4#9x*(~e%me5Q z8m6EPnczfGOPBYdwbBC^-SxvA^dA>_uynsOmRs!r?J%chBh!s;irfH#Q#%@=lLtA( zjVz617jm(FsRp4XMUv-WQ0*0yfYnNmn8?;|=_`;NPPpb9`p9JQDOL~}@hgiGzjMadHJS_Uy2jAthFaD- zsa1CmU5b0Jqa01aEnzI7r_LcwIj=Xl#LV#)46h4hj&o^x%FTt2_k=vPCABL1i!s_q zDXf-UxABGMk>BaBWU9upqW5@5&hBt+fR9mdbLNI4r`sZ#%(rmlw$Ya$Hi{SvJCO;! z?2r2*x*Kl0EoFJgkaF5ETR|plg@6z3s;6b5OQXZzX`{E%q`Kw1Yq4sm=F~aK`8}Kq zP0mNFc$Ep7(Xs=(u~~fF$wf^`VCIxq{(W%vXvxL(Ww@%K7mesFX#q6XFMunp?{3fj zfkM$jsc!GTrG>mTK%YU#_t*4PEJ;sMUp&r+{Qjum)D2)4tnD zdZDt!D?3}HY&l~{YRVs5&j@TEN6U9(pBv2_G&iW-nyfDRvn$}s$GHc+KI%i1`GZEf z$2HDINlOzsdj{UM4h)Spajgz?hl7^{(enW7yAO2avRzfbl~dx)YEI)EKyQ{}b3O_bs#}BV&ABs+GzeBS(BYwbvNi%MiTU?hD0;BM!lj zgrH@_^A@&^Abl6`xbQjKsIp8F+vOvwI!%LbMNSjYk`TDaYxAsnu=3>bREQ`9?5w{( zeK$Y|g?SUlZ6pD4jW8?%3<6UE77q>%GZ4sTRRqGv@b6Rm!Vxmde9DB2?Ss%V2#yLJgoY0jvJPGV|D~m41bh7nS zh$7R(Aq_p*2i(jn1~(1}eiJj?zN+kX51T=vpG>GQZNU%U7WtV%{bqJYG<<82|AuUw zZo+MFz2_phDw18Vusix0OGBSox=@ROvwI@os3XbMnom}1pWV{7DcERfxtB6m=(?T9 zrjXGYfCy)W?P>&W{w>VlkQ5G3wVS?w=zgz4h)rkU2f3yRJx>;=boIcL>lbv5*P%{< z=XQrSyv%9{R(-v6EmpFWW7fV8W$x4!3SR<(Z`z@|0gMEvQf7c`1tiHL99YhZk9_$p z4Mzqzy(+%kL07h0(;#QjsH_;_W^@NHT{{mLmYXOq2EMp9+nH+XQVK6pLnF&N~7-fbx@=FVS6z~@YN+g&e7QC;((skKe+m^2x@4T6d z>q3aH%e>myz{1j;l^Q6fG@g8j@w(^|2q0)Y5$G5~Gf2OV42ppof z2N~L$w=aG)6UOg%TdzZw3%9)&;^sCs+1Os%bte1K9A#ipTj_BXk+Uu`H9c2sAjU2P z5D9u5`dNcxWbf2o2FaCJPAbRd#45pPJ95~`5T0iu){0Y=7zYw{mWzB#4A;)bt40~E+c5Xvz&;MDNn0+rCx!&mt@ zC=6j_RP$N^m&Gx(JX|d&B4&y}?<|$iqPAgg%*-3j(}{CYxRsjj;I^-Rvi63maL|?Y zWmpbMkGPd%)DZ!RD(amSH4zh9EJ zj|KXwhPSX*LgF|rNgMCS=Z{L+rn%|bQRq~Z<8oon_3$h{r%7@uoL?L1?|AqDDNyNQ zWu9a@U1R9;lL&0I6HiaUoOVcNcwCMyaCF}W;9cJ zqpkbKS3fw)^sg|I;|RvDzO&PEtzJdNRe^@U3!SNdRZ)W0&F230uU3{n{;tlxl7P+F zC8T$Kq(z?YJvLkBRmjK z-x}xBsn8;iYfk>^D}mY#)cSv)157ut;t(kt&-y#p)i}*(KG@U0afD)@4HWCt#&Aak zROk%TZ|3)hy{_OM3UQG@e_PZX<|;D4kYy80(*B}GA~;m6n0mp6xy26k#RwAuJshoj zSNvLlq)Q@cqe=Wkc!(hMDjO9@(d{#buY&2WnYHE|*XHFt!rI)2Jbvk03V*W@eK`!CSeK)^rHDo;)7Ll1N+k@}%HO4dBg9galp- zHlm6O(526*{P@}|#Jl4{@7-?`Li~-~%dE3+03&toTDEVoQk#PGvQt+*JOq8rsE5MP zn04vAh&7CRv?xBnnXNi2LrT}O=tJN&{=UL=Mw*>;(`aH?&Koifcp*$U-+EjIH@u^B zWAFGzsIAtfYQaLEu`ZzPl^EbbPn&N2CN>O7H4q)x2bW)xy1;Rjm2z%RC$Q;(P}C@c zCs;Q2vf=ZZ)0x=33Mq(PQ+n`iZZMi2hnI$T7hru+y877VS~RiyGmo8zYi1Not&*-; z-41H~k%7y?0wymUSt*OsW4jB*F$D#@8g=XP!L;p8$aL# zjNDJlci_@^=LLO9Pg?jq7u?mRKC=#*K?yL|Z$^kV;aEoD-(wq_KyjHOM7DrRLF!P; zyOAVz$`UhcG&k&i5MueZ&dbTQizxuCbqRGA=tiX-K{VGpVq9lFsfI$j8fxTmTEBWnLQ9upzzMUDsxg#{(~Qc$WimP>tl4}{9JUAtf-m!B(e~AmIM{ zeVr8H|6{(gNPSPcs(lU-#b!eVE21Jh>W7LCW==1Gg0*aQZRF6RUK1@#YenOuhOp<_ z7+kFjEj&IQoZFmf^!4Z}|1vsF@$9_=VdEv&VmMqq;2H zewwUY-@qWWT@z>%%5N_}J*ZD%=^6{7;2E&Vw89-44Ko&zZ92sXCJ*>BM8pb*B^&6jM2~XMkjhAWLkZ&GOmTGAU65-1zK^jN!1y)cMzL5L=dIV^<;QcMFp(Y zKU+wI_->fjD!#nV4J+$A=7r%T#RG9=Q(mj9NL+@(mwM|ea&K8~=l1GiBWdx-t))jaLyLa29En@u+HHuY%_1pwCmX}I@wRXhRHzR=}B$G-kg7ga)q#QhZHb` z=V?TMVxuk?uJKwkOpj9UT$Tss`+}{vKH41Ec=xq}XHA8lXq(y{EA?_GpEW zuQBBgO&|kC6ZXg5?QMc?HikFpAqIUk%Nd zETW6B@?ysX#r`B`M@l0@$YxSLO9oX)+xM3S>L47x0m5&5Y51B>%9!sDX1iIPfplSn zPav(qnMF)9DnWO0CT@feMJ81y_l?(J4pK?I!hf7Y$JKQe9ib$Gq7A|XZ{lG*+U6OS zhQCYU1=!HO=0TvPsheo&LFf4S+XpK52b|J&)th|Dv4hw z)co5HX0uU1(gvj>iEAV_%KHwDls~QgFNx9{DI+Uu6P7zJ!7;;wiCKB!ShQ>0s~gc2IRFh5{a z$?mu7AWV}-mLbFZ<{9r?g8gpaaf`w}DkKp(Uv>UubqFDZs`B z(F9s!BedkVHK&)FGE;L{HGBBP2Fh+Lsop_UqE@ZuBd~hUCE{+dGqQ)*fzS%qEv|F%-xD|v@5=!D47yP4Q}^lDO^ZCfqB?K zhrj1Lvu_<~Oq6(AfE7-uhVv|*M3y&k-2{VT3u{C_`t%CX!M{|ng$74wz*TNk<%`u- z7DGlFu{NnWBmM0Ml-<=AF>e>TdQ5){o-U?KXb zoNq>2J{A!k;k$XvAL`rYBsnFr1?die`cVbjZs!pE?8H$(;W;LDk48m0K1(?i&cFu- z{D0a6D8oDd@O=LaWM*)0Xv7EGZ(c)5hdGFMuE}<7{P}43o+iZ}>5Rc?$<6+BXu|Te ze{m!gR=-;C-#XT$46S3D9QmOOy($4aH+lS`T-sAh>q^cZP9rr4_tU-22PDXEB%?KT zG@P&!FUDzET$5~)T-Bl9&h|fg`UiJtkX>65HXQ5|4^)On)C^eMFQmM;^TE6KH=1EU=N|;M37`ZuL zpO4tzWLk>Yh%g|Tn|3pIXz+idJ2%mTWdkRBStz!E`^WRotRN@IkC4C*j`pUaOn8Z$ zi~fz9ZDHQe8g_4Wk0+}?QYeH(vA&V{Ih`9t>K$1Apuwn|lZz{QN?Sa*IcjJI8BnqE zLq_hA>|qA)dy;?Ay@bucjOKeO6zxr6W`>Z=tpYqzdyQK}ToZFKKLkv_ZkKW`bJ}>K zQFi^ZP`ux7Jve>*C8p*wsFan&`{)#KAT&KQaC5-BLw4;MQIBePT9lg$$QmmlfZ4+$ z)7LJSsm7aOr8-IZYnreE+wR=-44SP^hS3Z^@7;R*{nI&eE^%H2Rwxh-)cg5E5(dqP z0B}V#7e5I7^5qsDzURAVg6z=Yz(pQ7?&Tx%MLL`-Q9Tz7~?;i>P_I zvmy2V?pHrcmh6q8DmDvI*nWT|-S98aOA|+vZI?`7 zEeWp2kMe9O#p=R|#W;r~ipu6x2mge4_GMdq*yLyM>DqB6KGZx|A)9Ij?*AAUJt-r>nxBj!TXFB=ho$9X875J}Y*lS|9c z$Yq(^*}yk3$VM+;+V{T~g5OQ`kKb8p87gt3+Rtd(!eXn0ELsv5+E1uWWizMqS9#vw zy4g>jMVlNeJs{^@-r5`qHEw?)`@0=c_GQzE**;6ePWjs3!QBBRPge|?TR0UPll~!* z2BHhB@L+`x)*wOY;+;l1-gx`5m%0>qI?6J7z+(nlD%LT)GSIusEfc}3Hdm!^F0p-? zMPg9Y)R{&A==lyX8i`C4mG)oLW~}{(@?}ptC}j6;)v>h_ zIlC(0l4xZ|v3I=Mi3T9eDZ0wA%axI``g|d*5t$D)7Nge*Z$~ku&qvmMf@E&zG0poQ z*%Ys11}5$&AZT~lcF+~+rsNz5X4dr?5ROQpSM*gwZ+5-1dYgAG7SKi>Pc-%w(PeTRtg}bY%2pfYpalJEh4lk{d#gNflUcXOOTN#?zN$GE zD02;v-1$E@|IkWj0jb#DOsUa@$6Zn<+%wxoBc)^XcjW=?D(~fw?xn zBg0awD~Zs(90C#rOFrOBgu(5Y`+(QZqlVC0%D%UJ+{(MFQBch6T)TH!C%Nx+S%*>& z1f|Qyg&GA&u4h{0k3Ns^ci~e6g7_&GcG+X9F!LkT#W~MZ6Lh2o@3S9V9G=TB^AYRu zDsM{Ra!IfS-kYys)VKsmQTnfQWSf_rmFv3@FX#)ZC^fbX!Pb0!OfPD#9(7 z8$O+Q+)$aBiJ+G5>SxjWVB%|}iFX=;>P|@+@ZYgar#@Y-Q2#kT26HlU%7&sylE&HV zED>KPC)4)(agl*Q7^+|vc@F{5Z6j5qy2S3l3<^6kzbWrNO0-Zq9D{bw;%8uLtV~b1 zH}ThfD9oqXV?~>y-q?cHyT@@@E`-3j&2dy|UYtAKNNRU(T>Ztqw^6zp$zZ$rKH?jf zM8Hr5XwJ`Yo6ta^m}h67lS!slvYowO<*F31YUo%=CGqKUp_O(pIkZJjawTYdZXLW! z7GN~6ihu^%xq^RdsX#ymSygYD6zJg3(k9qJTS8EJ_K`?aZNaI^P68n1mNV2`T<3DD z5^A3p8$GFGavV6}-1bHE;?sCI)CK19ne~6p#Wb6)sCF($9`ISlK*9i6H;mG5o!zwqU@QmyCbBnU5VcFWp- zqVU>-7-v4@P9y=@KmQ2Q;k_`^7#i_c{H3u>lN;S^EoiVV?^zkuMFMXH_SAu``gMC~ zG<~XZw=lVa;nT<|fMZpn!lirPo_yF=3}OE}Z>elL46~34&FSOipp`seAE}NCul=3r z!@OmrL7ZT5s7LTRcnP8Li{ZEo5{%TNNPy%exkro}s{wy7rO!8kWX!rX!$Ad*UW1U> zz6h|TOXA0sv+ZFT41h%)tPy#?C~C|p@b)?9AN7fIV#-JEUsrmIP{xPiO^cPW_2ka< z1$uZ-nEy4%I~B%Vl>UsT7NNb(`treS+XRuo5u(4L^h#KNy zS0rWod+8`+y_d*xBCdByQXaLhX*y1j`2XL`lps}D(f9Y$LFUbXSYa9_E|9%#*t(lL zqU{S8V-R++?fy=52;!uNtw{7ziYO_Rc`3&f@;)9bGL#cJyE|H|_)A?*>`E_Z#Au&7Z2$86`4x-(Q)oia0sun9(NyEwzc6M2`C&zBF8qrS z`N8-P9)uCoP*-r+i~0Sx94H3&tr@79UVR!oa~{r#q=>0^>cUFsYeMYz1)c!g$*4)a zV24(5p+kPpT8`P$RRxqo$vCgSyH3I>$V0LULdc)7**QqaD6+d8EZZRIp;t8l^fq^G z86%s(kcOa0Rn*|N!fnoB_$9x8b!9ho$x7GC_7KkZh{Xquv3+i;8@3QbfRAwbmmWqj zaUoR-ER$4GWzF1)|BFf&1yFHN^(O9IJMZ}~qa_d8^`PK<*x{bjJ6LQA>Ux!s>yy^% zxg03nrs4D@$*60UtLXUd6n@koheC{Ld;L5LGFj=Rb(*N7MjFZhR^!seU4$!4cB4TA zqV3o*o2~!OCHe+m{e2?-liFFT^LVRBE=uFE$I!T)(lH=S##nGW;0Agy2E(GGn0y;m zun%?iUXa|m0W5+F_<`wCRz-X^{Ezke>6TJ^3qUfVYOF)6ic8N>jIM>opnO{YXAW1Q zf#1rSxyxB6%?UL3ZL|+rsPzsHjZsf!sJHCfQ1rz-ci%9n2=sLZ>Nv^gjC~bTe#`-i zKY=RH-U)6m3dq~R%))iE{QQ*)lO(D;d>Sm=d*}9wFkFTX*kn0RBVT>93|2}ERM!MC z6EAe4?Us=5d&5bLKRVex2hlfk54C{gp4*BMi2SS<4%J@kOqx-cz2iIiWVk+nX_*?|x zww(Lp_~`z|Ff0@$b5ItAQjB{7D<{?Z+94baG4Q@fWzKC50vtKVRi-cB#1M3&`|t=x zgwUua>HpOk44pn|4O;JHvIN4+cGRpBPe!4GujycHF8(y7XKm%$u$hT18K4S#`^T@( zTlJ&cG`c%fWM02*u`{l=9rr`A zb>!uj$bY>FwB-jDI6G($KqEc2CA(zP(Gju{VGRjOaC`0_izo?47_US$gQY?^Z)Vni zrm0xY8H{|@(ZL;tzy)5^FLf9Spggw53o>Zz;6-1&9smY8{&)RLlp-zAtir>T^g`u1 z28Lc9BPW79ck;6Zk(!aP5L1h1l(x)Cj;#H)-$%xPA9i7&cyy^VpI~3IR6r)Jp43cS z%3MnYBIsQ9WRC83lNgF_7Oq+T)P~t#Pa|LD^$|_ylH3w&OLdiqU4OJ$XhDDie=3rx72aQY z(ii}pTz}X8vg_0SO7x@K?( z#W!j;BS;mOJtC8MwMzty@E+jHQ^S8flV}y^bp&+Rs;92J+85{zI!s|d1Bdl|yZiYu@~)bJM9A z5+_ML6FlB4qlj?~5^|J|AaV|b*IvSu)VI*A#%4Sizq8>F*#4py(ZK&n76|Ix))x1h zVL#1(#8gJ1PR{lNkl`$#(A?{&p=yAA{DPv!ROMJP7;Eu9nfLw_3RUb?LHmvI{=POS zg}V$^wMB88U)Qf4V=b7YSmN-i^j#r}T* zHyOz0*IF+(_c^N8MI<2OVL)ODc_3e)JMMZ}=KPoJ^Eb+8`#0$K3+k-9MNV>1(%cIQ za%p!nEX-;eM1&f;AJdjG+WyxxF{#Nmx#uPdYll|Dbz*wYgbzyfOhywuqU;F-Shs-o z>hh~=Z9dy&zLaH(hv~UOA_jBYrQ9N(on@ad*~%59Xb8h4lDf6R2DNqj6_MmMt$F1B2vAe32s%viC!P(ay@mo-bMBJ|RG*ClZC85U zH2|KoGkQ(#$w*jCjb25o0|Z=qKOg^|NbWk@_eemh4?G}T`Ujr1d&zoi-N;^RVM@j;FXv*Sr235}a~-m1UvfSUjluSps04Ixrxg$ zF(X`X9NS`g@4z@C7l2{&BvGYQYKx-}z19He$8ql&OXf%ulVX-&?fN&RLj-)W^#><0 z6mDQ;^_5O6B+${}#?P(r*SWM>=TDVe zC$QMU9Tf?DfwHe%@5fWClEGCP6OZC77%5eNDK9vyF83gxGaihq(|%SR&3x|oyGMmd z%&sBhi;4^M<1b1~DLru}9}LN9Egw$57CCBD@lGa{cwTMpr6uF|A{G+DLL~%JpaeLT z9o!X#71pL2saz>T8Gvm}S6S}svN=^Qt(TLC*}@u=I2RU(D7qZFp%0CaHHb|OLFB0V zoi5(|M)UM62oGejG)IA6O_&~r5mQ5oA-F!Tt>A%`oqJ>~XM2>}eV8y7q8Ubr#)H!c z*RemKW*|(!-K(A?n7?3eBE2d@UQoyZw~w0#d};LDegpk_0`z6ktxbTO)r}a*)LF^k zn*GQ0aO908zRJ5LU7d~C$Q_mrS0kxlk;_5W$NUDk=uMOTr4kP5iUE%J!UfpU{%?s9 z%oqvjye|p{jBwa6-@iC&rn?0OE2F!j7f)|NB2EdzA+in5KX)l!9j+G3P@C{o5LIEK zutl<%v&;2=B`?bl-bCd-f(7YI6CI+iJWf3>0ic>}%#12+sDu>;Mi*z}uJj0Fl1eSH zQ2O9eI3VsxP@0a`c?>dq)f^EBA9m^F;Jij*XVI&4mpOvB<)T^4Kt>k`O?A#?@(?|OWCy&4q~^ERk7#wo0nyNy6a8^|v`H&2CQ=pZHy zU1b0fC~6!^lv=K-?>ZW$@z8^#Z!Lz%Xxj;rMb5>@1D9$a-BAvvw+Mi|;T0=&(}+f) z)(6gvDSl1}V^OlgJ<_W|@tqyEHfzUk0q4K-+En|^ZYenT%GnmL2|Jws@pxn_uO7oT zwt)!*IR^&M8rj~-_m|?J5aaiO!W%@tTcYGy(OchvWIgUUl(Ih98XFtuJsxRHVLBAs zLi}w*gt`5L`sSDL6coKEJ$Lm2%kH@dyz$umiJP*_q1-r}PDKLZTLD*=OpG*ME>itk zFil<@$C@l=j==J96LcI~j~CV!@bj|b*{Yz9tlku2YZ704MvMb^fuFD8fsPcn7 z1aHnE96e3ilX^{`V!uGMnnWm;z7QDUp+r2miGI5u!NPWV6GhlT1Q$J*r@7g0ycbMT zThLSWD*985#IzrwWdDH%xUob+ESU31?!614p&mIrI$6L4afcVoSgc{HPuAMNq}pup z?bSS9Bn_>Qg)y1kG~vF7)bF6ZWyXgi07zo*tmW?g4KO8#x&Sep%kbK!6|p};D_1jq zZ2vNk4asD(M;e<}4j$$~8#i>5=C%!w0TEmrH@B;EvLQSZaImZmm)i)3Jx=aoGR5`C zTMyu5KhzUISvS)8+s{mMx9dtuw;$v$`NMlI$KRh|i}T1$Nz%lEEco&x2|_4%x;GHZvfpAyA%SH{-rsl~N|4qm6pS2($|;2_*CWDUYLF$<-?c7L z2Wuso2yfYZNEB;dG4F5V8%&K%!yxJ1k(@Yl3+g!%_BZMMcn1N&8Z8g&R9tIEJ6{5;>M~D2e41n~y9IJ_^SopD zg0A0PdJ=P51I8@FGkNZzb(F%v^J3K% zJg=R`OBUAd8Uy2cc|n#66X=GcMV?s0ZBpih)J+%D$OJqhl&cV-C-JXjo0L?rouC+~ zR1Yd4g$k1DIkrPKWU#DQU8h#pK24g6Ia>4+$%f%w!8XSiIHJzJ=dFOe6<;Wx($TR+ zCigFt?S<`!z0$%p)-?@<$S;OXH66yxQpJZfmG>gH?5Ew{vv>`u485 zby|Hy#o|`!hz~Mm{<}1+IQA#IL}R8I`n1%{)Fw4z^iKJ?j0>?lX;$S`gRZIkpf?b+ z&D$t=^Mxkuo<*lMnJ{{0fIE5|FoSu#@9K^TZ31Fz?hgo0ai*IRMN!+NhyC=fA!b%X zie{@m57}g|a88huiYkT_7SiZj(dVd3OT!ibt%xjlOqi-~d3etBqs}doFG@&h>rmIXeZBhCp3QOFuLD6m^ zMo2!fKJdw9$@kVE^bDhNMOrGP5dq}$de)Cy#o$wL5>j-O>#IxEFeYa!;i=Ki93-~` z$KGQBT1D zK1JLf2%|Bvk!XXkP*#?BUcKd)Y7BDj6sPkglh9 zo0}a?c-n9F;VaUB5t%8~`FzCArqEJLfcK4g!CRxV7XSSbk;@&x2*?`esWEUKIi?U- zemAmP@N4iWR82P8qhc^DT|(-ysJl9OiQA(=@f7F+O|dCdQi;pobi^%>t7Bet64Yta zD#U#h1L#ztN|ypmrr_BR3~t zq}eIv`c~SS?av2Ma!9@s5%=_Ve{F)hDW`xXWpZ9QN5cTcy zo@g8ufTo`y@Vug;gZ7T1B(OTC^YqsTtlt?7qE`Is$h4>W?RpbU!V^!-M6Qg~pI|6b zDf)!*WH&j(4&{=cP<^*C&b$>ykao$m_jSuOv{yKipbn%lc5@U^m=IED(GhFEBdx&e z7Q}E`R{$kPOMOsdkAwDW{6RqV6ul8p6(1nj(zs`Fc0B4m2SdUZB@F)ACK_o?{fv+F7^Wq1HfDZ+$g3Ra5=Zs`eSIY zO4Dd2Zt=21?35eoA%Br*SIqVi#|X1tRL!ZY5y!XaR@il{sw6l#h+#ad`bR}m-rJy$ z0x774PT(RmSjgqXom`0O&3c=$CUh-k!p@lO+z4@9_H>%GIkwbA9TfoB=^dstZZiNYNuyL0v@ImPEs?qSD8!Pho_=Dwq(-+*630rZQV ze3_}2I1Nr^_&lWK;F{nIIL~ORl(a=u8-c(>6Yu-@WX<&FP757;o&6*#XXRpPWNaQl zo@A_N?cQmap*$9^i5`cRpAn-->@{bmv^yXQ7wHX8KVnE>xTg=P$vN64W~F5QAP5js z)_a8nr@?d+L``nWLQkor2ZRyTa)4_yA6aq@u{1>|8*8>uz8He^Bm$ud$JLII5fCH$ z;RC@Az4YcSw@@|ur4D|z%9*>R=3JM;CCs}{_nrBkk8i(DmpGJUc?htMQP*VI$2r!i z%S;_}WKoFOD2%|cVVqq?RJ!QbQ=2cutwWA+<>qX!#FRy|Thwb1=~OB3(9YFy1rv%-w_I&3+b4! z?FLYF8a=yF>9L zK%HQ1^R*{)6oKdODXX!kA7g4pHYUSC67SrakKp@ zWaw+CF81mU(#m6^Li^J8Hq;X2$0*H<4c(d6GQnrekcMnL>xImRA*B%5Tu|&LsPZrv zGhX(vj%G!9G5Z>*+6+AZ_9EU+=e|w&G=e+cm4?S&yeGQytM;qjkMtfvf+3%P3jfmE=+o>ni26sLt?H&coWaY2uhG>@6ihd>?Cd&18NdNZ2hX4dFgUIWq}~N?%L_E2aJ_6AjV_ zLxj?<@HB`O9>4-a*sFltO-;V9B;+$&L;seI@qtGlVWuD*>^9SNiWwZ|!;wdX$G ze}0Tr#_$UJgcQMZg%9yk_F2?^Ze@_9rIB~bi)l8$PU5}jri+Y_HQSU@>ihu5se7U{ ztICa`%TGHbgbn7g{#Vv2sflX$r~F^pL2;$KwG%c8lqtIk8us;X6BCC>_^wonHm>r{ zfiJ~+S^)2l1&ZJ|DZEi3%<+7NeFZY0;0I*F4R#hccXggR%%zw#h{^KuiG_q42M~tN z`hlB|+7E6*99K8m(yhhk2CvLQJ-O>fZ-%K6XrC)PD;x(AXW_<4Jl{CU)gG<2fPwlN z*X&j%RI5ubWOt;7&258E?~pHZ?c{4TM?0hZO-+diJ?!1?U^%-Zjyzp;08C19e}_%H+mEJGBBY+8 zx4AwBuq(gDm#Aj1os7{GNi1 zmv82_@C?d|JF5uW*WuGow@w zp(0xG&zH?MTUXsmBLFfN?VULY>z1Y(Smf5!6CHH35~w}aF4qBgdcFP{$(#Dh4{UOs zc5r;={+1)Cr2rb-T(Lhs5si0>#GhBx$c8zeS}e){a>Et4L*uZiIiv~~!ZOp@4MWWo z&(u)ok_m>NywQjlcJ{x!Q3X$l#RF_oW3XaNY%=jYp|#UfJX?=MPv(o+0wdOZgcz~c zH|?L%k3NpsKxx*Vv*#5?RKiV@WAfJ%%@c#9@5`}E)KUR3(w+1#TOIJ5Z z2+<#wrbuW3T{$`!8pibXMO@E&mh`<#Lx~6)*~8|3mA=}3#%{MY%6X6LX3$})19jS; zDSLJr2PtWR-@eYNtYWEu5;R_Y1b4jd2#AO){Un{!7NNi7cI8udqunTALfgO(lXbTZ zXHgj2U?pSZ?CRoGefJhM$9XyjfN`Ji-+5Ku-5E~`1!iN~?xahnrJAvqhWqG`XorgF#21)1Dto~#*qDr9 z02hvm?v=fW$RKKnKCv9gf&mD63T19&b98cLVQmU!Ze(v_Y6>+sHy|(|Z(?c+JUj|7 zOl59obZ9XkF)=nX3NK7$ZfA68G9WZGGcYp>FHB`_XLM*YATSCqOl59obZ8(nH83(c zARr(hAPO%=X>4?5av(28Y+-a|L}g=dWMv9IJ_>Vma%Ev{3V7Odw*z!$-O?=_+jgGV zwr$(a6WdNYw(X>2b?lC9b!>OXc5?eY=lkyc&-dRu_SkFhwbrazRkLc1u{Q;YvKoVk zxuYpi($T?Z_J382T|JzW}(aqWn2%r{obn2eeYybcfF`a$bAY>pInV{*W(5RDX{f0K{z(fU z|2IGfS0I3a0pJP*0<7HJoOqd-+}tf09bGJ$EdD}Vne6_Jab^0)j6qUKQJq0vModCc zO@h(Q%k5u@m;>ERtnFMG;r>sm`qXCnG=EcN;b353WoBgqC|H|WnE>qoYE~vLPC)wq zTUwAtUor(pb88Fhzi1OXJAkYGKckt66Ts5W+sVom;N}SU zTQ1ALG_KY@|KfJ|TU~R2YwcEdn^H*W!y~u ziZ9||X$J%_|9eT}pEB85I{=mcOAY`73-kZn``dYDwhlm7R{+Pqw}5{Y`A?S;4rYIA z>0k*^bNefoiHrIFUijCxvWc~Wo4U8t|MvS&pZ;sk@_*(ECT=d)UI0DjzsHW{FYxd6 z-){#0!=0L&i=!=2%i7${>i^3qD(dJ3@MYj&!w!jBrd>XOs%0x%NTyele-Z*lwUz5+HU0POs z(Z67(i^dNk{$~p@#fU|o+|SnIgkr$E{gqvbx$+G?UkMi&SElR^oi^jwk>whEVQ z%Iro${C;`)ZyMHLM4!!hrFp&$bGX*|+gyB%hBzw+oOUVbrb+paT(caT2^@4&8^oR! zkULaY)l<+V4E9K{q@IW?E}3N_2?0KiH=-p8D@3VM((7tGPG|3A;_ufYbou(+HBN@a zdG`yxYY7SSlOHdTk6s^{@yf<=d4k7>m)kldZmuf{i6kIsNkj9+MF!{YZBt+{2da;2 zpkMlR?|ZY^0di##wsHk++mL=LJt1w z|EP=n)#3+oLn8SY24VCNvO-6>$g`c-^6g(TQiJo)l!dnqeSMM5)Q{(8NX+n+AJv^ zHkW9?go1AgGNtlRM&?M}I_RyOLn62E;xJEV=?FDo@0K*oi}8u6@LdQkqtFl<8@cki zrFQJ=84T|iss#WAI?{)R(?S>8&pR zkXzZ_h75Sjd-*)|xU3zC$A;dZuw|m|w6hKJH4f`E?x;A|XlXj{6W^Y+VCEV-#sy(r zOgNopXeO!uMF{mAqc>aCZWR?!8$^9}tI^MtmF9M(42=(ws|zAo9mtRynCD>CC;Lor zBl1uJNC*Cg;*@X_okx*0&*3ShWitC3k62oYe%QMZs7|SZ_fpE$Lybw)9e6c;L-ncX zM|%I}#^tZ|(~pxbtsv%*A_47SpBz-&XJG(KrcJL|qcIu~rP ztP?sdRWKDafkDlm3|9Qx74B9vLwu!7^r_9*U2aUq(vaNsqpHz0cnV@<{__yA0GCtY zn?`;)Jf?xgZk7x;WF#Z6@gs<&mnyBaJjL(l)e+ilFTN)d2ef2AS@=T%BZGfPEks9E z#|nQhe?Us$X^M6aND`V2ndM+J60j)s}PMup$ym!EkZ*d5^dF6?%GhhZ(ldqxV` z=eDJlP70&WSH3rP&W}xLB)nCRnjeCbG}6$!kqfv^37Z}^Zu$XDx&Sh=k%-f)XQ9|& zQ9d6CS1#oFlx2*~vfU)ddC|K@2qz$3Qoj*qbeD%b{{4E9uo;57xT_lf@}5i8!^$jm zl2?B#FYMRv9Loj0ksAF&86&2nCYWCKEjnLibFLww(eEI~?E41JL<3HwCS~$@KSl|DY3>-3N34m%PW`r$0(W3F zB-=MFzTn0Ik;!2po^9my;?Oj(5!sKCu`|S#)IDXwWvg2}Ai&b%=hwW1$Q$EeWZ|Xy-9qai9tLX^6mt)f>e=8?i80g@Ia7Bv&1m%^s+g)$pUJ)a>4a6G(I7rd zEJG&ef*T2Is%Q}M(70f|uuJrg!-kixzyC+#%n%Yd4+VCwN(1o?uQOHcvK z+{=+<_;RjIr3B||eF-cQHz$3St}p^L#2&7K7x(X0GImR@Cc1%kC;99Vj(B}|#sx>l8sNPyxY;`PYkeG#z@ZrdFc(0lUFiA% zcOGn>DMiN1OR^*0GA4U94GCrk;S0;0?vw%aAysTGZQ4Ye&(kI?YgO{}Ec>0lDdg9K zm*C*eM!LN)NA>0crSqpFK>Z7>PiA#G%`aH$R3(jPDl;TL7t2h#j;UZ!^iN}fp9`WC z(0Phw1u_n!(RI)=5i`e6AA#46YcGk=p(q&weqWls+likUC|Edg1oc31oCyN6h{aZy z&;AYmoj+F>BV=TO2bjbsbPy%aOXNOXdhnRg81G_RzYyGn$<0$WXR%n+EH;2n5$4yG zk&$M5mk`qmIIc@Bj}GJxM&q8kJv~N&q2cDJX9Yb8sG)s*$8n3O1tE6gTksPcXe`tJ_Z; zjVHn*dz5K8opmw_0WCc^O4!gX;gG=$Hp-zJZBm4XP7B)jHdy~kH+r>v!C7J1SljzA zrXUBm?}Q1JRXPXzE}|o(!qKps7eF0Xu*eYLErQG&pVbEeWXg0Z!FcExRDX^-M!}3n zI^GYNT4%@*+qq2ykT_BMiLQcd2}jM^1#o}5HpkB@HNnY^NOOsP?4z4+F=3bXh?)jH zo~#wRds}_{5nY*;rTiNYaTWr__Y6TSMLR=?f(4O+vvIFmR5qC^)0-Mx4?4cHfG?@P7^j8N{%(O%pq@ z7DCLTtG}-cQ1lR{i25@{2^+WQ7L2(n*YAf5@YJRJ?E0OF&MYo+v5m)2$YDorDx&Co zQjsvcWU@)9M8bBLJMc8w#dO4uAqSt$=u`Z;t^~D}TGYW$+D8AVGwV`i{V~HnzKsG| z4t|~%Jux@Ll|eMMVpf_rTjhpR4I8zR^$+SpGRhe5~G<>#1*K-h8)ZZZ8~&fozCUw1!M&R;gbU%F7|1Qe_T4;}L2u&Hb9B3ia$NibLci z5+ptuw)HoQtA`5sU^ayR!~!#TgEf1cz7DQ+Fdo+P2~6oCbsVNf8a2O{y8<$Mm6q6< zcG#tU3Z%$!LN?`}DoE{l>+i00Ph#PX9604I?X|j7P%!6{u9&gQ%uOBQGuZ8oS?_ve zDDCtzhzGEZKhJ87y$w+xc_X*CPGK=N*njDJiJc-i-op%<#KJ8t?D{AirVqb%@YaW! zz+G{fab)?UW(UgS1sy66)4tv(2gDUgU8SIBL#qFtcQW09P?Rxe@!&8c-FXCK$j`x|Mjeu65|V);=6ho2{3yIz5a?gUoZ zdUK}`|C{AUq6qG`wt-g#>WYnp19ZY9-`^2%-=b>qWM3Fb7)NDUXPUY*m0@S|gOPhD zTCVBC4QaPWb}3Fi6Bb-#Sht@L8`T7S*h52m&_s7474 zAa7-?RFrpF4U73opNw=r0^TniniG>vTnH* zNaBteV);oD8=bIvM1Di{GLp9g`C-PEu``hz2?dn1GvZ{F*6FvWvhvn9y% zt%9dr#e5QZ8V%KD)GyxK*gJBTS~jL|{zAKr%FiA>`~+H1!3#bQlNzO4scDgOv5zYA z>Mv`W0BF;VKK~HqRKhr7pCq0_^6;xNhPv`jJeKt=qEZBFSQ&oSsbO*6wJ@mD5Nin2BMc z3nIFuQyKwIosc@EsEC>NEdlY#oJk*!n3B~t6`gM1StsRWi?7DYovmD1X&3x_OIgx_ z{ga?s>=f=2rC=LIxNPI|G)!^5rhW!^c(#{~Q8DuXjyEXVws)wRJAR<+u~q$AtUv?R zIh*8iV-7k?SBzVUKJq(vS6sruCGDu;s}nvMXmr~456R8`S%|GU8q*b~n^RiQtsdfw zM&h{LS@0*PM?q?xQ2o0Lk4Y)+VvfO@bsqW|u_u?dw$=}trc?-*6a*@CqzqSbTQ2>C zoiTt%1a1QVfsTk6R02ia$GOHMEG2O44Q^l8uOYHb;@J^WWalYUzT1teh~T*H z=f5H|8fGj`Z4yh@N?N_Rj>pCqZme?&xof@IbO8Z(Z2iLZ9yLX@@(XYVA8Wtu<{A^; z;kDG|lPRY((d8GD(D;TfphFL%45G4D${kpoDG=Gx_A**fr5Rz&ND;YSSP`Uwo3+h+ zgzuA+w+Tb)nSdv+e|R*3O4isG>34tblNzEF-l&rf?92J>R|#r9J>8bk{Hk60$No|( zS0o;Ci&F*?Iko5`pTJF)$O^OmsOX`!6DcU)%NyAOrD9@T=34vvi3IwA@0FFzokoD? z5+$;I={Z^x=M|doUTBC>NctRqB0ISUtU5<@HFo^=#(>KAhG@U^>_zWuiKx_X{*SM9 zMRt}?DJ+gXBsBd>4`W|QsRLa1tVY+{JTp}y$KO8D5)tP6-EZ&f-Kg+36IA`yyz2gF>z}8J;xhl@D^}4E4*H>>`;F0I zbm1_Pb)2LrMHwW9($8QaoQPKI=lPO?x+xMNNz4jN8L6BYG7*2=4X2tb_VHi=d@(;^ zPH~9=Cex+wD?*ZxXM{Lc@w&bTL3~YtMRkNNp9C@wP>{X$Zo%F*Rr@O~@rP>1f~HQ$ zvf>2w&nHzqGTmR5s4576pnfy`w1Kp1F-y#Q(Ejk!tR7avC|X>Vv4Kzn0R9V>oFG7qAgg7lvN(PVzmYIi{}}x+nQ%Y z#5Yor`EK=^%jO}x8DFo2l792F8oqMAOfNpx0*QGtOo6Y?h6#$TXL-=-MuwC686QUQ zpDh=}Uu_04BHx7^#CU8_5pUfd4>?}MzmKeaJI{GlVXgonpX2eCpisX|g&)ZzK{Bw7 zcNc5T42>#9CP4801SyhtzUSUafji1WHvE_qhj=OGmsD*V;waxwj(>VE%6^7=F0ANL z&kVEEFOh)#3|7AZAr;mEYu6%)W!&L|#<^~CP`hiW?al8SI9tQ!VWbR0fTb0LAS7kOAgn~rQ2gOxoI?L2~@`SI=y*eZJkLzHncl>ba zIZIa=O5IBO5}|V3dzFHDh|Zmxl|_AZZqXYilu70i`)bNPR;YEcnavW^H|7n;kib;O zH{+<#V2htW)?b%{na%nlBW?5faDbA93p*(LKP;eJxiqw*YzPO_r^SIDO?7yWTaPGd zWh$t+fb$2g%F}a_Gnkpz@CB44EEH@oEhO@<>JUYQDmE>}>{ktw=_y&<;P15tR_Ki3 z<1T+BV0LB%cm0a>&dheY6HxU5>yin>`{=*`-1OBR>WXA0DT20bzV|s;Fp8qc zG%qN;zmTZW=V@f`(LYp+b2c#w0UcGF%1(zZQ&q>dqO=A*-i|JGpMdWQmG|aoC*Pb) zD6A7CfX(|?2Y=tN{1jF2H=*Lv`7YLT!30p8uq~&;A4lJ@jqV-4%1n+ja_mM(%c9XG zHh(sT59=8U#mDWb)7ay!N8-5S$DWh#ZOPde2npNQuj9O9JUPv5TD~wWjvzo$+m_t9 zT{CAwM13rI=*`!y_-XBa-=3h-c90De?R&H%b8NSJc__ozh=j$rzpa6I^`*rsKtZ^iVQlN20Ogh;1XCSG}7M2vlgtn5F^^9#3ce%O$c)x_obAeA176&IJ5k}L^P zh%hBSQiUyUUn;HW5@2+Q{TUlnyD!jGH*1e`_!dz7Jh}x>5v#%*RmIB!db`mPJM{Vu zfAj913S~ITpi!(tIo2qk_*)O)XJ>Xax{(bzTQwS(1Bwh~FDzj3g5Z1vsa4S`AVrZb z2WW5bP=IZG3IyVlKu&|nBkW;4xa&8m4xZcUA6Z0 zcS}~mhlgiiyE7I~?P-$1oS?RsmiK{9afwjvv=FqKD%CVtA;quouH;r%3+U;m1Lgd3 z5xmGcu5Kz!UhtVm2?^^8k{vUEo2 zmb}w%>0ujGNn79DR%2LO!;tFvPHC&+N&M$J*3nJXG*7jz+AEb*d-d{uVha zdI@AA5YPCYi9wSxd^BOX#EZx4;#w_F589Aqb7>UML*|+O8kw)XscW?}5Z4Wz`z!R% zQG{ZEj@CY%z9~9oZp0JFO5wR$68~i{xfy` zx2&hcw&l4AIVu7*hi*mmKwQ_E`viDSQ&Vy|u5fNyd z@XMesUT` z9Qn@1ugqf^!Zlb!i8_V=NFS(mnr=nh{rlsGu_fju>(>rt>S$`O`byx5Fx~d}@t^JF zl@5W=EL%O}T_Hn-Q}~%qOp|N1Fk=lYuZ>*ifY7G{q?ty7^{@E7`ydVqN_@P?)HYk9^mu$Ne_M zoH*&k5J9K2Ypj%PftP<3``zV?%i72lJx0x@dLpyc@zrJbnBN5y;h}z_WxS1FqJG>! zsewW-k!_BmTTZz%NcwF#e21j9TEks-g(SzG8SP7$vd7>!2z)JSZ+{NgLKo?*yoH>< zj5^`4EhhM1P5t(_;$u@A*iRLl%+f5fOH`zq;4t{ zmrHY&S{6F{-mYMF0AsyBx+AXGv=3FE_S_}WegRnglE*}1p=m@`DlKP*@fbW&^JY-5 z&!4ZTWO0CShMV_hn97DwJi)kx4!4PwO4Iz&pZUty?*Td9b>_0?#f1;ViBB-VnU zTb&xKWBQdw>w#QP!yR|u7GgT<4?Q-Uc5t1?oXl-h6}x|Ihb zP-)?IPJ}`N?wBfvvO6{{xKuTK<3Jwd+Jhf=?>5g)5~Vf81ITu;+c0QH*&nX7xuwFO z0jO$@n}n}Tj7pow7Jh2hl7Ubbj*jA$QcB#7%^`#QxFBu{hnS_>g_7PP1SAQesbj>; z$!`PP72B|I+UC+S`VM&nL_?liU9A89s|rq(yX*6^Es4Nh5UYW5)wEKt-W-TDH0Jv7VNxaZqN8 zQ^IPOojN|p4$iF!qZv(*xG9uPCLJ<6kSA&&Kwh<)P~_1FtsNezyq%xpB589~AGs~H zxzrtm^{k$^~RZmC&7`H?BT??g7v9TJ1U20QX@ zyOZVzC8&+F3uev2KYU``P8mV2j4b)dl4~nmRN3oGbK7M1=J!-tp72xL!)w2G4f<&0 z5jCZL8RObaOCc+$mzvEc)gsuJHNYV7qp3!S3C98=?Z-Nbp6U+Ew4yo32oXv(Mvb$% zWJ~;g7O#*`b?;D;5IGPaG@jW> zj9kZvoccXvl^FU&6l`m%w*KBfvXGRKQB;?^Id;E?`PF;cW75MX`BRDGfsaiJP3Z}c ze7MXN2s7w>d`uXBSq^NR)-ur$7$KULLo25&ka<4-wzte?j^C&SHg{k0xh4(jgt;3Z%FCB z^cee_&5|g^=ouk`9dmLqmk{JM7{SuiKqp2oF))b0Mx~lDQ4274Z*yr@jHGpksN#+brqQ;At2waQCe)SUqkn!>&>l<$gpOe)6$1k*whf zUao0WJ9Z|xJh-QT(Wy!OHOLYGR&LrjeCnyNp=jMEHk zWk!suxF8qHq`^bLAEH+*a{aB8ggYj#_>AAuJhhrir5WHy_g%ey!%DhC;W3u6uQnZ$ zErbtYoaV9=V)>`EQN~-#c2Kko6=y7>@anMWRY;S--Dm-{H?$wK?8uPM;68)igS zI3}hS-sRE+Sc;nk_z%z$gPOF024ZXHu@lAIn{B8I-#n$hioUI8IxWt&(`CH5ck_b* zSLt(6lVa0|>ENH}Yv)Ot)tdh_N?wz+U6l_Y4>S$fTWehVZ4aUn()& zK!ir7{eq{=V%^m3a<-jBc}{@{{&^FStM+9DtUeD2C&{eVW}^%m z=HJOTv2pMIt{uR55xPd#p4{g=8T5HN?+owLhw#=KsBT9nm)S#&`MBP-+95)rsbDDy zabU|X{7UMv=SZ)m9p0qi<)|%ldg|wHUx0vFIcDDFJIr}7P$fD1J)3q+PubqX^S6Pt zXhQ%|P%bn4kP6W`5~b4=Seeh)YQINVcWAH-0i&KvNMHOOI`Rt(>x4FBDl3*OWeal5 zpTJwc5+sUf;%%qCi#W*KEeTSi21xFQGdFPF*^{ZCa@|HE;KZzPbp zyDsq0)L~$QZK^lSi;&)$Uu&{T-65Bn?lPu}0)|=zYd)>|e@Tpfp$Q$$Jm~B+V>de( ztTWcY9d+@Z4N5oXnMQLjcb5}2DDT-aW*MbG%yJazCE)AsTFdSap?{GOa^s$_eQr%L zZ=rA~JpfEgeOqB{UpOvEeE96Rw*&*}7GGiOXmX8Umd(S~nWL5;Q@pR7k7CN5a3gc_@Zb+{+3!zV>lmCvBEA zU{4+a%0m)UCImma{X*4wm~jF3h@4|3seXCYSsS&je5Xmg=xbEyajNa#>P2K-@xdZO z_u4(}teFc$spjtV=b+u!ZUVO#9YM0S+57^xgMA@i(|kGa)~!{GcI(t z*VaMN;PUI&i4q1R&8mYJQJQeZiksejhq!*A*9S<-SIbQXU8 z)|N0pL~z)d2qU-uiV?(+{5s)uo^`B@+SY$;VM0bF-(%l!+#ax^p`pxmBUMwVuy`ptOZ`j!MdTrUq+>fD+x_S%r$3t3-NOpD4Y2JhNbQ2VbFbG^Lbk2Aff{4^>F6d&5Z1?o?Rz0oZ{*>(&LK?q45S=McBmxL#duETkDd`dMjVVSsdFFKNPultDI z0YL0H{2kWu3fC}PSwwB0R7Yn$qwSI!4zU6A@pF@8htHqEXTZ5eX>nE-sHCSSlaS?G zq;}_KJY`$(3F6mMRY`2kf}`OSeXOJexW;Ylu2iaB-u?k-P+e0}BKjV^5UV!FAnLqh zRh^%b9AS#-AVUZPqbH&RyPY^;!Dy}rW^5{>Jw8N`U?fC~0_R-*FCr8oqK&RUSF8f! zxnk+bC6%olySqXnCbLqQNP??`a=Ymd_W#sEq+p=jU%XsL2bJFp4__l17*^XtmMa+| zm3SsA7&P1Kt6Dm~#Kp^MuX?F=A8>$=YQWg{HAM`U@z(59-|RJgvQO$Vjqsoj18D%_buY;q2PRLcWdLKlyX-kyv&*CXwf17=|R+(|f zrlE!5Cgca(Qpa|PN@;g%C7y`xEmFKXP+las?n9fhPT+pLb~6^Mv@y5I6P6qnh? zO-?%D^1nUM@YzHk+fNtBzd3i`exNAPlT8)n8$Bfu<+B+pls zAm&^Q*F0fG&mtjRdI5V?zA+IPn<5(9J=p;fV?fi_qRK3AVam~t0s zs!Azo(V$HDJ(^_R+*Q6f4*OpzUYG26(oGL>f6(Gtn2?Y^3^B*sPn9G##0#)`iS7(C z2ZWh~&*GPz`8KPFIJpg5vq+^x=zn;7*kcnI~OLg(hnEx<5nPO^1Wy9-m-= z$^&UR0cybGRY{Gmn7@^N_QAvqFly``LA<40j?=@&kG8iK$)O25zq*eIj>*!-f#7q- z=vjR7DqoA0w&xE1BTGvuRmHWT8}5~q`k9$v^hftm4M`|W9;uPIv=y2bK2DvN`a?uI z!%*P1ThGzY_1u0?3*bU;gV74T&ElhpvlYj@up9Y@|65^9k2YE7fF&r$w|**|&CwQ| z(0qCjnR7f%AJ+0oIPj*Ctk~o-5jHSc15L&iT37e9Fo{KdWS>y*#!1>{xZf{L>MLHA z{yn5IZ0$BEc%@p@`U#;?=cfjXYZ`X1)sS3MF@ieqr!66OnEUy2;T>ObA3OueHbsTI z_~DMz4N*jt>?IvFk4Nh05egm)&5Tb#ik#8&P2->e*Kcq zBRfGZv$Muwjs=_w$2q{gQO0k~Gs!VSpIllYZ`VVU(zzMHll2D~4^85*S`y394mzWK zvm`}xqA5D;&Uhw$Qq>)gL#=q|QyGp!ay-vdbXJGWCLn(dP+Mo;Mp;YysZ<0B3f3-M z3$cMitm3r7T9+TQI$pH9BVPonraC?wgo(u8Sxp|!1lj2=&_rmMQNT7hGS#8V`Z6ZB zAX+~YZ@8E)k|Fb5U#rm=!e6J=bEb7ban|ft(eIZTDni9;dS4;Ql=*!netzVM!stZF6k7=@fCH+z*h^ch#`LJF%-oi_Oh4CF{FJ$bjH$P4&&3`6!Ay07ZG{E z{p*FhcU!exdq|I38J(26EXP7bIN5@XGp}D>oUcXK!cbaktl;81ALhU?3zgu9OS|WZ z&CsP|!A_09G80pBg>mOpNSnD!i|@C*GgCF47Vk+uk-qgg!M_tkcSO`k4hJS#sYtgJ z;X^OlY3%tz30z19cLeDBSH{l7(}3so+)9`lzgKieoCPANN>Mq?X%c~R$=g8nQ~;l` zFdAXf5(V3NXu%FKL8XH~&tp?f%0GyeDECYa?CQVUN;Z_#8tH_aUwoSGy}`hT3x?%B z(^O;ZHA6U9_cbDcv#y7Jqi!?9Zfc)*y3VuRZcoiPo^a>jbD*!Gp6Uu$7*?k`#?N7d zWn~bc(yXhf^R&WV8P*;U+3*`M^Cg$hF=4E~t>BN=Bldqj+fh4LgO zawIrQaEZ-?fgpa~A22w*lAb(hIfeUYl#Be*Jvr9Cv9+!vh+7E78Ld9a7YIw^jlq_? zmH<`eh(&JVL#8toOcVi1s`V$h?Afc!2sf3Whb}T>ISzNsmUJ->zL>&O4CZ|72 z)M=KMxA-C2S@lEtjuX!4(vu_JG!c-^>g~~$V@;7Q_Ek88B!%6woBTCHd=Q&OU3e>*wv9Pa{b#h3zjgN9jq;TwIhhf#iW zwt&mR|8f``>lkF8aQJ@DJM^40?=XI6T$vn4H+<_VOV1)!(lWB={HVoJ@O~^nb;lQp!D2wAQ=742|niL;J$?>*3 zEBE!^o}Bq%g;EtNW7IYRXVGE9_BRpP}jB<@{1c19xPk%5!XH~j+qYjW``Hl(9 z-7S9WkLG-QmJo!V(ChsUX1rz(8QSN-p(Miz(rj=6frL{jy@>u`QWVh<}R zw>3W#oUe{W78*PF>HYmb!I^H!iY|D-0gX`3sv$Ey8|4%hfbvhf^R0CPUi;_kj_={S zG@DsCz<}7xDQwA&|F%mYL{3W1CX{&T5A<{ww{S?XdMB-UftBIRU0YM;n`=T4nENyT z(Yb$cIF?neFwAj7b!;_EJLWFM65&YF{?zN^@4`WA3c->n0LVyXX><#U$3gc(dRAy@ z>JA~TU_mX#!Koon2|SCofIKf-uh#V5;E(h~Oqyfnyb@7fjG8M#E`Y-!O zCB%G-9kj%j zoH7!Ynt;4Z_W15JSwQYSmM~@YW&rFlKwFdL@qoy`Y)_Qvb~X0?o#Eq_EZ56Gza!qm z(GLwlToojN=c@@{5!d#~uK`N==P81?Aa*?;TryapSeey^7Mo{7+>epjA*eaO+~;e7 zc8I5JHcc8cBebHlTr^mcmpB4?LavZMmPup};Y%N*UR6okNJr-ss%>j#VTzJfuBIEE zJEX7(>&D1w%8M$W%AhO8y0uWoJ{+}6Fo_{OLgU%NAnF|ya3P-j=GoJs&_ux!3xW%T z(*90{NUkRBrSAu|d>e z5QfF>+E1>;2fJKWsqV!e=+}Q#8=Tw-P}=L{zhadB8I3T8Ivyn;Had+wf|LbO#$5cx zG^^gqflTQsr@SvtpmlWK4Jp*TbhzEVohEE_|0*JX{fa~CpBLLDQElWQy3g?E4ktu; z)nY}RfM*5x=fPx5q$dzAInQpXl(fWW{uM5GqX{c|Ew09%O9JF|`Y7o=5FGV`a)-(I z;)9_J;3k23NXEMzBPIQ+ph}+kBKIcBF0F)b`u2{%8U8p`sI0vXc=+L^Z$hJo!w9vEH*K>M z6~i&AxFKe`!vpieP$FA4V0^XCimvm%E0oe`OdEwf{9U*avYt0PU@==BlIvg_J5|1{ z&E?AVP@yxe&P6U-L$&}kT6=lf=OUqqpUK%IEn=8O5R{ESJm^p-D57OnqrZ+KZ-)gN zEBpAhlXW(tZ*U17A@H$h&RtPTO_-3n3TWvk&GpOcbxd-_=W;(m68*NqGmSr;FIbSg z!T^LUWk33orm0hmK={Y5AjbIgmYr6twv_<0|zjC5PDD7;?=p;2CAjJ|6WST-zWh5DK-mI?yB>M_dE zH%w7`xsm>W#8-i5hk7#Eu}xJ0ms_MExfX z0$*NE`gu&wIL3>zbdKENpx>HO1tUnR033Qt*Eb$(@eB6I)DD8eM`siS&+C`^jcj0< z)x{7M7v%~dMASIe0rViHg*?RoeS_zhr-=tZ_Dkd`aWa;HGNT7H5bnpaV%b?tFG+kf z&ZKhT8?5Tu8+s@RmU4yIe}2$AkV|sXyY)O<0g1$Q4vI)s)gMnT#tJ;6nTfCGf~Gr7 zpPPAB6rv@j_cy*-uQHLkiLDjlkPf||SN}41FbsPYhxxi9F&(!i!`hk27wX(mTow5+ z)M-;$YYzebiD1RF=Oo=MTh%t3txOE9x5`)C?~&zG2I5 zDELQxqp-Kt9}S-&%~e7JRCkg7C1_ih!_frfLY#3DEN?Qq7`@`wE3LHkJjp)~zA{s# zU*EVH<1VgqtydCmKHniqTf0ET*R;)&RF*xYuXPD&-sCN_2$eumLksdqzBxSwJFP!wv|%l zXfDQ@!1sO;Y9(v0Rvh#cB2;U$xf)4|LJC;La1hDIjIL^BVn~OjR6W|d%BL&{8>PYF zVq`-cp4T{3Z|4O@of+5>Onx5z=_}oqoqCR3)Zz0MU#?K4SNQ1GYvwo60#5Me;}yzR zo8Z+;xHy6des8mfcRhDKF`v_ySn(5Z<8c@3{r*%VI9qSep1sCSW3bNQEyBt0P;{<# z7wq~$6Xl%1@FeKqE{o2(WRDp$#SP7IO|JqeDe)-9BoCn?jdE|5NEyhaBH1R{7X|GX z;!Oe2pC2H}SUv^4ECHHd{pdtQ$i)*YbZOI8rMb*ddPFGVtu9p`6bR)fXd}ZNV5BaIk@A z0>)IvJpX}J5$?f(S>87C0qJoczGe)pT&+xOm4>v8wFT5DOyV(A)(&$!frND^mjghx z#+FvcS-K=|aUm05A)dH?Ps=u2_eBk4%=s%``O1qT!}skf zF%3x0l>)c327-kWmQNP&ABYt!iK4L3Usmh&w`izH2wNXu4k{RpJe<&QBUBiLKGVeS zIb#d1&Osw6DC0;_FLna<%yoL8nY+rOg6*edvG2Am;l1 z^qN9N+gz1wT7}7ctVCY@@!lJBc1Exy$ z1x+jOvq6wA8423lc7KqqIX#~(%TU$BBB^Au=B6SFWKh=$Pw}?b4^n9gyQ$mpOI9Y^ z3?OdNbu#0;K#x0ylkk=-fIvgMsqz8V${AHix$v2&Ja-s}H61Z8Z8wYNdHUM{xvei2 z#9tq1taC9L0#Hw4A9kCFzgCtaA^d=?OYuEr4?|_yGqH4+sxB~tmz{x*`pH^jJ z>*M))Ir3S$p0c<*&2}mdb1M9j(p@$;O30QKureN;!j1>t;^)1f%)wr=R(RgVG2=Ww zYYNoonO@&{vgCN!Ar-xZ36nXbiGkkDg6{r}*p3%ey5uG@-|_2jaj;h_*D!~n1Oz2w z7WELSXw#qenl033Kg~!;V{e>Gy?KrEeGiB_V>iWO@Gw$Fg7Rj%z>qTTwIDO&|JZ0q zAY8h{xSegBRI00=36l|jo~TGRSo$sd!KAPITj|hiY~t0i`PW5-V9p#`-I~}69@!^r z(+qI3F=(us6L2VTeA9r9!oXz**$dMWfwS z8z8~7zFAC>6!gWfVX4ls(ATA6e#sOndb{rG$Ej)o9td z@(h9>zO@$k<5;?|*4zFqHB@9pI2I_4)J0d6IAjp~qTa%pcAfUV$0|rnk^_RaYoTlW z&ubCYwndSSX_zFB;!6My^dz-!R*0il%)5GqCy!Tf#d?JoWp>|F?)ZpJ%8&U|cvM%_ z6qkzdmd0uid&8#FBRriAivfr{HKIW69opF2GLFgwMo@`!*TLK@R=!i}fk9#1|AnlFU@+;-VZFf3s6mU$JoR zF+VKL+=9Gh*S#B>Ft+qz!Eh@kIn1fGP?VO~u1z7q>Xc*aL^Z?V*e*r2dNfONfjWwK z#A^Bz?%Oe9Yo}+)D|L5!fyjeSkyk5MFVu58v7|AmWLHl+Tp(*~q|ac=$X z>==7e=6PjT>KD$)j4kQwzHJJ8oI+5lfw&O3*dNZAQkY&X zr)Lk6*zStBPW1p{D#)Z48y6lCHLd;HvOGZrqe8ampquKV|OAEE_ zqA8Tb%4>V)pPO+L6lSHc5qCOb!+pC{9$eZ3Y5n+!zFCTKjXlN5p$@2g4#GfcX(DXD zcMzD!@gyai7aVoZy7$H-VB1dhuHHf}tm;uyotjn}Qqa8PWN5-uTM~z!OWK5$vGkl7 z@sETz<&I?nB8>}Uve=((AK1gd56;NeA6xmZwR!dP31)itc!&Gm^7lnvbF+uv*-J2>*!__*O6G>ekn!m zn;}td&S(LHO^@DyZDrY;ZPr~6|J5(_z$c^R(07?m1(hQ?;EX_7Vdb8Hy@vvgv)|i zO;mw2kxZ}*@9W#39!0K}y3w2PSinK-D9qU=5WOJ{8P@>107jUCR_jq?tHswT5j^Ivx{~w57 zv3!nVaBk3n(ziHqu*quG+0WsHoBiK#c2ry9n=^`2a$o=6_b!)O_M`DRw%WeCiEi9 z%9?9{@SpPhsFwY{^@UxjK-^;Jk-AiSULFYv-|fjWyN_DO+D8Yu{vbY#+@*q^>DDSy z)+%frLc|$UHmkh#E)kow2Tng%MCJ1kb4N&67@#ogBMipzBP&)uQ`X5{8l9Btw0j-g z#$E*QQ5*s%t5aQ^qMAtyb_aG7zW2H))t-FT)u;G7{QKoI*l9xV{ei2#mxSt`MaMf*eTp>wW1r5N?=S!pT$si7~^d z5(4z-n5g_?B>BFE0;>?qSDPDJW(wMuZ7jx3*ml{<5q6QyH)70W0WQ94^jNBZ0k57- z$hMxkHCj?A9Dsc77`z32&D-#wq0hLT>+FmM5GPqKAPZ>j+l_nTM zg<{vR0i3H7_)Y=>!<|s*j1{+Pqsif8G?La?2*#OdL>>XRNAWuh6pymeVlm*xCaRp( z!&PPi;;$cmxK>Sn@e}zxAES&trt<#^qzHs_-|8}6eCp+9n=O4f#;AdCJ6I8&RQ87; zQajEO(=)+G%vO=(9Xk92~)Yx8BA#3e7m)s?@v>xBE$&3F5H8zVyIeQ(y)zS9{&j zv<;q=c3xXtg;izKQsx@R>t=Z%Y+BhrW4r73O?sS)bp^YWC^%AvNJ2Cl@D@s*BS`>n zhCbQEb~4%U$V@rkf+TXu?o^XTszr%nxvbazkC*8Tl-lY0nTz`2Rmn}!cBal9D+m{M zc3q>K_OP7?cBzxoUne%-Id${O`NkmeE3B2PpFMs~x2IeU-FGcvigB&Gv%l8=e7X3_ zJgy&!yOGc2!dOCQI@l-8Slehj92Kiq$$qs-ly!gq3|j8@FQLQy%a+eLCIVt?mS7Sm zJtW`t^CgYx)o0ss|IP7S0$~VB?*TamOF>=&*`IrM$<;OsoZ+SxeV!3i5(A1Na_t)O zxm#F5UpX^iT*p_ZXJ5+nV`9J%40p}+alI6ON$(Cq8;XC`Z=e^Y{t*;5$GI(#?RV}t z&26QfURSmlNs9>yHDdwsEvO{vQ z(88AYS-cVi&jz7ux~AKeizKk*_&_3t!_p$S%-zgsw;~;SB&tDy^%`twxIRpf z>PDh5=sLn@xURY;$5=>Xg@y3QMX}Wib6t)H6HNtuCJS&GIydm8Zg^Z$X}abhmQtaR{2xk4Ns01=9j@xut=nCJgj`rH-7A0ce3&g6pA$X^pU3T_E;3Ir5i ztueW!naNkWyibq=nYsM;CQegAN-PCo?*eIMb zt!>QCr1t6|D;LCZ%(1XfTttAt+)CZMwFe}u&lq|Ls^(_B*_B#xV*cDrj-jZE4i;Ra zm-KJu1>!YwnI(#Fk8Pt4|&(=4^e=j0OV+_diUX*Z{`Xu0-)*9c#UFju6Ffa#t zZ*D%V@m+@}lgtz>4YW|D^)i{3HtPf6ArKiiPL;rVCNi5Rs>Suf9ke7!EgZZiL`bx3 zwcEr4F}h&b7nh!I&`#mnzW#RyjE)_EGrpd(9l+vkYx*Vv@B`iw-@$T161u4^{;7P* zwonJ;cq2q2fdlqSnssQhfC}g3obzw@mlInU5y3pAGPs)hkDG3eNreu#58E>@uom)&)^ zDd6_nX3N3J`caI-1?-6lyo1Q|TB{08gO%sWxFouM8D9Q6N9ZRu{>ZlPAAWB z7V=>PFM9aE!j=fQ27U+tD?aZx%p3Wi_L`~v(GyIkViHnpVAu<%F-}Cqpi1Gv=5eFM zCi$-rb7{RBio1mb{7Z&xZ9rdVBGMt$l-7iAed2i=Ifz)_x5O`KxjF>&pN1~+W%hld z*X74-hH51S6EmdZ*u&9P!g_lUQ|*MuQ3yX#xj6jvuc@MG_g)+0D=mtdY*li%>0i0o z&EHWbifE6d{te)dwB>sK203n%QL)xg42=-SU8Z}jf^BA;TF9z5@A~u$TBVi?qQY{{ zSw`$D@YJ@;yfS{qU+(+7LM_TOT6t;sxM^jP?7pB%DwAvOwndsN{SPVA@B6K z>hnhymw%jmB@+#%^EN#0&sLo3=S&WwA>}V@6rT|^M9$MEYmoN+%Ki=Q`tCX`p_uTROpJg)y*6u8kq!3Riijb9{u!C7h;V!V z%uSZ#IECstK`YyGM@aGn02IOyRXaGSnz6t=hz&;(717o!Sx^^wUn}vQt>iF^qJKl- z56XQTx;-E@GGOZw%!J$F8z5}5aUx=iMnPTvC4g{9w!>)h_dPBMpT{VbULuia62eOc zx7z??$cUEPO-+?&nl`VhLNg4QL9zzN8N!Dr2~QY3bxE48&7I(rnK+s2PM0dY(RO4j zx&cVbREHg@^ORq%^Y@~2cEHtpCb0SU94B+>9WG(CdK|=w)86OqB4mOY7#Jhacdw~Y z33%wyojqKCiyt1Fzr@>yok?9WCu8?6E~04tg;LJ0M&5PvR1^Yq`U2<$k&I_Hlm{}b zNLlBw!b(zAroS&?Fy|pCf z|0oIP!G+@s^2;9Fl&yig!C?_r@m7}*6twfOEjl9@WbqmSLIIe7i$?8gBufGQ&1=I0 z7G>)$$y1OH^37&#SgYn?s(r*OYK0X45GzBiA(s?-w-g3+>#xtwRD%`ViF!?2qC!XF=L2P_d9kF=|=* zv^8t0Q!xbG@*zlN3W7}`+p%!w-7N`{MN62f>1&E=I(oL{Mw>q5JMb4r$VN7xe$)z0 zCH`=oj`1D2kZg5a??%C#DvCo)(gt;u7etoVoTWdQ79%gI@EuN`%_}ZFYb(MjBoX#* z{IsDsG`3K}nLeRFQ{-PNTA%#%>;FGh7C7l(^wQ-*wpz}P7llrHnyyFQQZt&-g%~WI zJ|$MEowaREW*xUF-=0WOPoP1IC6X3n{z!Tj#4;V}Es()9(9}O>s)$su>`tV9_=5mWJW1Q7#0-T469Ua>P%vrVSDn zUAr)b`ArAeOwQ(XadqMj)^eWa)-66T6n|#sZZ8RDCi1fL(_6zr?kVy1uL(wQyl61S zu1@2(s;+7x)Y005WxI$BLG^#IzOtRDPNzdxXSn3mWzUY?Q|ZZG`r8JSkGI`C&1US5 z9;hcgF{1H>k?74ZFvH*(Yu{iCc0iHzvT($LRA0?rT#SUzu58 zD`Lt7CCz!ILu*6X_oux1+})6IWQtsfoy5~~*f?J73w1_I%K1-^1v>2ipg~TH$=KQL zmjRV5hS*{*?;-R>Foa88q8R{z>iJiC|3&2P>HwYat2=BaF7%|5 zJJwmL(l+oI{j}w--#WFPpg>WdU*YrEL)m4bSCwB7#w|{B=>Mr|-Dr;_INZ_5=Q)5Y zO;jVlB8_1HgYG!Y0q}=?Am73~w~z}-B1Fqb02Nv??&~45ArR3S?G_MEV<)otx7m|4 zd%?%ae3&#$B`%}6s@5S!4XV*5Ziro~_t>zvl_aNKk(ritph$YWRdiB;vu#>B#LQ~z zuNDA%&*41(G!MK0_KG|^TUpH=?vtq0b4T{u02=Ueq{qINd};5;Vs&75bL=x>rdY4_ zg@06|TIks@-Cl7uWXB#UgJ=?9j~nJxpmeji(u)Tn4}*mE+wNxL2YI6@qwLxRduI-O zbPsU5N+9pw(K7DyFv)MT;?Vjzn<41%`8|VrNN63Xr*SGqzR)ooE*q5e>g2%zaKsoZ zg~@WubdZ4}V0L5f z5{P(?kkukamPusoffnwmH$YwaR#EyMku;V4Nh(Jn zcXf`|NG|S9eVbR|At(4!{W$gA)5E|=eKy#2vHQblnBRuO;=@e(0bd7!dmPw?7aAF6 zTeE3)a&%c?tDW8f1pxQ7?B)9KNy^2(o}aWh0vh!#$kQ3}ikqxURZ^}a9o(CT>=AhA zMVDixKmM*tlKW`=2;Co1d@XPB5b>jB20*hOgKR^iLqh%e;j6`|wdGY)kGDI-sh-#gV zU0-Si_3sXIS6;P}0-_&v4_a@rR^*E!ak1(?(Ji}bIn`oSMx>jJFE?q%58D?0nL_>M ziu4n9J)Uhhf2bl{sgbpjCsrpK&t1Dsl7V=EhkH)V3#juSlJpuSFt;!*>dv`>R zy8k|zN|DUGupTK4YAPQsx8?5@yD2tlsNAf=oR+(~EYuSsfH%2Q0mfO1{T9DkR3|9e zXvJjy`JF$;9iIVz9$796T`aNtfWDc1-N~%2>rW|z-L?dfb0?inef=qT+T%eBxA~W#^{BUZ- zs41D<3!J_D^BLqX;bH+8Mi*q3w$m;GLma&Ll&Fu`+&7E{=>tSM^OFtj`21oxQuj`w zv&SmvN(0^#$v_M&!r4(`#xk3&yirb*Db$DcH~^f!Jgnq`Osn-TIgF?&52xUH^FG%5 zYT9l)0H#F{lZ3`)eiiXC*Y5@M=BQRu%X+}c`*qFpG8i~(2axoQ=!h9a>hI;cpi+hXScnva~V?8fNP*?d=qe02~vXypK6sqa`H^-?1;tRq56O;-T5hNS-AS1 z{5YZKKqh~-`li1VhHV`%x~!mqsao6)K_fvB`-ux}Kahn|Pe=Nc14L--bz+c{;a4U6 z!90KLo7tb4s$lKjs+Nhb3(;Ij2R;oysIzq$GGF=wI?=+alvvnoPDq}?h(8EyS zf&;5fjt&(TZ_MAh0vFP7o&p!la6zvSAk}{ckl3<~P=JPPEgWqA@wpVLg@9vcSUqm% zf`hx+KxMPzR$uX&6ov>1T9N*sS4cS_t%Uv|Y|c{4PuFFhV_M{SD?{v!d>C51!Xu}? z=selKI8`LoJM-cnKx|_8a~sdnZbtgOI;8&L{g~pJw~w-q1V2r9N*EWuaMp*zRy8S* z)9Yov?8T0>x$~l6Ja+Um^b_r%JPgn>aWw`Zj!AWslF0k&sd%_>1~wgxmRe9)=h+M% zd8>z6Uq`(;eiz6xu&EJ@sTxDoz;lJG*Mh>p+C5BNR##>>;F!OGF|X|cuw<@sb*1Gl@=$W(93X^=>- z>y|HS3fNtcjy5sQ@I~x9>b&<0-3I)-)U?JRX0I?ZU5Y|Jw4>Hau)G2`1zHL`rn=){ zuaq(y*9>oiY6{~ii|Djn66O*M_PP=mOiP`rWF;4QUcX1)z zapRs2Ab4E!y$Jl?m`(4{ho*1@k9_osod>N19s=m^dJrECG%0H6ZZ#dr6~i?K<7#sU za>T*S#Q*DcgNE+SBFJXM;ZGY9$%&l`p0H!AgBKBRda*7!rfU4BMrQp=Ea!c^nK3ep zKfn8+=Afzcu-2ZFjhrl}I6hI7>l(1_I5I@B@~>f~DCZbG2K!UIdx_^i>)#1gzVh;5 zSDl=1=J*lESm(%dqkDWv|EZpL2*;=}AzOABV6Zw~u%s}6a7l(Q} zfKw%HyG!&jb3638coXMJggxjBHeStS}0?)cm< zf`8iw54bxW98KW)*eCD+WnOIz{Zwo}i+i3KqNi88FG1@A-jm^#{M)`FzC%j8XW4BA zV>k9}4{!0cvFyRIzr*#@`Jw&VqOYsk?ZxZ!|5_CI0h8$i27wh$p#q4jX6`<4W=`Yz z%Lt_yzW0LrJ?VCIUYtS2#;Z63gxi^=(=u$)N>B1`fSBSs$XSXJ0F$*ca<0sxM#vX4 z_FckWDhzOJ`G*5=-US{ocp^JI>sX}qNm0(u?J!mKm?U&mF~Sx(^v*jaL+90hrE1Y< zKeyf)F`)b2wm5?8K5mSVM_Gj7r|DYOI%TsUtVaeb+eQr?^8~EJskgUV0%)0Jn$x~l z#}kriU4&bL;79EA65O568$l2^(6`w68(VZIi@1FL3iI8B$*=o_ew@=y6{e{1Jk3i} zwDLuchV(VvfnN`CpE5tZJS#kHV3#jdC$x#gF+aq!p3xW%>mQ?Df@b`(jm@-#{Bq3{e;_f8-ry9yi{Ck7IFyOUFQB zX9ii(7SE(!_0sr@XvA$IhZFNax>X*t;M5xQc)LPDEJ;W+ShA`3_!F^qyBm_4=s1uC zSR!7&!>y=1;iB-=ArFfwVLu;VkXpY6B!k@?Z7Xx_$lnT2RB zDsd&3D~FuZr%w7Z|H^6nDnJY}t(;0GqQ`A4PS!$+tc;a_QNoi>2Avu}O{-Ut3Z2Kg z%8c@OJx;Zmh;#6gUg75g_#$B-w%C84{_Au|s-(i0!D&tfZWk9HQmY&Q^lngPS?F{jv2wiS`WsmK99cxs7@ z!gV4toRl`8>YHL!ST4eHoUQUB=y852*^t-)TCA?%vu-1Btn5GdL`)il0QZut1yEmm zU#0UpsOfh7E=G3Y%_hla@UvRNHtN~t6sjn|XTE}`hpu{uFE$QQONc+AGVh^BtKB-d zwtrws(bJG;@-scuH_?nB%o0e2cGOW-iGxvdn*VlkoRkaswS1V1-R+pzf6w1(Nj=YP zQ#q}ioFJ@R=F4)DZ@Dtx<5p1=H934cBbE8A&Q`vBJfg(jxSY5B4uyD=#<~E}uYjoE zDQ#+=FFh4-L_$$ss_1?at580AbD^=^&j0x@3N{(oyQ%i$+a4E-k@o-4ubUv(*;1VN zdy7m9`Mll*=VP^)Bz^&>oW%+De^g?oFc7L6F@X260jG$p0+`|&#E0!*m|iP<#gi?o zTKnM@ZEsHPduKhkOSH+>L@oh!wUzMFG9~4a=eR;__V3Zi_QG19gF_Gh#@Vl{HDo7q zbE2k_`Ba%lm!WW_!_$y+ox>-^;IM@DB>p}smx?I}ro+V?N}$||zx9v43%v-ALiKM% z3KFw#3!!B$Z)kDwp-0T{kgucARnS_DzzD&D2m8nxi%pxAyFdW0#lHL~W^_PL^)yY3 zuOTTXL_v10=7^RPq-wLdo|W0TZb`Rh3yS?$7{U$^x~1P0L6(vJNq4#}*PP}BNE`RqZlCgqgoQVsZK|H}!m-68;9npO( z1yxuWYeSQ*NBE+#?j^l%8i$eM7VI&rT{iG z>1+<4eX!4-HZrC-JB|zxYgbTISQZ#A!#eJGOrIr^{b}a4Y<_tg+#;-pkz?6SN%B%%^i&E2@ikp#bcf5;jg=qB_^+y0nK(@bZc9@&u_a6Vr zvFf2FCQ0G+)JRw|`8BnhB6O47Dlk9nQps(Im2aZ{K())R6CHHy-Tg5M{Nt^B={-orVU7chkN_*eE}brS&4vVNlBd!IGC0V4vgx>nxnbZlt_7!2p9n-Rs^D`^rM zxQOn=Mn>^)Cj#6COlFfL;} z&RS{O;zG0C6epQi&JL@27`3XeApy_EOfL8vYj;kqt`L(loZ>emyClS^37NV`6+d+gCrS!)k_c>Q@RnT3m3BN1V9_NWpQ4YB_vRv9g&41dH-S8N zmjQ4ZNj!a=KH2N3=`+%b9|KiJgwSG}3*G#ZK`N;j;c#<@DT!_TeGilA zAMUkOa$y|gUk{59BULzShm;IOIt3yHaJF}t7WQVsFK8t~=Kok|U}5S8^5;X5ovhoJ)$z30 zrke_d!|y&$kT_A0_3QT#{LM*kg_Fe&tU?4gA4Qz*f>(FYPC|6xRC?h?h9V;j<&*9HHW&G-9eCORa-{~Jz7EGYbk+f|EI}RoqGS{)c_=|Tb=EwwzfA{0CO5DHD+SP7nVPZD-x1W9z~~+Kr)Z9 zrij4tQOO}DS;rKA^fgh9|9=WmXKx0pvZp|wYDq*RC%KTt4^W*eQ;UYSUTzh8a4gv{ zMp{Ci*D;%`sj*lXaXPqNUQK(BIz>jrmBbt+5W{MRklFf7Efce;iltJcTsZKxE2lih zlZwnw-AKxAQW9#spd>ZVuQg$>KSMW@%90502h||Y9MCQ*H%YF@lw*(eiy6olMoLoY zmJha}!c7!Z_PNw_e`v_Bb$;419@>7i;pe&rd}h%4)j%2xjI$(B^!oWfSarpL&9w!r zuc#V@1BQQSDlr7r^kugv;Ji8t?0!M+-0MOHv@;TsRbaCs^t%OH_6jeBBJHrKTla*D`N!`cDNC5L=;Yd8dfX=znaH#rxqFIFC5JH-@o) zJ-0rlDzibw*N*yx^B;88muNNdPL`YR66TWK-LafHY_SJ!_yuN#3}@vnY;5|sYKIv{ z-!!4^(E%3KMk-WAc9R74LhVeGal=?M2Nl2{S zptRM@kJWGa6!gI^AI?#lPI&hwV$)VNSgqnarcn=oaboXkZDW`Fmjox^Hl`d--bv{p=-vuAcX$$R8H-#!DMFNB z^kgrF)TqvTuDU@xT}qlH6NEMRFPj3d>lxA=#wiE<$-ssb!SeJ>+>U^xIOg`TMb_3& z_^D|4fK0psZ*(w}@~H)R7OLjKUX@RFR&o@OwIqZq>=MOFy>-s9Q;kiwq5;vFHf{(u zjV8eJaZ-vJEzImlmD)4M1C!T_1ZNo2R7;@n6DtZbrkADCC+lQXjq~WWeFk!e zVi2Z4;v1m6H5{-Og!RJ!MR7g0gxEm!CG1W_^!7gz{M%0Zv4XU=3X|aYlYF_G#*-=X zd-HW8pm$+%lAy3vi6s|P5?~=(kXYV)^D&i@3G1li z7J9W6jntV#=L%c&ko{VsG8T{k7FNEAiX;MOj#EXvW*`$3L%URq+tNjq5NtvSzt+8= zdDa=gRvkw$*g>?~#j2582a1wbP=Qm~MnSyt9iZfy`PWTDM+O1tUWiF5l@d$ny#{~@ zT_$BKIDX^eVq-k!3e4+R5jysy81Ide`ed3*WN-YT!v&?Wylg0Hqg^$*=j`lichjX4FQ zPf?wZ34aw;sP*BIZE=s2MK_RlqoA~VZUr#+QD4=URFgOHD6@nR*qkRsu4a^!< z2ua=^U*m!5B=_x3%e3rlweIn13#hx5y7gd`dO$#Ca-=Q3M#yQFGO*W0Zv} zPrrxrTeS7aLuW%7(^1&Oc~WK9=I8oWe}2Y&riszu+k0ok`y3oZ+Ti}pkWUQ1C<{HL z1pd0uPUUTYZ%(hz9k zTza6+Nd~7BY6@)vfr#wazhRdvKgK?LQZS!JM#g=KCYWnF7GI6E7^qn;V?1;LSvJI5 z$Mi7Zq;~3OL8pF1(^KkFy3ok}r)82@Ap8QjI}c31P#1|2mH`1w6}y5k2y1gK5|s#= z9w8hiAi1&=dMaTQ9vUg~w{mvWTrHSl*B`A+GKz1Tr8_?x@&{|-WptwQt7w{VE;a6% zdgM;l$$N5T@tRe<^EKw(OW(YbTViN_C&KiY6}oP}H`-6%n1%X>y z4W8sP(lL~r{(+}2jMv?Rk!KQBeRayq3ZkPjCIS7GovIbR1FdC;m(Q$i2{ZC@UqKok??Ed47~r_5J^RQDtUSLY zh$;QxLRz&-fK;Nj?B;JQi0NJhUFy*TU|>&p9qlZbb!|?S>$n0z^o>@nLYMoJ9khua zsN_1~QfkJNTMLDrWcEgPcOXCq5*HK~!^3UFnXMrKb4tNSsbg)OruQ+Ozrn1U`lxPR zRm4Wl4)^w{?~aGD59k#X&cI(Kz%*$ByCZ3WDeiAr4n_dcMJ~{__EuAFSNu&+Oo1B1 zCGndyumoyZ2dW+y8+vxbrNWXur^HH_W{&y(F16>J#UsX)%!oXWu`*`#cCXk}yoBD~ zbrIE(9==DSyw-MGp!j&&q0Eb(3l+&pkPFe`(L|sb$5FF~;R(WnrLTpu zk#DS?pg_nC$mUi#KSCg2kqGZ+O5FPB3H{;}&H>tHa|ouf7f$wb|ByfWEWp2Boz-W>mBMC5{p_0sbZ@5&c>MK|Vy+5EC-{88kWqz7yQ(GW#LbkW&eKHqi(Xl1fDs z_y+p+{K7T3=Qo^$fO%yuS_^KlQ<<{36>btq4F;Q2Hfyd3bkIQ$C(gdmU*=Vbix8|Y zv2?db0DuuFyHEn1QMxtLH^HcLmon|mk;T3|HS5)hvm3eQVVpIz}3HxVrtnA+$f>N*cZ=?SS30TVYiwpZ<(slsZkqK-C0xBxBA zd6siXxBeaj(GpvInZ7cBH%gpu>2OdRV_@e$RvIwyW~o=It8PB$UXpi~io9oi)| zUDe~&pp~2X*d@a-2`)wrXN29h%gMD@LrcjKaqHMM1!DK7@EY$3L8>U3d!kHyMj&0K zlGT08#xY}LA<-Fe*Zs1e&8v+#YeAZ=3$>Gr@k41HhQuW&(|}ia%{(XuosO7)Pj#@} z7ZD3L3pgX_LkkFmyU&%nWBz)jGJHKidia>rj(gPWffSdM&ccq!{}Rf@-Cr5Lq9X^J z%sh>Ban$NL?Lj8I!>j~i$v@I%CIaJ#s%!Byc#XFj(Kl6xvGgo5y7;~qDs+5;MPt6x^Ru7$yVAyUHKs0e~s3B)IX@uR*_MIM|T-DSdzEzTy@b z`ha!!aUK^d&$V8U>XO9qo5Y>%LTu9GZ}I>jt|9OY@^A=M(N6~GHFagi@#Uq?VcER+ zSNny$Y4iEnDCcF(dq81^lV@Ln*B06M8;5()Rrq1kmOzZ0-hgZ(@7k%SC({7Knu);s zEdcM~1Pw7bi>^tQ<1|7jdW%VsF3%^xM5CmC@|6H+AQo3;K9YNlkv3#rUOX+yZHFh& z7Rnw*PEwMNR3u`Tgu!ekt+jcI##o6(QpVZ6EW;*rDw)Fznhv2395H}NY`fSMCG z*+(|ADk}g6XSd1oY@e7)liz_%{%CdupWZ|evkd)&Jv~<;5nQIU0nFO>jDasz;fM>@ z7{nIkPfbz%iJ6&OvBk~ivI-}Dz{lY+qRSxYtOYc7NrD~Z0D>EAfg1P{k9dLwQusPy z#W-c(0H=&k7b^AGYsQE4O%i_@MlIPHlue4@i$mgACXFkcJ$TrTf>XU@uCf0afYD~4 zb}-!lo&wj6-$V$NDiH&IXD7axR%c->vIS^lL$RFI{$IKza<~RZvmtaD&dNYy0AGQ8+`VS_M_iwumH1JY(D1TbTI+md8=*r z@2D_`Z2dj$?N>*7H{x44vG^^5w?Pls5I?XHHF zhly8g|7R^W6arbr?H0Nd8{;tcPG_Ze@ox#ky{ba1D)-hZi;T-s_gv`&crsv9-xbNB zFb2pe;_#7e|0n}{{v|2kh4UIx40aZ-oP*+a@N0ACANN>_7kXz`tz=e}O*}RsuMiO< zagXBa$@?0wU+8bHY!cw*(Cmt6AwiZo3_$l)H92e0{x6nSxO5S#tHsG;E@7)|Cq=h= zUSVlFc$rGU%(2V^l`iFaiWy=pP)I#d&A;_GV~j)~X#))l zFMHtOgM0^GjD+1woHLak3{<=y_^01d6&l@0d0gcg>P3D5!#kJum4($VA^btpg{Y};#Ex4>HMojU#lg#;=WwDgs z)BRhW(Gj>|=m|fQ!9OD`Pqlyu=h*-kpx5c0vNiQumgutyh)e9*MsR(p=Idaj(RvmZ z_n*_5Af}5rUe787leBc`y{36Rhwz!GfP@){b}}3F+}&Wqq~{l#lL`2Syg>6 z$}CK9B3UBhT3vO=C*}Rl`Jw)l7jW>fD=GWF10OR3qFxl;N5z^fJ&Vg=UD^d=s_`MQ zZT|`^Okd$E>$T85)~#4?#rQ16P_T^5Dr1P!EtWp>qRD1{LaMnPf!*=UJ|ngBrO&Y5 zs#&2&{0XJ7w2~lN=Nh}`OP-ym~tXAsyK8HGZpo%OJ zQQ4;STzlNlWt4mSpJ)Wt%90-(fz0m67Tk)+Bi0EWN{dNqlC|v05Mgt`B1qWOD{Hoi z;uug1TV^~d<;#LUd9e>t^v!Uu7zNUhNqJR}FjWpM8wct8P+YOV_NP`6!E|na4JfEj zQR?#$ML(zo3RgxB*wMbj`s1CcQ-~oP^Rn7x=JQqU_5bf1txr%=*lDJtPvb&1;@Dw_ z3U>qu!Lp5W=5UBoRFjLgu^C!M1t4}&2re#$Uyv~t15VSmyJ_`lTsnf6-XFkj`GN5i zHj4ESnPh;i$?ze)SK%yC>HRLlE4o>=V8R!C2YenPnh{-9LeAj`numQY?)EvNT{;i; zBba+?NY*xwUNzKE!|4QUBm#{h?9{Z07J)HAaRO=f@`y$jKG2?xf0CMaGE|>Vd{0u`4@8p0h1#QGC3OJrRA88t; z?Jd2?Fo2sXV>Z#WYC5L;N>ZxsXhtQTYNbA4J~ZgV^Y*1b#;j5YrXbMCXydYO7uyB>KuUEH`}Ym} zRI@Oj5wKhgzUy_Knz!S&hf<5g0S1Cea%a^hB*>=mtA2GxLPh&3Vy(9Cro3iFcIVpl zho|#1sIq>Q{z=ub(H=H5@X!vpu!AdWf*N(wT2DDxaWJpybUXXyS~7YP_kt8F*sK`Y zm{7kK_1K-bAigwnj6ATi3mUx+=1xgG5L@$$x2p+7cY?G0DmaP1{`%a%C={N$&`YB8 z46Y+ex1YAc9>mVVT}?A2VBUqT587oWdb1isHdXj5(?4XY{w%A9(gj)57s|ho=P19V z)%`Dx{(n`SQTuDdVpBvvmF@&9h zqHdm7wHR{=ecw|FR&n}sVk%#U6~WMqg^M%*ctOzZ!evJ;B^mS;VKl7yIt`H=hyJ%J zx72pwXFUcTdW|>77QacVmF%|@5nMD@%pkeX&TDgtH#r$C(7u4m+$>RYucHXV%vNL^ zF{n^R6CLEoe;JsMI@PR&_uTIJX`>>XU&KsF{%qt31k&w0H8^NK? zCSuvJ0mdjj>+c&hq*L_ard((F`kPIp+4tq@cc6#|=jdK+QmmygNnRdi+EC1rSzIB8 z{De;;B1<*-8?N)iL3A$eG$Uxz3+=+vI0UCBVKmgoKp>_yN$KAT9 z3i-$NaNSTR-14=%(yfB-770o>vEy?T+m2WFqR8xA@HIicM^aFwf_9-VmVo{_!`Hu4m8r{;6M6Gnozro9ggSTZm4^)_mz>Udio&G8 zFM4AM>(-=^_q!h(ff~OUKv`+KJrK-~)Yt*kcrfiTh_0h-C<@Jk9p~vJysC-izY}}8 z$`iHk!FS3225EPgtGtu|B}k27*T%hg~4ij zI?5uCAyY_8B*Sjpd}i9Gtj*wmqmdQrWtmkS=K&HmE1^Bc2Z;^caF7LkwPDaL`esu) zUbk98VF#T9q_6uSPJv34?#w{<+O24p*=6Fm9|fj5ZGDXRYjLWf(at&A2)Y z%=va!RxxTIs+Rt2Om@kuFW==3aW=4dCtddn93n_wVw*y1sy#9PHjKf(?Pq5EyOxfV zkTl9yp%_N=_seON?(vTJuZ`bc8b4wS3_GN?Yr61qJjIOlC1w+qvS!!Eb4#dRou2n7ezZ;;Y3my85$Ag{|3bo#p`3*A)O(%7B+-OOe&G~)7{9d0FknD zVnekuDnZ%DY@~SoA3>4FScF0WO5;|pCt5Kh<1zq^AoAr=$Vh-YT8N+%dgnX8mFw;*U75Fu z*r@{CY@%P<6Dfrvx6g>V$P*4P_f!JKvkGg*+mAwz$yw9yj--wMMKcNq(%z)IX1)bg z^k4eALccv&WKWLgMP;gaa9#UUY8Nt5yLMs6e!4@pH;Q7w^~K5lBKB~1ooKakYV{kR z@gfqn@K%r@ottY>WwGi})aokfJQ)BpNgoYp%D!hCtSLD*66wuM$%kRW^yTnuOcq*b zwM{ld=scP+qc>jtSbx>sdClO)<+m`;!1E{J5A2|M z!jW>YVoga*;iL>nH>C4g5LM<)n{oNO>i>(+xCtudWYZS3Jq2;rQ`j_Hv1@cqDIEqQ z&#vgAIfhGTgxGpwcaX=4L`fJGi79PA9}VtF{ZH?d66ph z#>Eb_WX-Q@$K^w1_yb}r*zp!LC~3Jr!xpGpT4#DV$9et$B0-})_$4#L-a z2!6rmcFQbgaq9yVX}_^TLk#V3S`#T^^CHhDRtN0uWfD(nI1VGXV^HLP^TSdqQ#U(- z4NBD2^6@3{>|pb!38DS<9O49*S}5D(If~j===!Z^Q^W2^lT4QJ2ntR7`@gsad<&7s zbmvrAuE6@0MY)84#K$Nq%V z>9~o6*~c`Zq`oo<1#$b*675;!}=)d-to>>ZNh##ApJrQ{N%{f3Tn^-wiX=CgnAK6oD3*o z#aQ@xB?F2z=TQtfQ$vk4-pwQ@11jA*0M7=)t=Vv~CRO?i5U- zeAb@&LfI~dGDPwBWPjD!FB*ZIx#fZKwZg7kShz#xvgk6+KD&4K604zAv^wI2W>2Hs z?_g~W_e)t8dn@c;B=d~1%+-$j8-Ff(!Co$#jVAkHl&*p}B=Kwnt3!50LGuA&T3tF9 zLSYO8cvUfxROh1Ep;-nmL0YRT<0vaQtzf+auJaP)k`JhjQGn)q zYy^Q|lCtup+v$(;(jeuNX4IBAr<~*Sj`^Ud?ERU|d?c;zm_}}JIV{uN>V}t%xLZjt zt!o&?s>$4QD_nH@5n_WY7Rc(`YLnn6^@WEEOmUtl8h?wReL-s$bT-2a^(3jHmBE>i z6-D_{Nhf3G0ENbCV%ptAbp2qNwpgBkhN7V4z#U-JLzjHA1%GFrzVqISUD0#1h&bU~ ziF|?9-tZeqe&#%z!q!d7Aod+4-2SmD&1*XTMlf=S#;6|t#+IA1ktFmT0?xQ->G(g- z^iU|S382B(Q>%Q9*?eD+xo$_kDxW%h2bS%heo$L2hhP5LF-*TG~G#?jU7HMN>Y!y9&%JSc(~-D5Q9C8}cDG0JU|!7#P>? zA4xgghiu;idjDp~s_arFFXfVNYQD~y*}_eoAJp(=XF{(jnbi!Bb#zn9_ag2g?Vano zTGnw5^1od)O{ym>|0^->93~?-%fK#Q`{tW$Rse5~{P@!hc>S-dP4J5C1RtxnQuekf z&?*st`sc<)AUsC0C2Gm%o#Ot3*}QH`w;Aa7kM%$Qk5iY>|36n~G7M!od!sxDnsO>5 z%sp6jU%5AWkPt$}`z^;jw%4!1f%n*`Ms~v#E1ECX<*uL;XXCoqCdWRFsgOjyQP}^* z*Q1@O!TT{LGn{J39W~VrQfSOnVaq~YlY#Sts=~r`c6ID#&1hfIPP(I z{u`>+cG)4!SFNVqZO%#t3yVK3QlyelFSgJ~KaH){yb`{TG(Iv;$tdLjcMUxz);Aqp zD>f;v(GtU#+dS9`a<8Sh%3h|AZm!c9j?y&u_1BQ#KvavSgG$?z1ScgQR&=b~=#ReKl+%*$?&GO&ww#_G zR)#`YFkc}c==i@1#jrQs2V=WJ-HGanF55XV;?Nj=X>KX+%qe3)12-pB4yfkMeglSP znnTH5O{O}pb(hElKV z45h}|2B-;~dkjJ>c_ak;?KEMJxapI`4RNtb<8ts{@_m{CPMN{rIN010e6o2`r1b6> z=$cl0VYN^J@r=#*m2ehdp)QUkx9;$(OOk$(l$by(&82Bu#G=+@z}z1ig~e8@Dm#6bwP7I>6YUEERDq!6gLLIW5u(+Rh)`Te zB#4ZE-r}ES72eB#rLT0(#=ON20b0(=5DcaA33N9hO@mf~)`8y|Hq}k%+nmv9I|Vb& zG1NF9k(PPcc(ck25E(u_ozP4^yLRETC5~3sY?xgv}Zk zqbu^E;P%4O9R0n@U&a7ch{Q79bNg}(6@ z72ercZs}!?6Kq|C1A+~ZoGzNPU65U)`~B+Y_S$C!u1&uTB3L!U^)~lB>58@IQ&L5e zzo_!bi=kcTJTV0?q#j>7FNPSX0}-wFn40g5OKUr9$=ZdZ;4`*`lh?#(OO}R5!B|1UIznHp6+^RYbz7i*PK~Eqkj2O^TSj+c6z`6N(m^vw#0kR>$T1WISq4}bbMJAM<( zCn=t|L2bWP8KmhnXrySbJSUyY)Mi^}2%NKuA$JL?@&T$Sx8yo2CU2nKLU6st2@Bd@ z(03U0lg?!8F|pQi^S_9s_JP@bNvQSbE<71Xq$p~|iRHkXP94>!sy@Ygw3zP6`ajVI zyq_axj3WTkRi#w!iOAuSqgq86?0ps6Im0r>k#!|8Z6k#klvZaOeJ$*d^ur{T++V5| zeg|P|+1cgJJY|GzHM?T&5)G6-uUso&=RSpim_HPV&`WE08AJ+*Cd1-fi|#U3)AjNHE^nIJbXRbYa1o(ey2r2K zd4oNG?00ae7^v^tZ1u!F|#azF~e(;LbQ{D!E|6Ct9-=VX$|BI+~APjWGiuA-C>NTR#Y^ z%re&*@19-2TScLVbM&! zllijI1`XD^8U0ZRtB&I!Q%*tdc1Qm-(UX;KwJ&|!vZkijj2IyT zf2)!~Wf;Z+Ijd3uhJn1Cx|Xgi#ISCSmCA%=XMPcqiDVD?!+8DD1ExdLn}$LyiHC6()G!Gn#{I zoP$(gP6wKQ&?1{%8G(+{vr7F=4RFn)3rj?P(krr|E1)Fmp=bj=5HI<*o02f!t-dn4 zUw_|+dOrcqh*W-Os%q~k{*D2T7PeOuUcn&aSW6P+3!mt7x&-fL>HDr^tXUmhA+jWY zDNV}du;EE|1Odi`Vj>+D!11^TCXkk5C$hx{n8~bp=r-lZjH{kgy_Rt`Km;dFy^;3N zA8!NrYC2*6t>2_07K#I98ICtRM<^k3d^FR#=X~Je|2ddmASk9oePPMPlExR3DhFeA zSdZ6UO9^^;IzO}Es#KWp^Cy3XUOVVjwTc@}$QLHfP%NpuH-UWz%;zuBS0m^1q00=Z zO0L?3Cn@5UEl_}xS4mICx0jc^t8Db$N7o6*I!mgk!*Jq4T)gn#FBVFXT~+AkZurM1 zQLfL;XGu*}T=%&({U;kJPrVtfd(8#-PF@HUhRz7y7zQ$m3U)6 z<7lhfH$g57~X zxs-V9EU?PAVj^x;y(A&n&?!B^;T7XW$L+U7EA>Z&-dG;kvXg3cZDPRQN01)upX=E~ z))#+ay;jFsNXA>{*1#peV12>Oww{_iOi3J#a-g=stW(DT%UE9Q$pXkW#clA_in1PZ zU8B!Pxw5=ut&}|iyUKHe0Le4$HZVjF2fdgTo)Yxfa^tM(d5lV>lwO=f`Agw%9)$B% z9|SjBC-B=n%bY1pU+<_7tN>vC2JZ`*CC?4$#s!Rdfy)gBIk5S{?s_g%PWOg1BduBZ zlmPp}8qNS1Qz#$87z4Yw81}Eg21gwuS`|RVjcB!7lAh{;Rz0?$E3e!Hh0xL7uX-|o z^b~QuWsV?j1$CW(_ruFDCW4kq-Tg6G;MUltz7; z<#^m}(y91~2^tjobw#y($WAAA-{AZaejPI`aFrXX$e)fan{2sQAgA>>5E5MLc+8TXeN zs8F)EqjP&>LmkF-P&8YTxu;SFc{mKI|H?4-=d3WKF|=Q9TGJ(1;|6o*SC-?aEjhEK z)B>Z17Z|4;F?|jLWnAA4C}!(03v-nab{x4M3*HFk$X8N;VfIVUS>sH^AyncFv{r_P+q^Q3Y$Ba*rBwzW58kJ0ZDU zRymZHv%`W5Qf+4a zMOVSqz|?!JtA!nIuBfqr8e0O%2BQSvZ!@gy%bUK4w!zx{{VP|_v@r>=y;$~k!p7b8 zGlhy&*&+SJp2rE*Ciin6f|WO9+IZO#ZrAocQ6x;uF#(o|AlDV+E24I{oZ&b=Ej63%{*xDJhRFNx|ASj$Dx!yFRgp;0W)+Q8Uw;2)k;5b^S|66(yjCyj|-R z3_H2`Sl(s*O+Z}c)@d)K@=0javsj@Gc#ejmZ~b*&SX9n_O4wqUoup&k8g;n}1hK4N z!l6Dz`ujm>WMcH+a^kI03pAV?n7-iofUunSmMHqBZ-W5NLVNd9@wqH=l3Ua-Dc{-r zC*o^_m*v-|AQf(Z0y5iU*Ki)0t5ZZwRTy1G#XmPg!A1*XGxdVFYx5T4gw}aVH3>x` zRL&b#$4jWaU&|pkbc5U~m?BX_%_Oijd2^&jDIKe(7@1UC9pLBy#s&pn0c!C6gt^8# zh4-<%Ls~iZIYspGYqC zr$NFCum8DsP>8Rd#@G5r@K?}Yidl(TDCwwYs&dWgs+I4h2fJCBhuP>rCxb7oM>qCZ zF|a&VN2Y#w*kwUIPf4SwdcrDr*_Q)ol_+j&7c!|6ub$68UafErsurHc-MMYn(;M_r zc03>UT@ON(`D1BOEk7!oD7HFg$YfNC1u5$B577`D3o}x24xZ8w+41V^JW>@ZP=mb- zG4*2qG|*?P1D74E4K4(WbANAU6H}$IJ;WdV_CCk}(8?8dcbH$!t zAWh07=jzZ@Ndpy8icE{OfRmGbZ0X#*ntvxhsS32x5w4QD+B3(;+gJK~-fX(IOpDJM`Uz0sG1Y$KF~ZWNdLeRujB*-9T76XTklOHRSRjrmRQ=^o zL8LCcP0TBqy7bFmwn`4jU*#V3(ypN?MR>=P8 z%^k|nl9XxK?qBO+OaE{R30}$tQk+iWDm&}jv7tzgLyAP-lN3u^n&G`JGeaq? zF-+IXhK|kK=|d>Dsy1$=R>hneerADJ&g+0-e zE2^MHD^$RF!xSV}51NXt)y8|3FURd>+z`H~kwHT7lmC-gf#xL$81i$$!N+pjp|1Ya zA0|>#mJPBrQU4m+ioS~M0oJQt;T%MPtdpl=&ARBD2S;P=iY~tt;Pn4zOrU92gJZ=c zSrg(83Vf6)!aZ8UO{PBLO8kx&0NKOj1OG4e33UV+Q;;^|AUX^0_#%)eN>TFGx2*{8 z(Yhr)*Y+~w<;J+jLk)D1^f{arb(5eX#MkA$Cq;B3;nxt}$Vo*$i7oX`O|$LKV41&j zia)1O)m5>}1lbV=Y71{tHrr6wTk2>AXJ>Xq_$0gnK~*BJfNuy1RnK!)xN=!Og~=;% ztu1mgMlSHyLHoZ8iHsS-^a@*)Q^C%PAX5}20EqO(P@)*$X2u^F<@+ss5_u{%r0?T> z))tUGFAPWK37n)eTuH;vPf^fL>ut!Sl#MoJ9SG`FyVhk5#An0>>&S^3Gm8DnwR2=U z)FtzI@)RG#w?swSZFaW*$!jmx$kRM+$EOz4F`h zI34U)2OFX>IJ!-`%y(PaWOoGgrv^t8x2^?+uy}fT=5;trb4$^xD**9_uRDZiBYu^` z=#{tcF1W$W~CT)sXuC!!29EJWt+sr)5LTx)G}F6u%OB63(KxUNF4-9O!N05ky+kB z{HdzP{Tz;CaD?vdDQXK%jg=0|aqqGT4Y1ifyhkp{dZ#WxUl=EzN&zWtpX})m?F#ez zqLuQ=1twl<6w{AZ2SMl3a&{W5z@s~^8M)K4IxRfId-)S(SLhv~q$!O>`pZDu2`Ato zd74?79APhF?|bLb?)KX^_#8v<@bLV;sClimqOPOFx*;wJ73!eSwSHA`re02~Djm#uo$~jW4-M$U|tGHd&OCKQk)F1MtKPPowKL zWka|l#bpsBV;jk5s|W*j#{kwKS`sBL;w+017mf|Kc+zyiU6vv3(nhZ_L7CG&)H(YD z>0ht?ci^-Js`zxtXMo--S6?J|n`=Aut7NQEEXL}TEew&Z^k-|ct`A53dQb^K)X%F) zkp%ybv4J<~Q!$p%x31gLC!@ROwikS##^u_tU@s$B5U;_a&G|-F067BA{TA}Q{B{V( z8!=8vf$%2}rT&szUw*KvO$s@Oa_nk_tRpHLX3v@5EYWcX1)^q{5#X2HCNjaNBciIq z7Q>2?KBQs=u~W4nG>6i}A{1)NhkhW)>oV_ZflH%H{)?s$yQzoZwe)U0ct0GVEmW5p z23yAeT6t4;5ucI_)r%|Bw?2c9Ct}c&v33U0aNG{&(iXd6-O1KkITo(=P$ z-iaU}B^>1WP*luJINcS2DAa?DUyFXjI{!*nNI%D@XA?P;BXp5Z>tNAqL=4uN+0oyJ zg7MV)a-Ff((?nWgAhLnbZD%Fd;DUb0)*SyMD+*A5`|ciW@2{B+7xQ` z97-1Fj#m&36ej9WDL??U5lW3`G4J|~G|kF;4D(+6cSn#`G*@pzbEG+Q)b3TfLkGqW zd^6Z)yimZa6&`4cd*Lxq2l^fk(#NvFu)|l6g^SpFrL~;YTP!V>cG^e_XTCRk6U(9C z*a@dCAiXipJ8I73e71z|6A;;1G3l0(+Ag5S1e1k9Dtb+S=+9?)-SLx3AJ#ciH6wdf z=OoCsl#$;po$UAUT0;WK`3N`2nX8HE_VL;u5PKD1T_;nSE%#ucUFu17Nl^f^E%wU> zsaecM=;>M3L`dNCogmhQ0AE0$zw^bdLoq~nVN(;*abp9}C ze2}VmBCMEOUC?D*39t24NX&XWQvqtcj|aSy)-(&H^g)&50$&w;mcfDk7myQVBE3K5 zBIHCj;MP5ZybQhFF#hD==jk)vG{w%3vX1fZ99#ye0T&*lqvD&lfOO!)Xl`l(VG)@k z>oCX%BP&#c8?pzTprHEl^KbL_Z+PiwNCOUwHV86IV(agjI*>uLEmWv^x-2)Ec@2bR z!K8jSd^(Dx>XLx>Zar5Z%XHA0Y#4{n)f5*sXvr4PJYy!y0AmWkPmmo0WAx;bL)FWSI!rV2`<#4r{J3uS2%pe*MwA@YJ4;FZseaG8UT1RYW?w%0fFUK9GMGU`% z91MHqCCU|fIchz5(M?d+7Y|0cu-IUm%`rY~C!Q$|Yxt4-j&z5gLg<||^DHRf5n4=1 za)0nFBJ;OV!3D|phH^;7cjsLemu)w#kR*)nQA-ux+(O|iyQquo@V3)|?3IS06Q{&eM=uNOW zUsLm@-o2lo;#c6aQ#fRs|3w;f7wB0FcPge1)CFx8$K37lPU>qPyfzjUf6G=6*DhbC z4GS^V{0!uTeYH4X(Wte%uQKkt4p~Ak7kI5*Z%@e8c4c8i zAB{FO3U({}!qalHj!R~S$FEO7eZMjE=zCDN?7$#$i9W{G$xqr&S_W; zv+|JM{+*wnJuM)})&fQ}mdZ|Lc%gr*_xbd-&Gh&@;xOs{{C6z#jnz>3d--6MZR{Ns zolAydtr`ThvHXCz-}oG%ng$*tmB^9Q_}C6gAxEFr<);W7@@*~b&r@Ttn~VQ(xRWM- z+ns*-kMP?lF&H&&b%(xW%Hla~`X^PiF{&{k6JSZ}tX=jILzr1j{X596i;X%ii#ZS{ zr`gfg%(}OTIM<#lpky>#o40St84>w{05)hqfdu{!M0cyi|CZC;;@tse5BJ66ceZT~ zMz?j|1Ha#2nkZwTLfIF44K^dIwn;UlWM=3wVt6x{6&Ah2uf1)0B$)DVsYvX-&m%CN z>Aook;5P*fbyV<9DL2ZCMzu73>nviVseN3^4hTzjpa~`%Q^3SiX)-9vKhDra7gB*r z2IuLt!n`Kanb~lO?7~`qiHAU2fH7^a9r@GU&w+$h;jrO|Yezg5cM4dAoeJ)kFYsv|F3jA)0?%y-)g}pn z4@nJ(Eh94TTZ1TO8ECR~L!Z%D^ysDYcm5c9Fcq8U^enH$JZd)~6|<<%Y_!%K*O$PW ziS<~aTGJ9Nfn|C3xD7bbIN1@IMm-hBSD>YCn*5W?PRE@l;Wg}MY!Nh%BF{9VO)GkW zIAnIufXKVl*V7?c=R^Nyu_Q&WAs14Qs!}6g7fjb$ibWVX(Yq&>*q_@Z-rLbGiWq7) z1M}KGc)^ivd88^@QKBWf0g9N;$gwjJ8_T}nrlHRGaYQ41%w=lH0LwKClsgStJFzl} zpg9DptfHbSLsTo4zO8oPsEZ@{v7RgS6a&pJ+OPUZ8<-irZ*Rx@b(Ncd-5r;{atsy` zV-KhU5!$zoDbkxcgVu5X`>YLyVJdF11N4cv{R8q8k+01IwaY}QaZ#5eA7y*w(;q!4 z_R$XErvhmGofz)p=a1k?n4r#5!VHdUb@{*y>fgRjV^;)j`Ete!XehpWkh9T zZ)9Z(K0XR_baG{3Z3=kWY_iU)Vs27hJs+u)xR;@iY4do|I76}V`GoY+J#D#@}m0b{^1=N%C zbOLGto$P?ldMpYqreKgcfRmM-of}yIU;#3B0hj@;KoDfMe_~Wk?SKF_O_k3I+R{w_ zd#DA_@;|-`5KDW&e?eHdI{Y6PH=vU<$Q}Zq|2GE%zzJw-VGjX&{wGk?-UVb11kg*_ zJ9s*QtgKxCob2qp%>O(E0G9Sn|L_I;lNA8;Z~;R8L2?F|c>*L^0g9&Pw)XDMwjh8h z!~&qms=^9TwRiu!2?7|<|4nEHv^E7>0_-jS#c*~0%L8ELWbf+WY{0+@(6R4;=wb>2JF_DH-%<_e z%?%j-mde4y!otbU$pugWnOmCz!2nHbQzr)?^Z&*R=;#V^GX(?xA@=E$GT^^5v;GBB zv9|zOg8q7&g24c1yMLbMrVaoru&0ByGr+|j05)~9`U~R>^7K3;efJ^cp}b*T3@pPcz6_7UTi6_yqcgrR84{f&bb6Pg)6x z75MMmaPa(RQ~RGLft^8RZA{#E3^=ahk%|7|qH z3ZUuoS1wa0i~o1yU-KtZ5X42x(*X!z|G#JR-!;eoU8|V7IDtF>U)cYiNshmcf3N@Z z&+vbw({ypNw*~5gEL^Przlf5O_8tIl7H)p-zg4jF064h#*Z~~ueC)peJ^pXc{!W_9 zzi)-Vwfvv8CFrj*K%fWE9C>BY-drfeCM`4FrIsLfXhK@AUAZ-f7tw*)BZ%6n{4q{H zuvseW_=Rg+c@}jxdXo0g^gZ!vds6DfP?9^1$Ip}pP8909$e;)?U$ zS@X}>!cfwu8)%8f9LgVkY~41y zuYHT%Eh#x+;9Ma8)0kb9?OiuR1j^au6Jj+YT-xIWC*qjJ=iKv6^Q?d4VVYPYl0-XD zqNg^N@I`Mp=*-H@>R2o2k~OPfAkT-#5iKIw6^)9hD;PxuQ4gvLe8u-hyF6ZkH^^D3 zGTv$Z$x_+|c9$Q`ImqWCF(lyoi$SA}LjkJVOgLZJo9eGw^uIRttHhuXefF=!5;Zql zl6oM~`uOV)a$XApNn}Eo1f90>OTrWDmWIQ#X40ni(h$;`?_T&`c&kqvUuIS3M`&pI z#o@$xkccY9z(SnzIIgyc5gFbK1)q>w(50m5$Or`t`CIt+wr!dp0WEt}(P|Ud+%SyQ z!gaGCo=!s&F=yAoO=?+C?ah=<#duCIEeOR|P?Aa68SVHNJtSy_#B|`fKH->vo@p$! z`gf}q_JcAl$$fUTSqFb^P|(V5RXJ)W6;Fu`b|jt(S)9nb2T!VzzlY6sD+i`8CO_7) z%524_ZO^=Or5LDoN|%9e4{l=_UjCmWl}yg2J=r~J#Pr+EZW4IX>yBeY*4g!?N0&aP zk5D@LY!1|yhM0zqQ*c)_+?#NQR$qK}MM6uPb_do$*$}d_%)Eq)86NF zehGn%U|E256g>Bn#bJ zhBrFej~VAJR*!}v%C|!mN41huL+t2@-OTPj@p`@etz%=^w6LqV9d(j?OTaH*rVwRT zXIac=Nazc@(Y$7~vDijJU{5HK@nah72)=FHRrSfEbECeBl$0HtEuwTQgSbFXyzqOt zhsM->H+J9&TC zUxncnL-KlXooEvhP`gJ`Qj@ozHC=lcCQdJ*y&JgO>=s5ATS-~aNy@4ORhcC&7thWz zYB~OJT-V+_aPy1&HK0Juh={!^R#EaQ zeY=U%m`s-e?h(>)jn{2o$k7BY*T>K0R6l%N1xBFTkL*k;tIqo2QoqS@^0P;ds;=5t zTp$CAD1=&xFxv;u#c+p~24L0lCbPiInm1&Xqx|tDABlHPrFKY%cRcba{7U6|uCAX@ zrojufIB2J{V2>8|{1ay9^(OR?--*!NbKgR29r8Yxp?$pUPJ~B;Z;d}!^bI_(2ut@* z4GEKts@+_c>JMs1MQVH1EdW_ri+CyY*+SGC341-wUdkda5F~*{=UyZQ~|1P^&B4aCKk?jH#dzL7i!0LU^-zxy3-b) zEQ1-;9fl}jxT4tWh$wzQH)N|W^H{41`n)mFxAT$x-pWb}2b6F+EzsTRdPUcs-@MaY zUw2SW`7&$Cz41|pl+h+k)KTD`_WQTK6SgwqJMDT997UT~Ru)>rYS7Zz*`RlUKErk7 zEkGe>&iHPPx5a=WNceJqG95!e@QLKQA6;5vIB`0}u8Q#|NbV}S*I5>ly>rTs5SRo( zQSzoCR1h9cv?=a(M)wSLI>+6EZhfxe1Nik}X3ODwf(bB9*L_aD6MIhtS(78Te@oBn zj^uy4m698I{IBE5)VDEZbiiayOJm;<Aoarn#3&NeM>l zsqD)^X_*pmPBXk}QCg?e0@%4LA ze?clC+=~Vcw+jJ!V=#q;3G+RP;^v+^>GuA_R_GeQ{cJI$+bLCx*1j*E_NK5*9tD@V z9X|%ct`u|ok4HIbA8eYD$aeuu%OJus!0Ok|5^{(_LO_$h_cdA>SA~PKnp-vtqsgCA z6SFCIH(zH@BY6V1Ev&^umgbz=d@#?akj_b3CgZ7AM^3LkAU#kvw#jyeicbeonC1CeV$~;;xj`gE86(IlQL56NTl$xi)#7m-K1De84Rz0 z6Tv${ON}UN8ve7@Zd*EGMdld zWEiZL(FpJa>i78jF7*}ao*2qaGOWd>oJ1;Aii1t!i;;}$N#I_`tLn~^=QDrxV+hEz zVI}6Y5=A!=my$IC%zQd$ca*6UQkFNYj>EMW5YhMcE-um8Phk03yZaYK+_u!GT0cXa zg0)dqbacxz?^%y8HmvSW!S5cRAxZr#_)x2UwH8z>R zZY{l;sTR2JeO(Y%5huGErxcz0hpxe?msZnN^Q8WW;w_nO&gjR>tpNv1AcCZxZYfLP zHyYcUIjS&^jxPuV5`oyN=p({jd#Ejka#R2oCXUDi3J99msb$1z8x}Q67~?yEQ#8n4Z_O!M9jI zJ4kwEdF&fLQZTl8j(|{!YiW3u6|9nwe6n$SKaJLrM9nRGON1IG6-HoID83Mz-_UYj z?61mfzU=h0y3OCJbtGK5Y2G!*tZ%2S4uE+Qq}%oQneh3A-9~Pr&s^B7%wtnL1r;w( zCy?R`6#LC~lZDJ6nCK1>^EH?uX#b6{X(CuaA`S$Qt4#%tdR0t`Z;Whq7Vb^V`2v4q+qespIWeBVwQjRe%s&ajqz znQ2uRgFahR;bdtqm_L5a_L|Al9bW$VFp|&yuJZG6a?G+O1G7z-mkn7@zihmc`{h35 z%cX-OE{_oDVa*S!ieea~NP`c_h;N$>sqkef@B6rkuvl9ZzA542V!*k6{}M$gKwi2q z;W0=w>ZW@^jO6qpTI%Qp;vJgOEgkRf5%Y30`F9glQJEfxvo`;YbX~(39GDM z3_fTi;+Z199Axmg2Ytcq50DUjexic74u}j6MUzyrs87PPwr?M2O>Ht$HxfJw9U)| zmy^95@DCe7^C{Z+MJ$1c zj0A_SR0-pSFlCN#SjGF5)kUTZFEHcL9bWZ{B)&r+YQYX~LujY&>t)CoY5#!RmPiCn zYeqlKbrPm{!C<#~!H@vuQn8X+}wCy6CFCl6vX_#sOY<9tI*H688GI9&qE zie)ecrF;_B51O8U2?RET-r@OpQLp)6RZmJ3uynE${5dJES-ivK+@0)Cqf*SbOBp0z ztadJn_+$;mc4U=x`_5XY;QD*b34T*=1(9&M^4T(IIjTLt-zRlR>ucPaCNGS6I0 zkS?0RnwnG`!S9roa7Dv-4(7NU5sgsLK8>{8Y_&E9ECn2`q>OWI2`h3#|A|Jh6qL-b zmU9&^#gB;HPGMqs3l;(4)W4J}D$2jJ4V^3goIqE=n9U(z{8=U3LOD^g(uQOc#p^vK zP@;08CZXPcqLY*abQMbS5=V>P$+z2lNDQRhMq8=&$uF zeUj(Du{^QPFiA$tS$Bp$?9*Ea(y!^?(8@@aDwP~dBFpj7Io$X81bC?y8^v)mukgYj z6S{rscyZfSVaS!R&u85#+Xox7%B<^>50kl|5KGtBz>Jr<`{O_Rhw(%5-B+mZp^K0+ z>OG9MT65XoIfla5$NMzE#frR29P^tFbN$-aj%#=Gn~5XIchlN;zTfcn)PkD&RIGp4 z(tb@@h6?oP134L#4cvEJVV4HCeQ%gDwR`ZC5m;SjJU^wtIO4kSzR)NlJ<>^4=q)}BCu!R)V; z6omUR(0y2x=`~9X&$jE*E+HWY;2e0RO^RyWZERVq&H;meF5sJwkOY5i|&wBxYU zU-*aX%QUN#U-T8Gs?jJ^&u2+BEOJaFC=IIq4ild%`-XUww(}E-1nrX+F$v!awL8G6 z%0*S7?!x!`d*uU*+JR zQcRa_44CnqnXRz1A$RvdIczT`r%Xjj4MNoKu$V6cKEJBAfCVN~$gU)M8tAd!Y||v= zq6^9t{=|F;ONWp(EqZzM#$xi@G1+e3rY%?a4yNdNisHSBPdLmx`rM`uI?Oeyx2q8J zhc~Eq0~$&d-M0*4OZ^^37;;hsX{NVIN0Bxu2j~slF~dpi@zP+98E+~QA!#S5!Hei0 zgG8=Ws(y6J4ouchERaWO6*iYSwi-0JP5*%h-B`4xL$E_Q@7lU9syZ8+eZG+^`MA#3 z!*W0SI{8B74ACNwT)Z#nM+BcLRzi@qmqr{fwtFXyvyj=lFZ=}}lgodTg6)Gi>C##W z#P;wdaB}t{dysRl&F1^pJANB|0xX|lfNKuW1swP$f!D{(CV!^EQQ^pCg-Lx_t%22w9*KSM#@oDLp5C}|6>#_LC;ExQw{ed2mfe70pebd!J z4o#ThGt(d)k zhhu0WLgwk!SzNFZCs`ZZx)S>pXB2%ZFmC(M+W8CpkG5Dwm40auz2zoJI@?AZJ`Z2z zxaVc}niCZ8A74B~Z?Wo{2g|2HTF?p0)9I4fAvPvP`D56tBk1PBou1D42)E_`x+8I1ki4Z?z@)B1MdWW zvnMT{Y@S5c>#4HMrn!U04JWB$Kb(~S0n3r7A|tjfv>gSKX+{^9+zANRYRQ$@dEf&R zQ#Iu{X)W`h78G21K7>{Goms6x@m7ttnK(0v#Cvd>gDQt3I^?aEsDD*5X2p)eJ^i?^ zCkP)U4Z~KGUI`$Hv_KP2_2f`x-?#`ki(?~p!G(pK)WZ&#w=VGijtC!XS8*Qi%Fu|l zI>GpCgrpx(#@&f*&2kOR_~r5iH(fmi`U5 z51&bj_Wk9XUT4%0S@RtA{sa7<`l;4W&y5dXxS%e4 z!@jQnP8P+x>~}z@e% zG2iVaND`UBtd#|=KqdM95v+RmNdh3q;>RE5UhNa9*Z_p9N(!eQtt$297WEsoHmJ8B z9EUg!AKUOgkp-pv5J_JP1g=c7v+QF_%2O*pF(VDPUEYPblEA-Wr-e$67t+3o-#kN>d;~-+k+G}RbKP2?@~`=OC~TySzS8I z&)4#5g{Li}y&-(lA?_PFW}r55gkA3!(~uUO(fE_-w<+%! zmb?GO{BI4#TjHj`>hcULsLb?gw**Ax%;`JJG2WvpGHG*lA{JRVvaZ|jt8m2v`58K% zr;p0BkvDIP$2ooO?hVtJn4H`4>%01%Hgyx(m^+3`Teucg%EWTZpuX?&o8%%{U-{O; zIa^zp6+;b#VkDD?6MHc3B@f9bRW2M@7^rC~odTVS4l-xV5SFFfmUMz|YmnGJ!RlKc z3BMDp=6IGDcnR1u`NJu3`T|tIhW#W7LmB&DW5<<7B3XxsKUfLfxOp()zT>;A?1bvg zyK5>F!sDcE?e+a+63O_DM$qJ8S^A|^=6?ToFwma?`q$WSuHOVS8M|e79SoUqdxb_{ zs!l4$wOPVD;P>tKWU|sNib6jAn4K*_5okptg_*h!8(lZ)l6xd$@H9~X-c|`EWCKu&IKNzT?4}PnTlLI3 z`yBI|IdYnE@z}H(vP~#XeOLZc&9NQRj~7}BO%?#LiFN#@NlEX(=^6c_V&7|3W%I5h0ti%Rzt-ecPRLlZqX(F&}`EJrpc#x$D^rLA`Ja`uR!$j>wT9@Tl`uQ<97MAN_MB; z5j7T(hwuf(PZ@=4(W5#$h}sHmr&B}krU22_!wNWE1&F@MsA(65VS16hO!+ZGRI006 z?a142w=sG&gzd)az_K5qi47dd!iyv+|3lj{+{a^@$q?aiVA382|<49R6OiQsk{!9n0U3ewg)17 zUps0w+AWL;HR|j-1B}Zp7D@Dv_yu-FxP!&mpGW^B+L3bUy<`Mt=q(&s*_R5=+g*v zqgg|0)(T%NDi{m!IbS|IU+}avAUowt0=##Ei<0F{yd1u59u1nad%Z1l2Pj=p*Ip-= z2~z!GqdM83kuc%Y*GVKY&h|o?nwX1}dXeA!?B>0Z@=CJk>xOjdcQbK_W5n{UUShy6 zcx5qSJ2T^kEFmdkMT)UFryn?8PHQr${J58jtYqg6$Vv90MWMl(xVBr4*z13|x0I@& zzRR|+y(n_4A-jLt#8DmC(ix|+7&?r`c6<$juGqHjn(%rLtp5_tj>=A&7uEM^`gYWy z>{w}b*3+{zo9@>d6HV~+vZ46q<-o7FFQ_QR!`PJR3>RvyTs{kPqvbNiF;Za3M_3{^ zsmVR*qC|Xr+f|j`U){nSvOLW8@n;A;F`n7?OrN-kkbG7%IH=9#LFqK0lj^nQj1*1@ zY=b~w$Y7P)m(;9W@7nH-V+1cT29?JXPd0{--vYY&_x|J9ni+L(CbNMIMXM3fc2qLn zvJIiQQlUVJ)w@(;^s2Poz5E<1Ha6vCQw9>)x0T5u@@z~DsfnfdpC|M2#LN>rq;aE@ ztMS@>KvvPS;Ta++gy98h=s$m<=T~GT*I!GVHUiSnqet3RX@w``? zw?&twF=cc1aAVSg0jsi!nA5?8PRXqzBAGZgU*3E+|!3n;d!_v8yVI<<^0qSJ!SW2AqX zkFAV*SFf@(nIdx5CyWhzFl5jKzjQiIMpl@*CS5(&L(+7CKWV2~1w*2P*9J5&9ba`x zt^%U^z;G}Yi>7X<_aWkZ20{^z_}2y~iCqy1;*|o#{8jeFB?DMI8;ixhe_&H!_4e}e z_@CTpn5R_EN|S~vO#`q_;mV6^XB5rV-k*GsDJNfb)m>)pn*}YuucWpacze}` z7w#`8b(*ZGN%S#kX;w##b?~9(#=B3`$w_l`q~|<~mk@gagU-g~Y2H zSO_n0pJDd~@81fY?V;5t`26sU>&=9n8Mr(|v=$RCn9*TEN~8V01Q}{-2y^LYK{eVA zWrW*)y+m(OR`%>$8o+ODnkRC{RD5ED93@8b4whb8Af!}}l^&IIv6L2RgC84bkEmYp zeNRHRcc_uvFMNHGzp)Y5ertqZpi(dyP#MC13zTTn&y!?qIuk3`g>lclIhlOu;A?`B zYS0cL_SN0_SWca+Rx!7O-ngknA@Y0h3S+8h2T_48oFBTfN#{mGO42N%mhY!TJG^pD zhj+8XZKs@!nVi{H6Txfi&N13u*G~nPFAgv{eK>16GqJ9L*;99xpz2P14uy;~*h$sj zF>G+OqAlhN2jk&mMNdW=xlZ0*f+Xra<2)3`+L2siWra!jxJ|^0B1Py0p7YX=(K_W7 z7vyPC5&(aL!4T%Nx$Bi}-3*?Fzn|h!;zH1GF{21QVlaUN=)K*)Z1KIDa2N1SE)=K* z{M?1<8C*Y8CuCfET4w?ZJTw}l;kqNCD&arx0avP;6MoaAPY-|lqq7kiNu=`oZdD|Z zT*xNJ&>(&0-fSpkU)8wQ8+|+6N7l+0nZ8Jc<`VULU+!{p@sEW(lIAoET)#e8_WlF4 zZQ|23*L4Du2M!xmaVB&Y$ z^?12Br&Eb;^guZQAJ<)QneN7d?i3$yvzOU+kO)bxs}TKZiOjtH*9&{`HXRg3FkV+_}?qq8i_+$wTKW+Np4(VgkvMPQmb2&e_Q6IRBQ^qCP$f#2|O1)Vcs&Jd0;T9Zj(&yH&O_) z97e#oUUYP_9Jw3OouR|I6y1$`vgf}!ZJR|%-KjaE0DX4I*oT{?cLeu#Hi60N2HL+9uX-J!h3W*P9X|1<21`ro@T>n z6&(SV3wGvh>=UK5Z=ULt{_?$f*Y1wK#zxnuD(3THT4iSEHgeSkwdVUCa_JWFN|cs68q9G$2k~CjtPcD$6wu9QC98o-FUq z_u+ig=+zIR@QpG2ZP5?E-ijXYtLYd(C=k0ABs!FJ@$rm0bLneyN~-8<$0AQblN|3o z@=nq(B`T>Yx~oKVCW-1E$GqF$`WM(j7;Q`_6SIF4Z-o}f+@KMjoPPzypMMoY#j?Ce zJAJ69fITCeMTa9W(+euBREDrRJ)P?N=ZriCWmEJ!Ovm$Qf$+E`Iu zir75Jp*H{3F-~z+n-}vVN--4+=}#^0@zTcq5wEWL2y-0%G0wIZ%PWm>H=iyKz2BaQ zMRxLwW$R@aFBHu$T z3D)^Q%32=>tF7Du71@f99Yq=IOm7=uiT*3q|I4fF8ph!=e{LoOljPTTo%H>7H}g;R z@4UwCv5b~WvVw=pkoN>A@^3U9 z?I_J&jN~V6D{jW|fw&2uBV3U=NSG0+SR*GUYG>ey;|D`JPsoQ*J=QssDL_Z~7;cTB z?y8n+%hz;RE>B$D%GG5AOAqVnBAC@@u5@Kl$a=%aB5HhXS z`EU$Oc@c~nNaY+RF@0=8QK7(?Rv6E2jI;Uut*?Tf@kInj$xkCfxWy;2`prx2`Eri| zT!D^>u+@hg3nO@uF7T_-I1c*6N&pKBz1|2LBWR9L_gURP9RnS~Y?V+*!nVF{dx3V5 ze9o8q0rrrp#Zp}YPg!ie@+c|}6?;kYXU8u(ncL8;5+X^2vSrp4FS1ZRjiCtJQ>3YM z&)P-v`7fX9&1oCUS%ogK<4nIa9PS})*Yt1QADyg)sfSm~HaiI_G=xCe(pkrOpHX@f zRM4Zrz`;G+@hzSNPykSa`VyhThd~5PWhn#P&c+`6qX8!SE1kB_kpW8^uuA@-q$`B= z<_@B`nt5aChx%qbL`uBzTijr<^UgOE`&QR#u8h`t=0?QW+2W zobC5T1HC*n6;jCvWsOj0bEhPq!H|dR(S{?l~N7cvysw(C)shiOq}A{6r5qN|-r9er}}hQ16>=$U@LAl*y*&$GzSL z4Sl1ZN<(vtXTy&!?w@{KjC~qSx0C zIywyvU*AUUv^WAGZ0>)yR9`F}49#Vsb$n`c#6OLkaPx|?FiuTQ zazbJ%u3t&&*=5UEPtwzJO=mW!BkX7L7~v2W{4UPs&NazJ3sclW%xOv0PZs4po{be| z!tHN{#$(E+V_!Eavg3ZOG!K?G%tuvjX5Dr zkxHBbW*e=`NQ%Ix%O6IMg|MmZO1Mw6o%w;cxKm<|v%{?~hTV!*gxZ}xo<)UPkL(Ju zx#wf#$=~P%eM;6&em%ZZRKq4Qer%}9U*Ge{r0%DeHfC35_;i9E(o>k?8?(yi%~y>m z{LE;Kixw>Tc*gVhv>;qf$PbB24wzy1#mqnnlxsCnQv5I^fY5;BEQpfh;G z_IDcJfk}&IqGCMb#*!j@fF3~~JfW_nNSH`99}y8!0_$tEnE}w?NVRRalRSS%mSrJ& z?69CD3c=;GkjXhd1@T%BzM5IjL$-E)hq*S#Pk^+UHEYDuXz4~=YyxRR-)=J~L_&Jg zOg8D6KPNR2Rl0?u3_{Hr?meF;oy6t;Ae>t8p>?oA*!UeDYjlD%)p%<^lCrjPtn0H>~2U>M9hRjW)Q&aFis6wfi&g z1IHc?JJaLo?{4EJXaHd2lA@oyD`aax18!2a1hXy4l3LPI8s)de?Sl(PQ}Gzux790A zagB8B{JjD-go{$m%G5S6S5Lqrq3Dchlu>SiI3-`jJUyP z0+fp)>d@o71#|_whx?1wS>20DB)RT9uE)Dofu#~*Z!LMdY}4k%u&cgep946;HV15? zy*H%hNS=_CIgIA(^%BZZWugQ@WHx{GsYRVAei?_By!@Yt-;UTwd4_P0FT(|&e0lvE zhU_ryW%-_$h1{tcqnNMJZ%WY?N>;=RBB~5J!@qSM@mCwb;)W+4^f}kTTUIV&7r0f9 z4Tp-dhWHJG0K9Y+N9~PvkhqbI>XDCrGj?k~8NMk*KA4&pJIX;*$HO>vVB^#xyOy;2 z9B+wDkZU4P|FjM*inzb*n#~qVY~2i{(xaQ7_$0a;i@wW2k6WOx@Uq_J=6Pa_+#$@TG(R%L(k)xfSi4+ZvlCVQ8IZ)fe9vzXPsA+krGhg@MYb`x2 z=&?}(KE$HJ{kVgn?V(-&NvC9z9Y>fb@M|xYF;c;~<%Y=EwrV=t}FQ zV#zCabP-(4K8#KFr-DM9J~f@CxITE&n4)||0c@ej8{a}J z?2!agi)uL^J==Fo1M+XW@GrOOl5@i)Wq@{j!8=#p1^v^=0Uw0F&a#e@_tIL?b&Z>8 zY9DThq;pW>v3eLJeH4Oajll9~@C|F1Mie}&+dR6aULmFkPX2A1k`=)JI2@b1ft7jN zliRMrAf|9|e~4{QUVo5MN|Q_J3!RfoqgaU#DOw}lW9XFsS&bOEn=^v@zBmx^5-9%w zQ+&wdTs_Jc$LBOage-V{W_Qi6<;b=c2pj!tH$S~CF2(V4p?goIYqI$uSD+0$dO??7 zfe8LcYRfNXLsnap&e)iPM1?CK!gRGeEZhMU2|vcLdh{PSeL}$MS-CVTJO>R1E=MJH zI7(9niw3l4;zo65dHe6BBjkzCYRb03?KMXBtstX2xSEo$*J3!x+qoQh{yOYIgFUE^ z0P0p{BMjssRY3ntpSk}#!%f3mZg_~s(q_{4F}6lr0KUM+H*Y`a*TyE01jAZHxuc%K z7k1b0qT1rr9Uoa_h1;ST(YJ~G2chE;{v32Txy(-Dk&u@iL8C7Z9{MECp_R>ly7%_a zmrv49_Tse<*iyzSXgP)i2Yb1_!`m%gb|SKDydwwS#u(H?IGQ6&K03AHkUo|5defgc zTB)O1RMXt|k9KTf{b=ELYgs3{+j{IHT-HD5yzKOM-*rE4HOSsRho4JoBBm&eiA`Qj#Z|j_7<)EU!b*17qIOIz#R=l~eC~Rxhm45~f13Rt2GYa< zpE{#}pq8`2u^Sjn&v;T|e?Hb9lS2*U9uhb8zh7l*a0RKe_&~oAddoDKQiM ztMnbmeu0CJ#(_~LayQ!1075{$znJ+XZ*R_Bjc1yOx^n;ySq4t!g7v#Ht4z3o!DX*M zzIL3s)3G;{cXWnSFcWvuO?e&Mg$-V;Zkf(1uwAF)*D}w9Ev~i~(Dm7{ww>%%fesb88LyR+@ zq1aVo$B#{C`MSvy=!(ZP=wAb{lDS%|(7V4Z@wV-UnTzWqZxjGOjN+BLaw1q!GcJPb zy1oUu{>hm3*4Pdcp5Dn0;xG=}>C8KtDxhe9oz0^YquAfo0|j7pC+`&)D@}dz7@{hZUJ@Bph3AWqZ-~N^9i5aOf})G@?O>R zs^^Y`ZBGcAgrz3x$Er=*-3WvNuSDfCFVIGa$&*t<41?^Od;f!7xrOZiJf z4VtUE>BdZ4LTB~A8TGnAM#`Pa7qgf7VSHw)&J|mh#HgxLyF}5TPbtM0?yL(*0I_~w z7Ohl=SNGl);C(TP6zCb;j?Ab@w%D>iMKd1uN}o4-I}=h(BC?=2bUQ1)ZJnJ;ye6+a z76M&a(HVn*cJapa@#ZIn#tKVKq{P6QIB%}&Y0OKR(B?#_cfz&nvZe!07He57i)c}p zgIoju@*~sX2Z_S94VrrWO#QM_?_KbQ{CM}y?+TlNC&X$f073ergK!5|%xT=j{AJ>0 zOC&w>SbTpg+eAivNfjC;*7iUs5WUB+$Z_gJpuZ1H4)(#%MBCl?5}t5c(GnHi1 znqX~pUkgQ!c`z87b2f!z76DYR&gi8~%F>ryoy(2(8M#{I{PJeLi}kw|PG6{V>8)>v zJZtxn2HsEtYbOh5`ufYTA*d@j)1Cf(3r)TL(WT%pJx`jN%p9&&=k8D0d>oAmYO8dy z-A+>uC(}XXd<&Jb!v$cRw_lL$hZMOa4R#iAnm-oL9ZDQ4-y=I63Fk`n&!~NM>O;ey zKQtB&!UIO_+ATdF{8eQ7)nZYkZLTs!6##dpVe`{T#cYqaD*2KfWk2X>HMxZV4aB|$GWBYxZ>j;qaZt6?0gWcy~Yrd@8BP2a8C*GJ{3kl!+ z=|!3_>iVSz4$nEaO>v~lV!xGWu*fyl|9qxtEhEKrk~@c_u!(p85rGuyNUW@7>8X60 zdB~nqy19RiJK+L3Xg^v3?MvNzANRNv!Lx$fg!y(Jb_J&X_g2N9hS`7sW4DUEZWhaD zzbZ$w&(91A#Fio?Um6d6ZFk?Fv&r{^%`vgoI+%`s72 z*Z%$B82y!l7&}|F&NSnor5ho&vj4 z5v)UmOn^q+jo|@SJ^)mR2|-wBY<2Q`n`VOiXGfJ5d*A+=Hi?2 zq9FT4S`eQ?(`cayeWwX9(>q5KIxgU#7p&tO3jp|rhj~er&A)A)=8rqb7^^ljHg?Q7 z$}K)3$YTS}>5dEWQ;a~L*!&WhzgVT#VFVgzoS?DXI>TDKMP9rXL~!#|D8*`Galip$ z_G(Fh<4y0NquURe;6=MAE|iaXI*8(;#a;00TFuklcLJA?ICNqZWhw>X#$ZU0VC?$b z?Uq+zt`xiUSf|+zgS(Dz*$zm&bfh|dKpdeYYp`jhRuXjg5}TVF3#Q_fBz< z<6rl0CH|aSO>D4-B@2WMK6NQ^VRt&1p{4%7Pxj;`X^V;Ze*&R7UdGW(VDLhKJ$2a9 zIsbg-P+c43G-7W{;2MKk1A-f{xvL_Z3-+6-Ra)*?(8QsV2f~R8#vH`v! zZ}s1JWM#{7t#_Kwnwl%a7GKI~Y^_=Amf0c}ukVN)q8IuYO2nSIBw!W4eXPp+$aCTz zxV{eBmgpdNrCs%sV=6vpFgSc#C0t)uVyk%yc}Dw!o!@SFco2Z~Ul3nf4<|PVaL)Qy zHe*Z>!#NdIov@PnO}D+bLmpd6xMW}4i6+03ml?l2x;(!0mdIB@RhkPfzAG6g4yJf| zO=fM`cnp1qG^c=7wsdZVZrG%PcE@5RML||I<1h0T+)8bY^9sig@Sa_;iPLi5?tPzy zE{4F_Sg*E)v9BDehQXjTZ0!$ZYd_i($$5;G=&-0SPB&Jn`9ZfgSQb{4b&ml)p+Omp z8|-6S-?B_oriEJ9%A1T?brmpH)wfw#>A>2C2z}uzI@>3a$eBv5G#7qI%Sn$)dcB_y zHFl4$_Dx(35`}PgS?Vc?p?--bxBQMjtV9-?_!vuVTy)XrT7d0kAi|3ZGb~|ZIL43U zKG0y>;?y##3)o^l&#B2I0;0eePHmJ{_5O8$=?PBl7-x_WuOGI#b@^rGqh zT=Md<<3#?iVLRhG#8WuQZY0>m)Yd2AP2Y%eL4WKxizaYYjm%|lRPTBKRWk<@W%7nU zB=h!~9cjHqQfxEkO>UjowI=TZ>|NY2K;wzL0OMYDSYS9c$%%X@eX8~f!39bJKo}*L zPreKnLMw6c{L^u`5xXM>k<#AaajZG?hMiu8Ebojz;U4w7f%}MY_-GiaBX5kwLJF(+ z#KdJP=Q@g?6RLF>2V1?n@gpng*;o=OKyV|b+L{ZWAQI?Kbqp3g%*mR_o37P#f`R3) z4<1j6JduFJ>>AyPa=%-h)k6@C-GD2;3I<{~|B6wbIM zfpZ|4c}7ic?kUm9TFeE-TVxZcf1WlP9$)_NaiWePOFR7;io_A;v9%K(ez1jzuIBK! z9RQ+?MD+07re(RV>j(d$5G}Xi27y|{pcnEPgHJyX>8ZY@L5X?W`$D#DYEbU3%9%gb zo3^G>_!W?AuV6@MAgv*gGpxu?6I{G(b#|q8+6?mK++C<3_IE*_`FeZ)W3|PRmtGi4 zs8By ze;D(hUUXR)^cPFf36cfURSAY4a+pV8C2Y&0$gqGBH-myRsU3(`vXQ6%m+`g&OQPJ! z5kpb-pOsWE5-#(xX=A%m=epwLpwo*j?uj1rqNYfpjk$^AXn)#Ex_A@$jlJ1IsxvMb zt$g6G7$xg4Cb75JY8N!1VZi6+i5%!o#yhlgRx@h+-&pa*)5bOV-k(t@)MQT5_QRuw zDrhK*#EwTAw8~is{Bip9Y7A?Tm+hS4x0eY`+I6WTp#}y8ZR{mPo-e;x&krN+r^Y|~ z(oj~Oc*hK0svWt>{6z?Z-pr@}28@X3p4b&J1Rhwf_sSXOJF5|$7eIsXXCR;van^hg zE4(T+ZzfQ&`oWdAu(1ee;+Zk zKkU@T@;o5Dj|gfwIEu7agO0A#y{Yut{-?%{X}c*TeF8)D zMEj*3$S znkR9FxA+HO%2jGA+{vq9^SN{MUnF4;0)uJ&bBvYtce|sGS4R5@Sa>HIXChZq0u!S; zOB!hBaTv61cvt%~R7k;WIe@2T0OV+QU}n&iV(~v?ek5BrS@m+lW2SY_+LDQeWcsXm zQqPpJ#Zduon#T3`uqR*BE7OlZ_kY(r?LO11zMm#d_52Ksr_WU2v1CBmTG;yn4ROJ6 zKkL9BEf@|yG|ZP#1j>IyhSbuA9Pp4hR&&pL`yiA114u5iCNgtY?aBMLtip3wGh+a1 z<8_{vywe~A{-iH?dtn^i_9zzVXBp9WsNH2XG*AoB9@+|+IZYP^`j8ObbcpDFI`!My z;rvY5fW(jFn1;ZGi9_8qDK}PtL(t@*@?9kqn=d5o8tpSCL>>Hw6bSRw}a z2TALsjCh=@QYF~9Du>@Vwet#TDiaZRrf(5L+VWWFkP)jCqQr_bL^-D?-Ei$|4PV1T z*(`CzEXm&nx|$U65BmR>hU06%`OMi|jyIBP2QDRgMTKT2kf2_vG5gRl#4xWdq&lHp zx=a@cL`FBw&4Y+miQS=}`Bn|`sz%|za!lWcA9fR;Jsq+r0vgRN27E57m~d69`n~kS z28|URiP{Gew*7dsnwK zMZOlv;zM*S#Xn%TRpt%7_lEI`gM~Kp{s~_wd}|1ggc}sBQKq$o=J`cm=+*#@+1|WV z@6}X{NazS^wAad-Ow6`6`O1ZRJMQbuA|C2|ey%gZ*#n%ztlNY89@TRO5(vTZFl~*f zg<1kE9io)=;j&QqoMf!Q7|VvD+EX#NvCG(6hm++Gqsz`4oM84#B@TDT6NTW5Ue-D} zZb?m(CQ1x=uuD zWfpT6;NFk8DfUmFJ=Co)>+gzpi@2nDWjS#E4;Fdf2(w+S0&b7ewOx7TCCh!ldpWU@ zQsN?ig&^!L?Y68;{5zhs(YPc-Ps7^!LP2IrbJtx#B0wCDk2g;S$LND1B^rlh)Jq$| zxwDN!AO2bnwwDFiIW+aVY(aol*ZLqHF9p6ki;(eUB_^nN_PG6#y-Sb7jjC%ZS4Uv%qu+ z$ExKkx-w}%8pOn+Nb1VRExoIV<4UuGBo}q!&O12y2QIj6aO&^!-0%U=L1wBel}GS* z({AQncmAn^I$}vk;$5iD5ShN-TMUjR(0MzkMXW8o0jb-LsUIS0_Q9n8KqS_}(pcV0 z1C6w0dK;$ zIap6%-HXm9WY9z@URXKO(MBn1rmp-Ly`!q&8J{xE$>m8e7hzrK^)>rk6x&Kf!ShI# zNBaLTp7oodECi8rekQ2fowgWR^1M=8d}&*vOSl3-d%pEvN!p(U46bTvA@$BF7}bJl6k zlxB=?9O_?9++r-34Ik|bUAtDCEw#PqJ|I(oN^}K4b}yJ{P(#CO4mkJe&7M>(gTgx^ zd{8jT#9p=%qwGhGwios8lx8gzWE+;&cBd1cNF*~vM|a? z1cW{qwIy9lOB+j-|JNas3cIpm!@J_v^WhUt;CK0*{x{S5yPK_q5R95ufq+BvQ_Poq z(o6y5>#h1V^@b{gS)(ZtZEVp&1$aD_-ziVw`qvsUGL43Uz+U>u9*~_1B!+!pJ zw6Z-b$2NUYtAX$$g~kvngvchXNoK8t z2u^f@-E7%3!^$u(HcXze)Kh?;_0~f;{?f4%W0~i1d(opEt&9CQdjzPkT;r#> zyShZQt||-FZg#pX2^Qvrky{hOribL<6JTy^GLGc|Dqh!3{zXrcyd1gm~Y z5LaoR8Jy6xtNy<0{z(G^EG)|hs6&W^%wW4~!q9Exb7iUTN^ZcM*(C~P)^pUVAG9y= z!(TZ+Sbf>YG=n<(T=um(mKG&_@R$Mo4q)`rf5Dr2tP}5b$a-9A(A(wJMP(PC6(vf| zuf&$akNYvDOcn#}bE6AKhTpwGUhZM`?Mc|A5z+qfAhCHgn1$6+jgukC_88-l36L=I zdb~JgJqBOdBzt~X%KW{E3qf0Yv6VYQIvrG`VyUyA<8x_K$(8vxd;`NmKiUf`yLs!` z#ZwU`g;*sG!&m4%V3vSs1&Yo$U{`NIJuWo`^n5%3 zeLx@1?#_ajdjU%$IBTA-t35!Sywyt@86e2v^}2I#l(=iQy%T85U(|R5wc>v0K`o}DW6%$-O#Vu5V-N%D zJY&J)1uCYqA4vwuqs~y%>dvk6kUF7q-jqSldHZ05Hbm7~kb2XA1XUzT;>J$A-t<%< zD(Ha9>O7|dZP9n4B#2xgumtkTQB+PI|ujXTk&H5?TXf=;vniE*EYZ2a2C zd`7~%Amdi#n&sKU7a>CT)oyXe;GABS_mY7@)8$EhrM0Jd;&H^fqmw#3oov*^VG8}v zp1~~rBCaD_ThdoyImU=I8Jm0*owoM@o)$b?lyyA{I=dr&OtCm^ zXweSblya^VL;O79=woj@jAy zwrhgDo45Ji#cp@}6x8N5MelGSJ|ZvpkdC`fXv22WNp(CS;%I_j@g`KCd_bhJ+DLg!)n ziW?(&UjWt2%}$c2Ee<1RJmr=w(yrMzJ=ftyw6M^C&2 z?w&*D{Q%bq+f|mY^{TX^l*KlBGX^kH7ig%|x;kO`I1P?03y`jVbb-}nVZ<8>Vp(`0 zYUkn8ofe1^f%paU&A60|lnUxqWMuV3U{1W__G>@JFOtKl_})9l<&XG2@6tLk_NH*t z*Sx=OP_4g(;B5jCQMJ;KH3+kYR@1;2OeKmi;?f$;gQ*q9gpQ6vM1i9Q`A@ZQ!b!R8 zrhGLE>~8VAlwfAXR?=bzFoFPHQcYD~<50TPuJ96M>fyzD)nIn>t|faD^SZBctOe@9 z3Te)L#MjX1EsL27bYzK`DFVa%r=1rs`{r5EHji4!0*e=Tl_-cGg$E1I#S_Yj zy>QP*S0f+KYOdJmc8Zr%a1(=gK^X(Y;-4G36OyUrRk%7^g~OwqgvrjJ{An^RdiIU% z*1?7C=_e?*Oo};KYR1`k{ir9WvoDtO-SLNUYXG>(eV?ZI7qEDj)Tv=tZFs+UUqaI_ zNscltL78(QxU)ioN`Go)SGBmpN5Y*JE$?A#8}VdFqT_q3EKfswg25{;T{7>Afx2V# z#`J9<_0eT%Vg!b0xZOP!i@@GVrTkq}{FDoyBM4DI7Tys+7PKxrm2msD?i7cKZP^Eg zWE~KKTpNJZSN31aV!^;Wr4WYINcXHsHk=#RoJq(U4bl5-XIp z`@IhObD?FXnmo1SasXh&+gd}{r86D^f5^hV^zJac;8J9>Qly87o(-SE5L8HVS8_6K zsrV%+$_PA$5|OZo_4omWuyKp|4NOCe4RM2y#3?541%Ia*ngTV-*BWl>EtD|fETyfMZl24lglA`VC-pVa8)x+mL`<=?wQ@4P=N;9&xqWs<&hLJ&F+kTZ=`RL<@1^CUJCKjvZSua}An2*>ml!fTSZt$2V91j_4 zi+QpW+Q)WE9AZ_)jL(h9nPgZrmJ6J1zqpfy*dbxNgPY$@aHMHAy}d6l{2(o{v(Cnq z48td`J@ES>S{WLiwsFsb<$wwza7ho8Rv_AYi2~&qgMD(3T<3&&larJ%Ggfd0Z$TTb zz=-bgF{zp+102i7gxubHYdma15c7@@N-$DiR-hs`a|a^&qful%wGMThL!(ALgX;wm z8(y6AFv2oadP^-L@=!y~sivb7%094Qsdu+kV)N97SrtdpW6i%vIgC?p;&KN;98e6j zL?lCM$D>D{idvd_jCgc`sB1$`x6BA|3WA04j;2tF#GzmLo0(!`b8|&Nt{7#Wsco*J zkWW}YfOG;dMKM9J(QO>Ub*7}>A&VLv?zA#7m1|h4`8U?9^Rg%T{u!(o2))zds0od^ z=;4(_3pNT|NtOm-f%&*u7L*yUWK0H;Ff6$5UwXUxEAMl@z*(^S*S{0X2iEUhT_bo^ z!&L@rW3E;?OM|@3<4_;mn*Bpz=mtVcpA{Ym-KMSNuw!-V!;B?K?*1OC)UJMB-gf8#5X6!7MPbJm8Kb-vqgVnZ&IxiL@H_H)BJn;7Q*%@_ zg|2cXy}uhHph>$;Zu*ji(tk|I4rfxb9U+o1D~hQdZ0h1*tygNjkSZIeDju^&c8wtc1R`dYbb~%Ke|y9*=>Y#ZHYQK4H6{&*LhnICQq4`XY!7?&Ouz>X{BQ&{CB_h%UWHDc&M@oZ}vo zQ$;jlG!aN(lr!?A!oOf49Y z>KAGD{6Nu8*p-|7Oaqbh)n+F(OdVcy&4)q{N5W7s29X8052vahz z4b6<@5d++IKJg5qcW-I|5T4pvvzkcEt%O?KsQ4UGBFu)66*CnCAksZ6&a8$%Y6TW1pGRK6G@f%~GDr8}x3RN@MGdUy$AcrfzD0cqx| zG|$+G5&;6Wj0jpfou50qY+n=SBWBom6)%c*?Wngm082$Z2Dv1Da!@@<3;K>#OnOuD z^65}t9oaodIsQ86ylxtRtse352=0)g+4ZHef$Xm>^P{uza=NnEvl&Q$p*Z^MWs4h& z{@@sml|Lz=!5z9YYl}*pZ^^N{l<%v%t)%3lq=ghZoAXEYHi~S|&QPWjCgXHyzDq%>WUki&ka*TYjIlC=gqZv+ZdHDz+ zh~}EM!P*_z(Qa3U!O>RI3M_n0$QWR?^Yw2e0S_HKS;(eh)=W94wYQexDc-TLH{S`h z?r^(%ST*A9GZf(4-uz_%{>x`5dWrk?q)pvL{JzvJisFTRJs&A01BNuyQ$pnU+}V`d zL5v{k-@UngVKoz~d@8~+7ZO_mSC&kSG*?X-|D_=5hU$u&B;0kTxA@}m76`$P>p#Jb z-fYL^(M~hAC0Z$Al{+q^mDr7Aa$eUEY`!+XfGM;PSIYZFmTRb57KvW$5#=wwRj5(G z4_H?hjMxj!xXwexzf6Cv>6AnCvtN|_lJfH)mZau1S88nA zu?ihUOW+7m)n(kPoik zh{%{^z%S0w;JA{EH>Q-)i7od2@?q$B7(h8MHB*sg+5+3?q=IIJZ1X+#p(cRC^76F} zXmx2TqGvlmY%4^Wt>L1cw75wh#nKq4A2uMOJFKx z9&Ui={>Zl1S8J-`OmRQ8qzU0EzZ=p%qrs~x&~)5ciVf7B=c_^PyK&^|f~lp3)5E1+ z85{vj@NQPP7>c*;F&6sV@+N(vrOsUts{!n68Ywrl zN#G3F-iBcViPo6*8nTR0I1IP^h>E_tWv95Z;y}~DFsaiZJwm!sk_~c;da$^?r6@gjZWh5vaChRJ(qf!%J77qUOE!*y08M{o9a&) z?Ty{_tH<^3-R}33{vFhLolAHw~S?!hFZJ{9?O+VzPa%BFuc&KaJ zf-aT{K(EFmLP)>4NdQv{$g~EsNW5=FA>yaRBuvik604O1Nx1uCx11Af*nB0 z6aQ9rKCdc)Zv;o?_`F7~mcvHnJ|Q8a;=W{a(BN1kjC*m@M_}nPTHx51zokdannvMKJEJ24uK#hc&a|+2Zc7Uj!uaelrnLIHf_3p`y0<8Jtj_6HL zRNq{w^NhkJxM~bsN!`z>1GwdfV4EXG5k3ZDIJyAY&Q%=LMI}sLK8*|BXg4h-&yIZX z=s~ML_rjV0`1tO|$*BvTwSBO8xJ9ez9^Cw}{BM5ZY~4u9zv1<(YQ*^nt_`&pj)I5v zI%FJ)slrvF;o9pIf5KuVbJSPiol_GfGhGAYY&0?z!(4aXFH{yo5_ zk?{I15!5TA9>f4EZy`YA$0U1}>qu&Te_{JA&2wCRXce*5?g0G*{SZ?j??v#J+_ zU~!f90%&$3@XF}5Z}cNY`c&aMF81Dn!0hq`bLE_$!g(U|DhOZ`iJc*mJ znxv}x6bd69*{D$47lQnWVx|%6%-d*rL*H{rT%wh}X*01z8iG?ZOTs>n`rP`_j%kQ= zx*7AY!X?E@^z0Z~;g-~&`mL5ci3YuQ`uHZ<7$`y}3bElEHX9%8o8>QL+;jz!ECA;N(+&G9Ths|mO)RbU6ap6$S z2+ufb1M+gH9)+u*`aRT;R~Mn-@gWNsGG;J3Y&pT}^$cOzsEDSw|1YV+>vz`9w(2cU(%lD_*vK-)+z-lq?I6R-k+| zF60p{^LCD^6y-3@Q6{rH^`|>D<8PFmnv&#}3ar8b9)u;;L9=O))s+4V1xsANsWA{{ zzuUzV$!3&iiN9P_ zupeO4CJ(Frb(7&4K=1ZP@93YCK`~qO2j$@`9gS*5XeyBbof1d{&<1LlR>QHD@hO>P z+E&WqarE2?_a7@v9>?dmaw~FYg!|Iw_*59ikHlo^MZCBT&`h{dXaV3l)Ew7h%iT^H zf(#AsW=_APO;nehT-IfFJ^`}(R=5GU!!@q5?#&kF{lLR{c&a|5u|3cCf`z$FY9wc- zA_P5%Y{_#N{W@2Q&3B-S@2OFfaNnpUcdL>!GlEVXk4wYgrvKD8hXZ9QYNS}htdDp! zs78e{6V-Kvk?P9kG91Y4AL&AfVpNBACRzZ5A|G`~IiH&CgqY1#U?x{V3+9giEQRsX zm52TL`w3hAVI|7U@Qbdx;Xb`wtF3SfN5jW|qZAB!RK@nJOeG-L%N<$DEs2ao^>I@h z4wh6eZ;9y>&?-_cfT>&r#P|@8xu}~(z-Z=2V@zCnw!fy%$xoEHo^5;9l}3-#2X;C< zBhhUVwqBr6b2GZ!gCILl#aL^}XvKr6TiTb!iAzb@tg+4$nwRo4FNn zE+p5Qgj$%FHYJeb5&$CjOh~YFZeuw*+S9T&`@bQ?XfTD4?u9`iX%I#1pOKjSY=>qV z=JrfomZj>zX*~7D=fS;d4wG`@EFk$BxpTXy(rw!V@4CoUr%={1?I8KD;pY%ChLM)1 z%WmY|y{NY}xi=CG=U3d8Pv65#KhN2K7D)HtN4*v37Y1P~&21ezf zFVN+K1eo4+pzvl&-R9`y%0R$3R)#KD|a(FCNdPMn}Wl2A-F)>fFP!*EhtTDF6Z?0->Y*|@RbC^HW)h?Kodgl zOu|qmTr+yo^(O6tN(W(BmFE03gO;OM=e{%^6iI1J#konDG{Wi@BN9atFln5>c5qzN z`fgKh%o{8_>p*1*59nDu@`*1kMOhjBUA*30h1#1xaZ~um5gTosKc+Y2*k;G6*}HR2 zWO1V66w!2}ccCK_tQuM%SpQ7h=1o#8La%!iyT|IPZ|$`PETa^dE^z|dlyxougdM)6 z;cTjK>ymane4Y~@Ot84^QCvTaL|j>TF)gF?>wN>>2-hXXgb9cY64>O;me1^J2ORDJ z++Dd+yT2LEc^sOq?GIroVZj&C@U$AI$Z;CZtEmvGM@15gobTaO28a#aA*Z*H#@9g4LbuR8xb zkkD1aR-E}kpS$oxg#6dP+?j~9g-!O@#eJ~HeLOVJbyE0cNXmQD&}kMQ!)QbEuIe>8 zhmBrWj2l=5SAl(k7m?llP>tVry$mrKTXaW~pp7O?M_N7}x5eeB%Db0{ZOzdB66|NE#`E*#SVtEjnWt0$}@Z$~#`@i7L`rz+0S06;2e540}Y1 zPiYX`LB0`8lgt3#_*foZ-4EQlu$Yqa5tl_w5a_#1Bc(c>w0$^p+0Q&Tst7APXL!Pc z20J+?ZrJHx;4IUb3mj_sFRnzGYr)!p7EIkF3R9dCY+Yh!aJ1+%cYN3a&(t zp?}uD)G7MYyV1WLByOos$dA&}0Zb=pIb*%tr{EK~oMH(lzfNa?mQ0;^%20Yis7 zU`j4J#^Q@lPpe*U5x(8sFcgfj*qCG|qnYdlcqvNGs$nZS7g`uS(~Zv9$>ZBfvIBDu z2}SiRLw*)B#rMT-ZO+x(%0#Rmt;H5Ak>OMn2$;@8uyf*u1es!i4CMROlpY#i*UkvQ zvLBM&;#o5jW_Clml2{Cti$={2?g!ducs(*nVO@+lvbJV>WI34jCF2{HklF8K2NtV99MPX;SeD0#;m8bfUwGZ|x>16%?t#!0c zEMmYS;yEGpQ>o*s;u-vX_YMv9GpWd77S9)_=t@ri5ir~sPtla`0W^PwrC5VN;gxf~ zqc2013Y6-czT>Cyf#}uyusM?$55%q%%^%Vl@TH44@XhT=L`i8W02{A@8^MuuJ@oS; zvLR>KI~cM~3N-VcDzaUg^UU>v#sY=wdVqoWfMv59ZLGVp6ATN;X|wJ~VO!-km&y-A z^37F}%5KoaE5pR^j|i5X({qos!HC5(ju51?8On6sx4FDztgZ8y^Q5IOKG#Kj67ym4 z0~tdUp$qpfrF%d~@F@+p-yZ2|9mD4woz(+;f4opjhrew{`Z_x!ii6yIqsj?u``9W^ ztwdE7Kh?shmwY7p4~O5tx(MV*Vsn4cSx*l*&{C(WxEvQ-n-@{qUt~WQKuS!OCqg_n z>6E5ud3c6Ox?SDc(i8Q-l^MNA=STw>Jfry4`pVt}U}{bfTe1_g-U-2>L_S<`Rix0QF`i?zqGOI~c+I$c$xBWgC3JC@N zE+Xl>yn^fQHp$w<WLAqOhel$H1!gE@emevoxm5>13Oo$2JFAZZ_m+wQ>^C?kFE1aYh7|b z$wick@`WPvfTN)XFiSBTLhE2=X?o|?F=&Ly<&8T#8V$`ynYBAz5jpTl^Gk_78SOfM`O!-kGhtoy%xI{{WmZ? zwN_!z-Cy#EdM4K2lJRy99@((c0a1}yqp`8SwU<4)YQb=C0K@d|-BC2%x!qN+H)(fw zy19m)SN9hmrf285FMx1bGEc;g5TKgdbz#j*{y7I#KvlUS&|JRi8RHv{5xI9aT=fXG zA51MX@e=6m@!BS#tx|9f(IpPq4Vn*BdFsXiCjJ53VFa?6ag)P7%Ab_hyE*79R0vh5 z?3`SnbTJn==d8Qf-Z_0Dbn>Eztou08u(&AlzB$#SQGGpXDzM|^jibmZXGV1|uDy>C z&73=`pqf8uia0ok>$D2i#U`aR67NCcehC5`wW?J>c9(>eKwSq&O}1giV5tb4dxY(E z&ou3);C0)1xRGD?*c`29eXe=h0#bc)-4?icR)^KE$eDI-LDnhW>S6Up$60glYgNpc za3lLr_W_d$fRcyNXqwwQpUVXgDRb0v{Rd*q5o z)=azzC^tj|;Irwd80oPXZ5~*`{@KU#85rvU4z5loJR+crVw%+PG|8IvJURo+x3Xl# zjVY>%PPw2XK7yadkZ`-@G14o-9`$~N$h(;s5#fzch9(<5{hc;Xilz-aeH196JyVQZ z);jU1M&w^t`3PaI6c)Gb%hobfZ)P?aW0MU<2zB-D_|*h$Ms2ZHAQcnr7?Z~QWi%e) zAI=Rspw*EDc0m?z&yZnSKDMyQ&w{y}aeNedo=&`UkDqYWJ6?ILw?blk#{RDGjn-i* zn)_8Bu~y>O(-YWt@+f06HLmIzb|VUWjhvcf2HllCfKvNY)r)5kc=s3@eDXM>&fohg z93Mug;lfQau(kXTa zejw+obRD9pz*c5GiAh3;M_ts$vECVgB7VqK7tNjEGGHc*irJljh%&8suz`vH$*E5D zc0LaowNr1KWy^Rj6~=cY2O1^#CXf$E`v41PsopjCQlO~r3JT$r9GQPu5jFINmC%g2 z{{7YNVS`mma44dco}HUP!XH3BqqMo>q=!gD0TckM2V?!ELBqF%!zu~93#z@H0nP>4 zU3~tD-qbzr8m~EnC$68#&Y^Fk|*kalB%U7xZ)~!2_{@4-nLtJmjy>_7<3O@j8n03Ok_fE%)IsxqY105yha)+ zhiBFK59O%8+rPkhlBGj3I?2{TBxvRchS@T%q@-yH5SZ>!= zL{8;gk^{txK5o3`8M6&$qd?+?LcPoL&w6>p~wY3SH-tsk3eytbZ& zN~>I&BCkf*w5+{40cX?^^g751hyitGwmRl;a#A#+mSJ@?;RgMIo55|e(6UfZKl$`b zlA;t9f+}_5!dF%{g;WctHvz$rQ9FECd)<01`03kb}Z`&hB_}mV|j7y;Z00@CsFz#1;bYfcorq~^=v~rK~QV;{oa`}7uYjF zH7AdiDnuEBA|e523YZT~>ZAB)@Krx2+tWvE@Hthz$j*qI(aluByF32~WjvbK-xR>+ zV*W1$u!%T0S^uhP{q^|wg{^H>WUY6ZklJ6>Ef7N=62uZarJ;u0-~cw->D71XOHzvo zO9PG?FPG}}XyQe8NUXPaVqUitGwW50<1+K>dXsPB6B~EUt#HZN`g+cB$2@Gd zd^+Scd|+O}wJoIs)?%9{CUipA2e-T@vn_ls8%Es+_GZ_-2swo}-5H&5^#th;!R!9B zW(yoC9RgNI25Z_3n!gdvq<6b-FAA664ms=#&&{jS3UxG0Rn;8q1W0TmSY7XTRMWEKR7H<2X?LSachPaInm z9Ny4rHJ;+Jyt8$in`(mf24Btbdp^@MO=8h)s7<|@{RhY<5J4c9TVFWD;X$ zPb`;Ehg0ykPRWzItv0r%-{C*P_sT;cElrHm%`C-ZH8d~DC)9d&wrFqYcCJLf^XvBr zps%Ki*?ba%tf*MoL$s;iiEjgU%4HSDNJoh8x^l4_Y&|~NCh-djb;@SQI{C|UZR~1?NoiE}K)jbd;&I65jHqnKhP)Hn) z!eO-TVIq&kg1|y^RX^WfVxcR`&I2tDo@3t+;2-uDPrb@c?0)_F05!LNS<`)ylE3|R zKAa&FRoQ~)%EN1|d+MdbCs4cCQNG}1;Qm~uQ9tfrKUChptz*Sk`yoGaCqBy!GhyhC z`_AvX;Lmv}U+&P-$`+ny`3vt(mK6`~91?<+=@o&M=I!^Z^K5;*46nlke6`JK8&7^( z9cL?yQJvyWhvn%rjATGNO8( z&^<96pYkPg7`JnxY-LCrK9PWJY3eyaH)vl9t^)Lb$te$z_tiPJZqFB8j_?0!47Fps zaGE_?y>&j3E6FEp*WPuGJS)2U0DdR%JpDW5S^xinyvP4V{{JHX|3ID&+4KJIkZ1cF zO=OrhKRx_GsB2kYnbMKIwh4cjQ|B1#v~Stk$+sw1AURha~J#k>n zereEJRn$2sH`-6X9*I`3Dg9v7TJqi9AYT&K=~&9i!}Sl3eozSCHN3bdQeM}+ z`bcWXfFHAd{Tcr4jDSO&)=VFJNPA?|+B~#a!*Nkndo^!WrD3zF9bSq0kc58J(?Iuq zQmY4hb)8JMO=V-pwAO;45T{uyFoD3kCmsV4OWKbyG zfO%L^f=0ww7Jn_ZwBTg1Sm2Ri9N#Umq@X?~ko|T^0cQYq3wJV&`TPf5B`YVm(WVX``on}GcGURzVdyWcl zCR3PtuPe(Fi_V~+u?GX}9a5G&wsV9Ast_vuGU-FC)I(5ujS1g_EX+ZkyoP?RTu=M< z+tFwqBlsrA#qlbAnVjz>BbKgN3sn?oRq=E%C1R8}vQl=jg<+HA>tXP48K;*{MpY|;xQ(`ERFIBd(4$<-5YYdb&qCxL~& z*P1LxdbhH=Qw9Ph5IHz(I3PG|^{8oPqccqYp-(s+`+?9rTv1uubC~7~+&rPvzh=^5 zpl8A#GM22#grk}PUgZvRS<7O+ZeAoBCqIhfNyVC_&QB|qWEor)RofL{qqQQFLayID zhBY2E88Z(24I4;i-#rQ?0V_NQCRZzZlGZJ|07GNS0uL?>d`Yy6O}L*D;~b{JF>M^! z2$|QsfBJR|BNVs`7~Bu%s?TZ+0k!c6g0;F~2lkHxcs7PdkmQcz_hpnTZEMyPTn?8m zrEUVBgP=;c3izYp9p^(9ePHkgpfa{!hqP#T9W)w1N^n5%#RlDZKt!-U*f1Oo0GOi! zAPy15X8d4iNNoc#A5#E(2MG*ar<29lVmB~2#uE&%$WqymaAex*NRaST&yMRc97Xsd zc|^Uj&?e0x-BhKMg$UTGP6i5jtWq8*OzpQ7K_ z&j{v~wnNVte4wO9*cA{u!AbaBnyp)!J&oDl+_#|5hZhm{7CL5p!WaeN|A0y#8FU{b z*t%ZjEjb3Rl>r!rLhxd?ht{^7ctN#~%StMSVk$uf9a242^VM(M|Ca_K{ZJ+Fyk5%i7(w zXn0OMecOgMj%f%Lj$Ab1sE`7Tks>li5PYsI9+_T0US-;c=5~)qEsG!O?B&GG)$6<* zc-wShijx8>JH|h*_aT1x^Ssw;e%%Ewn90P+<1()INbsaVE+fqR)TxhSN8)#CN5!D2X(84bpF zWy+!pk$}XpDi|~7Q6W0!R*fk#;Y6pyRKP3EFGVuySLoe8Mt4R0WBD3qMN&Uko8VOn z>~?R%!RMzKTmW?SAylllkb|-O%AdrKn?28yx^1E;kKtSy`<7Y3m7_NvciLRxcylD_ z%Yt(uCt%vsrXd8O6I;y%Hv%K`Y{MmjxwXK{f%%YQ>`>VQ7AeJkAeyvF;Vw%cWKX=7 zG2h$o^3(U$q^Su{9&>kE@gWb(f^X_GDT@&!<+hX7b-Dvr#@u{!q(Z(j?)1Yvh4R<( z^wra&ms4WPQv)^-OP&0TZ{4T6g+Y#bLfuHun@7TG=l=6%$2n=tk5Dwk!}=)=z7-x` zpQoMH_m{@)@^Z|38yA?9c(dF7jb{glUK8{bN*-Q`Kg9=w^Qqc~$AJ{sk5zc4XHTtA*~+On^n zExdw?^xs>vW#?U#%KHaPwK6A9w6cQw=Z=p=ejfzPd#c7yOkk!hmVNA6afK=7X9so9 zjaI-|mBh`}a4WJu>t~fZvkOy~Y|d-wmPDqPNfULIrKOjLwkQ$KiQh~5V%RBFiBm@3 zHeVGvJh5WoUK+pUr=azPPfFH|A0}rZ+2gE!@bKWk%v+8M3U}Ua98n2qk4uKEPGY{@ z-beiYT`-k%bZ6U$ivV9BGc(p&O`q;0TAhv;+b>bFyJ8&mO8yv+4Ry?-PSP>Lh_q6i ziX+H5#;8}V=cPpKrxGO1rEx_D-M{C@?uiohN7? zHeZScuXqhGi4?&JJQ^t)E|p(+_$*KWu?uLIsMw}h)RPP+sJx<$Sm<2-44hY_5vv95 z9rqu^Z!4~>cQS9^s3%yh)AOj$7TAKtMTNc9%Z|p_fFj}q>w!V6K$9UNXwh(q_;Z)w zK9T#)OyZ_u-mn?uSy^0>thleKRj>u`iKH^NECUf=Cb=WeVLcem-7Pwqiymoxe~32M ze6ALwL?5iUUuj;hq<~U-bH&^Rk%beD@{EE_E)_QDjiA#p@IM-8S_ZpUFR?%_)ZD=F zBV_qv-Q49X`1#W&SF2%u6wweLsgbj-FcqjTN>DUHS8*yABG)@o}ro-&Zn>6jjBxbk$r8(7rxO&Uws9j3*VGg z|6BK~WK+}m`JYk9jh4>7&@+$NwKSR)H{Yt7OY2ImSo;sxJ6y}5?}&_3k@TGdd4EJT zT&bFYpIs4(oIe3^`85x2aXS~Y=Hc;r>m!P6E4t34O|d=&xHZL(-maugzVG*OByeD^ zl09*!v!Zn4`R}V-acTFl*rf-%+i!0Oeg)-oZ5;uuJ*HYdgO#f#as{&`eyzi%WEb=O z^B&ACc#po~7b{gBybC|wuRRuKzKVl;*figHRQyiNFxJgp!#j0xa`w2Hc64sK?f`s)Gnq(AW^% z)OEYOF_sQ2Z`Ihq>ZOpTeV2mE73^otgBk_A82bAhFCM7;{2ZnQa$eu|!Jh4|D&yg2 z+>LtLgC%T}@yf6Ei`KM`08-nS<%6(xnX;y=ZBr#c_=8btyYGbPt% z)-^{*KdCP!km_oN2OWfo7D9`7JHq&y+HuOA)2uUkXGG{wDVw?2hGcZrPGm_@EqyW!krx zsXgzqmlqQcuA@!t=~dbTF%BKNX!AFeqjUSq$NOhzRNjjdaA2=Y+?6>38@`j38;YAB zoa@}}o1TZIk|Rn++3kz@s-;C5?u8sWT!2dVDVaGFhwe5`or-66H6yVkh>9?om@*SC zgi#-L4%6Z}IRx8g+2F@xT~k}_17}^MclKff3&sU=W0U!vV)KL|st?$Xn3bQjfyqRD zrn+j7q{9w%M`5bqygL1a-LPu%mA@;e_t)mMpMzKjohXr_&Eib$(=~zYyNw#=mf+OQ zp**T*a5*2aSmb*9)tHZT#0;_67W^j^Hx9KnSiv%Q(!a)2uYgO_@2^cla)U5{B+T1) zx9M$P=YplJ#vZbiib;QjOCNN%8KlazFf4p-rbwPjk{Jcy{xLd=XQl zrI_tIpG`H)pOU^&mEm1+vwo@Jr-C(VT<}Rz$X~I+$=vG|b^M z<=2^*8B$>{5mNNrA^Wvi^5C&%Srt)J9ut@aOKbV}Je+Y5CRm!S#r5oOR z=@|e+vO&qpScaesg${rbK{E5=X8ju0LqwZEmchKD~fO-3oNI4!Q zoQSz`QT)`gA1wQ}WhC_^>m&g=-CTs{{%Hw9vIq|+O*?=3$#q&EiOKJgMf|H741#he z^3EWFWTP#Yf93~7*ym07cZI-}aCg&^HwM%bdCam;jBD zD9Q5mev75|4PpRPX`&g)j)d_j_B|oou(1>GQ}_->bM0x@=*YeyoP%=3@#Bs9OiELd z-(fKZ4YfgGR;^5R1on?`9}c^1aQZsKgJiZP_5`DEWaWL3;_;g1@1Jpr-xN%Kr0K>jkslJxja*VCTA zL)7($YTEXC%9)**!xR)iQH2du7)*{sVu|7yoG%il&|PbDbR!t0Z0)41_5=wGV!lV? zr2aAzX4DI7>4v2#JuD+S7PGgMEelXD-hY8~*b7e_{^LU;@Ggdo+`PuW170LYy0Tz$ z@h8l~NWV?dM+ri52Uc-4!8>;s#0=WQM;!ig3mM7+ zV?`wf%Jx-`*4kKcNK2MZ5dphTWFq&SWQjQlThEzPT$Q1Z*1iGhVj3q%e7;{L!U5gL z%#rwJPP5nslOns^sJR+tmzr|{O4q`I_~*9>wXQOXOUwIx?;5d)Qug7i7jz3{1KyPY zIKd*<-qBb%TKcFk@t61DPwaIm2_q*XV5y)FNesh9t-TbIqF8cGr`VG}vdvrEnpuCu zmMd#C$jP-dRuBGspSi6Z@6)MRLV)kj-rbidyweSeZa$J~BW(Z=%byNC3+KQa64xbo zE{;$s&DV3yf6dGkM@?#DlJK4s6ws3WY*p$IO~wmmkxC<>z=_@lS6=?JMpRjA zeW|FHfvtUtGM8^n*bmga6>Z|QfK4GLky4ndP&QnCNWid2{y4Z1CgXu67y;2%dcDJE z7dFC;?fEke4=SGH`DE?GKd8%k)z4qD_Lgh7Y3uEBzG>wxXW1FHG*F`KLjzWc3lo?eoB4khm| zr{(>JFbc_FSMod&(-nrSg(B$03li^A&Jn#8dWk&=ANQoQX}?#c8`o_U0ZDUpF-FOS z@4p0$f02$mqYLP!NqeUJLOm^`bqcJAjezj^Q5^)%gdshz&R!4%WuAv1J7W}lw4#+n_u zScm59S@c-rH#pW$*H;m<+?#qnV8dtznYher#%)GnCOT%6voJ{aU`g0d=XbGl$nZ6D zXJ~M7biovNcXVkatXqnXZhu;ej&Fa03W?8p{+glQfGmXIbLb(U>86TK08)B(I&N+mlk_#Rv)=mbK#5U%bD(oHGiL zEjVd=NL#D>z&odDGcDps5!_K6-Dfetc6i+FT}h@JEOB(Ny&a>*%+!hLxY-++b!PoS zJL8+tM9{-)qS}$?iH?vYeadH}K z!IRERW8O`)vOcX@CIv))df3}PI1N3o2vg73Gi#dgdyZ_&yrx8e9ztt8jIv;NJC*T3 zod|l@TTbkD^!kc*qaohtlpkk3E2z=m>-FIrQE<24;>nDU;MhfgN$q1Ta(E;+#p5pd zn1n?^sIlY+S6+R|U1hMuqDWyMj)Lo+sA&PrwWq(|w%jxho8!*)vDstXaejk{F~Prdj_saNGe>Q=?Yq^%X zhv3sO{tpM$eL>D~D64jHMzxeXrwolv+XYPT6Tx?7zVD^*jNQFCB;et7%XhoO(;7>y zrLqEYMv@GY5~a5#8nxoLo87C2Cl*|&fU=_cA4aEf758AWdHdjIi3=Z4Ll#cl z=`k2N^G(^4y9L*GF}3WOnN-lgmY_$AdfOH5H)J zgt{?Ll%PBJ2%2DoFG73(WVdX&1AnGZg~@|n$N0BxIq89m$;q6y;w}aZ-1+PS4!TdK z#PbUD4Rt;{j$&!=PD!=V%8C~P5kz&n+&&AAfm(L8#rlMXkG9Lg%SVFCj`!zlcg9oB zc!9~2l>4*%>`u>W9{d~|L6Ku@*RFz)%t6kUwHTO9SmB}bV)Wb+j-8#nl~B4fks-vn zyccAaYt+IW?82Q;uTnkEYkw!^d`;uXW-M-xGC6s1sSkgP9oWbz3alFMx^P2z8VIsV zB2PVk!PVy@)G7R3H0+us621J;m!-~Gmh0W^czFc<3ZqX?UpMfdn}R@~AWS}ZU|V8t zZ44_&xp%%GE{pI5qp{M}%S{e0&{rBgtEkA?W&ZUcPonK0%zmo&p+vas&{V29F$2a9 zPZn-Iu0qWE#Co@o!h*fs`;HT-Mqe z?j+}nX2jN;2TYcbK(!A}un}WI6sf~|Jl|HGdu*t%wrf00nK0UzdSA8gt6W+Aot}@O z+*EqU(`qDRSh!I64s>oQ-J;5(!*CguA9nxPd*;<{Kbih%kP1p*sOS#d$QRuLxS~s{ zspU&^dy$n#p{vf&BxvibwsjZ@pXI9!|G8FV*sO}az5q8p-2h*oe@{!UzHlLT#DKtYi`on2t{7eX#x2v2nR`r;WDefuf?6liZ|z@D4nFz2Z@SfGzaG{Bm;bpld!*g&zsB&{ zx!C_DhX2nPJqI(#|0(TB>i?MbRPN|(QRcvV*5jta=SYA={QZ3<1~TWq9&B562Q@jj zR=`Gp?RP8YiI&uHWKsF;1Jt1i6OTKiSyGPf?#YLZkc}agg4UcYdp4<;y10NaS(SeH zG=aL^B7`N4*`%K%)~mXcBl;M@>&ipyYV@eQ*ZsD8)v18wt>V!keHym-rC4ovu0f3P-EvYIE#$|O?d0)9Z z+$d`9l|ym;^OL#Si~v`v*8|w6*8>`_2JsKQ@qo%<9P_@}p1iW`1t9L4Xkv4hr?1~T zlT^=dN@Ir;Ce-!UfKRlD?6u@#Qm|?1Xr_?;PR4R2cz#fpo zwkL~Gxc!}RTl708dUvOhKKb#ovRpd3H5=1wu5bN85={W=z?D^#9E5(@DV7D?q-h{Q z{w=jJ0#ux>E%0m z0jw-0xO$=E!})}}Z8)9Rkva4G6cXRhZo;v8+tI%Rm-Fwy<^2B&+`uo<6jbYR;f-bJpELghkuSpE8xVs^g|xQ)iz~;p>|AzP%PBGB89{aue9_@M~zVdUgT5gha{fB63$n$}abAHjZ+2pWh-)pB5CxIA| zAmML0&@TUK6n4dOohGBOO}GFrySM@CN?&x>SNr?+0_gUOTx_Je;h6?YYj-DNy>04$ zkW12{4k;i?R<#*6O`vw(06U{H=EF12)WUi57rDB&y0aI3`{v_b*Dp(t-q8aga0kwc ztG@+$C$9~itBoG|hcoN+HoR!r`lGz7T2>I=i)-y>FKiN|0GO7V6QHXLpS;psyRJNv zsEo;tO=)!LkfmakYD9&D*-S)66mU=_<3W+y@~Jbs5zz6$Y+_!C$lHzy6dtL2RAk9{ z9mNQCV{?l4*1K};($3*YWG5E%#s=fO2tUHEL7u{{QMilZEv|bZ2OydE>XNxpr=t{rLrIun$aIBPV4rti}!BKj~Lwl=4p-^&V|{RS$l}-PbEc)LjH~D|b{gBds$^Ai&Bqq?o-+e4l z`qk!X|04H)%KfD(_X&70oBSJNZE5SK59g+s#jdYrBF>ck<@hVo`pZnOdT!k|KO5)w zZPuSmY;8}E&#$QTUK&=G71vBweNNM_7or<0xpn^`_pci7en=^ZubrJwPQ`Zis(ZI( zj_~8jhdY#fS>12DgevNxD1e7?kDD5OXjDB~{vvdho$&MUV%K=#bxCTz8b{ZKZ02a< zRQ*@v+tOZhvFkg1p6VthG)i7~Dk0!+i^kT?hAt_|Aqe{!C_WTFECV^L6 zoJD23;7Xv)u}u6 zhL@>$>Br0LP-)3emgxdEAzU=F-kqAbK9MlPkI>T591};Q4t=n(TTdCKe93i)bN23k|ps~x1y`D1s`OSRec$vL>GcCg{#T_9WJNe~&U5~x}iH}fb z0tXcBGT)ni>8=t-`MGrennY2X6up9E5;6<>J4yln)xI*burmDv(|<4C+p3M)?Q$Wt zz0&>(2TBv6K(UP6>4Flx2HvY?4VJ84vNmGX1g)`o0=D_^Q`L2$(A-PNRXq_`b&o#i zClt7>T(f#jt-4!mc}YfGo%KtrddopdePaP^#*L=T(&V)ByW`y4dcB+oeSFb>{-jr;ZN=j)S} z$?vN#YjBTTL3UKtCQ#A1+gSFjwRjKK=eXa9JH$0s37dTKB6DHMSNRYse-GtWC#G6{ zf1{DT_vyrX58CZs6>hw-7AL>DY^13BnXX3=_p1Ko4Z6J>caFT?Kk4ZD6>_w<@SM9b zVzZTbxj8HUzM1{uIP>{#m{fi$&))I8)_LJnwAV~rweOaU*cxuSArLd^*-&ztVfB4x zoG!lnvwij_alHJc@2T0#FO|hjnE;Sna3=NuK)_D|WI@O-sHtdi#IBCfZD250?N zT&Z3fWCD06w!~g|`mdqw>40IofU#0V0-%V1tDymCi8K`F_&`jmJ#){%f{YuNya_7s zUc>IMYMpv zLU4XSQw1plQ^N)eXc8=j=_BbeE+ql#6Maxg$3rFnP$3#2Z-0M3Qs5Zohhbj#02=yE z2dp4(p!dv4nq9Xzd8FeGf}v?V_XNeRi-hI%wQcv3tZq0MgGna?bY1qSz0bJRVcRRmPyqPyj|850 z?iN7igS(BQIr!c)QgrT%0l~YRtD}IyHwb=_XSMInMB9D}0)dfsNo^ypBRTrO7`&o;@{;3c0(G{WzyQ0=cyFRxD4avs9tjOd zgj5|u6I6iuAo)G&N(D|OAEeA3Qz2vI_|RxHMo3jU$nq_l7aatD(QG4W=fm2y(minz z)LTv?)=r2$T$J3Q=3aj?Rlk=O`7^4-dLR)(SzAnd&hGsc>hJVR$ zgLUP~b`rb4HH?*%r_rqa#N1dz)hTN?`0j$&I1G0gV%diNBc{=YU4ZdCHArIY(w2vrf2LWPVN_HkX{ZCOD>4;Fnk9SjVg3pjIBNg5IOkPt+5L5 zoQo|&z}M7zfbdcCljrFX-P>%_hDRE}`@Gh-jkWrSwc+C!zdh`J!Q2KWOjsGt2Zh&r zg#woD`}Cp&=8 zK)z0(qdQ+RBk-;?_zC{aKJA`Rdr_F-@9_JZR2VL{|9)bCt?H=VDmU8NDeaPIfXj9| zB42b@0C|)khHVr@)6v5BR2|?NlOYy|+s72=IJ$aK434<4LuS0Vk_>qP0?t=Aj|bda z(WsmcUY&sf`!f4*Wfy1T@UP^G_q+Aig~rACTlH&WymWDa$kmgLjmPyn0NLi__-fJp zVnuwC)aS~*HOxW8D&?m&>%03ILMO*$%7yL8OYFBBWzP1{Lc~Q;p#GfP+NIa@J!c>Z#1qg< zlt3_j-Vjbee-~lN8P zri~awIaF++)wm=UizRjyrID6#Jvx*)sgN5cQl~6DIf*&g?8TDPtfM^YgCAUqjRAx) zu^6i7*2@4W5CKv6X9Y`C7&Qmk2J%Z`pT1REgzeqE^)t2ffxQa8Et+~Y7B@WP< ztQxXFDaY-I5iNFz>qX9%%p+pk7SySiZE7S%8Cbn^2W^ZO%+KbL+(lPXGY_|UWxE|i zQ_&Wwxzd(*x9n1t#p=hXK?$TQ`#X$92&O}n{YCk9gW5r}P{x_BOeUYFNT>mXuQFwu zMy*bA5yAPal25{>WBfBkH1fKwEm zO-%fOv8$xZ+k|0dHtQs z-Kn)#>JIwLveu3ae(=NA4o^Vi$Ig9?@^((EpQBLL(?j>CmaM;A)vx*8X798XKk}{D zDOtwu!-7G_t{ry`8DfEVkCU(uX*vN|Vi1K#TL9ivFSXVr@6LVbvhrf%t_+1ASiMA9 z(|Nk(hv?=waYW$O>*DMlqWk+Sk}b#ei@1kJ`hdF86SbBNyR*-0oTi79KRZ! z;1b|7sA?cIbU`z6XZQdZEC@QF5%E3~aKu==bZ4$B36pB&ISi7eX^K$s5ds(hTnJO5 zh~QW*K%~AN2oV&9!juRUh))KgYXOLkOLG(?FREW8x>kw`IRH+T1%s*@0Z5nyVVWv~ z*iuX~iwN;9d6b+CvPOrI7!a|4jG{!;wq=JNku0G#A>*h(4#s%eXxDqJAEyc92zWer z^ve|gMjfdntTY1>1ug0($}#)hgfirE=s-<6Gmr@n))35{&QT)>L0<*kCjSSDc#ZM8 zg;c5t2;s06*4}sQQH7p~Wk`71?pfgRWD{J^A4a$}Q2;VFC2T~*=R=#MPUDO>+Bk4d z)w-_?w6_uTwi6_EV+ zJ51DmuiKQ+tsAsW$w5&<2&;dj9?F7vFQut}D7L(3EBcF+CpGI8r;i&(W~?bLFZ^Qt zko^JgkYy9YI!f57n#tl`PNKl+&syZ#ULDPB<`x{A z0qz9)&V$f;_Kfofym8yU(w9zN@jLJ< z_aEo3{&;Z_`H1iGO>eZ_Rrak%j1s@89{$dV{7oJdJLmt7gZUq+e_@x;=o_FYe1%I0 z&Jkt*Uk)bW%+2K@Ytocef8dqb+r*eLtJ%$|@1dBeE7+p<&mGW} zGJ7Y7q|NY^G?^Ki%PNBw^#G74$9iAnOWEYhT+~^U3_ICnKF!;Oy_AlC>H8(L-e$H< zj5E?=?PU5vt*tx{jW{92?)@^Js)E{tP8J9U zaB@`pS|J(ZE5OQcjR(#H*KAst3p6QDS0w)DAzGThLE$9IZGkY~e}GEkLQU0d2?oST zXybdLRpYeA-5*+=rTF8g2g-QSP8Lg^6q*KTIpdv5$GgUOO>m2xvle~BuP)}MUHE}( zC|gu&CgT+v!D1-;VaNzOhpn7sF2db#h(WWXLNI{tBz2yrZ@Iz2Lbc(@D=wSKO^oPx zg64@vX!4C66E9wY5s<>$$EbYst&nw3otgP*r~)O*erWOy+LPtMVs1>{m;L6nc|a%D zxBic9=G0^=sk?5(AqpUGy4g-2D8-latmfz(`*#ON?i*;#IPd3wMUsR0UqeC)PWC3Q z#->h0R1PL)DyACr%nWQy3@n_~FpNU3E*ADqRMdqh^#g^>nv)GI6H*+Qxr0w6wSTX6Rx{MD>lEg^7iO z3Bb(B1YrA`(=)MAGBHtpwUf0s`FBlJoD3ZtOihRw#SCqnO<@=nR75ox#9VD`j12AU z{yG5_3rlCBujfBbgpNqf)XDj4*F>xg%xui;02U5*R(ci&j{iKvuMS_wE@SCtO7wM9 z?9@co4u&ok&ZdS$ZvX7a!NA7AObx@&|JRpjZ)63-&c*RBKNtVssWR-$|CuSn#sc`C znKI0rT>re<|9!BnUt`s7jRVPdvuewDjikl{9U2Tcu}LngulXqv3*m-nHHzNFy*~Mm z<=bNxF*&`c6Q`>l3|6iyS0=!j3(u+ud^n;w6G8*w*ZrF%Od@sCS&FP_CI}VS z4d<=9b1pSg(Br2hU^kH|LWnCA*}&Y1J1L&6lB^Ly#42V698;TMzD>+e&;*eao zK!C+?U_J$J5+0IB7bqo7@E>rj@)(rLrT~AN!~{MQIu?ye79p?#z#C~`K@YWP$T-qh zQqrD`si6Mh5fX@bLRyfer8+RZ0$L1p@LXU}eizXok_u8Ou-X+7e7`CdW3MWfe0f@a z6Va%-e9pwf-rg&1Jfg zhn;Ih^|A8PG+keXhDF)>lM{#?a|JNU0Cm~b_BL9aDaVG-1}p+BUf)W)mR5`KQlTL}!&v(Sgs9id%e~KS9^Rgw&y-i1D*d8C3OXJH#QUcO#{75NKKy?Cyq_*c zoOF4JYW5r#&di0KI$w_yI@bi5c^Za)Rb&jU<$Ikg)-v=|p4vXL7 z7Hb(BwP^;5Tn3ev2^-w^=H>eP^& z7wWlgO&LyR{?XWENozS~IY!bBUJA7R1tD!vAJep32-+{&otTs2>|W^`KA5gB`X=g5 z)lsXpv=~vQGdS17L|&RtJxp9})F5oGV$TR8t}@aG{;WvXJ|EUdRpEl_5}Z+C6@#D1 z_H?)mA}YjX!fcRpx(EE%&>4~U2s;{xG%yB8@b|*)mZ|U}mg}@(;ZAI(z!Blg?cPP2 zr|pr~ZZk_@9unV(vC7g(v7*Y5;gmTvWPAht%k387@$QW3OHqF}qCj#-k|V!QeKW)z znMH=5NZZRq=RF(-ln2Vm84h1{D36Wy=R3SJoQOgHEb#r2n9 zvX~ZcP=%e!4b}$L%L@jC8p$t)XG0=~G5BKKdO z#$Q0ZVUR>&!s&9gF<0KQ{-?oE+Xs(P47JxofTC?Fj>HHr$;FOjz>JYCHiC0M-{Qk* z(X>iLTd&~WzH2bpUq zW8$O3#3&B84G$f){ftRZm+xy_pICBRD|H@I2ZzceLpMl0RdLRkRnf}$z9BVx_WW~I zW5p}oj*I{o87YYGhmebcq}+aAg0W&n)d183zMw6!mA0(`e$xG+Q0BIRyu<6m&#oF8 zSCH^760`Dz-)Hac{qt+qnGIi+CD_R@tD8_HchfO3D(hvy|If!@fuUnW)UQvi& zKN3nh z7^z0wvR85}3tx)!UXqGS){6Sw+#yY27s@`*IiHViYmcHleR-VqV>k%&!s3NC?3uOc ztZr{_8+a@2N8WE6)_ee7oir(vwo%_=J8?e2T_AVn{>DvV`Pb0wKkl^}N~UHojFNUH zrXEDvOhgh zdH?&HJo$p91BNQbz%_qXBdFw1MuH#_F)T4KiUbjKLUk2MG}H)%G#tt|Y#~!KK`H9}DlVn?!R?dVSk73ExS>}ObwI_MaTqNvlq#1QTnd@zf6Rwp-9G8} z_Fc}o-BA>6I2T8@`aGBn=f_c$L>BGY6Va-Tqj-8RS_UyR6Dy&W0Vxl_AgHq_%JaqUPX{shV%N~AFIYtDGGYT@ATgFRu=c@+F&G-45hlWTsJAG6 z+`DgrAutq%K?_}F(tSIv6eQqVI|4?+C>RZ6U@VM-NpK!a22Yq{ zQ9due*Y8^di=ow`)&(BB6xv{kMQww0^OAOPSuq?FOHYVp%f#i+isj#nE1nQno)v$1 zQ>^SFR&n#ySBq=bi)-Hyt2c>14HkbM4KIr|Pm8r{#dV#;x((v`ZDRf7V#73X!$Gle ztJt($+%!vUE`hJa&9{gxpNXv>iw>T)tpIrVtyhZeX|ZF3*vWHuekpc6CU##ZZofy| zabsjQ>q*uiP@%z7%FCs@_U$bD7Q9lZkKnbHY0-hh{tN0iP_V%Z)~m5AiR4@1#Y71O z4vIkGg#JMCheC^<+3&rS0wolvnLy11Y9>%Kftm@_OsIicpaQ}`sDpZ-jsg`BsDMC9 z1yU-IQh}5TGcEc#(X6c9r$vSZDkV@AfvN~pMW8AInH~CBbYzDQvKd-nI8YUVst6QB z7(=;aF>~)~A+3o}4?Z+fIgrYMR1TzaAe93J5#|FK8_2{k-lDIM`EyC&K*k0#HnhS7 zpt=K<7pS~I<%RRj(i8n>lwX(%)6DX|&?a{+6b1K+!f!IHl zRjhQXD7#yf&kz;AiOL-KMf7<^RLv39uZzAPi2m1#nma{pqZqJD)U6ZsN5r6aM8hi4 zm?4_(hcjZxcVgH+(fq1tp}*m)#fSzma=I9`(vQ9dXawKnCBT6aJlFg@SZ20jir?%Q z#E@b3)CZhHk-zC`&AxosZ}GHkGaLPbU#1`dKMp&A?`k&WK)Iz(@AaDpLIw1Io=^<_ z!srG5n&<<$X06-(XJw>{_nd}2$OkW+0w^?FlkYcogYHmdw)R7<#=11HR@Yx4)_*H* z7$-Jv6q|;L8+DA>=Go$Ac5v%bV6i*c?v7u@wx`6cFN)j7i0yUogV@m_cK#%G>kGhc zuK}H5wx>$ml?%tkUdD3w8nKVqz7=A>PE5NmOWZ$H9B6^h#RCtE2mdG@+94i(QanPM zJv!TG*$h=?2S4%40Z<1+U?>cOYUm67pg+`@Jx6xFz-0bH62AC9@e&FAS0?kfvEt=B z#4C%%-yal*t`e{55VY41i8plk+FPu_VI7wCHj{ow=c$G9a8$g<(!IxGAE5${mcdKn z!=At+KVn_}@rXFaGd^ZhK4BL=Jta;sv=fKLXFT$sE5*Oqp3nD-e@_x8Gkp4!pvCMz z)qXhvhQkOL38P>%j4}KEwBI)t#=&^A9~r@agE%u+{6t~@d{X>E<^6hEoNW@n>E&hT z?ut|@`bBJ-U>s`-acZ7O+Chg$oT=@OJKZNb*Nd!&M7DA$?xGh}oU6i)(>fmFJiT_} z{3g)*GA>X$#)Uc$;%@JYqO0IF(L=AmxL64sm+TR}_KMPc&^E?p+K;$gwI5epE_!$P z1gF4Mm zw=S{xg#+GNVDa&Z-dboGxkJ6R$TCU~d26v{Jb%zzt(IEx`!-%`sgtS5QeP-HQeS;7 zPH9P!%mt!S(@(Oxik!DaTBVT`Dn*l`JW;IrNXowRPh$Vx&4E(z!^6L0E5N@%{gbl~ zR6#ZLg?`}QlQrO<@dLoW9qXVT2EkxxfJSJtq_;jWlG=gM{{bzM*^LTiZe(+Ga%Ev{ i3T19&Z(?c+b97;Hba--QW(qSiI5IIg3MC~)Peuy)N0`U} literal 0 HcmV?d00001 diff --git a/docs/design/model-pool/learn.tex b/docs/design/model-pool/learn.tex new file mode 100644 index 0000000000..2499b96503 --- /dev/null +++ b/docs/design/model-pool/learn.tex @@ -0,0 +1,512 @@ +\documentclass[10pt,a4paper]{article} +\usepackage[margin=2.54cm]{geometry} +\usepackage{amsmath,amssymb,amsthm,mathtools} +\usepackage{newtxtext,newtxmath} +\usepackage{booktabs} +\usepackage{graphicx} +\usepackage{microtype} +\usepackage{authblk} +\usepackage{algorithm} +\usepackage{algpseudocode} +\usepackage{tikz} +\usetikzlibrary{arrows.meta,positioning,shapes.geometric,calc,fit} +\usepackage{fancyhdr} +\usepackage[hidelinks]{hyperref} +\usepackage{caption} +\captionsetup{font=small,labelfont=bf,width=.92\textwidth} +\usepackage{abstract} +\renewcommand{\abstractnamefont}{\normalfont\bfseries} +\setlength{\absleftindent}{1.2cm}\setlength{\absrightindent}{1.2cm} +\setlength{\parskip}{2pt} + +\pagestyle{fancy} +\fancyhf{} +\fancyhead[L]{\small\textsc{Preprint}} +\fancyhead[R]{\small Crews from Evidence} +\fancyfoot[C]{\small\thepage} +\renewcommand{\headrulewidth}{0.2pt} + +\newcommand{\E}{\mathbb{E}} +\newcommand{\R}{\mathbb{R}} +\newcommand{\N}{\mathbb{N}} +\newcommand{\roles}{\mathcal{R}} +\newcommand{\mods}{\mathcal{M}} +\newcommand{\crews}{\mathcal{K}} +\newcommand{\front}{\mathcal{F}} +\newcommand{\seat}[1]{\mathrm{#1}} +\newcommand{\code}[1]{\texttt{#1}} +\newcommand{\pclean}{P_{\mathrm{c}}} +\DeclareMathOperator*{\argmin}{arg\,min} +\DeclareMathOperator*{\argmax}{arg\,max} +\newtheorem{theorem}{Theorem} +\newtheorem{proposition}{Proposition} +\newtheorem{lemma}{Lemma} +\newtheorem{corollary}{Corollary} +\theoremstyle{definition} +\newtheorem{definition}{Definition} +\newtheorem{assumption}{Assumption} +\theoremstyle{remark} +\newtheorem{remark}{Remark} + +\title{\textbf{Crews from Evidence}\\[6pt] +\large Privacy-preserving, decentralised measurement of language-model teams,\\ and a cost-aware learner that picks them} +\author{Santosh Kumar Radha} +\affil{AgentField AI} +\date{September 2026} + +\begin{document} +\maketitle +\thispagestyle{fancy} + +\begin{abstract} +\noindent +An agent harness runs a task through a \emph{crew}: an assignment of one language model to each of $n$ roles (in our instance a worker that does the work, a checker that reads it and a planner that cuts it). The crew that pays best is neither the cheapest nor the strongest on paper; it is the one that minimises the expected cost of one clean delivery once failed runs, wall time and shipped defects are priced. We give that objective in closed form, show how a crew is computed from a published catalog of prices and benchmark indexes alone (a pareto front in bill and quality, with a scale-free knee), and then how measurements from real runs replace the catalog's proxy. Runs are scored role by role by a model outside the crew. Each score becomes a row that carries no text, no file name and no identity beyond a per-install nonce, so nothing an install sends can be read back into what it was working on; rows reduce to sufficient statistics that form a join-semilattice, so any number of installs and a relay can merge them in any order and reach the same state, the relay is a convenience rather than an authority, and the published index is signed so that every reader can check what it is blending in. Participation is a setting: an install can contribute, read only, or stay out, and still learns from its own runs. Judge severity is removed by a two-way fixed-effects fit on those statistics, drift by a local-level model whose forgetting rate is estimated rather than set, and heterogeneity between installs by a hierarchical prior whose variance is estimated by moments; every constant except the user's price of a defect is an empirical-Bayes estimate. The crew is chosen by Thompson sampling with semi-bandit feedback: because the objective is linear in role qualities and every role is scored separately, a run on one crew informs every crew that shares a model with it, and the sample complexity scales with the number of models rather than the number of crews. On our own 120 scored runs the learner beats a fixed table by a third of the remaining regret within a few hundred runs, and role-level feedback cuts the regret of a crew-level learner by a further factor of three at four hundred runs. +\end{abstract} + +\vspace{4pt} +\noindent\textbf{Keywords:} model selection, pareto front, empirical Bayes, Thompson sampling, combinatorial semi-bandits, CRDT, judge calibration, privacy, decentralised measurement. + +\section{Introduction} + +A harness that drives language models through software tasks has to decide, before every task, which model sits in which role. The decision is made under three kinds of information that arrive at different times and in different units: published prices and benchmark indexes, available for every model on the day it appears; the harness's own past runs, few but on the right distribution; and the runs of everyone else using the same harness, many but on other people's tasks and scored by other people's judges. This note is about turning the three into one decision. + +Our contributions are the following. +\begin{enumerate} +\item A closed-form objective for a crew, the expected cost of one clean delivery under reruns and a priced defect (Section~\ref{sec:objective}), and the observation that on a catalog alone the objective cannot be evaluated, which is why the catalog picker is a pareto front with a scale-free knee (Section~\ref{sec:catalog}). +\item A decentralised measurement of role quality (Section~\ref{sec:pool}): text-free rows, sufficient statistics that form a state-based CRDT (Proposition~\ref{prop:crdt}), judge fixed effects identified from the connectivity of the judge-cell graph (Proposition~\ref{prop:ident}), robust aggregation across installs, and drift handled by a local-level model. +\item An estimator with no tuned constant except the user's price of a defect (Section~\ref{sec:eb}): the catalog's prior weight, the between-install variance, the noise scale and the forgetting rate are all estimated from the pool by moments. +\item A picker (Section~\ref{sec:decide}) that is Thompson sampling in a combinatorial semi-bandit: the crew objective is linear in role qualities, roles are scored separately, so one run updates every crew sharing a model with it. +\item An empirical check on our own measured runs (Section~\ref{sec:exp}), reproducible from the script beside this document. +\end{enumerate} + +\section{Setting} +\label{sec:setting} + +\begin{definition}[Roles, crews] +Let $\roles$ be a finite set of $n$ \emph{roles} and $\mods$ a finite set of models. A \emph{crew} is a map $\kappa:\roles\to\mods$. Each role $r$ carries a \emph{shape}: a token share $v_r\ge 0$ with $\sum_r v_r=1$, index weights $w_r\in\Delta^{2}$, a cache share $\gamma_r\in[0,1]$, an input-to-output ratio $\rho_r>0$, and a predicate $\mathrm{fit}_r(m)$ saying whether model $m$ can hold the role (tools, images, context). A symmetric relation $D\subseteq\roles\times\roles$ names the pairs of roles that may not be held by models of the same vendor. +\end{definition} + +Our instance has $\roles=\{\seat{W},\seat{H},\seat{M}\}$ (worker, high, mastermind), token shares $(0.90,0.08,0.02)$, weights $(0.2,0.3,0.5)$, $(\tfrac13,\tfrac13,\tfrac13)$, $(0.6,0,0.4)$ on the indexes (intelligence, coding, agentic), cache shares $(0.75,0.75,0)$, ratios $(100,100,10)$ and $D=\{(\seat W,\seat H)\}$: the two roles that read the same work never share one shop. On the chat door the planner role is absent and $n=2$. Nothing below depends on $n=3$. + +A \emph{task} is one unit of work; a \emph{run} is one attempt at it by a crew. A run ends \emph{delivered} or not, and a delivered task is later found \emph{clean} (no major defect) or not. Money is in dollars and time is priced at $\lambda$ dollars per minute. + +The \emph{catalog} lists, per model, a prompt price $\pi_m$, a cache-read price $\pi^c_m$, a completion price $\kappa_m$ per million tokens, and benchmark indexes $I_m\in\R^3$. The \emph{pool} is the set of measurements described in Section~\ref{sec:pool}; the \emph{own sheet} is one install's part of it. + +\section{The crew computed from the catalog} +\label{sec:catalog} + +\paragraph{Role cost and role quality.} +Per million task tokens on role $r$, model $m$ costs +\begin{equation} +c_r(m) = (1-\gamma_r)\,\pi_m + \gamma_r\,\pi^{c}_m + \kappa_m/\rho_r , +\label{eq:seatcost} +\end{equation} +and its catalog quality is the weighted, pool-normalised index +\begin{equation} +\hat q_r(m) = 100 \sum_{i=1}^{3} w_{r,i}\,\frac{I_{m,i}}{\max_{m'\in\mods} I_{m',i}} . +\label{eq:catq} +\end{equation} +A missing index is imputed from the median ratio, over models publishing both, of the missing index to a present one; an imputed quality is flagged and loses ties. + +\paragraph{Shortlists.} +Role $r$'s shortlist is +\begin{equation} +S_r=\{m:\ \mathrm{fit}_r(m)\ \wedge\ \hat q_r(m)\ge \phi\max_{m'} \hat q_r(m')\},\qquad \phi=0.8, +\label{eq:floor} +\end{equation} +the floor being the statement that a model which cannot do the job is not cheap at any price. The crew set is $\crews=\{\kappa\in\prod_r S_r:\ \mathrm{vendor}(\kappa(r))\ne\mathrm{vendor}(\kappa(r'))\ \forall (r,r')\in D\}$. + +\paragraph{Bill, quality, front.} +\begin{equation} +B(\kappa)=\sum_{r} v_r\,c_r(\kappa(r)),\qquad Q(\kappa)=\sum_{r}\nu_r\, q_r(\kappa(r)),\qquad \nu_r\ge0,\ \textstyle\sum_r\nu_r=1, +\label{eq:crew} +\end{equation} +with $\nu$ uniform by default. The \emph{front} $\front\subseteq\crews$ is the set of crews not dominated in $(B,Q)$. Sorting $\crews$ by $(B\uparrow, Q\downarrow, \text{imputed count}\uparrow, \text{id}\uparrow)$ and keeping a crew when its $Q$ exceeds every kept crew's yields $\front$ in $O(|\crews|\log|\crews|)$, and the tie order makes $\front$ a function of the set $\crews$ rather than of any input order. In practice $|S_r|$ is a few dozen and $|\crews|$ a few thousand. + +\paragraph{Why a front and not a score.} +The objective of Section~\ref{sec:objective} needs a probability of clean delivery. The catalog has none, so any scalarisation of $(B,Q)$ would fix an exchange rate between a price and a benchmark index that nobody has measured. Dominance is the only order that needs no such rate; every crew a user could sensibly want is on $\front$, which is small, and the remaining choice is one-dimensional along it. + +\paragraph{Knee and knob.} +Index $\front$ by bill, $B_0\le\dots\le B_1$. The three named picks are \emph{frugal} $=\argmin_\front B$, \emph{max} $=\argmax_\front Q$, and \emph{balanced}, the crew farthest above the chord of the front in $(\ln B,Q)$: +\begin{equation} +\text{balanced}=\argmax_{\kappa\in\front}\ \Big[(Q(\kappa)-Q_0) - (Q_1-Q_0)\frac{\ln B(\kappa)-\ln B_0}{\ln B_1-\ln B_0}\Big]. +\label{eq:knee} +\end{equation} +\begin{proposition}[Scale invariance] +The balanced crew is invariant under $B\mapsto aB$ for any $a>0$ and under $Q\mapsto Q+b$. +\end{proposition} +\begin{proof} +$\ln(aB)-\ln(aB_0)=\ln B-\ln B_0$ and $\ln(aB_1)-\ln(aB_0)=\ln B_1-\ln B_0$; the bracket is unchanged. A shift of $Q$ cancels in both differences. +\end{proof} +The log scale is not cosmetic: catalog prices span two orders of magnitude, and the knee should not move when a currency changes or every vendor cuts prices by the same factor. A knob $k\in[0,1]$ picks the best crew with $B\le B_0(B_1/B_0)^k$, so equal steps in $k$ are equal ratios in the bill. + +\section{The objective: the cost of one clean delivery} +\label{sec:objective} + +Let a run of crew $\kappa$ cost $c$ dollars and $t$ minutes, deliver with probability $r$, and let a delivered task be clean with probability $p$; a run that fails to deliver costs $c_f$ and $t_f$. + +\begin{lemma}[Rerun until clean] +If every defect is caught and the task rerun, the expected cost to one clean delivery is +\begin{equation} +E_0(\kappa)=\frac{(1-r)(c_f+\lambda t_f)+r\,(c+\lambda t)}{r\,p}. +\label{eq:E0} +\end{equation} +\end{lemma} +\begin{proof} +Attempts are i.i.d.\ with success probability $rp$; the count to the first success is geometric with mean $1/(rp)$, and by Wald's identity the expected total cost is the mean count times the expected cost of one attempt, which is the numerator. +\end{proof} + +\begin{lemma}[Priced defect] +If a shipped defect costs $H$ to find and mend and only non-deliveries are rerun, +\begin{equation} +E_H(\kappa)=\frac{(1-r)(c_f+\lambda t_f)}{r} + (c+\lambda t) + (1-p)\,H . +\label{eq:EH} +\end{equation} +\end{lemma} +\begin{proof} +The count of failed attempts before the first delivery is geometric on $\{0,1,\dots\}$ with mean $(1-r)/r$; the delivering attempt costs $c+\lambda t$; the defect is paid once with probability $1-p$. +\end{proof} + +$H$ is the one quantity that belongs to the user, and the budget word sets it: frugal $H=\$1$, balanced $\$10$, max $\$50$. Both forms are monotone in the same three quantities, the run's cost, its time and $\pclean=rp$, and the picker works with +\begin{equation} +E(\kappa)=c(\kappa)+\lambda\,t(\kappa)+H\,\big(1-\pclean(\kappa)\big). +\label{eq:E} +\end{equation} +Figure~\ref{fig:front} draws our measured crews with the iso-lines of~\eqref{eq:E}. The knob $k$ and the price $H$ parametrise the same trade-off; $k$ is what a catalog admits, $H$ is what measurements admit. + +\begin{assumption}[Linearity in roles] +\label{ass:linear} +$\pclean(\kappa)=\sum_r \nu_r\,q_r(\kappa(r))/100$, where $q_r(m)$ is the pool's role quality: the run's outcome credited to the role, on a 0--100 scale. +\end{assumption} +The assumption is what makes~\eqref{eq:E} linear in the role qualities, which Section~\ref{sec:decide} uses. It is a working rule rather than a law: a product over roles would count one outcome $n$ times, and a fitted interaction becomes available once judged scores separate the roles' contributions. Nothing in Sections~\ref{sec:pool}--\ref{sec:eb} depends on it. + +\begin{figure}[t] +\centering\includegraphics{fig-front.pdf} +\caption{Our measured crews (56 combinations fitted from 120 scored runs): cost of one run against the probability that it delivers clean. Solid: the pareto front. Dashed: iso-cost lines of~\eqref{eq:E}; a larger $H$ tilts them so that quality buys more.} +\label{fig:front} +\end{figure} + +\section{The Model Pool: a decentralised measure of role quality} +\label{sec:pool} + +\subsection{Judged runs and rows} + +After a run finishes, one model outside the crew reads the record (brief, deliverable, the checker's report and claim, the files touched) and answers one question per role with a score in $[0,100]$ and one sentence. The judge is chosen deterministically: the cheapest catalog model with coding index at least $60$ that can call tools, holds no role in the crew, and is not from the worker's vendor; a role the judge's own model holds is skipped. The floor exists because a judge below the worker's class rubber-stamps. The judge's calls are billed to a role of their own, so learning is never free by accident; at present prices it is cents per run. + +One judged role of one run is one \emph{row} +\[ +x=(\text{role},\ \text{model},\ \text{score},\ \text{dollars},\ \text{minutes},\ \text{size}\in\{S,M,L\},\ \text{door},\ \text{judge},\ \text{day},\ \text{install}), +\] +where \emph{install} is a nonce drawn once per profile. No text of any kind is in a row. Rows wait in a local outbox, which can be inspected, until they are sent. + +\subsection{Sufficient statistics and order-free merging} + +\begin{definition}[Cell, sheet] +A \emph{cell} is a tuple $\theta=(\text{role},\text{model},\text{judge},\text{day},\text{size},\text{door})$. A \emph{sheet} is a map from (install, cell) to a triple $T=(n,S,S^2)\in\N\times\R_{\ge0}\times\R_{\ge0}$, the count, sum and sum of squares of the scores an install has produced in that cell. +\end{definition} + +Every quantity computed below is a function of the sheet. Sheets are merged by the join +\begin{equation} +(A\sqcup B)(u,\theta)=\big(\max(n_A,n_B),\ \max(S_A,S_B),\ \max(S^2_A,S^2_B)\big)\quad\text{taken componentwise on the triples } A(u,\theta),\,B(u,\theta), +\label{eq:join} +\end{equation} +with a missing entry read as $(0,0,0)$. + +\begin{proposition}[The sheet is a state-based CRDT] +\label{prop:crdt} +Let each install $u$ only ever publish its own entries $(u,\theta)$ and let every published triple be the running total of $u$'s rows in $\theta$. Then (i) $\sqcup$ is commutative, associative and idempotent; (ii) an install's triples are monotone non-decreasing in time, so a later publication dominates an earlier one under $\sqcup$; (iii) any two nodes that have received the same set of publications, in any order and with any duplication, hold the same sheet. +\end{proposition} +\begin{proof} +(i) is the componentwise max on a product of totally ordered sets. (ii) Scores lie in $[0,100]$, so $n$, $S$ and $S^2$ each grow by a non-negative amount per row. (iii) follows from (i) and (ii): the join of a set of publications is their least upper bound, which does not depend on order or multiplicity, and by (ii) the least upper bound of an install's publications is its latest. +\end{proof} + +The cell means and pooled variances are functions of $\sum_u T(u,\theta)$, so by Proposition~\ref{prop:crdt} a relay and every install compute the same estimates from the same publications; there is no coordinator whose order of arrival matters, and a relay that is lost is rebuilt from any node that kept the publications. This is the sense in which the measure is decentralised: the relay is a convenience for bandwidth and signing, not a source of truth. + +\subsection{Judges are not the same ruler} + +Let $y$ be a score of role $r$, model $m$, by judge $j$. We posit the two-way additive model +\begin{equation} +y=\mu_{rm}+\beta_j+\varepsilon,\qquad \E\varepsilon=0,\ \operatorname{Var}\varepsilon=\sigma^2, +\label{eq:twoway} +\end{equation} +with $\mu_{rm}$ the quantity of interest and $\beta_j$ the judge's severity, constrained by $\sum_j n_{\cdot\cdot j}\beta_j=0$ (the judges' weighted mean is the ruler). Weighted least squares needs only the cell means $\bar y_{rmj}=S_{rmj}/n_{rmj}$ and counts. The normal equations are solved by alternating +\begin{equation} +\beta_j\leftarrow\frac{\sum_{r,m} n_{rmj}(\bar y_{rmj}-\mu_{rm})}{\sum_{r,m} n_{rmj}},\qquad +\mu_{rm}\leftarrow\frac{\sum_{j} n_{rmj}(\bar y_{rmj}-\beta_j)}{\sum_{j} n_{rmj}}, +\label{eq:als} +\end{equation} +re-centring $\beta$ after each sweep. Each half-step minimises a convex quadratic in its block, so the residual is non-increasing and the iteration converges to the least-squares solution; five sweeps suffice in practice. + +\begin{proposition}[Identifiability] +\label{prop:ident} +Let $G$ be the bipartite graph with the judges on one side, the pairs $(r,m)$ on the other, and an edge where $n_{rmj}>0$. The parameters of~\eqref{eq:twoway} under the centring constraint are identified on a connected component of $G$; across components only the within-component contrasts are. +\end{proposition} +\begin{proof} +The design matrix of a two-way additive model has rank equal to (number of vertices of $G$) minus (number of connected components); one linear constraint per component fixes the remaining freedom. With the single centring constraint, exactly one component is fully identified. +\end{proof} + +The judge rule of Section~5.1 makes $G$ connected quickly in practice: it sends most installs to the same few cheap judges. A model scored by a single judge so far has $\mu_{rm}$ equal to its own mean and the index says so. + +\subsection{Installs are not independent, and some are wrong} + +Rows from one install share a task mix, a codebase and a way of writing briefs. Under an intra-class correlation $\varrho$, $n_u$ rows from install $u$ carry the information of $n_u/(1+(n_u-1)\varrho)$ independent ones, which saturates at $1/\varrho$. The pool's cell estimate therefore treats installs, not rows, as the unit: it forms the install means $\bar y_{u\theta}$ and combines them with a location estimator that is robust to a minority of installs that are broken or adversarial. We use the Huber $M$-estimate with scale $s=1.4826\cdot\mathrm{MAD}$ and the conventional tuning constant $1.345$ (95\% efficiency at the normal), which has breakdown point $1/2$ in location, and fall back to the precision-weighted mean when a cell has fewer than five installs. A cell backed by fewer than three installs is published but not read. + +\subsection{Drift} + +Models change under the same name. Let $\bar y_d$ be a cell's day-$d$ mean over $n_d$ rows and model the true level as a random walk, +\begin{equation} +\mu_d=\mu_{d-1}+\eta_d,\quad \eta_d\sim\mathcal N(0,\omega^2);\qquad \bar y_d=\mu_d+\bar\varepsilon_d,\quad \bar\varepsilon_d\sim\mathcal N(0,\sigma^2/n_d). +\label{eq:localevel} +\end{equation} +The Kalman filter for this local-level model is, in steady state with constant $n_d=\bar n$, an exponentially weighted average whose forgetting factor $1-\alpha$ solves $\alpha^2/(1-\alpha)=\bar n\omega^2/\sigma^2$; the effective memory is about $1/\alpha$ days. The drift variance is estimated by moments from the day-to-day differences, +\begin{equation} +\hat\omega^2=\tfrac12\Big[\widehat{\operatorname{Var}}(\bar y_d-\bar y_{d-1})-\sigma^2\big(\tfrac1{n_d}+\tfrac1{n_{d-1}}\big)\Big]_+, +\label{eq:drift} +\end{equation} +pooled across cells. A cell with no evidence of drift ($\hat\omega^2=0$) keeps everything; a cell that drifts forgets at the rate the data show. The day is a cell dimension for exactly this reason. + +\begin{figure}[t] +\centering +\begin{tikzpicture}[x=1cm,y=1cm, + box/.style={draw,rounded corners=2pt,inner sep=4pt,font=\small,align=center,minimum height=8mm}, + store/.style={box,fill=black!6}, + net/.style={box,fill=blue!6}, + arr/.style={-{Latex[length=2mm]},thick}, + lbl/.style={font=\scriptsize,fill=white,inner sep=1.5pt} +] +\node[box] (cat) at (1.4,0) {catalog\\prices, indexes}; +\node[store] (post) at (4.7,0) {posterior per\\(role, model)}; +\node[box] (ts) at (8.1,0) {Thompson draw\\$\argmin_\kappa \tilde E(\kappa)$}; +\node[box] (run) at (10.7,0) {run}; +\node[box] (judge) at (13.3,0) {judge\\outside the crew}; +\node[store] (outbox) at (10.7,-1.6) {outbox}; +\node[store] (own) at (13.3,-1.6) {own sheet}; +\node[store] (index) at (4.7,-4.1) {signed index\\(mirrored)}; +\node[net] (relay) at (9.4,-4.1) {relay: $\sqcup$, judge effects \eqref{eq:als},\\Huber over installs, drift \eqref{eq:drift}, sign}; +\node[box,dashed] (others) at (13.3,-4.1) {other installs}; +\draw[arr] (cat) -- (post); +\draw[arr] (post) -- (ts); +\draw[arr] (ts) -- (run); +\draw[arr] (run) -- (judge); +\draw[arr] (judge) -- (own); +\draw[arr] ([xshift=-7mm]judge.south) |- (outbox.east); +\draw[arr] (own.south) -- ++(0,-0.45) -| node[lbl,pos=0.12,below]{at once} (post.south); +\draw[arr] (outbox.south) -- node[lbl,right]{rows, no text} (outbox.south |- relay.north); +\draw[arr] (relay) -- (index); +\draw[arr] (index.north) -- node[lbl,pos=0.35,right]{read at start} (post.south); +\draw[arr] (others.west) -- node[lbl,above]{rows} (relay.east); +\draw[arr] (index.south) -- ++(0,-0.5) -| node[lbl,pos=0.25,below]{the same index} (others.south); +\draw[dotted,thick] (-0.1,0.75) rectangle (14.9,-2.55); +\node[font=\scriptsize] at (0.9,0.95) {one install}; +\end{tikzpicture} +\caption{Where each step runs. Inside the dotted box is one install; the only thing that crosses it outward is a row without text. The relay merges publications with the join of Proposition~\ref{prop:crdt} and publishes a signed index; any install can host a relay, and any node that kept the publications can rebuild one.} +\label{fig:flow} +\end{figure} + +\section{Estimation with no tuned constant} +\label{sec:eb} + +We now combine the three sources for one install $u$. Write $\hat q_r(m)$ for the catalog quality~\eqref{eq:catq}, $\bar y^{\mathrm{pool}}_{rm}$ and $n^{\mathrm{pool}}_{rm}$ for the pool's cell after Sections~5.3--5.5, and $\bar y^{u}_{rm}$, $n^{u}_{rm}$ for the own sheet. + +\begin{assumption}[Hierarchy] +\begin{equation} +\mu_{rm}\sim\mathcal N\big(\hat q_r(m),\,\tau_0^2\big),\qquad +\mu^{u}_{rm}\mid\mu_{rm}\sim\mathcal N\big(\mu_{rm},\,\tau_1^2\big),\qquad +y\mid\mu^{u}_{rm}\sim\mathcal N\big(\mu^{u}_{rm},\,\sigma^2\big). +\label{eq:hier} +\end{equation} +\end{assumption} +The first line says the catalog is an unbiased but noisy predictor of the population quality; the second that install $u$'s own quality for the model scatters around the population's; the third is judge noise after Section~5.3. The install cares about $\mu^u_{rm}$. + +\begin{proposition}[Posterior for the install] +\label{prop:post} +Under~\eqref{eq:hier}, with $k=\sigma^2/\tau_0^2$ and $\ell=\sigma^2/\tau_1^2$, the posterior of $\mu^u_{rm}$ is normal with +\begin{equation} +\E[\mu^u_{rm}\mid\cdot]=\frac{k^{\ast}\,\hat q_r(m) + n^{\ast}\,\bar y^{\mathrm{pool}}_{rm} + n^u\,\bar y^u_{rm}}{k^{\ast}+n^{\ast}+n^u},\qquad +\operatorname{Var}[\mu^u_{rm}\mid\cdot]=\frac{\sigma^2}{k^{\ast}+n^{\ast}+n^u}, +\label{eq:three} +\end{equation} +where the pool and the catalog enter with the \emph{capped} effective counts +\begin{equation} +n^{\ast}=\frac{n^{\mathrm{pool}}\,\ell}{n^{\mathrm{pool}}+\ell}\ \le\ \ell,\qquad +k^{\ast}=\frac{k\,\ell}{k+\ell}\ \le\ \ell . +\label{eq:cap} +\end{equation} +\end{proposition} +\begin{proof} +Integrate out $\mu_{rm}$. The catalog and the pool inform $\mu^u_{rm}$ only through $\mu_{rm}$, whose posterior given them is normal with precision $(k+n^{\mathrm{pool}})/\sigma^2$; adding the between-install variance $\tau_1^2$ gives, for $\mu^u_{rm}$, a precision $1/(\sigma^2/(k+n^{\mathrm{pool}})+\tau_1^2)$. Writing that as an effective count over $\sigma^2$ gives $(k+n^{\mathrm{pool}})\ell/(k+n^{\mathrm{pool}}+\ell)$, which splits into the two terms of~\eqref{eq:cap} in proportion to $k$ and $n^{\mathrm{pool}}$; the own rows add precision $n^u/\sigma^2$ directly. Precision-weighting the three means gives~\eqref{eq:three}. +\end{proof} + +\begin{corollary} +With $\tau_1=0$ ($\ell=\infty$) the posterior mean is $(1-w)\hat q_r(m)+w\bar y$ with $w=n/(n+k)$, $n=n^{\mathrm{pool}}+n^u$: the plain shrinkage blend, which is what the picker applies today with $k=30$. +\end{corollary} + +The cap~\eqref{eq:cap} is the point of the hierarchy. However many rows the pool holds, its evidence about \emph{this} install's quality is worth at most $\ell=\sigma^2/\tau_1^2$ own rows, because other installs' tasks are not this install's tasks; and the own rows are never capped. This replaces an ad hoc "own rows count triple" by a quantity with a meaning and an estimator. + +\paragraph{Estimating the constants.} +Every constant in~\eqref{eq:three} is estimated from the sheet by moments; none is set by hand. +\begin{align} +\hat\sigma^2 &= \frac{\sum_{u,\theta}\big(S^2_{u\theta}-S_{u\theta}^2/n_{u\theta}\big)}{\sum_{u,\theta}(n_{u\theta}-1)} &&\text{pooled within-cell variance,}\label{eq:sigma}\\ +\hat\tau_1^2 &= \Big[\,\widehat{\operatorname{Var}}_u\big(\bar y_{u\theta}\big)-\hat\sigma^2\,\overline{(1/n_{u\theta})}\,\Big]_+ &&\text{between-install variance, pooled over cells,}\label{eq:tau1}\\ +\hat\tau_0^2 &= \Big[\,\widehat{\operatorname{Var}}_{(r,m)}\big(\bar y^{\mathrm{pool}}_{rm}-\hat q_r(m)\big)-\overline{\hat\sigma^2/n^{\mathrm{pool}}_{rm}}\,\Big]_+ &&\text{how far the catalog misses,}\label{eq:tau0} +\end{align} +each a within-minus-noise moment estimator of the kind used by Efron and Morris~\cite{efron1975}. Then $k=\hat\sigma^2/\hat\tau_0^2$ and $\ell=\hat\sigma^2/\hat\tau_1^2$, and $\hat\omega^2$ of~\eqref{eq:drift} sets the forgetting. These are published in the index beside the cells so that every install blends with the same constants, and re-estimated at every relay cycle. The relay withholds an estimate until its denominator has at least ten degrees of freedom and publishes the day-one values of Table~\ref{tab:const} until then. + +\subsection{Covariates, and why no row is ever wasted} +\label{sec:covariates} + +A row carries more than (role, model, score): a door, a size bucket, a judge, a day, an install, and any field added later, such as the harness version or a task family. Call the vector of such fields the row's \emph{profile} $c\in\mathcal C=\prod_f L_f$, a product of factor levels. The hierarchy~\eqref{eq:hier} extends factor by factor: +\begin{equation} +\mu_{rm,c}=\mu_{rm}+\sum_{f}\delta^{f}_{rm,c_f},\qquad \delta^{f}_{rm,\cdot}\sim\mathcal N(0,\tau_f^2)\ \text{i.i.d.\ over levels}, +\label{eq:factors} +\end{equation} +so that a (role, model) has one population quality and one partially pooled offset per level of each factor. The install factor is the case $f=\text{install}$ of~\eqref{eq:factors}, and Proposition~\ref{prop:post} is its two-level instance; the same derivation gives, for a query profile $c$, a posterior mean that is a precision-weighted combination of the cell means at every level that shares a coordinate with $c$, each capped by its own $\ell_f=\sigma^2/\tau_f^2$, with $\hat\tau_f^2$ estimated by the between-level moment~\eqref{eq:tau1} applied to factor $f$. Two properties follow. + +\emph{Old rows keep their value when the harness changes.} If the door algorithm is rewritten, rows from the old door are a level of the door factor; they still inform $\mu_{rm}$ with the full weight the estimated $\tau_{\text{door}}^2$ allows, and the new door starts at $\mu_{rm}$ plus nothing and specialises as its own rows arrive. If the two doors turn out to differ little, $\hat\tau_{\text{door}}^2\approx 0$, $\ell_{\text{door}}\to\infty$, and the factor pools completely; if they differ a lot, the cap keeps the old door from speaking for the new one. The data decide, per factor, how much they transfer. + +\emph{The schema is open.} A row with a field absent (older rows, before the field existed) is at a level of its own, \emph{unknown}, which the estimator treats like any other level. Adding a field therefore invalidates nothing already collected, and the sheet's join in Proposition~\ref{prop:crdt} is unaffected because a cell key is only a tuple. What can be extracted from the rows is the set of contrasts the factors expose; a factor that is never recorded cannot be recovered later, which is why the row carries every cheap field now and the text of none. + +Regressors that are not factors, a task's token count for instance, enter the same way as a linear term with a partially pooled slope; the moment estimators are the same with a design column in place of an indicator. + +\paragraph{Interactions: the crew as a group.} +Assumption~\ref{ass:linear} scores roles one at a time. Whether a particular worker and a particular checker do better \emph{together} than their role qualities predict is a question about the pair, and the same hierarchy answers it: a pair $(m_r,m_{r'})$ is a level of a factor over $\mods\times\mods$, with its own partially pooled offset $\delta^{rr'}_{(m_r,m_{r'})}\sim\mathcal N(0,\tau_{rr'}^2)$. A pair that has never been run sits at zero offset and the additive prediction stands; a pair with rows moves by exactly the amount its rows justify against $\hat\tau_{rr'}^2$; if pairs in general add nothing, $\hat\tau_{rr'}^2\to0$ and the term disappears. This is the whole difference between learning crews as groups and learning roles: the group term is there, but it is charged for. A crew-level learner with no structure has to visit each of $|\crews|$ crews to say anything about it; the additive model needs $\sum_r|S_r|$ observations to say something about every crew, and the pair term then spends further observations only where a pair is actually run. Some group effects are constraints rather than parameters: the vendor rule $D$ is one, and it is applied to $\crews$ before any learning happens. + +\begin{table}[h] +\centering\small +\begin{tabular}{p{2.0cm}p{5.2cm}p{4.0cm}p{2.6cm}}\toprule +constant & meaning & estimator & day-one value\\\midrule +$\sigma$ & judge noise & \eqref{eq:sigma} & 15\\ +$k=\sigma^2/\tau_0^2$ & weight of the catalog & \eqref{eq:tau0} & 30\\ +$\ell_f=\sigma^2/\tau_f^2$ & cap on evidence transferred across a factor (install, door, size, \dots) & \eqref{eq:tau1} per factor & 30\\ +$\omega^2$ & drift per day & \eqref{eq:drift} & 0 (no forgetting)\\ +$\beta_j$ & judge severity & \eqref{eq:als} & 0\\ +$v_r$ & role token shares & measured from run token accounting & $0.90/0.08/0.02$\\ +\midrule +$H$ & price of a shipped defect & the user's, by budget word & \$1 / \$10 / \$50\\ +$\lambda$ & price of a minute & the user's & \$0.01\\ +$\phi$, judge floor & structural floors & fixed by design & $0.8$, $60$\\ +\bottomrule\end{tabular} +\caption{Every constant with its estimator. Two are the user's by design; two are structural floors. The rest are empirical-Bayes estimates that the relay publishes with the index.} +\label{tab:const} +\end{table} + +\begin{figure}[t] +\centering\includegraphics{fig-shrink.pdf} +\caption{Shrinkage of one role quality from the catalog prior (80) toward the measured mean (65), $k=30$, $\sigma=15$. The band is what the Thompson draw of Section~\ref{sec:decide} samples from.} +\label{fig:shrink} +\end{figure} + +\section{Deciding: Thompson sampling with semi-bandit feedback} +\label{sec:decide} + +Given a posterior $\mathcal N(\mu'_{rm},\sigma'^2_{rm})$ per role and model from~\eqref{eq:three}, the pick could minimise~\eqref{eq:E} at the posterior means. That never tries a crew the prior underrates, and the catalog prior is wrong by a lot for some models (Figure~\ref{fig:cells}). Thompson sampling~\cite{thompson1933,russo2018} draws one plausible world from the posterior and acts as if it were true: a crew is chosen with the probability that it is the best, exploration is proportional to what is unknown, and it stops on its own as the posteriors tighten. No exploration rate exists to be set. + +\begin{algorithm}[h] +\caption{One pick under \emph{learn}} +\begin{algorithmic}[1] +\Require shortlists $S_r$ on the posterior means, budget word $\Rightarrow H$, task size bucket $\Rightarrow$ expected tokens $T$ and minutes $t(\kappa)$ +\For{each role $r$ and $m\in S_r$} + \State $\tilde q_{rm}\sim\mathcal N(\mu'_{rm},\sigma'^2_{rm})$ \Comment{one draw per run; never redrawn during the run} +\EndFor +\For{each $\kappa\in\crews$} + \State $\tilde E(\kappa)\gets T\,B(\kappa)+\lambda\,t(\kappa)+H\,\big(1-\sum_r\nu_r\tilde q_{r\kappa(r)}/100\big)$ +\EndFor +\State \Return $\argmin_\kappa\tilde E(\kappa)$ \Comment{ties: fewer imputed indexes, then id order} +\end{algorithmic} +\end{algorithm} + +\begin{proposition}[Role draws are crew draws] +Under Assumption~\ref{ass:linear}, drawing $\tilde q_{rm}$ independently per (role, model) and minimising $\tilde E$ over $\crews$ is exactly Thompson sampling over the crews' posterior costs. +\end{proposition} +\begin{proof} +$\tilde E(\kappa)$ is an affine function of the vector $(\tilde q_{rm})$, so its joint law over $\crews$ is the push-forward of the posterior on role qualities; a sample of the latter is a sample of the former. +\end{proof} + +The structure that matters is the feedback. A run of crew $\kappa$ is judged role by role, so it yields one observation for each of its $n$ (role, model) pairs, and the posterior of every crew that shares a pair with $\kappa$ moves. This is a combinatorial bandit with semi-bandit feedback~\cite{chen2013,wen2015}: the unknowns are the $\sum_r|S_r|$ role qualities, not the $|\crews|$ crew values, and the regret of Thompson sampling scales with the former~\cite{wen2015}. Section~\ref{sec:exp} measures the difference on our runs. For Gaussian posteriors the Bayesian regret of Thompson sampling is $O(\sqrt{dT\log T})$ in the dimension $d$ of the unknowns over $T$ runs~\cite{russo2014}. + +\paragraph{The learned front.} +Under \emph{learn} the front of Section~\ref{sec:catalog} is recomputed on the posterior: the bill $B(\kappa)$ is read at the run's expected tokens, and the quality is the posterior mean of $\pclean(\kappa)$, so the front is the pareto set in (expected dollars per task, expected probability of a clean delivery) with an interval on each point from~\eqref{eq:three}. The budget word's $H$ is a line of slope $-1/H$ in that plane and the pick is where the line first touches the front; Algorithm~1 is the same pick on one draw of the front rather than on its mean, which is what makes it explore. The pool command prints the mean front with its intervals, and the catalog front beside it, so that a user can see where measurement moved a pick and by how much. + +\paragraph{When is it settled.} +The same posterior answers whether learning is done: the probability $\pi^\ast$ that the posterior-mean crew is the best, estimated by repeating the draw of Algorithm~1 a few hundred times. The pool command reports $\pi^\ast$; a value near one means further exploration is nearly free, a value near $1/|\front|$ means the ranking is still open. Costs enter as observed dollars per run and are not sampled; their noise is small next to the quality's. + +\begin{remark}[A new model] +A model with no rows has $n=0$ and sits at its catalog prior with the prior variance $\sigma^2/k^{\ast}$. If that puts it near the front it is drawn into crews soon, judged, and confirmed or pushed down by its first rows. Nothing is done by hand when the catalog changes. +\end{remark} + +\section{Experiments on our measured runs} +\label{sec:exp} + +Ground truth is the table of Figure~\ref{fig:front}: 56 crews with $\pclean$, dollars and minutes fitted from 120 scored runs of our own. Each replicate draws a catalog prior per arm as the truth plus $\mathcal N(0,0.15^2)$ noise, roughly the disagreement between indexes and measurements in Figure~\ref{fig:cells}, holds it with $k=30$, and runs $N$ tasks; a strategy picks, observes the outcome and pays~\eqref{eq:E} with $H=\$10$. Regret is the excess over the oracle crew, averaged over 200 replicates. + +\paragraph{Crew-level learners.} +Table~\ref{tab:regret} and Figure~\ref{fig:regret} compare, on the twelve chat-door crews, a fixed table row, the catalog prior alone, $\varepsilon$-greedy and Thompson, all with one arm per crew. After 300 runs both learners are about \$0.30 per task under the fixed table, a third of the table's regret, and still falling. Thompson pays a small exploration tax in the first twenty runs; $k=30$ is what keeps it small. $\varepsilon$-greedy matches it here only because $\varepsilon=0.1$ happens to suit a dozen arms; it neither grows when a new model appears nor shrinks when the ranking is settled. + +\begin{table}[h] +\centering\small +\input{tab-regret.tex} +\caption{Regret against the oracle, dollars per task after $N$ runs, one arm per crew.} +\label{tab:regret} +\end{table} + +\begin{figure}[h] +\centering\includegraphics{fig-regret.pdf} +\caption{Left: regret per task. Right: share of runs on a crew other than the current posterior-mean best. Thompson explores heavily while a dozen crews sit within cents of each other and less as the posteriors separate; $\varepsilon$-greedy explores at its fixed rate.} +\label{fig:regret} +\end{figure} + +\paragraph{Role-level feedback.} +Figure~\ref{fig:semibandit} and Table~\ref{tab:semibandit} isolate the structure of Section~\ref{sec:decide}. The ground truth is three-role: a role quality per (role, model) read off the measured rows (a worker's delivered rate, a checker's and a planner's clean rate), combined by the linear rule of Assumption~\ref{ass:linear} over every crew the vendor rule admits, with dollars additive over roles. Three learners run Thompson sampling with the same prior noise and the same $H$ and differ only in what they see after a run: one arm per crew fed the run's clean-or-defect bit; one arm per crew fed the mean of the judge's role scores; one posterior per (role, model) fed each role's own score, which is what the judge of Section~5.1 provides. The last is the semi-bandit. Richer feedback per run helps (the second learner over the first), but the structure helps more: at four hundred runs the role-level learner's regret is about a third of the crew-level learner's on the same scores, because every run updates the crews that share a model with the one that ran, and there are sixteen unknowns behind 138 crews. + +\begin{table}[h] +\centering\small +\input{tab-semibandit.tex} +\caption{Regret against the oracle, dollars per task after $N$ runs, on the three-role truth; same prior noise and $H$ for all three learners.} +\label{tab:semibandit} +\end{table} + +\begin{figure}[h] +\centering\includegraphics{fig-semibandit.pdf} +\caption{Thompson sampling under three kinds of feedback on the same three-role truth. Dotted and dashed: one arm per crew, fed the clean bit and the mean role score respectively. Solid: one posterior per (role, model) fed the role scores, the combinatorial semi-bandit of Section~\ref{sec:decide}.} +\label{fig:semibandit} +\end{figure} + +\begin{figure}[t] +\centering\includegraphics{fig-cells.pdf} +\caption{The seed index shipped in the binary: 14 cells from 120 scored runs, \code{role\_quality} $=100\cdot P(\text{no major defect})$ under a Beta(1,1) posterior. The spread between models a catalog rates alike is why the pool exists.} +\label{fig:cells} +\end{figure} + +The truth in these experiments is our own fitted table from one team's tasks, so the size of the gain is the shape of the effect and not a promise for every install. The script \code{figures.py} beside this document reproduces every figure and table from the two CSV files it reads. + +\section{What the user controls} + +\begin{center}\small +\begin{tabular}{p{2.1cm}p{4.3cm}p{8.0cm}}\toprule +setting & values & meaning\\\midrule +\code{crew} & frugal $\mid$ balanced $\mid$ max $\mid$ custom & how much to spend: $H$ and the knob $k$\\ +\code{picked from} & table $\mid$ catalog $\mid$ learn & the rows we measured; recomputed from today's catalog; catalog plus pool plus own sheet\\ +\code{model\_pool} & on $\mid$ read $\mid$ off & read the index and send rows (default); read only; neither\\ +\code{explore} & on $\mid$ off & Thompson draw (default) or the posterior mean; \emph{learn} only\\ +\bottomrule\end{tabular} +\end{center} + +With the pool set to \emph{read}, the own sheet still accrues and enters~\eqref{eq:three}; with it \emph{off}, only the catalog prior remains and the pick is the catalog's. The index is a JSON document signed with an Ed25519 key whose public half is in the binary; \code{codeaf pool verify} checks it, \code{pool show} prints it, \code{pool status} lists the rows waiting in the outbox. The binary carries a seed index of our own measured runs (Figure~\ref{fig:cells}) so that \emph{learn} has numbers on day one. The relay's code is in this repository; its addresses are settings, so an install can read from and send to a relay of its own, or to none. + +\section{Status} + +On the branch this document ships with: the catalog crew of Section~\ref{sec:catalog}; the shrinkage blend of the Corollary with $k=30$; the index document, its reader and signed pull; the outbox and the sheet; the judge and its deterministic choice; the settings; the seed index. Specified here and next to land: the judge fixed effects, Huber aggregation and drift estimate in the relay; the hierarchical cap~\eqref{eq:cap} and the moment estimators~\eqref{eq:sigma}--\eqref{eq:tau0}; the Thompson draw with role-level updates and the settled probability $\pi^\ast$; the relay itself. Until the relay exists the pool is the seed index plus each install's own sheet. + +\begin{thebibliography}{9}\small +\bibitem{thompson1933} W.~R. Thompson. On the likelihood that one unknown probability exceeds another in view of the evidence of two samples. \emph{Biometrika} 25:285--294, 1933. +\bibitem{russo2018} D.~Russo, B.~Van Roy, A.~Kazerouni, I.~Osband, Z.~Wen. A tutorial on Thompson sampling. \emph{Foundations and Trends in Machine Learning} 11(1):1--96, 2018. +\bibitem{russo2014} D.~Russo, B.~Van Roy. Learning to optimize via posterior sampling. \emph{Mathematics of Operations Research} 39(4):1221--1243, 2014. +\bibitem{chen2013} W.~Chen, Y.~Wang, Y.~Yuan. Combinatorial multi-armed bandit: general framework and applications. \emph{ICML}, 2013. +\bibitem{wen2015} Z.~Wen, B.~Kveton, A.~Ashkan. Efficient learning in large-scale combinatorial semi-bandits. \emph{ICML}, 2015. +\bibitem{efron1975} B.~Efron, C.~Morris. Data analysis using Stein's estimator and its generalizations. \emph{Journal of the American Statistical Association} 70(350):311--319, 1975. +\bibitem{shapiro2011} M.~Shapiro, N.~Pregui\c{c}a, C.~Baquero, M.~Zawirski. Conflict-free replicated data types. \emph{SSS}, 2011. +\bibitem{huber1964} P.~J. Huber. Robust estimation of a location parameter. \emph{Annals of Mathematical Statistics} 35(1):73--101, 1964. +\bibitem{harvey1989} A.~C. Harvey. \emph{Forecasting, Structural Time Series Models and the Kalman Filter}. Cambridge University Press, 1989. +\end{thebibliography} + +\end{document} diff --git a/docs/design/model-pool/tab-regret.tex b/docs/design/model-pool/tab-regret.tex new file mode 100644 index 0000000000..47a27626f2 --- /dev/null +++ b/docs/design/model-pool/tab-regret.tex @@ -0,0 +1,7 @@ +\begin{tabular}{lrrrrrrrrr}\toprule +$N$ & 10 & 20 & 40 & 70 & 100 & 150 & 200 & 300 & 400\\\midrule +table & 0.91 & 0.93 & 0.96 & 0.94 & 0.94 & 0.93 & 0.94 & 0.96 & 0.97\\ +catalog & 0.82 & 0.86 & 0.88 & 0.87 & 0.85 & 0.83 & 0.83 & 0.84 & 0.86\\ +greedy & 0.72 & 0.79 & 0.79 & 0.78 & 0.76 & 0.72 & 0.69 & 0.64 & 0.62\\ +thompson & 0.98 & 0.97 & 0.90 & 0.86 & 0.81 & 0.75 & 0.70 & 0.65 & 0.59\\ +\bottomrule\end{tabular} diff --git a/docs/design/model-pool/tab-semibandit.tex b/docs/design/model-pool/tab-semibandit.tex new file mode 100644 index 0000000000..38846605eb --- /dev/null +++ b/docs/design/model-pool/tab-semibandit.tex @@ -0,0 +1,6 @@ +\begin{tabular}{lrrrrrrrrr}\toprule +$N$ & 10 & 20 & 40 & 70 & 100 & 150 & 200 & 300 & 400\\\midrule +crew, clean bit & 0.47 & 0.50 & 0.50 & 0.51 & 0.53 & 0.54 & 0.55 & 0.55 & 0.54\\ +crew, mean score & 0.29 & 0.43 & 0.39 & 0.38 & 0.36 & 0.35 & 0.34 & 0.34 & 0.31\\ +role, semi-bandit & 0.36 & 0.35 & 0.31 & 0.27 & 0.26 & 0.22 & 0.20 & 0.17 & 0.15\\ +\bottomrule\end{tabular} From b7b8b4012f3ef65dafa700357962f79ea531cba7 Mon Sep 17 00:00:00 2001 From: santoshkumarradha Date: Thu, 17 Sep 2026 09:55:54 -0400 Subject: [PATCH 095/173] =?UTF-8?q?config:=20the=20crew's=20seats=20are=20?= =?UTF-8?q?picked=20from=20a=20row=20=E2=80=94=20table,=20catalog=20or=20l?= =?UTF-8?q?earn=20=E2=80=94=20and=20the=20ladder=20honours=20it?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/config/auto.go | 92 +++++++++++++- internal/config/auto_test.go | 32 ++++- internal/config/crew.go | 191 ++++++++++++++++++++++++++++- internal/config/crew_test.go | 66 ++++++++++ internal/config/derivation_test.go | 4 + internal/config/seats.go | 47 ++++++- internal/config/seats_test.go | 120 ++++++++++++++++++ internal/config/settings.go | 30 +++++ 8 files changed, 568 insertions(+), 14 deletions(-) diff --git a/internal/config/auto.go b/internal/config/auto.go index f8f72ab5e9..99cc6db0ba 100644 --- a/internal/config/auto.go +++ b/internal/config/auto.go @@ -56,7 +56,20 @@ var AutoIndex func() *index.Index // crewpick scores a seat on, with one cell per role and model. const PoolQualityMetric = "role_quality" -// AutoPick is the word's own answer for one tier, computed from the rows. +// AutoPick is the bare `auto` word's own answer for one tier, computed from +// the rows. It is [AutoPickWith] with the measured quality the Model Pool +// holds carried as a prior — the behaviour the bare word has always had — and +// it stays the seam a row that says `auto` resolves through ([autoRow]). The +// pick row's words answer through [AutoPickWith] directly ([pickedSeat]). +func AutoPick(tier, family, preset string, models []catalog.Model) (modelID string, ok bool) { + return AutoPickWith(tier, family, preset, models, autoPrior(autoIndex())) +} + +// AutoPickWith is the pick itself, with the measured quality NAMED: prior is +// carried into the front as a rating seats read on top of the catalog's own +// published scores, and a nil prior leaves every seat on those scores alone — +// the answer the `catalog` pick word carries, where `learn` passes the pool's +// measurements and the person's own judged runs ([autoPrior]). // // It is PURE: no disk, no network, the rows are only read, and the same rows // give the same answer however often it is asked and in whatever order they @@ -74,8 +87,8 @@ const PoolQualityMetric = "role_quality" // whole catalog. preset is one of the three crew words, and any other word // has no answer. No rows, an empty front or an empty id are no answer too; // no answer is ok false and an empty id, which is the caller's cue to read -// the family's table row instead ([autoRow]). -func AutoPick(tier, family, preset string, models []catalog.Model) (modelID string, ok bool) { +// the family's table row instead ([autoRow], [pickedSeat]). +func AutoPickWith(tier, family, preset string, models []catalog.Model, prior crewpick.Prior) (modelID string, ok bool) { seat, ok := autoSeat(tier) if !ok || len(models) == 0 { return "", false @@ -88,7 +101,7 @@ func AutoPick(tier, family, preset string, models []catalog.Model) (modelID stri if len(candidates) == 0 { return "", false } - frugal, balanced, max := crewpick.Presets(crewpick.FrontWith(candidates, crewpick.DefaultShapes(), fam, autoPrior(autoIndex()))) + frugal, balanced, max := crewpick.Presets(crewpick.FrontWith(candidates, crewpick.DefaultShapes(), fam, prior)) var pick crewpick.Crew switch strings.ToLower(strings.TrimSpace(preset)) { case CrewFrugal: @@ -225,6 +238,77 @@ func autoRow(profileDir, family, tier string) (model string, source SeatSource, return defaultTierModel(family, tier), SeatTable, preset } +// pickedSeat is THE PICK ROW'S HALF OF THE LADDER: where a seat's model comes +// from when the pick is off the table and the tier row does not name a model +// of its own. Both ladders call it — [tierSeatUnder] for the conversation and +// the settings sheet, [resolveSeat] for every headless door — so a pick +// cannot mean one thing in chat and another headless. +// +// The rule, in the order it is applied: +// +// - the two seats that read every turn are NOT asked: reflex and small work +// always read the table, the same law their columns in the shipped tables +// keep, and the caller decides which tiers reach here; +// - a row holding a model id that is not the preset's own table value is a +// person's own model, and it wins — the pick answers for the seats nobody +// named, not over the names they typed. A row holding the preset's OWN id +// is the preset answering rather than a pin, and the pick computes it; +// - every other seat among worker, careful work and mastermind is computed +// at the crew's preset ([crewPresetUnder]), on the rung the pick names: +// [SeatComputed] under the `catalog` word, [SeatLearned] under `learn`; +// - when the catalog cannot compute one — no rows, no pick off the front — +// the preset's own table row is the answer, on the table rung, the same +// fallback [autoRow] keeps. Never `auto` and never empty. +// +// ok is false only when the pick has nothing to say — the pick row at its +// default, a hand-typed id, or a tier that always reads the table — and the +// caller answers with the row's own rung instead. The DEFAULT PICK IS THE +// FIRST CHECK: a profile that has never answered the row reads every seat +// exactly as it read before the row existed, which is what a default is for. +func pickedSeat(profileDir, family, tier, model string) (Seat, bool) { + if CrewPickAt(profileDir) == CrewPickTable { + return Seat{}, false + } + if tier != ModelTierWorker && tier != ModelTierHigh && tier != ModelTierMastermind { + return Seat{}, false + } + preset := crewPresetUnder(profileDir, family) + if row, ok := CrewModelsForSource(family, preset); ok { + if model != "" && !strings.EqualFold(strings.TrimSpace(model), row[tier]) { + return Seat{}, false + } + } + id, source := pickedModel(tier, family, preset, CrewPickAt(profileDir)) + return Seat{Role: tierSeatRole(tier), Model: id, Source: source, Crew: preset}, true +} + +// pickedModel is the pick word's own answer for one tier: the catalog's pick +// at the preset, on the rung the word names; and, when nothing can be +// computed, the preset's own table row on the table rung — the id a preset +// write would have landed, and never `auto` and never empty. +func pickedModel(tier, family, preset, pick string) (modelID string, source SeatSource) { + pick = normalCrewPick(pick) + var prior crewpick.Prior + if pick == CrewPickLearn { + prior = autoPrior(autoIndex()) + } + if id, ok := AutoPickWith(tier, family, preset, autoCatalogRows(), prior); ok { + if pick == CrewPickLearn { + return id, SeatLearned + } + return id, SeatComputed + } + if row, ok := CrewModelsForSource(family, preset); ok { + if id := strings.TrimSpace(row[tier]); id != "" { + return id, SeatTable + } + } + // Unreachable for a tier this build knows — every preset row answers all + // five — but the never-empty law is carried rather than assumed, the same + // last word [autoRow] keeps. + return defaultTierModel(family, tier), SeatTable +} + // autoCatalogRows is [AutoModels] read with its two ordinary absences folded // into one answer. func autoCatalogRows() []catalog.Model { diff --git a/internal/config/auto_test.go b/internal/config/auto_test.go index 0f7eaaa6ca..9d72655cf1 100644 --- a/internal/config/auto_test.go +++ b/internal/config/auto_test.go @@ -240,8 +240,8 @@ func TestThePresetAnAutoSeatRunsAtIsReadFromTheStoredRows(t *testing.T) { if seat.Crew != CrewMax { t.Errorf("max's rows and an auto row read as %q", seat.Crew) } - if seat.Rung() != "crew "+CrewMax+", computed" { - t.Errorf("the rung reads %q, want crew max, computed", seat.Rung()) + if seat.Rung() != "crew "+CrewMax+", computed from the catalog" { + t.Errorf("the rung reads %q, want crew max, computed from the catalog", seat.Rung()) } // A profile with no auto row anywhere reads as it always read — the // default rung, no crew word — which is the unchanged-behaviour law: the @@ -297,8 +297,8 @@ func TestTheDefaultPresetWinsWhenTheStoredRowsMatchTwoPresets(t *testing.T) { if seat.Crew != CrewBalanced { t.Errorf("rows matching both presets read as %q, want %s, the default preset", seat.Crew, CrewBalanced) } - if seat.Rung() != "crew "+CrewBalanced+", computed" { - t.Errorf("the rung reads %q, want crew %s, computed", seat.Rung(), CrewBalanced) + if seat.Rung() != "crew "+CrewBalanced+", computed from the catalog" { + t.Errorf("the rung reads %q, want crew %s, computed from the catalog", seat.Rung(), CrewBalanced) } // And the same tie on the table rung, with nothing to compute from: the // default preset decides the fallback id too, not just the budget word. @@ -426,3 +426,27 @@ func TestAutoPickReadsAMeasuredQualityPrior(t *testing.T) { t.Fatalf("the rated row did not take the worker seat: got %q", after) } } + +// THE PRIOR IS THE ARGUMENT, AND NOTHING HIDES IT. AutoPickWith carries +// whatever prior it is given: nil leaves the worker on the front's own +// answer, and the same measured cell the AutoPick test reads moves it — which +// is exactly the difference between the `catalog` and `learn` pick words +// (crew.go's [CrewPickAt]). +func TestAutoPickWithCarriesThePriorItIsGiven(t *testing.T) { + rows := autoTestRows() + front, _, _ := crewpick.Presets(crewpick.Front(autoCandidates(rows), crewpick.DefaultShapes(), crewpick.All)) + bare, ok := AutoPickWith(ModelTierWorker, CrewSourceAll, CrewFrugal, rows, nil) + if !ok || bare != front.Worker { + t.Fatalf("with no prior the worker pick reads %q, want the front's own %q", bare, front.Worker) + } + measured, ok := AutoPickWith(ModelTierWorker, CrewSourceAll, CrewFrugal, rows, autoPrior(mustIndex(t, priorDocument))) + if !ok { + t.Fatal("the pick answers nothing with the prior in hand") + } + if measured == bare { + t.Fatalf("the measured cell left the worker pick at %q; the prior never reached the front", measured) + } + if measured != "a/cheap" { + t.Fatalf("the rated row did not take the worker seat: got %q", measured) + } +} diff --git a/internal/config/crew.go b/internal/config/crew.go index 68f35a1829..7a374dd710 100644 --- a/internal/config/crew.go +++ b/internal/config/crew.go @@ -87,6 +87,103 @@ var CrewSources = []string{CrewSourceOpen, CrewSourceAll} // family's balanced row. const DefaultCrewSource = CrewSourceAll +// ── where the seats are picked from ──────────────────────────────────────── + +// THE THIRD ROW THE CREW WORDS ARE ANSWERED THROUGH. The crew row says how +// much to spend and the family row says which shelf those budgets name; the +// pick row says where the models for that money come from when a tier row +// does not hold a model id of its own: +// +// - `table` — the rows this build measured and shipped ([crewModels] and +// [crewAllModels]), which is what an unwritten seat has always read; +// - `catalog` — the same three budgets recomputed off the catalog's own +// published prices and scores, on every read, with no measurement of +// anybody's own runs in it ([AutoPickWith] with no prior); +// - `learn` — the catalog computation plus the Model Pool's measurements +// and the person's own judged runs, carried as a quality prior +// ([autoPrior]). +// +// THE DEFAULT IS THE TABLE because the table is what a profile has always +// read: an unwritten seat names the preset's own row, and nothing about a +// profile that has answered nothing moves until somebody answers a row. The +// other two words are an opt-in to a read that keeps moving — a seat that +// follows the catalog follows it whether or not the shipped rows do — and a +// person has to say so. +// +// A PICK NEVER OVERRIDES A MODEL ID. The row answers for the seats a person +// did not name, and the seats they did — written by hand, or by a preset — +// keep their ids until the crew is picked again, except that a row holding +// the preset's own table value is the preset answering, not a person pinning +// one model by id. The rule is [pickedSeat]'s to apply and the manual's to +// state. +const ( + // CrewPickTable is the measured rows this build ships, and the default. + CrewPickTable = "table" + // CrewPickCatalog is the catalog's own published figures, with nothing + // measured on top. + CrewPickCatalog = "catalog" + // CrewPickLearn is the catalog computation plus the Model Pool's + // measurements and the person's own judged runs. + CrewPickLearn = "learn" +) + +// CrewPicks lists the words a person may WRITE, narrowest first: the shipped +// table, then the catalog on its own, then the catalog with what runs +// measured. A pick word is not a preset and names no budget — the crew row +// above it still does that. +var CrewPicks = []string{CrewPickTable, CrewPickCatalog, CrewPickLearn} + +// DefaultCrewPick is the table: the rows this build measured are where an +// unwritten seat's model comes from until somebody answers the row. +const DefaultCrewPick = CrewPickTable + +// knownCrewPick folds a word and says whether it is one of the picks this +// build knows. It is the ONE place the fold is spelled: [SetCrewPick] and the +// ladder's [pickedSeat] both go through it, so a pick added to [CrewPicks] is +// accepted everywhere at once. A reader folds a word it does not know to the +// default pick; a writer refuses it. +func knownCrewPick(pick string) (string, bool) { + pick = strings.ToLower(strings.TrimSpace(pick)) + for _, known := range CrewPicks { + if pick == known { + return known, true + } + } + return "", false +} + +// normalCrewPick folds a pick word to one of the three this build knows, +// reading a word it does not know as the default pick. +func normalCrewPick(pick string) string { + if known, ok := knownCrewPick(pick); ok { + return known + } + return DefaultCrewPick +} + +// CrewPickAt is where the seats are picked from on this profile, +// [DefaultCrewPick] when the row is absent. A word this build does not know +// reads as the default pick, silently, the way a retired choice reads +// everywhere else on this sheet. +func CrewPickAt(profileDir string) string { + if value, ok := persistedString(profileDir, KeyCrewPick); ok { + return normalCrewPick(value) + } + return DefaultCrewPick +} + +// SetCrewPick writes the pick row ALONE, in one file write. The word is +// refused the way every choice row refuses one, so a typo cannot land a pick +// nothing reads. It writes no tier row: the pick says where seats are read +// from, and the seats keep the ids on disk until the crew is picked again. +func SetCrewPick(profileDir, pick string) error { + known, ok := knownCrewPick(pick) + if !ok { + return fmt.Errorf("pick one of: %s", strings.Join(CrewPicks, ", ")) + } + return writeProfileValue(profileDir, KeyCrewPick, known) +} + // crewModels is the open-weight table: one row per preset, one model per class. // // THE WORKER COLUMN IS THE DIAL. It holds glm-5.3-flash through balanced, and @@ -308,6 +405,15 @@ func CrewModels(preset string) (map[string]string, bool) { // family's five ids are not any preset of the other. func CrewAt(profileDir string) string { family := CrewSourceAt(profileDir) + if CrewPickAt(profileDir) != CrewPickTable { + // WITH THE PICK OFF THE TABLE the three dial seats are computed ids the + // preset tables do not hold, and comparing the live seats would read + // custom over a crew the person chose. The word answers what the STORED + // rows make instead — the budget the seats are computed at ([pickedSeat] + // reads the same rows) — so the word on the sheet stays the decision it + // summarizes while the ids underneath move with the catalog. + return crewStoredAt(profileDir, family) + } live := make(map[string]string, len(ModelTiers)) for _, tier := range ModelTiers { live[tier] = tierSeatUnder(profileDir, family, tier).Model @@ -321,6 +427,63 @@ func CrewAt(profileDir string) string { return CrewCustom } +// crewStoredAt is the crew the STORED five rows make, in the family given: +// the preset they are, or [CrewCustom]. It is [CrewAt]'s reading when the +// pick row takes the seats off the table, where the live comparison would +// compare computed ids. +// +// Each row is read through the ladder's own row reader ([crewRow]). A row the +// reader cannot answer because the key was NEVER HELD reads the family's +// default-preset id, which is what that tier runs until somebody writes it; +// a row CLEARED ON PURPOSE reads empty and matches nothing, because +// "follows the conversation" is not any of the three; a row that says auto is +// skipped, because auto is the one row with no opinion of its own — it runs +// at whatever budget the rows around it name ([crewPresetUnder]). +func crewStoredAt(profileDir, family string) string { + defaults := crewTableFor(family)[DefaultCrew] + stored := make(map[string]string, len(ModelTiers)) + for _, tier := range ModelTiers { + value, _, source, cleared := crewRow(profileDir, tier) + switch { + case cleared: + stored[tier] = "" + case source == "": + stored[tier] = strings.ToLower(strings.TrimSpace(defaults[tier])) + case IsAuto(value): + stored[tier] = AutoValue + default: + stored[tier] = strings.ToLower(strings.TrimSpace(value)) + } + } + table := crewTableFor(family) + // THE DEFAULT PRESET WINS EVERY TIE, the law [crewPresetUnder] states: a + // profile whose every row says auto is a crew with no opinion of its own, + // and it reads balanced rather than whichever preset the loop met first. + if crewStoredMatches(stored, table[DefaultCrew]) { + return DefaultCrew + } + for _, preset := range CrewPresets { + if preset != DefaultCrew && crewStoredMatches(stored, table[preset]) { + return preset + } + } + return CrewCustom +} + +// crewStoredMatches compares a stored reading with one preset row, skipping +// the tiers whose row says auto. +func crewStoredMatches(stored, preset map[string]string) bool { + for _, tier := range ModelTiers { + if stored[tier] == AutoValue { + continue + } + if stored[tier] != strings.ToLower(strings.TrimSpace(preset[tier])) { + return false + } + } + return true +} + // sameCrew compares two crews class by class, case-folded, because a model id is // matched case-insensitively everywhere else on this surface. func sameCrew(live, preset map[string]string) bool { @@ -418,7 +581,33 @@ func SetCrewSource(profileDir, source string) error { // them would be facts that never vary. The ids are shortened to their base names // because the vendor prefix is the half nobody reads twice. func CrewSummary(profileDir string) string { - return "crew → " + CrewAt(profileDir) + " · " + CrewClasses(profileDir) + return crewSummaryWith(profileDir, "") +} + +// CrewSummaryPick is the confirmation with the pick named when it is not the +// default one: +// +// crew → balanced · learn · brain claude-opus-5 · hands glm-5.3-flash · checks claude-fable-5.1 +// +// The pick rides the preset word because the two are one decision read at two +// heights — how much to spend, and where the models for that money come from +// — and a confirmation that said only `balanced` would drop the half the +// person just changed. At the default pick this is [CrewSummary] itself, so a +// profile nobody has taught the pick to confirms exactly as it always has. +func CrewSummaryPick(profileDir string) string { + return crewSummaryWith(profileDir, CrewPickAt(profileDir)) +} + +// crewSummaryWith is the line both summaries are built from: the pick named +// between the preset and the three classes when one was given that is not the +// default, and never otherwise — a profile at the default pick confirms in +// the words it has always confirmed in. +func crewSummaryWith(profileDir, pick string) string { + head := "crew → " + CrewAt(profileDir) + if pick != "" && pick != CrewPickTable { + head += " · " + pick + } + return head + " · " + CrewClasses(profileDir) } // CrewClasses is the three class names alone: diff --git a/internal/config/crew_test.go b/internal/config/crew_test.go index f397415ef5..ceb7b7e972 100644 --- a/internal/config/crew_test.go +++ b/internal/config/crew_test.go @@ -536,3 +536,69 @@ func TestAnUnwrittenSeatFollowsTheFamily(t *testing.T) { t.Fatalf("an all profile with no rows reads the crew as %q, want %q", got, CrewBalanced) } } + +// ── the pick row ──────────────────────────────────────────────────────────── + +// THE PICK ROW DEFAULTS TO THE TABLE, and a word that is not one of the three +// is refused the way every choice row refuses one. +func TestCrewPickDefaultsToTheTable(t *testing.T) { + dir := t.TempDir() + if got := CrewPickAt(dir); got != CrewPickTable { + t.Fatalf("an untouched profile reads the pick %q, want %q", got, CrewPickTable) + } + if got := CrewAt(dir); got != DefaultCrew { + t.Fatalf("an untouched profile reads the crew %q, want %q", got, DefaultCrew) + } + if err := SetCrewPick(dir, CrewPickLearn); err != nil { + t.Fatalf("learn would not set: %v", err) + } + if got := CrewPickAt(dir); got != CrewPickLearn { + t.Fatalf("the pick reads %q after learn was set", got) + } + if err := SetCrewPick(dir, "measured"); err == nil { + t.Fatal("a word that is not one of the three was accepted") + } + if got := CrewPickAt(dir); got != CrewPickLearn { + t.Fatalf("a refused write left the pick reading %q", got) + } +} + +// THE PICK-AWARE CREW WORD READS THE BUDGET THE SEATS ARE COMPUTED AT. The +// live seats are computed ids the preset tables do not hold, so the word is +// read off the STORED rows — a profile that applied a crew and then set the +// pick keeps saying the crew it picked — and a row the person emptied on +// purpose still reads custom, because "follows the conversation" is not any +// of the three. +func TestTheCrewWordReadsThePresetThePickComputesAt(t *testing.T) { + dir := t.TempDir() + if err := ApplyCrew(dir, CrewMax); err != nil { + t.Fatal(err) + } + if err := SetCrewPick(dir, CrewPickLearn); err != nil { + t.Fatal(err) + } + if got := CrewAt(dir); got != CrewMax { + t.Fatalf("a max crew under learn reads %q, want %q", got, CrewMax) + } + // And a hand-typed row makes the word custom, as it always has: the stored + // comparison is the honest one, not a second way to say balanced. + rows, err := os.ReadFile(BudgetConfigPath(dir)) + if err != nil { + t.Fatal(err) + } + values := map[string]string{} + if err := json.Unmarshal(rows, &values); err != nil { + t.Fatal(err) + } + values[tierKeyFor(ModelTierMastermind)] = "openai/gpt-5" + raw, err := json.Marshal(values) + if err != nil { + t.Fatal(err) + } + if err := os.WriteFile(BudgetConfigPath(dir), raw, 0o600); err != nil { + t.Fatal(err) + } + if got := CrewAt(dir); got != CrewCustom { + t.Fatalf("a hand-typed row under learn reads the crew %q, want custom", got) + } +} diff --git a/internal/config/derivation_test.go b/internal/config/derivation_test.go index 915051a057..e8c0ae04d7 100644 --- a/internal/config/derivation_test.go +++ b/internal/config/derivation_test.go @@ -127,6 +127,10 @@ var settingReaders = map[string]string{ KeyVisionModel: "VisionModel", KeyAttribution: "Attribution", KeyModelPool: "ModelPoolSettingAt", + // The crew's pick row names its resolver: the surfaces read the pick to + // say it beside the crew word, and the ladder turns it into computed seats + // ([pickedSeat]) — the identifier proves the word reaches a surface. + KeyCrewPick: "CrewPickAt", // The prompt-profile row names the session field the word lands in, which // is what the far side actually reads: cmd/codeaf's applyV3Governance puts // PromptProfileAt's answer there and internal/session's promptprofile.go diff --git a/internal/config/seats.go b/internal/config/seats.go index c0ffd457ad..84d29275ab 100644 --- a/internal/config/seats.go +++ b/internal/config/seats.go @@ -73,10 +73,18 @@ const ( // derived, not named. SeatComputed SeatSource = "computed" // SeatTable is a tier row that says `auto` answering from the family's - // table row ([autoRow]) because nothing could be computed — no catalog, or - // no pick off it. The id is the preset's own, and the rung says which kind - // of answer it was. + // table row ([autoRow], [pickedModel]) because nothing could be computed — + // no catalog, or no pick off it. The id is the preset's own, and the rung + // says which kind of answer it was. SeatTable SeatSource = "table" + // SeatLearned is a seat computed at the crew's budget under the `learn` + // pick ([CrewPickAt]): the catalog's own figures, plus the Model Pool's + // measurements and the person's own judged runs carried as a quality + // prior. It is a rung of its own, beside [SeatComputed], because the one + // fact a reader of a run is checking — was this id derived, and from + // what — is a different answer under the two words: the catalog alone, + // or the catalog plus what runs measured. + SeatLearned SeatSource = "learned" // SeatDefault is this build's choice, for a profile that has never said // anything about models at all. SeatDefault SeatSource = "default" @@ -265,6 +273,17 @@ func tierSeatUnder(profileDir, family, tier string) Seat { model, source, preset = autoRow(profileDir, family, tier) return Seat{Role: tierSeatRole(tier), Model: model, Source: source, Crew: preset} } + // AND THE PICK ROW ANSWERS FOR THE SEATS NOBODY NAMED, before the row's + // own rung is read: a pick of catalog or learn computes the dial seats at + // the crew's preset ([pickedSeat]), and leaves everything it does not + // answer — a hand-typed id, a cleared row, the two seats that always read + // the table — exactly as it was. A cleared row is a deliberate answer + // ("follow the conversation") and no pick unsays it on this surface. + if !cleared { + if seat, ok := pickedSeat(profileDir, family, tier, model); ok { + return seat + } + } return Seat{Role: tierSeatRole(tier), Model: model, Source: source, From: from} } @@ -339,7 +358,9 @@ func (s Seat) Rung() string { // substitution this rung exists to report. return s.crewRung() + ", inherited" case SeatComputed: - return s.crewRung() + ", computed" + return s.crewRung() + ", computed from the catalog" + case SeatLearned: + return s.crewRung() + ", learned" case SeatTable: return s.crewRung() + ", table" } @@ -599,8 +620,18 @@ func resolveSeat(role SeatRole, profileDir, flag, tier, fallback string) Seat { // fallback except silence: a row cleared on purpose says "follow the // conversation", which a headless run has no conversation to answer with, // and it lands on the same bottom rung a profile that said nothing does. - model, from, source, _ := crewRow(profileDir, tier) + model, from, source, cleared := crewRow(profileDir, tier) if source == "" { + // THE PICK ANSWERS BEFORE THE BOTTOM RUNG: an unwritten seat under a + // pick of catalog or learn is computed at the crew's preset + // ([pickedSeat]), the same answer the conversation reads, and a row + // cleared on purpose still falls through — the pick answers for seats + // nobody named, and a cleared row is somebody naming emptiness. + if !cleared { + if seat, ok := pickedSeat(profileDir, CrewSourceAt(profileDir), tier, ""); ok { + return seat + } + } seat.Model, seat.Source = fallback, SeatDefault return seat } @@ -614,6 +645,12 @@ func resolveSeat(role SeatRole, profileDir, flag, tier, fallback string) Seat { seat.Crew = preset return seat } + // AND BETWEEN THE ROW AND ITS OWN RUNG, the pick: a written row holding + // the preset's own table value is the preset answering rather than a pin, + // and the pick computes it ([pickedSeat]); a hand-typed id keeps its rung. + if seat, ok := pickedSeat(profileDir, CrewSourceAt(profileDir), tier, model); ok { + return seat + } seat.Model, seat.Source, seat.From, seat.Crew = model, source, from, CrewAt(profileDir) return seat } diff --git a/internal/config/seats_test.go b/internal/config/seats_test.go index cf84e5b4c5..977aa960c2 100644 --- a/internal/config/seats_test.go +++ b/internal/config/seats_test.go @@ -14,6 +14,8 @@ import ( "testing" "github.com/Agent-Field/agentfield/sdk/go/ai" + "github.com/Agent-Field/codeaf/internal/catalog" + "github.com/Agent-Field/codeaf/internal/crewpick" "github.com/Agent-Field/codeaf/internal/router" ) @@ -841,3 +843,121 @@ func TestTheReceiptNamesTheSpellingOfTheVariableThatAnswered(t *testing.T) { t.Fatalf("rung = %q model = %q, want %q and its value", got, seats.Work.Model, ModelEnv) } } + +// ── the pick row's half of the ladder ─────────────────────────────────────── + +// A PICK OFF THE TABLE COMPUTES THE DIAL SEATS AND LEAVES THE OTHER TWO ALONE. +// The three seats that the presets dial — worker, careful work, mastermind — +// are computed at the crew's preset, `learned` under the learn word and +// `computed from the catalog` under the catalog word; reflex and small work +// always read the table, which is what makes a pick a dial over three seats +// and not a second crew. The ids are asserted against crewpick's own front, +// the way the shipped tables are (auto_test.go owns the method). +func TestAPickOffTheTableComputesTheDialSeats(t *testing.T) { + restore := AutoModels + AutoModels = func() []catalog.Model { return autoTestRows() } + defer func() { AutoModels = restore }() + + _, balanced, _ := crewpick.Presets(crewpick.Front(autoCandidates(autoTestRows()), crewpick.DefaultShapes(), crewpick.All)) + table, _ := CrewModelsForSource(CrewSourceAt(""), DefaultCrew) + + for _, pick := range []struct { + word string + source SeatSource + }{ + {CrewPickLearn, SeatLearned}, + {CrewPickCatalog, SeatComputed}, + } { + dir := writeProfileRows(t, map[string]string{KeyCrewPick: pick.word}) + for _, c := range []struct { + tier string + want string + }{ + {ModelTierWorker, balanced.Worker}, + {ModelTierHigh, balanced.High}, + {ModelTierMastermind, balanced.Mastermind}, + } { + seat := TierSeatAt(dir, c.tier) + if seat.Model != c.want || seat.Source != pick.source { + t.Errorf("%s: the %s seat reads %q (%s), want %q (%s)", pick.word, c.tier, + seat.Model, seat.Source, c.want, pick.source) + } + if seat.Crew != DefaultCrew { + t.Errorf("%s: the %s seat ran at %q, want the untouched profile's %q", pick.word, c.tier, seat.Crew, DefaultCrew) + } + } + // THE PICK COMPUTES RATHER THAN READS: the front's ids are not the + // preset table's own, so a seat carrying the front's id is a seat the + // catalog answered and not the table in disguise. + if balanced.Worker == table[ModelTierWorker] { + t.Fatal("the test rows field a front identical to the shipped table; the pick would be asserting nothing") + } + // REFLEX AND SMALL WORK ALWAYS READ THE TABLE, and a pick does not + // touch them: the same rung an untouched profile has always read, with + // the table's own id. + for _, tier := range []string{ModelTierReflex, ModelTierLow} { + seat := TierSeatAt(dir, tier) + if seat.Model != table[tier] { + t.Errorf("%s: the %s seat reads %q, want the table's %q", pick.word, tier, seat.Model, table[tier]) + } + if seat.Source != SeatDefault { + t.Errorf("%s: the %s seat reads rung %q, want the table's own default", pick.word, tier, seat.Source) + } + } + } +} + +// A HAND-TYPED MODEL SURVIVES THE PICK. The pick answers for the seats nobody +// named, not over the names they typed — and the headless ladder says the same +// thing the conversation says, because a pick that meant one thing in chat and +// another under `codeaf do` would be the divergence the ladder exists to +// prevent. +func TestAHandTypedSeatSurvivesThePick(t *testing.T) { + restore := AutoModels + AutoModels = func() []catalog.Model { return autoTestRows() } + defer func() { AutoModels = restore }() + + dir := writeProfileRows(t, map[string]string{ + KeyCrewPick: CrewPickLearn, + tierKeyFor(ModelTierWorker): "someone/else", + }) + + seat := TierSeatAt(dir, ModelTierWorker) + if seat.Model != "someone/else" || seat.Source != SeatCrew { + t.Fatalf("a hand-typed worker reads %q (%s), want someone/else (crew)", seat.Model, seat.Source) + } + // The seats the pick does answer are computed at the preset the stored + // rows still make — the hand-typed row costs them their preset match, so + // the budget falls back to the default preset, which is what an undecided + // profile runs at. + _, balanced, _ := crewpick.Presets(crewpick.Front(autoCandidates(autoTestRows()), crewpick.DefaultShapes(), crewpick.All)) + high := TierSeatAt(dir, ModelTierHigh) + if high.Model != balanced.High || high.Source != SeatLearned { + t.Fatalf("the careful seat reads %q (%s), want %q (learned)", high.Model, high.Source, balanced.High) + } + + // AND THE HEADLESS LADDER SAYS THE SAME. The work seat is the worker tier + // and the plan seat is the mastermind tier, and both must answer exactly + // what the conversation's rows answer — the receipt's own claim. + seats := ResolveSeats(dir, "", "") + if seats.Work.Model != "someone/else" || seats.Work.Source != SeatCrew { + t.Fatalf("headless, the work seat reads %q (%s), want someone/else (crew)", seats.Work.Model, seats.Work.Source) + } + if seats.Plan.Model != balanced.Mastermind || seats.Plan.Source != SeatLearned { + t.Fatalf("headless, the plan seat reads %q (%s), want %q (learned)", seats.Plan.Model, seats.Plan.Source, balanced.Mastermind) + } +} + +// THE PICK'S RUNGS SAY THEIR WORDS, so a receipt never has to be guessed at: +// the catalog word computes, the learn word learned, and the table's own +// fallback keeps its one word. +func TestThePickRungsSayTheirWords(t *testing.T) { + seat := Seat{Crew: CrewBalanced, Source: SeatComputed} + if got := seat.Rung(); got != "crew "+CrewBalanced+", computed from the catalog" { + t.Errorf("the computed rung reads %q, want crew %s, computed from the catalog", got, CrewBalanced) + } + seat.Source = SeatLearned + if got := seat.Rung(); got != "crew "+CrewBalanced+", learned" { + t.Errorf("the learned rung reads %q, want crew %s, learned", got, CrewBalanced) + } +} diff --git a/internal/config/settings.go b/internal/config/settings.go index acf820d241..b5c438a1e7 100644 --- a/internal/config/settings.go +++ b/internal/config/settings.go @@ -233,6 +233,18 @@ const ( // worker row's own reason: a repository that could answer it could send a // visitor's work, and their credit, to a vendor they never chose. KeyCrewSource = "models.crew.source" + // KeyCrewPick is where the crew's seats are picked from when a tier row + // does not hold a model id of its own. The three words are read and + // answered by the crew's own machinery (crew.go's [CrewPickAt] and + // [SetCrewPick]) beside the words [KeyCrew] and [KeyCrewSource] take: the + // crew row says how much to spend, the family row says which shelf those + // budgets name, and this row says where the models for that money come + // from — the rows this build measured, or a computation off the catalog + // made again on every read, with or without what the Model Pool measured. + // It is PROFILE-ONLY with the crew and family rows, for the worker row's + // own reason: a repository that could answer it could send a visitor's + // work, and their credit, to a model nobody on that machine chose. + KeyCrewPick = "models.crew.pick" // KeyMouse is whether the surface reports the mouse at all. ON is the // default ([DefaultMouse]), because hover, click and the wheel are v3's own // language and the thing they cost is bought back by a key: an alt-screen @@ -2317,6 +2329,24 @@ func (s *Settings) build() []Setting { read: func() string { return CrewAt(dir) }, write: func(raw string) error { return writeCrew(dir, raw) }, }, + // WHERE THE SEATS ARE PICKED FROM, one row under the crew. The crew row + // says how much to spend and this says where the models for that money + // come from when a tier row does not hold a person's own id: the rows + // this build measured and shipped, or the same three budgets recomputed + // off the catalog on every read, with or without what the Model Pool + // and the person's own judged runs measured. The words are the crew's + // own (crew.go), so the row and the ladder cannot disagree about what a + // pick means. + Setting{ + Key: KeyCrewPick, Category: CategoryModels, Kind: SettingChoice, + Label: "picked from", Choices: CrewPicks, + Hint: "where the crew's models come from. table: the rows we measured. " + + "catalog: recomputed from today's published prices and scores at your " + + "crew's budget. learn: catalog plus the Model Pool's measurements and " + + "your own judged runs.", + read: func() string { return CrewPickAt(dir) }, + write: func(raw string) error { return SetCrewPick(dir, raw) }, + }, // THE FAMILY THE THREE WORDS DRAW FROM, one row under the crew. It sits // beside the crew row because it is the same decision read one level up: // the crew row says which five models, and this says which shelf those From f673a6c2b3397043146fa439879744298d7572fd Mon Sep 17 00:00:00 2001 From: santoshkumarradha Date: Thu, 17 Sep 2026 09:57:53 -0400 Subject: [PATCH 096/173] docs, manual: the paper gains its system architecture and the seed sentence keeps the routing word for lanes Co-Authored-By: Claude Fable 5.1 --- docs/design/model-pool/learn.pdf | Bin 340120 -> 350262 bytes docs/design/model-pool/learn.tex | 52 +++++++++++++++++- .../manual/chat/running-from-the-terminal.md | 2 +- 3 files changed, 52 insertions(+), 2 deletions(-) diff --git a/docs/design/model-pool/learn.pdf b/docs/design/model-pool/learn.pdf index f0c88de96c32f309617afe52a5bdc4c0c8af6294..81becb4895feda7ab9abe3b5648c2a404fe9bda7 100644 GIT binary patch delta 50752 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%0A7< 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

  • L>UyQr>d0I(8kdG9La5lVXuH}wl!-Ib|&}~%- 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 delta 40691 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 diff --git a/docs/design/model-pool/learn.tex b/docs/design/model-pool/learn.tex index 2499b96503..58f7e8a860 100644 --- a/docs/design/model-pool/learn.tex +++ b/docs/design/model-pool/learn.tex @@ -13,6 +13,7 @@ \usepackage{fancyhdr} \usepackage[hidelinks]{hyperref} \usepackage{caption} +\usepackage{float} \captionsetup{font=small,labelfont=bf,width=.92\textwidth} \usepackage{abstract} \renewcommand{\abstractnamefont}{\normalfont\bfseries} @@ -479,6 +480,55 @@ \section{Experiments on our measured runs} The truth in these experiments is our own fitted table from one team's tasks, so the size of the gain is the shape of the effect and not a promise for every install. The script \code{figures.py} beside this document reproduces every figure and table from the two CSV files it reads. +\section{System architecture} +\label{sec:arch} + +The statistics of Sections~\ref{sec:pool}--\ref{sec:decide} fix what the system computes; this section fixes where the bytes live, so that the privacy claims can be checked against the deployment rather than taken on trust. Figure~\ref{fig:arch} shows the four components. The design rule is that the relay holds nothing worth stealing and nothing that cannot be rebuilt. + +\paragraph{The binary.} +Everything that touches a task runs inside the install: the catalog read, the posterior, the draw, the run, the judge call, the own sheet and the outbox. The judge is an ordinary model call through the install's own provider credentials; the record it reads never leaves the machines the install already trusts with its work. The outbox is a newline-delimited file under the profile directory; \code{codeaf pool status} prints its pending rows, and a user who wants to see exactly what would be sent can read the file. The loader reads the cached index if one is present and the embedded seed index otherwise, once at start-up; a refresh runs in the background and takes effect at the next start, so a running task's picks never move under it. + +\paragraph{The relay.} +The reference relay is a single edge function with an object store behind it, deployable in minutes on a commodity edge platform (\code{wrangler deploy} from \code{relay/} in the repository; the runbook in that directory is ten steps). It exposes two routes. \code{POST /v1/rows} accepts a batch of rows, validates the schema, folds each row into the sender's triple for its cell and day, and discards the row: the store holds the sheet of Definition~2 and nothing else, one triple per (install, cell, day), so its size is bounded by installs times cells times days and not by traffic, and a row that has been folded cannot be recovered from it. \code{GET /index.json} serves the last published index and its detached signature. An hourly scheduled job merges the sheet, runs the judge-effect, robust-aggregation and drift estimates of Section~5, writes the index, signs it with an Ed25519 key held as a platform secret, and mirrors the pair of files to a public git branch so that they are browsable, diffable and forkable. The function keeps no request logs and stores no network address; the install nonce is the only identity in the store, and rate limiting is by nonce (a cap on rows per day) rather than by address. + +\paragraph{Keys and verification.} +The signing key's public half is compiled into the binary as a list, so a key can be rotated by shipping a binary that carries both; the private half exists only in the platform secret store. Verification is client-side: the binary checks the signature before it reads a single cell, refuses a version lower than the one it holds (Section~5.2's join makes a rollback detectable), and on any failure keeps the cached copy beside an error. Because verification is local, the index can be served from anywhere: the mirror, a static file host, or a directory on disk, which is what the \code{file://} address is for. + +\paragraph{Trust and failure.} +What a compromised relay learns is the sheet: counts, sums and sums of squares of scores per (install nonce, role, model, judge, day, size, door). It learns no task, no code, no prompt, no user, and no address. What a compromised relay can do is publish a bad index, which the signature stops unless the key is also taken; a taken key is retired by shipping the next key, and the min-installs and robust-aggregation rules bound what a flood of fabricated nonces can move in the meantime. What an unreachable relay costs is nothing at all: the cache, then the seed, answer every read, and the outbox waits. Any install can run the same function against its own store and point its \code{CODEAF\_MODEL\_POOL\_URL} and \code{\_SUBMIT\_URL} at it; two relays that receive the same publications produce the same sheet (Proposition~\ref{prop:crdt}), so a private relay and the public one can be federated later by a one-way copy of publications with no reconciliation step. + +\begin{figure}[H] +\centering +\begin{tikzpicture}[x=1cm,y=1cm, + box/.style={draw,rounded corners=2pt,inner sep=4pt,font=\small,align=center,minimum height=8mm}, + store/.style={box,fill=black!6}, + net/.style={box,fill=blue!6}, + arr/.style={-{Latex[length=2mm]},thick}, + lbl/.style={font=\scriptsize,fill=white,inner sep=1.5pt} +] +\node[box,text width=3.2cm] (bin) at (2.0,0) {binary\\posterior, draw, run, judge}; +\node[store,text width=3.2cm] (outbox) at (2.0,-1.8) {outbox\\rows, local file}; +\node[store,text width=3.2cm] (cache) at (2.0,-3.6) {index cache\\and embedded seed}; +\node[net,text width=6.1cm] (fn) at (8.2,-1.8) {edge function\\\code{POST /v1/rows}: fold a row into its triple\\\code{GET /index.json}: serve the last index}; +\node[store,text width=6.1cm] (kv) at (8.2,-4.2) {object store\\one triple per (install, cell, day)}; +\node[net,text width=3.4cm] (cron) at (13.7,-4.2) {hourly job\\merge, \eqref{eq:als}, Huber, \eqref{eq:drift}, sign}; +\node[store,text width=3.4cm] (mirror) at (13.7,-1.8) {public mirror\\\code{index.json} + \code{.sig}}; +\node[box,dashed,text width=3.4cm] (secret) at (13.7,-6.0) {platform secret\\Ed25519 private key}; +\draw[arr] (bin) -- (outbox); +\draw[arr] (cache.west) -- ++(-0.4,0) |- node[lbl,pos=0.25,left]{read at start} (bin.west); +\draw[arr] (outbox.east) -- node[lbl,above]{https, batched} (fn.west |- outbox.east); +\draw[arr] (fn) -- node[lbl,right]{fold} (kv); +\draw[arr] (kv) -- node[lbl,above]{sheet} (cron); +\draw[arr] (cron) -- node[lbl,right]{signed index} (mirror); +\draw[arr] (secret) -- (cron); +\draw[arr] (mirror.west) -- node[lbl,below,pos=0.6]{pull, verify} (fn.east |- mirror.west); +\draw[arr] ([xshift=-2.2cm]fn.south) |- node[lbl,pos=0.75,above]{cached index} (cache.east); +\node[font=\scriptsize] at (8.0,0.2) {no request logs, no addresses stored, rows discarded after folding}; +\end{tikzpicture} +\caption{Deployment. The relay is one edge function, one object store holding only the sheet, and one scheduled signing job; the install verifies every index locally and never blocks on the network. Any of the three network components can be replaced by a static file, a directory, or a self-hosted copy.} +\label{fig:arch} +\end{figure} + \section{What the user controls} \begin{center}\small @@ -495,7 +545,7 @@ \section{What the user controls} \section{Status} -On the branch this document ships with: the catalog crew of Section~\ref{sec:catalog}; the shrinkage blend of the Corollary with $k=30$; the index document, its reader and signed pull; the outbox and the sheet; the judge and its deterministic choice; the settings; the seed index. Specified here and next to land: the judge fixed effects, Huber aggregation and drift estimate in the relay; the hierarchical cap~\eqref{eq:cap} and the moment estimators~\eqref{eq:sigma}--\eqref{eq:tau0}; the Thompson draw with role-level updates and the settled probability $\pi^\ast$; the relay itself. Until the relay exists the pool is the seed index plus each install's own sheet. +On the branch this document ships with: the catalog crew of Section~\ref{sec:catalog}; the shrinkage blend of the Corollary with $k=30$; the index document, its reader and signed pull; the outbox and the sheet; the judge and its deterministic choice; the settings; the seed index. Specified here and next to land: the judge fixed effects, Huber aggregation and drift estimate in the relay; the hierarchical cap~\eqref{eq:cap} and the moment estimators~\eqref{eq:sigma}--\eqref{eq:tau0}; the Thompson draw with role-level updates and the settled probability $\pi^\ast$; the relay of Section~\ref{sec:arch}. Until the relay exists the pool is the seed index plus each install's own sheet, and nothing is uploaded. \begin{thebibliography}{9}\small \bibitem{thompson1933} W.~R. Thompson. On the likelihood that one unknown probability exceeds another in view of the evidence of two samples. \emph{Biometrika} 25:285--294, 1933. diff --git a/internal/manual/chat/running-from-the-terminal.md b/internal/manual/chat/running-from-the-terminal.md index 1a6f92d3bd..ef62360bfb 100644 --- a/internal/manual/chat/running-from-the-terminal.md +++ b/internal/manual/chat/running-from-the-terminal.md @@ -529,7 +529,7 @@ codeaf pool [show|status|verify] [--json] [--key key] and the addresses in force with the word saying where each came from (`default`, `setting`, `env` or `ci`), then what index is cached and how old it is, or `no index cached yet · built-in seed of `. The binary -carries a seed index of our own measured runs, read until a fresher signed +carries a seed index of our own scored runs, read until a fresher signed one is cached. `status` adds what is waiting to be sent and whether the mode allows sending and reading. `--json` prints the same answer as one object. Neither form touches the network. From cc63191429a9868d6e8cd6f51ffde4d64a3d5d3e Mon Sep 17 00:00:00 2001 From: santoshkumarradha Date: Thu, 17 Sep 2026 09:58:14 -0400 Subject: [PATCH 097/173] tui3: the picked-from row takes its place beside the crew word, and /crew speaks the three words --- internal/tui3/crew.go | 107 +++++++++++++++++++++++++++------ internal/tui3/crew_test.go | 119 +++++++++++++++++++++++++++++++++++-- internal/tui3/settings.go | 19 ++++++ 3 files changed, 223 insertions(+), 22 deletions(-) diff --git a/internal/tui3/crew.go b/internal/tui3/crew.go index 528536b62b..f9c63cfca0 100644 --- a/internal/tui3/crew.go +++ b/internal/tui3/crew.go @@ -38,21 +38,45 @@ func (a *app) runCrew(arg string) { arg = strings.ToLower(strings.TrimSpace(arg)) if arg == "" { a.closeLists() - a.crewPick.start(config.CrewAt(a.profileDir), config.CrewSourceAt(a.profileDir), a.crewInheritedLine()) + a.crewPick.start(config.CrewAt(a.profileDir), config.CrewSourceAt(a.profileDir), + a.crewInheritedLine(), config.CrewPickAt(a.profileDir)) a.touch() return } + // THE THREE PICK WORDS, beside the presets: `/crew learn` says where the + // seats come from without moving any of them. The row is written alone and + // the confirmation is [app.crewApplied]'s, which names the pick beside the + // preset ([config.CrewSummaryPick]) — the same word the segment and + // /status now carry, so the answer and the frame cannot drift apart. + for _, pick := range config.CrewPicks { + if arg != pick { + continue + } + if err := config.SetCrewPick(a.profileDir, pick); err != nil { + a.note("could not set the pick · " + err.Error()) + return + } + a.crewApplied() + return + } if _, ok := config.CrewModels(arg); !ok { - // AN UNKNOWN WORD CHANGES NOTHING AND SHOWS THE THREE. A refusal that - // only said "no" would leave a person guessing at a word they were one - // letter away from, and the listing is the answer to the question they - // were really asking. - // The listing is a two-column page — a class word, then the model it would - // set — so its right-hand column is what THE PAYLOAD RULE lifts, read back - // off the text this call just built (payload.go's [columnFacts]). + // AN UNKNOWN WORD CHANGES NOTHING AND SAYS THE SIX, the shape every + // choice this surface refuses takes (effortchip.go's [app.runEffort]): + // a refusal that only said "no" would leave a person guessing at a word + // they were one letter away from, and the words are the answer to the + // question they were really asking. The listing is kept under them, + // because it is the comparison between the presets the words set. + // The listing is a two-column page — a class word, then the model it + // would set — so its right-hand column is what THE PAYLOAD RULE lifts, + // read back off the text this call just built (payload.go's + // [columnFacts]). + words := make([]string, 0, len(config.CrewPresets)+len(config.CrewPicks)) + words = append(words, config.CrewPresets...) + words = append(words, config.CrewPicks...) listing := a.crewListing() - a.noteFacts("/crew "+arg+" · not one of the three\n\n"+listing, - columnFacts(listing, false)...) + facts := append(words, columnFacts(listing, false)...) + a.noteFacts("/crew "+arg+" · not a crew word · "+strings.Join(words, " · ")+"\n\n"+listing, + facts...) return } a.applyCrew(arg) @@ -99,7 +123,12 @@ func (a *app) crewApplied() { if id := a.talkingTo(); id != "" { facts = append(facts, id) } - a.noteFacts(config.CrewSummary(a.profileDir)+" · "+a.crewUnchangedClause(), facts...) + // THE SUMMARY IS THE PICK-AWARE ONE ([config.CrewSummaryPick]), so a crew + // applied under a pick of catalog or learn confirms with the pick named + // beside the preset — the half the frame's segment and /status now carry. + // At the default pick it is the line the command has always confirmed + // with. + a.noteFacts(config.CrewSummaryPick(a.profileDir)+" · "+a.crewUnchangedClause(), facts...) } // applyCrewUnder is the chooser's enter: the family and the preset are ONE @@ -194,8 +223,9 @@ func (a *app) crewSegment() string { // ── the one reading of the four rows ──────────────────────────────────────── // crewReading is the profile's crew as this surface last read it: the preset -// word [config.CrewAt] derives from the live rows, the three class names beside -// it, and the settings generation the pair was read at. +// word [config.CrewAt] derives from the live rows, the pick row's word +// ([config.CrewPickAt]) when it is not the default one, the three class names +// beside it, and the settings generation the pair was read at. type crewReading struct { // word and segment are the two readings BUILT AT THE READING and not at the // draw. The status line asks for the segment on every frame, and a surface @@ -211,6 +241,11 @@ type crewReading struct { clause string preset string classes string + // pick is the pick row's word as the reading read it, and is the default + // word rather than empty — a surface that wants to know whether the pick + // is off the table compares it with [config.CrewPickTable], the way the + // word and segment above were built. + pick string // dir is the profile the pair was read from, so a surface handed a // different one answers from that one and not from a snapshot of the last. dir string @@ -280,12 +315,27 @@ func (a *app) crewReading() (crewReading, bool) { } if generation := config.SettingsGeneration(); !a.crew.taken || a.crew.generation != generation || a.crew.dir != a.profileDir { preset, classes := config.CrewAt(a.profileDir), config.CrewClasses(a.profileDir) + pick := config.CrewPickAt(a.profileDir) + // THE PICK RIDES THE WORD WHEN IT IS NOT THE DEFAULT ONE, in both the + // page's word and the status line's segment: `balanced · learn` says + // the budget and where the models for it come from in one word, and a + // segment that said only `balanced` under a learn pick would be the + // half-answer the crew segment exists to prevent. At the default pick + // the word is exactly what it has always been — the table is where the + // seats have always come from, and a word that named it would say + // something a person did not choose. + word, segment := preset+" · "+classes, "crew "+preset + if pick != config.CrewPickTable { + word = preset + " · " + pick + " · " + classes + segment = "crew " + preset + " · " + pick + } a.crew = crewReading{ - word: preset + " · " + classes, - segment: "crew " + preset, + word: word, + segment: segment, clause: preset + " crew", preset: preset, classes: classes, + pick: pick, dir: a.profileDir, generation: generation, taken: true, @@ -440,13 +490,19 @@ type crewPicker struct { // file, so a picker that asked the profile per frame would put a disk read // on the frame clock for a fact that cannot move while the list is up. inherited string + // pick is the pick row's word as the door read it, and the default word + // rather than empty. IT IS NOT STAGED: the family below is staged and + // enter writes it, but a pick word typed at the door is written at once + // ([app.runCrew]), so the pick on screen is always the pick in force — a + // reading, and not a row the list's keys could move. + pick string } -func (p *crewPicker) start(current, source, inherited string) { +func (p *crewPicker) start(current, source, inherited, pick string) { // source is already a word this build knows: every caller hands it // [config.CrewSourceAt]'s answer, which folds blank, unknown and retired words // back to the default family. The fold lives there and nowhere else. - *p = crewPicker{open: true, current: current, persistedSource: source, inherited: inherited, source: source} + *p = crewPicker{open: true, current: current, persistedSource: source, inherited: inherited, source: source, pick: pick} for i, preset := range config.CrewPresets { if preset == current { p.cursor = i @@ -489,6 +545,12 @@ const ( // crewCustomLine is the fourth reading, said as a fact about where the person // is rather than as a fourth row they could pick. crewCustomLine = "yours is none of the three — picking one puts all five back" + // crewPickLead is the pick's lead in the chooser — `picked from learn` — + // the same words the crew word and the segment ride when the pick is off + // its default ([app.crewReading]). It is said as a reading and not a row: + // the pick is written by a word at the door, not by anything this list's + // keys move. + crewPickLead = "picked from " // crewInheritedLead and crewInheritedTail wrap the row a profile older than // a seat never wrote: // @@ -515,6 +577,9 @@ func (p *crewPicker) height() int { if p.current == config.CrewCustom { height++ } + if p.pick != "" && p.pick != config.CrewPickTable { + height++ + } if p.inherited != "" { height++ } @@ -634,6 +699,14 @@ func (p *crewPicker) rows(width, n int, pal palette, hover int, a *app) []string if p.current == config.CrewCustom { out = append(out, pal.dim(fit(crewCustomLine, width))) } + // AND THE PICK, a fact about the saved crew like the custom line above it. + // It is said only when it is off the default — the table is where the + // seats have always come from, and a line naming it would say something + // nobody chose — and it takes no cursor and no ground, because the pick is + // not staged here: a word typed at the door writes it at once. + if p.pick != "" && p.pick != config.CrewPickTable { + out = append(out, pal.dim(fit(crewPickLead+p.pick, width))) + } // AND THE ROW NOBODY WROTE, said last among the readings and before the door // out: it is the one fact on this list that is about the person's own five // rows rather than about the three on offer, and the answer to it is the diff --git a/internal/tui3/crew_test.go b/internal/tui3/crew_test.go index a65f8c470c..6c09e54d81 100644 --- a/internal/tui3/crew_test.go +++ b/internal/tui3/crew_test.go @@ -169,7 +169,10 @@ func TestCrewAppliesAPresetAndConfirmsInOneLine(t *testing.T) { } } -// AN UNKNOWN WORD CHANGES NOTHING AND SHOWS THE THREE. +// AN UNKNOWN WORD CHANGES NOTHING AND SAYS THE SIX — the three presets and the +// three pick words — the shape every choice this surface refuses takes +// (effortchip.go's [app.runEffort]): a refusal that only said "no" would leave +// a person guessing at a word they were one letter away from. func TestCrewRefusesAWordThatIsNotOneOfTheThree(t *testing.T) { a, dir := sheetApp(t) a.slash("/crew cheap") @@ -178,14 +181,20 @@ func TestCrewRefusesAWordThatIsNotOneOfTheThree(t *testing.T) { t.Fatalf("an unknown word changed the crew to %q", got) } text := lastNote(t, a) - if !strings.Contains(text, "not one of the three") { + if !strings.Contains(text, "not a crew word") { t.Fatalf("the refusal reads %q", text) } - for _, preset := range config.CrewPresets { - if !strings.Contains(text, preset) { - t.Errorf("the refusal does not offer %q:\n%s", preset, text) + words := make([]string, 0, len(config.CrewPresets)+len(config.CrewPicks)) + words = append(words, config.CrewPresets...) + words = append(words, config.CrewPicks...) + for _, word := range words { + if !strings.Contains(text, word) { + t.Errorf("the refusal does not offer %q:\n%s", word, text) } } + if got := config.CrewPickAt(dir); got != config.CrewPickTable { + t.Fatalf("an unknown word changed the pick to %q", got) + } } // A CREW SOMEBODY ASSEMBLED THEMSELVES READS AS CUSTOM, and the listing says how @@ -883,3 +892,103 @@ func TestTheStatusSheetCarriesTheCrewUnderTheModel(t *testing.T) { } t.Fatalf("the sheet has no crew line: %+v", items) } + +// ── the pick row ──────────────────────────────────────────────────────────── + +// THE THREE PICK WORDS ARE DOOR WORDS, beside the presets: `/crew learn` says +// where the seats come from and moves no model id, and the confirmation, the +// crew word and the segment all carry it. `/crew table` is the way back. +func TestCrewTakesThePickWords(t *testing.T) { + a, dir := sheetApp(t) + a.slash("/crew learn") + + if got := config.CrewPickAt(dir); got != config.CrewPickLearn { + t.Fatalf("/crew learn left the pick reading %q", got) + } + if got := config.CrewAt(dir); got != config.DefaultCrew { + t.Fatalf("a pick word moved the crew to %q", got) + } + // THE CREW WORD AND THE SEGMENT SAY IT: `balanced · learn` on the page, + // `crew balanced · learn` on the status line — the word a person reads in + // /status is the word the frame carries. + if word := a.crewWord(); !strings.Contains(word, "· learn") { + t.Fatalf("the crew word reads %q, want the pick beside the preset", word) + } + if seg := a.crewSegment(); seg != "crew balanced · learn" { + t.Fatalf("the segment reads %q, want crew balanced · learn", seg) + } + if text := lastNote(t, a); !strings.Contains(text, "crew → "+config.DefaultCrew+" · learn") { + t.Fatalf("the confirmation reads %q, want the pick named", text) + } + + a.slash("/crew table") + if got := config.CrewPickAt(dir); got != config.CrewPickTable { + t.Fatalf("/crew table left the pick reading %q", got) + } + if seg := a.crewSegment(); seg != "crew "+config.DefaultCrew { + t.Fatalf("the segment reads %q after the way back, want the plain crew word", seg) + } +} + +// THE CHOOSER NAMES THE PICK when it is off the table, and says nothing about +// the default: the table is where the seats have always come from, and a line +// naming it would say something nobody chose. +func TestTheCrewChooserNamesThePickWhenItIsOffTheTable(t *testing.T) { + a, _ := sheetApp(t) + a.slash("/crew learn") + a.slash("/crew") + text := plain(strings.Join(a.overlayRows(a.width, a.overlayHeight()), "\n")) + if !strings.Contains(text, "picked from learn") { + t.Fatalf("the chooser does not name the pick:\n%s", text) + } + a.crewPick.close() + a.slash("/crew table") + a.slash("/crew") + text = plain(strings.Join(a.overlayRows(a.width, a.overlayHeight()), "\n")) + if strings.Contains(text, "picked from") { + t.Fatalf("the chooser names the default pick:\n%s", text) + } +} + +// THE PICK ROW SITS UNDER THE CREW ROW AND CYCLES, like every other enum row +// on the sheet: the crew word above it says how much to spend, and this says +// where the models for that money come from. +func TestThePickRowSitsUnderTheCrewRowAndCycles(t *testing.T) { + a, dir := sheetApp(t) + a.openSettings() + toProviders(t, a) + + // THE PLACE: directly under the crew row, before the classes it seats. A + // pick row a tab away from the crew word would be two errands on two + // screens for one decision. + crew, pick := -1, -1 + for i, item := range a.sheet.items { + switch item.row.Key { + case config.KeyCrew: + crew = i + case config.KeyCrewPick: + pick = i + } + } + if crew < 0 || pick < 0 { + t.Fatalf("the Providers tab holds no crew/pick pair (crew %d, pick %d)", crew, pick) + } + if pick != crew+1 { + t.Fatalf("the pick row sits at %d with the crew at %d, want it directly under", pick, crew) + + } + + cursorTo(t, a, config.KeyCrewPick) + drive(t, a, key("enter")) + if got := config.CrewPickAt(dir); got != config.CrewPickCatalog { + t.Fatalf("enter on table landed on %q, want %q", got, config.CrewPickCatalog) + } + drive(t, a, key("enter")) + if got := config.CrewPickAt(dir); got != config.CrewPickLearn { + t.Fatalf("enter on catalog landed on %q, want %q", got, config.CrewPickLearn) + } + drive(t, a, key("enter")) + if got := config.CrewPickAt(dir); got != config.CrewPickTable { + t.Fatalf("enter on learn landed on %q, want %q", got, config.CrewPickTable) + } +} diff --git a/internal/tui3/settings.go b/internal/tui3/settings.go index b31c29f669..ea306ae17f 100644 --- a/internal/tui3/settings.go +++ b/internal/tui3/settings.go @@ -380,6 +380,21 @@ var settingUI = map[string]settingMeta{ about: "which models the crew word draws from: open weights, or the whole " + "catalog with closed and frontier models in it. All is the default.", }, + // WHERE THE CREW'S MODELS COME FROM, beside the crew word itself. The crew + // row says how much to spend and this says where the models for that money + // are read from when a class row does not hold a person's own id: the rows + // this build measured, or the same budgets recomputed off the catalog on + // every read, with or without what the Model Pool and the person's own + // judged runs measured. The about is the registry's own hint whole, not its + // first sentence, because the three words are the answer and the first + // sentence alone would send a person hunting for what catalog means. + config.KeyCrewPick: { + tab: tabProviders, label: "picked from", widget: widgetCycle, + about: "where the crew's models come from. table: the rows we measured. " + + "catalog: recomputed from today's published prices and scores at your " + + "crew's budget. learn: catalog plus the Model Pool's measurements and " + + "your own judged runs.", + }, config.KeyTierLowModel: { tab: tabProviders, label: "small work", widget: widgetSelect, about: "cheap · the small calls — names, digests, the safety gate", @@ -808,6 +823,10 @@ func modelsSectionOrder() []string { config.KeyRouting, config.KeyPromptProfile, config.KeyCrew, + // THE PICK ANSWERS THE CREW WORD'S OWN QUESTION ONE LEVEL DOWN — where + // the models for that budget come from — so it reads directly under the + // crew word, before the classes it seats. + config.KeyCrewPick, } for _, tier := range roles.Tiers { order = append(order, tierSettingKey(tier)) From 38bbf1fb4089cbf9f41004dbc271146f5b703d81 Mon Sep 17 00:00:00 2001 From: santoshkumarradha Date: Thu, 17 Sep 2026 10:03:59 -0400 Subject: [PATCH 098/173] manual: the seats are picked from table, catalog or learn, and the rungs say their words --- internal/manual/chat/models-and-cost.md | 60 +++++++++++++++++++------ internal/manual/pages/models.md | 4 ++ 2 files changed, 50 insertions(+), 14 deletions(-) diff --git a/internal/manual/chat/models-and-cost.md b/internal/manual/chat/models-and-cost.md index 2a281ecbc1..85d90ba9d6 100644 --- a/internal/manual/chat/models-and-cost.md +++ b/internal/manual/chat/models-and-cost.md @@ -345,7 +345,7 @@ that answer a handful of times. You can pin any vendor's model on any row yourse can connect that vendor as a direct service; the [services page](services.md) explains its names, limits and missing Phase 1 cost record. -**The `model family` row** (`models.crew.source`, just under the crew row) decides which family +**The `model family` row** (`models.crew.source`) decides which family the three preset words draw from. `all` is the default and is the table above: the whole catalog, closed and frontier models included, costing what those models cost. `open` reads the same three words, `frugal`, `balanced` and `max`, off the open-weight rows only, so no seat is a @@ -427,9 +427,13 @@ enter applies and esc cancels. If the five classes make a custom crew, no row is the chooser says picking one puts all five back. `/crew max` still sets it directly and confirms in one line, which ends `· you are still talking to deepseek-v4-flash — /model changes that` — naming the conversation's own model by id, because the crew changes -nothing about it and the model segment on the status line goes on saying what it said before. -The **crew** row in `/settings` → Providers is the same thing: enter or space walks it -frugal → balanced → max. +nothing about it and the model segment on the status line goes on saying what it said +before. `/crew learn`, `/crew catalog` and `/crew table` move the **picked from** row and +nothing else — the three words the row takes, answered without touching a model id — and +when the pick is off `table` the crew word says so beside the preset: `balanced · learn` +in `/status`, `crew balanced · learn` on the status line. The **crew** row in `/settings` +→ Providers is the same thing: enter or space walks it frugal → balanced → max, and the +**picked from** row under it walks table → catalog → learn. **The crew row is not stored — it is worked out from the five.** Answer any one of the five rows yourself and the crew row reads `custom`, because that is what is true. `/crew balanced` @@ -437,15 +441,43 @@ puts all five back in one write. A profile that applied a crew before the worker existed reads `custom` until a preset is applied again, because its four old rows and the new fifth are not any of the three. -### A class row that says `auto` - -Any of the five class rows may hold the bare word `auto` instead of a model id, case -folded. The seat's model is then **computed from the catalog** — the three published -capability indexes against the three published prices, under that seat's own call shape — -every time the row is read. The word stays on disk; the id is worked out on every read, so -a catalog that moves moves the seat with it and your profile never holds a model id this -build chose for you. A row with nothing to compute from — no catalog yet — falls back to -the table row for your preset, never to `auto` and never to empty. +### Where the seats are picked from — table, catalog, learn + +The **picked from** row (`models.crew.pick`, just under the crew row) says where the +seats' models come from when a class row does not hold a model id of its own. The crew +row above it still says how much to spend; this row says where the models for that +money are read from: + +- **table** — the rows we measured: the ids this build shipped with, the same ones every + preset table holds. This is the default, and it is what an unwritten class has always + read. +- **catalog** — recomputed from today's published prices and scores at your crew's + budget, on every read. Nothing is stored; a catalog that moves moves the seat with it, + and your profile never holds a model id this build chose for you. +- **learn** — the catalog computation plus the Model Pool's measurements and your own + judged runs, carried as a quality rating the better-measured models read on top of + their published scores. + +A pick moves the three seats the presets dial — **worker**, **careful work** and +**mastermind** — and never the two that read every turn: **reflex** and **small work** +keep their near-free ids, the same ones in every preset. + +**A model you typed by hand wins.** The pick answers for the seats nobody named. A class +row holding a model id that is not the preset's own keeps it, spelled as you typed it; +a row holding the preset's own id is the preset answering rather than a pin, and the +pick computes it. **A flag or an environment variable still outranks the pick** — the +pick reads the profile, and `--model` and `CODEAF_MODEL` are what an invocation said. + +When the catalog cannot compute a seat — no catalog yet, or no pick off its front — the +seat falls back to the table row for your preset, never to `auto` and never to empty. A +seat the pick computed names it where the preset would be: `crew balanced, computed +from the catalog` under **catalog**, `crew balanced, learned` under **learn**. + +**The per-seat alias is the bare word `auto`.** Any of the five class rows may hold the +bare word `auto` instead of a model id, case folded. The seat's model is then +**computed from the catalog** — the three published capability indexes against the three +published prices, under that seat's own call shape — every time the row is read. The +word stays on disk; the id is worked out on every read. **The budget it computes at comes from the other four rows.** `auto` has no opinion about cost of its own, so it runs at whatever preset the rows around it name: four rows that are @@ -459,7 +491,7 @@ as `balanced`. Pin the worker to `max`'s own id and put the `auto` row on a seat if you want a computed seat at `max`'s budget. A seat on a computed row names both facts where a seat is shown — `crew balanced, -computed` — so the reading is never a guess. +computed from the catalog` — so the reading is never a guess. ### The roles under each class diff --git a/internal/manual/pages/models.md b/internal/manual/pages/models.md index dedb696413..a186a093e8 100644 --- a/internal/manual/pages/models.md +++ b/internal/manual/pages/models.md @@ -90,6 +90,10 @@ that works, one that plans — and both resolve the same way. First answer wins: The crew is the same one `/crew` sets in the chat, so a machine told `frugal` there runs frugal here. It answers only where a crew has actually been set; an untouched profile takes the build's default. `CODEAF_HOME` and `CODEAF_PROFILE_DIR` decide which profile is asked. +A seat computed from the catalog says so where the preset would be — `crew frugal, +computed from the catalog` — and one computed with what your runs measured says `crew +frugal, learned`; the **picked from** row in the chat's settings decides which, and a +pick of `catalog` or `learn` seats a run from the shell exactly as it seats the chat. Every one of those runs opens with a line on stderr naming both seats and what chose each: From 8d782d5a7ba80472994be756b079b11a02fec98d Mon Sep 17 00:00:00 2001 From: santoshkumarradha Date: Thu, 17 Sep 2026 10:05:14 -0400 Subject: [PATCH 099/173] docs: a change note for the picked-from row --- .../unreleased/1103-crew-picked-from-row.md | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 docs/changes/unreleased/1103-crew-picked-from-row.md diff --git a/docs/changes/unreleased/1103-crew-picked-from-row.md b/docs/changes/unreleased/1103-crew-picked-from-row.md new file mode 100644 index 0000000000..59740d603d --- /dev/null +++ b/docs/changes/unreleased/1103-crew-picked-from-row.md @@ -0,0 +1,43 @@ +--- +kind: added +title: The crew's seats are picked from a row — table, catalog or learn +pr: 1103 +surface: [chat, engine] +invalidates: + - "A seat on the computed rung read `crew frugal, computed`; it now reads `crew frugal, computed from the catalog`." + - "The crew word was derived from the five live seats; under a pick of catalog or learn it is derived from the five stored rows, because the live seats are computed ids the preset tables do not hold." + - "An unwritten seat read the preset's own table row on every surface; under a pick of catalog or learn the worker, careful work and mastermind seats are computed at the crew's preset instead, and a hand-typed model id still wins." +--- + +`models.crew.pick` is a new choice row beside the crew row: `table` (the default), +`catalog` and `learn`. The crew row keeps its meaning — how much to spend — and the pick +row says where the models for that budget come from when a tier row does not hold a model +id of its own. + +- `config.CrewPickAt` and `config.SetCrewPick` are the row's reader and writer; a word + the build does not know reads as the default and a writer refuses it, the way every + choice row folds. +- The ladder applies the pick in one seam both ladders call — `config.pickedSeat`, + beside `autoRow` in internal/config/auto.go. Under `catalog` or `learn` the worker, + careful work and mastermind seats are computed at the crew's preset + (`config.crewPresetUnder`), reflex and small work always read the table, a stored + model id that is not the preset's own table value wins with source `crew`, and a flag + or `CODEAF_MODEL`/`CODEAF_PLAN_MODEL` still outrank everything below them. +- `config.AutoPickWith(tier, family, preset, models, prior)` is the pick with the + measured quality named: `catalog` passes a nil prior, `learn` passes the Model Pool's + role-quality prior (`config.autoPrior`), and `config.AutoPick` keeps its behaviour — + prior included — for the bare `auto` word a tier row may hold. +- `config.SeatLearned` is the new rung. `Seat.Rung()` says `crew balanced, computed from + the catalog` for `SeatComputed` and `crew balanced, learned` for `SeatLearned`; + `Seats.Report()`, `Seats.Line()` and the receipts follow. The headless doors read the + same pick through `config.ResolveSeats`, so a run from the shell is seated exactly as + the conversation is. +- internal/tui3: the row is on the Providers tab directly under the crew word + (`picked from`, a cycle widget), `/crew` takes the three words beside the presets and + refuses an unknown word by naming all six, and the crew word and status segment read + `balanced · learn` / `crew balanced · learn` when the pick is off the table. The + chooser names the pick on a reading line under the presets, and a crew applied under a + pick confirms with the pick named beside the preset (`config.CrewSummaryPick`). +- internal/manual: the class-row section is now `Where the seats are picked from — + table, catalog, learn`, with the bare `auto` word kept inside it as the per-seat + alias, and the headless ladder page names the two rung words. From 5e7f0f0b4d4c4ba275c05746c42129cab1dda1fc Mon Sep 17 00:00:00 2001 From: santoshkumarradha Date: Thu, 17 Sep 2026 10:24:50 -0400 Subject: [PATCH 100/173] docs: the folded change entries carry the pull request they ship in Co-Authored-By: Claude Fable 5.1 --- ...rage-round-buys-once.md => 1093-coverage-round-buys-once.md} | 2 +- ...-directory.md => 1093-no-git-command-without-a-directory.md} | 2 +- ...-room-and-refusal.md => 1093-redispatch-room-and-refusal.md} | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) rename docs/changes/unreleased/{1100-coverage-round-buys-once.md => 1093-coverage-round-buys-once.md} (99%) rename docs/changes/unreleased/{1099-no-git-command-without-a-directory.md => 1093-no-git-command-without-a-directory.md} (99%) rename docs/changes/unreleased/{1098-redispatch-room-and-refusal.md => 1093-redispatch-room-and-refusal.md} (99%) diff --git a/docs/changes/unreleased/1100-coverage-round-buys-once.md b/docs/changes/unreleased/1093-coverage-round-buys-once.md similarity index 99% rename from docs/changes/unreleased/1100-coverage-round-buys-once.md rename to docs/changes/unreleased/1093-coverage-round-buys-once.md index 09a3ed0453..887ad35e73 100644 --- a/docs/changes/unreleased/1100-coverage-round-buys-once.md +++ b/docs/changes/unreleased/1093-coverage-round-buys-once.md @@ -1,7 +1,7 @@ --- kind: changed title: the coverage round buys once, and the envelope says when the requested work was done -pr: 1100 +pr: 1093 surface: [resident, docs] invalidates: - "An unexercised-only finding bought a repair round at every later delivery gate, because the finding stands until a measurement closes it and nothing bounded how many rounds `no check exercises this behaviour` could raise. A job buys at most one such round now (`maxUnexercisedRounds`), and a second finding is recorded unclosed rather than funded." diff --git a/docs/changes/unreleased/1099-no-git-command-without-a-directory.md b/docs/changes/unreleased/1093-no-git-command-without-a-directory.md similarity index 99% rename from docs/changes/unreleased/1099-no-git-command-without-a-directory.md rename to docs/changes/unreleased/1093-no-git-command-without-a-directory.md index d6bdf92d81..277f823017 100644 --- a/docs/changes/unreleased/1099-no-git-command-without-a-directory.md +++ b/docs/changes/unreleased/1093-no-git-command-without-a-directory.md @@ -1,7 +1,7 @@ --- kind: fixed title: No git command runs without a directory, and the checkout guard reads a linked worktree's own share -pr: 1099 +pr: 1093 surface: [engine] invalidates: - "`worktreeDirtIn` and `UnsavedEditsNote` built `git -C ` themselves, and `git -C \"\"` is a no-op, so an empty directory ran the command where the process stood. Both go through `gitWith` now (`gitContext` is the same call under a caller's context), where the refusal of an empty directory and the pinned environment live once." diff --git a/docs/changes/unreleased/1098-redispatch-room-and-refusal.md b/docs/changes/unreleased/1093-redispatch-room-and-refusal.md similarity index 99% rename from docs/changes/unreleased/1098-redispatch-room-and-refusal.md rename to docs/changes/unreleased/1093-redispatch-room-and-refusal.md index 89f6a7fe32..542ac6dd80 100644 --- a/docs/changes/unreleased/1098-redispatch-room-and-refusal.md +++ b/docs/changes/unreleased/1093-redispatch-room-and-refusal.md @@ -1,7 +1,7 @@ --- kind: changed title: A re-dispatch after running out is granted more room, and one that banks nothing new is refused -pr: 1098 +pr: 1093 surface: [resident, docs] invalidates: - "A leaf that ran out of its token budget was re-dispatched with the identical budget and the identical brief, up to the round cap, and bought the same truncated ending each time. The dispatch's grant is attempt-aware now: `regrantAfterRunningOut` in `cmd/codeaf/subharness.go` grows the token grant by three halves per re-dispatch, never above `overrunGrantCeiling` (four flat leaf grants; a fan-in that measured more keeps every token it measured), and the wall the leaf is given follows the larger grant." From 6274a6bf1aa5628529daa57695e5e8814d4e7cfe Mon Sep 17 00:00:00 2001 From: santoshkumarradha Date: Thu, 17 Sep 2026 10:40:18 -0400 Subject: [PATCH 101/173] docs: the picked-from change entry carries its pull request Co-Authored-By: Claude Fable 5.1 --- ...103-crew-picked-from-row.md => 1093-crew-picked-from-row.md} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename docs/changes/unreleased/{1103-crew-picked-from-row.md => 1093-crew-picked-from-row.md} (99%) diff --git a/docs/changes/unreleased/1103-crew-picked-from-row.md b/docs/changes/unreleased/1093-crew-picked-from-row.md similarity index 99% rename from docs/changes/unreleased/1103-crew-picked-from-row.md rename to docs/changes/unreleased/1093-crew-picked-from-row.md index 59740d603d..84d9026fbe 100644 --- a/docs/changes/unreleased/1103-crew-picked-from-row.md +++ b/docs/changes/unreleased/1093-crew-picked-from-row.md @@ -1,7 +1,7 @@ --- kind: added title: The crew's seats are picked from a row — table, catalog or learn -pr: 1103 +pr: 1093 surface: [chat, engine] invalidates: - "A seat on the computed rung read `crew frugal, computed`; it now reads `crew frugal, computed from the catalog`." From dcf827025463ba65003eb1187700d43f02c25fd6 Mon Sep 17 00:00:00 2001 From: santoshkumarradha Date: Thu, 17 Sep 2026 10:45:16 -0400 Subject: [PATCH 102/173] pool/tally: Each hands a metric's cells to a caller in a settled order --- internal/pool/tally/tally.go | 29 +++++++++++++++++ internal/pool/tally/tally_test.go | 52 +++++++++++++++++++++++++++++++ 2 files changed, 81 insertions(+) diff --git a/internal/pool/tally/tally.go b/internal/pool/tally/tally.go index 553985424b..1688e6d886 100644 --- a/internal/pool/tally/tally.go +++ b/internal/pool/tally/tally.go @@ -160,6 +160,35 @@ func (s *Sheet) Cell(metric, role, model string, dims map[string]string) (Cell, return e.cell, ok } +// Each hands every cell recorded under metric to fn, one call per cell, in a +// deterministic order: role, then model, then the dim labels. Cells recorded +// under other metrics are not handed over, and the sheet is only read — fn +// sees the dim labels as the sheet stores them, nil when the observations +// carried none, and it does not run while the sheet is locked, so it may read +// the sheet again. +func (s *Sheet) Each(metric string, fn func(role, model string, dims map[string]string, c Cell)) { + s.mu.Lock() + seen := make([]cellEntry, 0, len(s.cells)) + for _, e := range s.cells { + if e.addr.metric == metric { + seen = append(seen, e) + } + } + s.mu.Unlock() + slices.SortFunc(seen, func(a, b cellEntry) int { + if c := strings.Compare(a.addr.role, b.addr.role); c != 0 { + return c + } + if c := strings.Compare(a.addr.model, b.addr.model); c != 0 { + return c + } + return strings.Compare(dimsKey(a.addr.dims), dimsKey(b.addr.dims)) + }) + for _, e := range seen { + fn(e.addr.role, e.addr.model, e.addr.dims, e.cell) + } +} + // Win records one paired comparison in which winner beat loser for role. // The call is ignored when a name is empty, when winner and loser are // equal, or when a name is longer than 128 bytes or contains a newline or diff --git a/internal/pool/tally/tally_test.go b/internal/pool/tally/tally_test.go index a42fcf2b7c..ca772e3212 100644 --- a/internal/pool/tally/tally_test.go +++ b/internal/pool/tally/tally_test.go @@ -517,3 +517,55 @@ func TestConcurrentUse(t *testing.T) { t.Fatalf("Wins = %d, %d, want %d, 0", x, y, goroutines*each) } } + +// Each hands a metric's cells over one call apiece, in a settled order, and +// nothing under another metric and nothing the call does can move the sheet. +func TestEachHandsAMetricsCellsOverInASettledOrder(t *testing.T) { + s := New() + // Inserted deliberately out of order: the order Each answers must not + // carry the order the cells were observed in. + s.Observe("m", "worker", "b/late", nil, 2) + s.Observe("m", "high", "a/early", map[string]string{"quant": "fp8"}, 9) + s.Observe("m", "worker", "a/first", nil, 1) + s.Observe("other", "worker", "a/first", nil, 5) + s.Observe("m", "worker", "a/first", map[string]string{"quant": "fp8"}, 3) + + var seen []string + s.Each("m", func(role, model string, dims map[string]string, c Cell) { + word := role + " " + model + if dims == nil { + word += " nil" + } else { + word += " " + dims["quant"] + } + seen = append(seen, word) + switch { + case role == "high": + if c.N != 1 || c.Sum != 9 { + t.Errorf("the high cell is %+v, want one observation of 9", c) + } + case model == "a/first" && dims == nil: + if c.N != 1 || c.Sum != 1 { + t.Errorf("the first cell is %+v, want one observation of 1", c) + } + case model == "b/late": + if c.N != 1 || c.Sum != 2 { + t.Errorf("the late cell is %+v, want one observation of 2", c) + } + } + }) + want := []string{"high a/early fp8", "worker a/first nil", "worker a/first fp8", "worker b/late nil"} + if len(seen) != len(want) { + t.Fatalf("Each handed over %d cells (%v), want %d", len(seen), seen, len(want)) + } + for i := range want { + if seen[i] != want[i] { + t.Fatalf("the %d cell handed over is %q, want %q — role, then model, then the dim labels", i, seen[i], want[i]) + } + } + + // Nil dims and an empty map are the same address, so a second observation + // under an empty map lands in the cell Each already handed over. + s.Observe("m", "worker", "a/first", map[string]string{}, 4) + checkCell(t, s, "m", "worker", "a/first", nil, Cell{N: 2, Sum: 5, SumSq: 17}) +} From dc73bef3129ea42f9983d9961876024c5ebfff6d Mon Sep 17 00:00:00 2001 From: santoshkumarradha Date: Thu, 17 Sep 2026 10:46:03 -0400 Subject: [PATCH 103/173] crewpick: MergePriors folds two priors seat by seat by observation count --- internal/crewpick/crewpick.go | 46 +++++++++++++++++++++++++++++ internal/crewpick/crewpick_test.go | 47 ++++++++++++++++++++++++++++++ 2 files changed, 93 insertions(+) diff --git a/internal/crewpick/crewpick.go b/internal/crewpick/crewpick.go index 4552b53381..c2eb0cc138 100644 --- a/internal/crewpick/crewpick.go +++ b/internal/crewpick/crewpick.go @@ -250,6 +250,52 @@ func PriorFromCells(cells []Cell, minInstalls int, canonical func(string) string return prior } +// MergePriors folds two priors into one. Seat by seat, a rating both hold for +// a model is combined by observation count — the two means weighted by the +// counts behind them, the counts added — and a rating either holds alone is +// carried whole. A nil prior on either side is the other, and two nils are +// nil. The priors given are read and never changed, so the answer is never +// one of them. +func MergePriors(a, b Prior) Prior { + if a == nil { + return b + } + if b == nil { + return a + } + merged := make(Prior) + for seat, ratings := range a { + for model, r := range ratings { + if merged[seat] == nil { + merged[seat] = map[string]Rating{} + } + merged[seat][model] = r + } + } + for seat, ratings := range b { + for model, r := range ratings { + got, ok := merged[seat][model] + if !ok { + if merged[seat] == nil { + merged[seat] = map[string]Rating{} + } + merged[seat][model] = r + continue + } + n := got.N + r.N + if n <= 0 { + merged[seat][model] = Rating{} + continue + } + merged[seat][model] = Rating{ + Mean: (float64(got.N)*got.Mean + float64(r.N)*r.Mean) / float64(n), + N: n, + } + } + } + return merged +} + // SeatCost is the seat's expected cost of a candidate, in dollars per 1M // input-equivalent tokens: the prompt price blended with the cache-read // price by the seat's cache share, plus the completion price spread over the diff --git a/internal/crewpick/crewpick_test.go b/internal/crewpick/crewpick_test.go index 9c66fa157c..f884530002 100644 --- a/internal/crewpick/crewpick_test.go +++ b/internal/crewpick/crewpick_test.go @@ -784,3 +784,50 @@ func TestAnAliasResolvedThroughCanonicalMeetsItsCandidate(t *testing.T) { t.Fatalf("the unresolved alias scored %f, want the catalog 100 untouched", got) } } + +// Two priors fold into one: a rating both hold for a model is weighted by the +// counts behind it, a rating either holds alone is carried whole, and a nil on +// either side is the other side. +func TestMergePriorsFoldsTwoPriorsByObservationCount(t *testing.T) { + if got := MergePriors(nil, nil); got != nil { + t.Fatalf("two nils folded to %v, want nil", got) + } + a := Prior{Worker: {"a/one": {Mean: 80, N: 20}}} + if got := MergePriors(a, nil); got == nil || got[Worker]["a/one"].N != 20 { + t.Fatalf("a nil on one side is not the other: %v", got) + } + b := Prior{High: {"b/two": {Mean: 60, N: 5}}} + if got := MergePriors(nil, b); got == nil || got[High]["b/two"].N != 5 { + t.Fatalf("a nil on one side is not the other: %v", got) + } + + // A model only one prior names is carried whole, in either order. + onlyA := Prior{Worker: {"a/only": {Mean: 70, N: 4}}} + onlyB := Prior{Worker: {"b/only": {Mean: 90, N: 6}}} + got := MergePriors(onlyA, onlyB) + if len(got[Worker]) != 2 { + t.Fatalf("no shared key folded to %v, want the union", got) + } + if r := got[Worker]["a/only"]; r.Mean != 70 || r.N != 4 { + t.Fatalf("a carried rating moved: %v", r) + } + if r := got[Worker]["b/only"]; r.Mean != 90 || r.N != 6 { + t.Fatalf("a carried rating moved: %v", r) + } + + // A model both name: the counts add and the mean is the mean of the means + // weighted by them — (10*50 + 30*90) / 40 = 80. + sharedA := Prior{Worker: {"c/both": {Mean: 50, N: 10}}} + sharedB := Prior{Worker: {"c/both": {Mean: 90, N: 30}}} + if r := MergePriors(sharedA, sharedB)[Worker]["c/both"]; r.Mean != 80 || r.N != 40 { + t.Fatalf("a shared rating folded to %v, want mean 80 over 40", r) + } + + // The priors given are read and never changed. + if r := sharedA[Worker]["c/both"]; r.Mean != 50 || r.N != 10 { + t.Fatalf("the merge moved its first prior: %v", r) + } + if r := sharedB[Worker]["c/both"]; r.Mean != 90 || r.N != 30 { + t.Fatalf("the merge moved its second prior: %v", r) + } +} From 4cc4192697f031a668ebad702ec55d10d919a980 Mon Sep 17 00:00:00 2001 From: santoshkumarradha Date: Thu, 17 Sep 2026 10:48:28 -0400 Subject: [PATCH 104/173] pool/record: judged seat scores land in an own sheet and outbox rows --- internal/pool/record/record.go | 215 ++++++++++++++++++++ internal/pool/record/record_test.go | 291 ++++++++++++++++++++++++++++ 2 files changed, 506 insertions(+) create mode 100644 internal/pool/record/record.go create mode 100644 internal/pool/record/record_test.go diff --git a/internal/pool/record/record.go b/internal/pool/record/record.go new file mode 100644 index 0000000000..551233202d --- /dev/null +++ b/internal/pool/record/record.go @@ -0,0 +1,215 @@ +// Package record turns a judge's seat scores into the two records an install +// keeps of them: additive cells in a tally sheet on disk, and one row per +// score in an outbox. +// +// THE MECHANISM. [Recorder.Record] takes the scores [judge.Judge] answered and +// does two things with them. Every valid one is observed into the sheet the +// recorder holds, under the role_quality metric the crew picker reads seat +// quality from, and one row is appended to the recorder's outbox when it holds +// one. A score whose role is not one of the judged seats, or that is not on +// the 0-100 scale, is skipped and named in the error; the others are recorded +// however the round went, and the sheet is observed whatever the outbox does. +// A nil outbox records locally only. +// +// The sheet an install keeps of its own scores is its own evidence: it is +// saved under the pool directory ([OwnSheetName]) and read back as the cells +// the crew picker's prior blends in beside an index's ([Cells]). +package record + +import ( + "encoding/json" + "errors" + "fmt" + "os" + "path/filepath" + + "github.com/Agent-Field/codeaf/internal/crewpick" + "github.com/Agent-Field/codeaf/internal/pool/judge" + "github.com/Agent-Field/codeaf/internal/pool/outbox" + "github.com/Agent-Field/codeaf/internal/pool/tally" +) + +// Metric is the sheet metric a judged seat score is observed under: the word +// the crew picker reads a measured seat quality from. +const Metric = "role_quality" + +// OwnSheetName is the file the install's own sheet is kept as, under the pool +// directory. +const OwnSheetName = "own.json" + +// OwnSheetPath is where the install's own sheet lives for the pool directory +// given. +func OwnSheetPath(poolDir string) string { + return filepath.Join(poolDir, OwnSheetName) +} + +// dirMode is the mode the pool's own directory keeps: private to the person. +const dirMode = 0700 + +// LoadSheet reads the sheet at path. A path with no file answers an empty +// sheet and no error — an install that has recorded nothing yet is not a +// fault; bytes that are there and do not parse as a sheet document are. +func LoadSheet(path string) (*tally.Sheet, error) { + data, err := os.ReadFile(path) + if err != nil { + if os.IsNotExist(err) { + return tally.New(), nil + } + return nil, err + } + var s tally.Sheet + if err := json.Unmarshal(data, &s); err != nil { + return nil, fmt.Errorf("record: %s does not hold a sheet document: %w", path, err) + } + return &s, nil +} + +// SaveSheet writes the sheet to a temporary file in the path's own directory +// and renames it into place, so a reader mid-write never sees half a document. +// The file is mode 0600 and its parent directory is created with mode 0700 +// when it is missing. +func SaveSheet(path string, s *tally.Sheet) error { + doc, err := json.Marshal(s) + if err != nil { + return err + } + dir := filepath.Dir(path) + if err := os.MkdirAll(dir, dirMode); err != nil { + return err + } + tmp, err := os.CreateTemp(dir, OwnSheetName+".*") + if err != nil { + return err + } + name := tmp.Name() + defer os.Remove(name) // nothing once the rename has landed + if _, err := tmp.Write(append(doc, '\n')); err != nil { + tmp.Close() + return err + } + if err := tmp.Close(); err != nil { + return err + } + return os.Rename(name, path) +} + +// rowSchema is the only row schema this package writes. +const rowSchema = 1 + +// Row is one judged seat score as it leaves the install: what was scored — +// the metric, the seat and the model that held it, the score on the 0-100 +// scale — and the facts of the run it was read from, the judge that answered, +// the door the run came in by, the size of the crew, and the day. +type Row struct { + Schema int `json:"schema"` + Metric string `json:"metric"` + Role string `json:"role"` + Model string `json:"model"` + Score float64 `json:"score"` + Judge string `json:"judge"` + Door string `json:"door"` + Size string `json:"size"` + Day string `json:"day"` +} + +// RowsOf reads judge scores into rows, one per score. Every row carries +// schema 1 and the role_quality metric; the day is spelled by the caller, the +// way the outbox spells its own. +func RowsOf(scores []judge.Score, judgeModel, door, size, day string) []Row { + rows := make([]Row, 0, len(scores)) + for _, score := range scores { + rows = append(rows, Row{ + Schema: rowSchema, + Metric: Metric, + Role: string(score.Role), + Model: score.Model, + Score: score.Score, + Judge: judgeModel, + Door: door, + Size: size, + Day: day, + }) + } + return rows +} + +// judgedRoles is the whole set of seats a score may name: the three seats the +// judge reads, and nothing else. A role outside it is not a seat the picker +// reads a quality for. +var judgedRoles = map[judge.Role]bool{ + judge.RoleWorker: true, + judge.RoleHigh: true, + judge.RoleMastermind: true, +} + +// Recorder turns judge scores into the two records an install keeps: the +// sheet observed locally and, when one is held, the outbox rows that leave. +type Recorder struct { + // Sheet holds the install's own tallies; a recorder without one has + // nowhere to put what it is given, and refuses. + Sheet *tally.Sheet + // Outbox is the rows' way out. A nil Outbox keeps the scores locally + // only. + Outbox *outbox.Outbox +} + +// Record observes every score it is given into the sheet, and appends one row +// per score to the outbox when the recorder holds one. THE SHEET IS OBSERVED +// WHATEVER THE OUTBOX DOES: every score is observed before the first row is +// appended, so an append that fails costs the install nothing of its own +// evidence, and the first append error is returned once every score has been +// observed. A score whose role is not one of the judged seats, or whose score +// is not on the 0-100 scale, is skipped and named in the error beside the +// others that were recorded. +func (r *Recorder) Record(scores []judge.Score, judgeModel, door, size, day string) error { + if r.Sheet == nil { + return errors.New("record: no sheet to observe into") + } + rows := RowsOf(scores, judgeModel, door, size, day) + var errs []error + var appendErr error + for i, score := range scores { + if !judgedRoles[score.Role] { + errs = append(errs, fmt.Errorf("record: the %s seat is not a judged seat and was skipped", score.Role)) + continue + } + if score.Score < 0 || score.Score > 100 { + errs = append(errs, fmt.Errorf("record: the %s seat's score %v is outside 0-100 and was skipped", score.Role, score.Score)) + continue + } + r.Sheet.Observe(Metric, string(score.Role), score.Model, nil, score.Score) + if r.Outbox == nil { + continue + } + line, err := json.Marshal(rows[i]) + if err != nil { + continue // a struct of scalars never fails to marshal + } + if err := r.Outbox.Append(line); err != nil && appendErr == nil { + appendErr = err + } + } + if appendErr != nil { + errs = append(errs, appendErr) + } + return errors.Join(errs...) +} + +// Cells reads the sheet's own role_quality cells — the ones recorded with no +// dim labels — as the cells a prior reads: the seat, the model, the mean of +// the scores and the count behind them, sorted by seat then model, so the +// answer is a property of the sheet and never of the order it was observed +// in. A nil sheet answers no cells. +func Cells(s *tally.Sheet) []crewpick.Cell { + if s == nil { + return nil + } + var cells []crewpick.Cell + s.Each(Metric, func(role, model string, dims map[string]string, c tally.Cell) { + if dims != nil { + return + } + cells = append(cells, crewpick.Cell{Role: role, Model: model, Mean: c.Mean(), N: int(c.N)}) + }) + return cells +} diff --git a/internal/pool/record/record_test.go b/internal/pool/record/record_test.go new file mode 100644 index 0000000000..d93ab196c6 --- /dev/null +++ b/internal/pool/record/record_test.go @@ -0,0 +1,291 @@ +package record + +import ( + "encoding/json" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/Agent-Field/codeaf/internal/crewpick" + "github.com/Agent-Field/codeaf/internal/pool/judge" + "github.com/Agent-Field/codeaf/internal/pool/outbox" + "github.com/Agent-Field/codeaf/internal/pool/tally" +) + +// A path with no file behind it is an install that has recorded nothing yet: +// an empty sheet and no error, never a fault. +func TestALoadOfAMissingPathAnswersAnEmptySheet(t *testing.T) { + sheet, err := LoadSheet(filepath.Join(t.TempDir(), "absent", OwnSheetName)) + if err != nil { + t.Fatalf("a missing sheet loaded as %v", err) + } + if got := Cells(sheet); len(got) != 0 { + t.Fatalf("a missing sheet answered %d cells, want none", len(got)) + } +} + +// Bytes that are there and hold no sheet document are a fault, named. +func TestALoadOfUnparsableBytesIsAnError(t *testing.T) { + path := filepath.Join(t.TempDir(), OwnSheetName) + if err := os.WriteFile(path, []byte("not a document"), 0o600); err != nil { + t.Fatal(err) + } + if _, err := LoadSheet(path); err == nil { + t.Fatal("unparsable bytes loaded as a sheet") + } +} + +// A sheet saved is the sheet loaded: every cell comes back with the +// observations it held, however many addresses it kept. +func TestASaveThenLoadRoundTrips(t *testing.T) { + path := filepath.Join(t.TempDir(), "pool", OwnSheetName) + sheet := tally.New() + sheet.Observe(Metric, "worker", "a/one", nil, 80) + sheet.Observe(Metric, "worker", "a/one", nil, 90) + sheet.Observe(Metric, "high", "b/two", nil, 70) + sheet.Observe(Metric, "high", "b/two", map[string]string{"quant": "fp8"}, 55) + sheet.Win("worker", "a/one", "b/two") + if err := SaveSheet(path, sheet); err != nil { + t.Fatalf("save: %v", err) + } + back, err := LoadSheet(path) + if err != nil { + t.Fatalf("load: %v", err) + } + for _, want := range []struct { + role, model string + dims map[string]string + cell tally.Cell + }{ + {"worker", "a/one", nil, tally.Cell{N: 2, Sum: 170, SumSq: 14500}}, + {"high", "b/two", nil, tally.Cell{N: 1, Sum: 70, SumSq: 4900}}, + {"high", "b/two", map[string]string{"quant": "fp8"}, tally.Cell{N: 1, Sum: 55, SumSq: 3025}}, + } { + got, ok := back.Cell(Metric, want.role, want.model, want.dims) + if !ok || got != want.cell { + t.Fatalf("the %s/%s cell (%v) came back %+v ok %v, want %+v", want.role, want.model, want.dims, got, ok, want.cell) + } + } + if over, under := back.Wins("worker", "a/one", "b/two"); over != 1 || under != 0 { + t.Fatalf("the win came back %d over %d under, want 1 over 0", over, under) + } + + // The file is private, and the save is atomic enough to leave no temp + // file beside the sheet it wrote. + info, err := os.Stat(path) + if err != nil { + t.Fatal(err) + } + if info.Mode().Perm() != 0o600 { + t.Fatalf("the sheet was saved with mode %v, want 0600", info.Mode().Perm()) + } + entries, err := os.ReadDir(filepath.Dir(path)) + if err != nil { + t.Fatal(err) + } + if len(entries) != 1 || entries[0].Name() != OwnSheetName { + t.Fatalf("the pool directory holds %d entries, want the sheet alone", len(entries)) + } +} + +// A second save over the first leaves the one file, whole: the rename +// replaces the sheet rather than appending to it. +func TestASaveOverAnExistingSheetReplacesIt(t *testing.T) { + path := filepath.Join(t.TempDir(), OwnSheetName) + first := tally.New() + first.Observe(Metric, "worker", "a/one", nil, 80) + if err := SaveSheet(path, first); err != nil { + t.Fatal(err) + } + second := tally.New() + second.Observe(Metric, "high", "b/two", nil, 70) + if err := SaveSheet(path, second); err != nil { + t.Fatal(err) + } + back, err := LoadSheet(path) + if err != nil { + t.Fatal(err) + } + if _, ok := back.Cell(Metric, "worker", "a/one", nil); ok { + t.Fatal("the replaced sheet still holds the first save's cell") + } + if _, ok := back.Cell(Metric, "high", "b/two", nil); !ok { + t.Fatal("the replaced sheet does not hold the second save's cell") + } +} + +// Every valid score is observed into the sheet and one row apiece is appended +// to the outbox, each row carrying the day, the judge and the door the run +// came in by. +func TestRecordObservesEveryScoreAndAppendsOneRowApiece(t *testing.T) { + sheet := tally.New() + box, err := outbox.Open(filepath.Join(t.TempDir(), "outbox.jsonl")) + if err != nil { + t.Fatal(err) + } + defer box.Close() + recorder := &Recorder{Sheet: sheet, Outbox: box} + + scores := []judge.Score{ + {Role: judge.RoleWorker, Model: "a/one", Score: 80}, + {Role: judge.RoleHigh, Model: "b/two", Score: 66}, + {Role: judge.RoleMastermind, Model: "c/three", Score: 91}, + } + if err := recorder.Record(scores, "z/fourth", "chat", "three", "2026-09-17"); err != nil { + t.Fatalf("record: %v", err) + } + for _, want := range []struct { + role judge.Role + model string + mean float64 + }{ + {judge.RoleWorker, "a/one", 80}, + {judge.RoleHigh, "b/two", 66}, + {judge.RoleMastermind, "c/three", 91}, + } { + cell, ok := sheet.Cell(Metric, string(want.role), want.model, nil) + if !ok || cell.N != 1 || cell.Mean() != want.mean { + t.Fatalf("the %s seat observed %+v ok %v, want one observation of %v", want.role, cell, ok, want.mean) + } + } + pending := box.Pending() + if len(pending) != len(scores) { + t.Fatalf("%d rows appended, want one per score", len(pending)) + } + for i, row := range pending { + var got Row + if err := json.Unmarshal(row.Payload, &got); err != nil { + t.Fatalf("row %d is not a row: %v", i, err) + } + want := Row{ + Schema: 1, Metric: Metric, + Role: string(scores[i].Role), + Model: scores[i].Model, + Score: scores[i].Score, + Judge: "z/fourth", Door: "chat", Size: "three", Day: "2026-09-17", + } + if got != want { + t.Fatalf("row %d is %+v, want %+v", i, got, want) + } + } +} + +// RowsOf spells every row the same way: schema 1 under the role_quality +// metric, whatever the score. +func TestRowsOfSpellsEveryRowTheSameWay(t *testing.T) { + rows := RowsOf([]judge.Score{ + {Role: judge.RoleWorker, Model: "a/one", Score: 80, Reason: "it worked"}, + }, "z/fourth", "chat", "solo", "2026-09-17") + if len(rows) != 1 { + t.Fatalf("%d rows, want one", len(rows)) + } + if rows[0].Schema != 1 || rows[0].Metric != Metric || rows[0].Role != "worker" || + rows[0].Model != "a/one" || rows[0].Score != 80 || rows[0].Judge != "z/fourth" || + rows[0].Door != "chat" || rows[0].Size != "solo" || rows[0].Day != "2026-09-17" { + t.Fatalf("the row is %+v", rows[0]) + } + line, err := json.Marshal(rows[0]) + if err != nil { + t.Fatal(err) + } + for _, key := range []string{`"schema":1`, `"metric":"role_quality"`, `"role":"worker"`, `"day":"2026-09-17"`} { + if !strings.Contains(string(line), key) { + t.Fatalf("the row's json is missing %s: %s", key, line) + } + } +} + +// A score whose seat is not one of the judged seats is skipped and named, and +// the scores beside it are recorded all the same. +func TestABadRoleIsSkippedAndNamedAndTheOthersLand(t *testing.T) { + for _, bad := range []judge.Score{ + {Role: "planner", Model: "a/one", Score: 80}, + {Role: "", Model: "a/one", Score: 80}, + {Role: judge.RoleWorker, Model: "a/one", Score: 140}, + {Role: judge.RoleWorker, Model: "a/one", Score: -1}, + } { + sheet := tally.New() + box, err := outbox.Open(filepath.Join(t.TempDir(), "outbox.jsonl")) + if err != nil { + t.Fatal(err) + } + recorder := &Recorder{Sheet: sheet, Outbox: box} + err = recorder.Record([]judge.Score{ + bad, + {Role: judge.RoleWorker, Model: "a/one", Score: 80}, + }, "z/fourth", "chat", "two", "2026-09-17") + box.Close() + if err == nil { + t.Fatalf("a %s score at %v was recorded without a word", bad.Role, bad.Score) + } + if !strings.Contains(err.Error(), "skipped") { + t.Fatalf("the error does not say what was skipped: %v", err) + } + // The good score landed in both records; the bad one in neither. + if cells := Cells(sheet); len(cells) != 1 { + t.Fatalf("the sheet holds %d cells, want the good score's one: %+v", len(cells), cells) + } + if cell, ok := sheet.Cell(Metric, "worker", "a/one", nil); !ok || cell.N != 1 || cell.Mean() != 80 { + t.Fatalf("the good score did not land: %+v ok %v", cell, ok) + } + if pending := box.Pending(); len(pending) != 1 { + t.Fatalf("%d rows appended, want the good score's one", len(pending)) + } + } +} + +// A recorder with no outbox keeps the scores locally only, and says nothing. +func TestANilOutboxRecordsLocallyOnly(t *testing.T) { + sheet := tally.New() + recorder := &Recorder{Sheet: sheet} + err := recorder.Record([]judge.Score{ + {Role: judge.RoleWorker, Model: "a/one", Score: 80}, + {Role: judge.RoleHigh, Model: "b/two", Score: 66}, + }, "z/fourth", "chat", "two", "2026-09-17") + if err != nil { + t.Fatalf("a local-only record said %v", err) + } + if cell, ok := sheet.Cell(Metric, "worker", "a/one", nil); !ok || cell.N != 1 { + t.Fatalf("the worker score did not land: %+v ok %v", cell, ok) + } + if cell, ok := sheet.Cell(Metric, "high", "b/two", nil); !ok || cell.N != 1 { + t.Fatalf("the high score did not land: %+v ok %v", cell, ok) + } +} + +// A recorder with no sheet has nowhere to put what it is given, and says so. +func TestARecorderWithoutASheetRefuses(t *testing.T) { + recorder := &Recorder{} + if err := recorder.Record(nil, "z/fourth", "chat", "one", "2026-09-17"); err == nil { + t.Fatal("a sheetless recorder recorded") + } +} + +// The own sheet's cells answer sorted by seat then model, each carrying the +// mean of its observations and the count behind it, and a cell recorded under +// dim labels is not one of them — the prior reads the sheet's plain cells. +func TestCellsAnswerSortedAndCarryTheMeanAndCount(t *testing.T) { + sheet := tally.New() + // Observed deliberately out of the order they must answer in. + sheet.Observe(Metric, "worker", "b/later", nil, 60) + sheet.Observe(Metric, "worker", "a/first", nil, 90) + sheet.Observe(Metric, "worker", "a/first", nil, 80) + sheet.Observe(Metric, "high", "c/top", nil, 70) + sheet.Observe(Metric, "high", "a/first", map[string]string{"quant": "fp8"}, 40) + + got := Cells(sheet) + if len(got) != 3 { + t.Fatalf("%d cells answered, want the three plain ones: %+v", len(got), got) + } + want := []crewpick.Cell{ + {Role: "high", Model: "c/top", Mean: 70, N: 1}, + {Role: "worker", Model: "a/first", Mean: 85, N: 2}, + {Role: "worker", Model: "b/later", Mean: 60, N: 1}, + } + for i := range want { + if got[i] != want[i] { + t.Fatalf("cell %d is %+v, want %+v", i, got[i], want[i]) + } + } +} From 8da59f7e8c60de4cda80ccc8063be2c91d5a8639 Mon Sep 17 00:00:00 2001 From: santoshkumarradha Date: Thu, 17 Sep 2026 10:50:22 -0400 Subject: [PATCH 105/173] config: the own sheet's cells join the auto prior beside the index's --- internal/config/auto.go | 67 ++++++++++++++++++------ internal/config/auto_test.go | 98 ++++++++++++++++++++++++++++++++++++ 2 files changed, 151 insertions(+), 14 deletions(-) diff --git a/internal/config/auto.go b/internal/config/auto.go index f8f72ab5e9..4cb78aed6c 100644 --- a/internal/config/auto.go +++ b/internal/config/auto.go @@ -51,6 +51,15 @@ var AutoModels func() []catalog.Model // seam existed. var AutoIndex func() *index.Index +// AutoOwnCells is how an install's own judged scores reach seat resolution: +// the binary holding the pool sets it ONCE AT START-UP, from the own sheet it +// read from disk under the pool directory, the same posture AutoIndex keeps. +// Nil is an ordinary state, not an error: the prior then reads the index's +// cells alone, which is what it read before this seam existed. The cells are +// never held to the index's min_installs — an install's own scores are its +// own evidence, one observation of which is worth having. +var AutoOwnCells func() []crewpick.Cell + // PoolQualityMetric names the index metric the picker reads a measured seat // quality from: a gaussian metric whose mean is on the same 0-100 scale // crewpick scores a seat on, with one cell per role and model. @@ -121,26 +130,56 @@ func autoIndex() *index.Index { return AutoIndex() } -// autoPrior reads the index's measured quality into a crewpick prior: the -// cells of PoolQualityMetric, kept only when the metric is declared gaussian -// — a mean on any other scale would be blended against figures it does not -// share units with — and dropped by PriorFromCells below the index's own -// min_installs, resolved through the index's canonical ids so an alias meets -// its candidate. A nil index, or a metric that is not there or not gaussian, -// answers no prior, which leaves every seat on the catalog quality. +// autoPrior reads the measured quality of the index AND of this install's own +// sheet into a crewpick prior. The index's cells of PoolQualityMetric are kept +// only when the metric is declared gaussian — a mean on any other scale would +// be blended against figures it does not share units with — and dropped by +// PriorFromCells below the index's own min_installs, resolved through the +// index's canonical ids so an alias meets its candidate. The own sheet's cells +// are this install's own evidence: they are read through a second +// PriorFromCells at a floor of one and folded into the index's prior seat by +// seat, the means combined by observation count. Their scores are on the +// 0-100 scale a judge answers on whatever the index's metric says, so they +// are read even beside an index whose role_quality is not gaussian. A nil +// index and a nil seam answer no prior, which leaves every seat on the +// catalog quality. func autoPrior(idx *index.Index) crewpick.Prior { + canonical := autoCanonical(idx) + var indexPrior crewpick.Prior + if idx != nil { + if kind, ok := idx.Kind(PoolQualityMetric); ok && kind == "gaussian" { + cells := idx.Cells(PoolQualityMetric) + measured := make([]crewpick.Cell, 0, len(cells)) + for _, c := range cells { + measured = append(measured, crewpick.Cell{Role: c.Role, Model: c.Model, Mean: c.Mean, N: c.N}) + } + indexPrior = crewpick.PriorFromCells(measured, idx.MinInstalls(), idx.Canonical) + } + } + var ownPrior crewpick.Prior + if own := autoOwnCells(); len(own) > 0 { + ownPrior = crewpick.PriorFromCells(own, 1, canonical) + } + return crewpick.MergePriors(indexPrior, ownPrior) +} + +// autoCanonical is the index's canonical ids, nil when there is no index. The +// own sheet's ids are the ones this install resolved its seats to, and a nil +// canonical leaves them as they stand. +func autoCanonical(idx *index.Index) func(string) string { if idx == nil { return nil } - if kind, ok := idx.Kind(PoolQualityMetric); !ok || kind != "gaussian" { + return idx.Canonical +} + +// autoOwnCells is [AutoOwnCells] read with its ordinary absence folded into +// one answer. +func autoOwnCells() []crewpick.Cell { + if AutoOwnCells == nil { return nil } - cells := idx.Cells(PoolQualityMetric) - measured := make([]crewpick.Cell, 0, len(cells)) - for _, c := range cells { - measured = append(measured, crewpick.Cell{Role: c.Role, Model: c.Model, Mean: c.Mean, N: c.N}) - } - return crewpick.PriorFromCells(measured, idx.MinInstalls(), idx.Canonical) + return AutoOwnCells() } // autoSeat is the tier word that names which of crewpick's three seats the diff --git a/internal/config/auto_test.go b/internal/config/auto_test.go index 0f7eaaa6ca..73342a393d 100644 --- a/internal/config/auto_test.go +++ b/internal/config/auto_test.go @@ -2,6 +2,7 @@ package config import ( "encoding/json" + "math" "os" "strings" "testing" @@ -426,3 +427,100 @@ func TestAutoPickReadsAMeasuredQualityPrior(t *testing.T) { t.Fatalf("the rated row did not take the worker seat: got %q", after) } } + +// ── the install's own sheet ───────────────────────────────────────────────── + +// ownCells is the install's own evidence for the prior tests: one cell the +// index document also carries, one it does not, both on the worker seat. +func ownCells() []crewpick.Cell { + return []crewpick.Cell{ + {Role: "worker", Model: "a/cheap", Mean: 90, N: 3}, + {Role: "worker", Model: "b/only", Mean: 40, N: 1}, + } +} + +// With no index in hand the own sheet's cells are the whole prior — and a +// one-observation cell of this install's own is worth having, where an +// index's min_installs would have dropped it. +func TestAutoPriorReadsTheOwnCellsAloneWithNoIndex(t *testing.T) { + restore := AutoOwnCells + defer func() { AutoOwnCells = restore }() + AutoOwnCells = ownCells + + prior := autoPrior(nil) + if len(prior[crewpick.Worker]) != 2 { + t.Fatalf("the own cells made a prior of %d worker ratings, want two: %v", len(prior[crewpick.Worker]), prior) + } + if r := prior[crewpick.Worker]["b/only"]; r.Mean != 40 || r.N != 1 { + t.Fatalf("a one-observation own cell did not survive: %v", r) + } + if r := prior[crewpick.Worker]["a/cheap"]; r.Mean != 90 || r.N != 3 { + t.Fatalf("the shared cell came back %v, want mean 90 over 3", r) + } +} + +// A model both the index and the own sheet rate folds by observation count: +// the counts add, the mean is the mean of the means weighted by them, and the +// cells either one holds alone are carried beside it. +func TestAutoPriorFoldsTheOwnCellsIntoTheIndexByObservationCount(t *testing.T) { + restoreIndex, restoreOwn := AutoIndex, AutoOwnCells + defer func() { AutoIndex, AutoOwnCells = restoreIndex, restoreOwn }() + AutoIndex = func() *index.Index { return mustIndex(t, priorDocument) } + AutoOwnCells = ownCells + + prior := autoPrior(AutoIndex()) + shared := prior[crewpick.Worker]["a/cheap"] + wantMean := (1000*95.0 + 3*90.0) / 1003 + if shared.N != 1003 { + t.Fatalf("the shared cell folded to N %d, want 1003", shared.N) + } + if math.Abs(shared.Mean-wantMean) > 1e-9 { + t.Fatalf("the shared cell folded to mean %v, want %v", shared.Mean, wantMean) + } + if r := prior[crewpick.Worker]["b/only"]; r.Mean != 40 || r.N != 1 { + t.Fatalf("the own-only cell did not survive the fold: %v", r) + } +} + +// A nil seam changes nothing: the index's prior is exactly what it was, and +// with no index there is no prior at all. +func TestANilOwnCellsSeamLeavesThePriorAlone(t *testing.T) { + restoreIndex, restoreOwn := AutoIndex, AutoOwnCells + defer func() { AutoIndex, AutoOwnCells = restoreIndex, restoreOwn }() + AutoIndex = func() *index.Index { return mustIndex(t, priorDocument) } + AutoOwnCells = nil + + prior := autoPrior(AutoIndex()) + if len(prior[crewpick.Worker]) != 1 || prior[crewpick.Worker]["a/cheap"] != (crewpick.Rating{Mean: 95, N: 1000}) { + t.Fatalf("a nil seam changed the index prior: %v", prior) + } + if prior := autoPrior(nil); prior != nil { + t.Fatalf("a nil seam and no index made a prior: %v", prior) + } +} + +// The own sheet's scores are on the 0-100 scale a judge answers on whatever +// the index's metric says, so they are read even beside an index whose +// role_quality is not gaussian — while the index's own cells are not. +func TestAutoPriorReadsTheOwnCellsBesideANonGaussianIndex(t *testing.T) { + restoreIndex, restoreOwn := AutoIndex, AutoOwnCells + defer func() { AutoIndex, AutoOwnCells = restoreIndex, restoreOwn }() + AutoIndex = func() *index.Index { + return mustIndex(t, `{ + "schema": 1, + "generated": "2026-09-17", + "min_installs": 1, + "metrics": {"role_quality": {"kind": "tally", "dims": ["role", "model"]}}, + "cells": [{"metric": "role_quality", "role": "worker", "model": "a/cheap", "mean": 5, "n": 900}] + }`) + } + AutoOwnCells = ownCells + + prior := autoPrior(AutoIndex()) + if r, ok := prior[crewpick.Worker]["a/cheap"]; !ok || r.Mean != 90 || r.N != 3 { + t.Fatalf("the own cell was not read beside a non-gaussian index: %v ok %v", prior[crewpick.Worker]["a/cheap"], ok) + } + if _, ok := prior[crewpick.Worker]["b/only"]; !ok { + t.Fatalf("the second own cell did not survive: %v", prior) + } +} From d73c2325e8d488337939983c4924e18c0b30dd20 Mon Sep 17 00:00:00 2001 From: santoshkumarradha Date: Thu, 17 Sep 2026 10:51:30 -0400 Subject: [PATCH 106/173] session: judge is the seventh seat word, mapped from no tier and no role --- internal/session/usage_ledger.go | 2 +- internal/session/usage_seat.go | 22 ++++++++++++++-------- internal/session/usage_seat_test.go | 18 +++++++++++++----- 3 files changed, 28 insertions(+), 14 deletions(-) diff --git a/internal/session/usage_ledger.go b/internal/session/usage_ledger.go index ec05ac8234..e95cf0f2c7 100644 --- a/internal/session/usage_ledger.go +++ b/internal/session/usage_ledger.go @@ -152,7 +152,7 @@ type UsageLine struct { // labelled this column with those words would be inventing the join. Role string `json:"role,omitempty"` // Seat is WHICH TIER'S MODEL ANSWERED, one word of the closed vocabulary - // [Seat] spells — reflex, low, worker, high, mastermind, talk — derived at + // [Seat] spells — reflex, low, worker, high, mastermind, judge, talk — derived at // the bank door ([TagUsage], [SeatOfRole], [SeatOfAgent]) and never typed // at a call site. It is the fact a spend page wants when it asks whether // the money went on the seat that does the work or the seat that thinks, diff --git a/internal/session/usage_seat.go b/internal/session/usage_seat.go index e5180019d5..a9711eafb3 100644 --- a/internal/session/usage_seat.go +++ b/internal/session/usage_seat.go @@ -7,14 +7,15 @@ package session // can already say what a day cost and which model took the money; without a // seat it cannot say whether the money went on the seat that does the work or // the seat that thinks. The vocabulary is internal/roles' five tiers — the -// classes the person actually configures — plus `talk`, the one kind of call -// no tier governs: a conversation's own turns. +// classes the person actually configures — plus `judge`, the seat a run's +// judge is billed to, and `talk`, the one kind of call no tier governs: a +// conversation's own turns. // // THE WORDS ARE DERIVED, NEVER TYPED AT A CALL SITE. The registry already // knows which tier a role sits on ([roles.TierOf]), so a call that went // through it is seated by one hop, and the only hand-written table here is // the five tiers to the five words ([SeatOfTier]). A seat is a fact about -// which tier's model answered, not a judgement about the call, and the six +// which tier's model answered, not a judgement about the call, and the seven // words are the whole vocabulary: no free text ever reaches the field // ([TagUsage] drops anything else). @@ -39,6 +40,9 @@ const ( // SeatMastermind is the tier whose one answer shapes all the others: the // planner, the designer, a division review. SeatMastermind Seat = "mastermind" + // SeatJudge is the seat a run's judge is billed to, mapped from no tier + // and no role. + SeatJudge Seat = "judge" // SeatTalk is a conversation's own turns — the one kind of call no tier // governs, because the person is sitting in it and its model is theirs to // change mid-sentence. @@ -49,9 +53,9 @@ const ( // list rather than opening it because it is not in the tier economy at all: it // is the word for the turns a conversation makes for itself, and a settings // surface that drew it as a tier would be drawing a dial that answers nothing. -var Seats = []Seat{SeatReflex, SeatLow, SeatWorker, SeatHigh, SeatMastermind, SeatTalk} +var Seats = []Seat{SeatReflex, SeatLow, SeatWorker, SeatHigh, SeatMastermind, SeatJudge, SeatTalk} -// SeatValid reports whether a word is one of the six. It compares EXACTLY: no +// SeatValid reports whether a word is one of the seven. It compares EXACTLY: no // trimming, no case folding. A word from outside the set is not a seat and is // never written to a row — [TagUsage] drops it rather than writing a guess. func SeatValid(word string) bool { @@ -64,8 +68,10 @@ func SeatValid(word string) bool { } // SeatOfTier maps each of [roles.Tiers] to its own word, and reports false for -// anything else. No tier maps to talk: talk is what a conversation's own turns -// answer under, and no tier governs those. +// anything else. No tier maps to talk, because talk is what a conversation's +// own turns answer under and no tier governs those — and none maps to judge +// either, because judge is the seat a run's judge is billed to and no tier +// holds that. func SeatOfTier(tier roles.Tier) (Seat, bool) { switch tier { case roles.TierReflex: @@ -148,7 +154,7 @@ func (a *Agent) agentKind() AgentKind { // through. Nothing else on the line is touched. // // A non-empty role is written to UsageLine.Role; an empty role leaves whatever -// was there. The seat argument wins when it is one of the six words; a word +// was there. The seat argument wins when it is one of the seven words; a word // outside the set is dropped rather than written, and the seat then falls back // to the role's own tier ([SeatOfRole]); if that fails too, whatever seat the // line already carried stays — which is what keeps a caller that knows nothing diff --git a/internal/session/usage_seat_test.go b/internal/session/usage_seat_test.go index 996025e04c..99a73ac23c 100644 --- a/internal/session/usage_seat_test.go +++ b/internal/session/usage_seat_test.go @@ -13,10 +13,10 @@ import ( // ── the vocabulary ────────────────────────────────────────────────────────── -func TestTheSeatVocabularyIsSixWordsCheapestFirstTalkLast(t *testing.T) { - want := []Seat{SeatReflex, SeatLow, SeatWorker, SeatHigh, SeatMastermind, SeatTalk} +func TestTheSeatVocabularyIsSevenWordsCheapestFirstTalkLast(t *testing.T) { + want := []Seat{SeatReflex, SeatLow, SeatWorker, SeatHigh, SeatMastermind, SeatJudge, SeatTalk} if len(Seats) != len(want) { - t.Fatalf("the vocabulary holds %d words, want the six: %v", len(Seats), Seats) + t.Fatalf("the vocabulary holds %d words, want the seven: %v", len(Seats), Seats) } for i, seat := range want { if Seats[i] != seat { @@ -49,8 +49,9 @@ func TestSeatOfTierMapsEveryTierAndNothingElse(t *testing.T) { } } // Talk is the word for a conversation's own turns, and no tier governs - // those, so no tier answers talk. - for _, tier := range []roles.Tier{"", "talk", "speech", "execution"} { + // those, so no tier answers talk. Judge is the seat a run's judge is + // billed to, and no tier holds that either. + for _, tier := range []roles.Tier{"", "talk", "judge", "speech", "execution"} { if _, ok := SeatOfTier(tier); ok { t.Fatalf("SeatOfTier(%q) answered, want false", tier) } @@ -85,6 +86,13 @@ func TestSeatOfRoleIsOneHopThroughTheRegistry(t *testing.T) { t.Fatalf("SeatOfRole(%q) answered %q, want false — a role the registry never had", role, seat) } } + // And no role of the registry answers judge: judge is the seat a run's + // judge is billed to, and the registry holds no role for it. + for role := range want { + if seat, ok := SeatOfRole(role); ok && seat == SeatJudge { + t.Fatalf("SeatOfRole(%q) answered judge, want a tier's word", role) + } + } } func TestSeatOfAgentIsTheTurnsOwnSeat(t *testing.T) { From 55c1c1288e6554739052abc244f7aaf568d79457 Mon Sep 17 00:00:00 2001 From: santoshkumarradha Date: Thu, 17 Sep 2026 10:55:44 -0400 Subject: [PATCH 107/173] cmd/codeaf: the own sheet is seated beside the index and counted by show and status --- cmd/codeaf/pool.go | 43 ++++++++++- cmd/codeaf/pool_test.go | 156 +++++++++++++++++++++++++++++++++++++++- cmd/codeaf/poolindex.go | 40 +++++++++-- 3 files changed, 231 insertions(+), 8 deletions(-) diff --git a/cmd/codeaf/pool.go b/cmd/codeaf/pool.go index 9b8f1217a8..dc9b1a3885 100644 --- a/cmd/codeaf/pool.go +++ b/cmd/codeaf/pool.go @@ -27,6 +27,7 @@ import ( "github.com/Agent-Field/codeaf/internal/pool/outbox" "github.com/Agent-Field/codeaf/internal/pool/poolcfg" "github.com/Agent-Field/codeaf/internal/pool/pull" + "github.com/Agent-Field/codeaf/internal/pool/record" "github.com/Agent-Field/codeaf/internal/tui2/reltime" ) @@ -119,8 +120,8 @@ func poolReading(name string, args []string, output io.Writer, poolDir string, c // printPool is the reading form's whole answer. The config first — every value // beside the word saying where it came from, one of default, setting, env or -// ci — then the cached index with its age, then, for status, the outbox and -// the two doors the mode opens. +// ci — then the cached index with its age, then the install's own sheet, +// then, for status, the outbox and the two doors the mode opens. func printPool(output io.Writer, poolDir string, cfg poolcfg.Config, now time.Time, asJSON, withStatus bool) error { var cached *index.Index // The cache is read the way show reads everything else, as an answer and @@ -168,6 +169,18 @@ func printPool(output io.Writer, poolDir string, cfg poolcfg.Config, now time.Ti return err } } + // The install's own sheet is said the way every other nothing here is + // said: a sheet that holds no cell yet is `none`, and one that holds some + // is counted with its noun. + own := ownSheetSummary(poolDir) + if own.Cells == 0 { + if _, err := fmt.Fprintln(output, "own sheet: none"); err != nil { + return err + } + } else if _, err := fmt.Fprintf(output, "own sheet: %s, %s\n", + countWord(own.Cells, "cell", "cells"), countWord(own.Observations, "observation", "observations")); err != nil { + return err + } if withStatus { _, err := fmt.Fprintf(output, "pending %d · can send %s · can read %s\n", pendingRows(poolDir), yesNo(cfg.CanSend()), yesNo(cfg.CanRead())) @@ -176,6 +189,30 @@ func printPool(output io.Writer, poolDir string, cfg poolcfg.Config, now time.Ti return nil } +// ownSummary is the install's own sheet as the reading forms carry it: the +// cells the picker reads beside the index, and the observations behind them. +type ownSummary struct { + Cells int `json:"cells"` + Observations int `json:"observations"` +} + +// ownSheetSummary counts what the install's own sheet holds for the picker: +// the role_quality cells recorded with no dim labels and the observations +// behind them. A sheet that is missing, or one that does not parse, reads as +// none — the reading form reports what a person has. +func ownSheetSummary(poolDir string) ownSummary { + sheet, err := record.LoadSheet(record.OwnSheetPath(poolDir)) + if err != nil { + return ownSummary{} + } + var summary ownSummary + for _, cell := range record.Cells(sheet) { + summary.Cells++ + summary.Observations += cell.N + } + return summary +} + // poolAnswer is the --json shape of the reading forms: the config flat, the // cached index under index or null. The three status fields are pointers so a // show carries none of them — a field a form does not answer reads as @@ -190,6 +227,7 @@ type poolAnswer struct { CanSend *bool `json:"can_send,omitempty"` CanRead *bool `json:"can_read,omitempty"` Index *indexSummary `json:"index"` + Own ownSummary `json:"own"` } // indexSummary is the cached index as the reading forms carry it: the @@ -243,6 +281,7 @@ func printPoolJSON(output io.Writer, poolDir string, cfg poolcfg.Config, cached answer.CanSend = &send answer.CanRead = &read } + answer.Own = ownSheetSummary(poolDir) encoded, err := json.Marshal(answer) if err != nil { return err diff --git a/cmd/codeaf/pool_test.go b/cmd/codeaf/pool_test.go index 9c47adbee8..c9b533950c 100644 --- a/cmd/codeaf/pool_test.go +++ b/cmd/codeaf/pool_test.go @@ -13,9 +13,13 @@ import ( "testing" "time" + "github.com/Agent-Field/codeaf/internal/config" + "github.com/Agent-Field/codeaf/internal/crewpick" "github.com/Agent-Field/codeaf/internal/home" "github.com/Agent-Field/codeaf/internal/pool/outbox" "github.com/Agent-Field/codeaf/internal/pool/poolcfg" + "github.com/Agent-Field/codeaf/internal/pool/record" + "github.com/Agent-Field/codeaf/internal/pool/tally" ) // poolClock is a stopped now, so an index's age is a fact rather than a race @@ -101,7 +105,8 @@ func seedOutbox(t *testing.T) string { // The reading form answers over the whole config, every value beside the word // saying where it came from — and a machine that has never read an index says -// so in a sentence rather than printing nothing at all. +// so in a sentence rather than printing nothing at all. An install that has +// recorded nothing of its own says so too. func TestPoolShowPrintsTheConfigAndSaysWhenNoIndexIsCached(t *testing.T) { var out strings.Builder if err := runPoolWith(nil, &out, t.TempDir(), poolClock(t), noEnv); err != nil { @@ -114,6 +119,7 @@ func TestPoolShowPrintsTheConfigAndSaysWhenNoIndexIsCached(t *testing.T) { "submit https://pool.invalid/submit · default", "ttl 1d · default", "no index cached yet · built-in seed of 2026-09-17", + "own sheet: none", } { if !strings.Contains(body, want) { t.Errorf("show is missing %q:\n%s", want, body) @@ -198,6 +204,9 @@ func TestPoolStatusCountsPendingRowsAndNamesItsDoors(t *testing.T) { if !strings.Contains(out.String(), "pending 2 · can send yes · can read yes") { t.Fatalf("status did not count the seeded rows:\n%s", out.String()) } + if !strings.Contains(out.String(), "own sheet: none") { + t.Fatalf("status did not say the install has recorded nothing of its own:\n%s", out.String()) + } out.Reset() if err := runPoolWith([]string{"status", "--json"}, &out, dir, poolClock(t), noEnv); err != nil { @@ -489,3 +498,148 @@ func TestPoolShowJSONWithNoCacheReportsTheSeed(t *testing.T) { t.Fatalf("the seed was not reported: %+v", answer.Index) } } + +// ── THE OWN SHEET ─────────────────────────────────────────────────────────── + +// seedOwnSheet records the install's own scores the way a recorder would: +// through the sheet's own observe, saved to the path the pool reads it back +// from. +func seedOwnSheet(t *testing.T, dir string) { + t.Helper() + sheet := tally.New() + sheet.Observe("role_quality", "worker", "a/one", nil, 80) + sheet.Observe("role_quality", "worker", "a/one", nil, 90) + sheet.Observe("role_quality", "worker", "b/two", nil, 70) + if err := record.SaveSheet(filepath.Join(dir, "pool", "own.json"), sheet); err != nil { + t.Fatal(err) + } +} + +// The install's own judged scores are said with their noun — the cells the +// picker reads beside the index and the observations behind them — on the +// reading forms and in the --json object alike. +func TestPoolShowSaysWhatTheOwnSheetHolds(t *testing.T) { + dir := t.TempDir() + seedOwnSheet(t, dir) + + var out strings.Builder + if err := runPoolWith([]string{"show"}, &out, dir, poolClock(t), noEnv); err != nil { + t.Fatal(err) + } + if !strings.Contains(out.String(), "own sheet: 2 cells, 3 observations") { + t.Fatalf("show did not count the own sheet's cells:\n%s", out.String()) + } + + out.Reset() + if err := runPoolWith([]string{"status"}, &out, dir, poolClock(t), noEnv); err != nil { + t.Fatal(err) + } + if !strings.Contains(out.String(), "own sheet: 2 cells, 3 observations") { + t.Fatalf("status did not count the own sheet's cells:\n%s", out.String()) + } + + out.Reset() + if err := runPoolWith([]string{"show", "--json"}, &out, dir, poolClock(t), noEnv); err != nil { + t.Fatal(err) + } + var answer struct { + Own struct { + Cells int `json:"cells"` + Observations int `json:"observations"` + } `json:"own"` + } + if err := json.Unmarshal([]byte(out.String()), &answer); err != nil { + t.Fatalf("--json did not parse: %v\n%s", err, out.String()) + } + if answer.Own.Cells != 2 || answer.Own.Observations != 3 { + t.Fatalf("the own sheet counted as %+v, want 2 cells over 3 observations", answer.Own) + } +} + +// An own sheet that does not parse is a loss, not a fault a reading form +// stops for: it reads as none. +func TestPoolShowReadsAnUnparsableOwnSheetAsNone(t *testing.T) { + dir := t.TempDir() + poolDir := filepath.Join(dir, "pool") + if err := os.MkdirAll(poolDir, 0o755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(poolDir, "own.json"), []byte("not a document"), 0o600); err != nil { + t.Fatal(err) + } + var out strings.Builder + if err := runPoolWith([]string{"show"}, &out, dir, poolClock(t), noEnv); err != nil { + t.Fatal(err) + } + if !strings.Contains(out.String(), "own sheet: none") { + t.Fatalf("a broken own sheet did not read as none:\n%s", out.String()) + } +} + +// The wired reader answers the own sheet's cells — the install's own evidence, +// read once and parsed once — and an off mode seats nothing at all. +func TestWirePoolIndexSeatsTheOwnSheetsCells(t *testing.T) { + prevIndex, prevOwn := config.AutoIndex, config.AutoOwnCells + t.Cleanup(func() { config.AutoIndex, config.AutoOwnCells = prevIndex, prevOwn }) + + dir := t.TempDir() + seedOwnSheet(t, dir) + wirePoolIndex(dir) + if config.AutoOwnCells == nil { + t.Fatal("the own sheet was not seated") + } + own := config.AutoOwnCells() + want := []crewpick.Cell{ + {Role: "worker", Model: "a/one", Mean: 85, N: 2}, + {Role: "worker", Model: "b/two", Mean: 70, N: 1}, + } + if len(own) != len(want) { + t.Fatalf("the seated cells are %+v, want %+v", own, want) + } + for i := range want { + if own[i] != want[i] { + t.Fatalf("cell %d is %+v, want %+v", i, own[i], want[i]) + } + } + if config.AutoIndex == nil || config.AutoIndex() == nil { + t.Fatal("the index was not seated beside the own sheet") + } +} + +// A mode that forbids reading seats nothing: the own sheet is the pool's own +// reading, and off is off for the whole of it. +func TestWirePoolIndexSeatsNothingWhenThePoolIsOff(t *testing.T) { + prevIndex, prevOwn := config.AutoIndex, config.AutoOwnCells + t.Cleanup(func() { config.AutoIndex, config.AutoOwnCells = prevIndex, prevOwn }) + t.Setenv("CODEAF_MODEL_POOL", "off") + + dir := t.TempDir() + seedOwnSheet(t, dir) + wirePoolIndex(dir) + if config.AutoOwnCells != nil { + t.Fatal("a pool that forbids reading seated the own sheet") + } + if config.AutoIndex != nil && config.AutoIndex() != nil { + t.Fatal("a pool that forbids reading seated an index") + } +} + +// An own sheet that does not parse is a loss, not a fault a pick stops for: +// the wired reader answers nothing rather than a broken sheet's half. +func TestWirePoolIndexSeatsNothingForAnUnparsableOwnSheet(t *testing.T) { + prevOwn := config.AutoOwnCells + t.Cleanup(func() { config.AutoOwnCells = prevOwn }) + + dir := t.TempDir() + poolDir := filepath.Join(dir, "pool") + if err := os.MkdirAll(poolDir, 0o755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(poolDir, "own.json"), []byte("not a document"), 0o600); err != nil { + t.Fatal(err) + } + wirePoolIndex(dir) + if got := config.AutoOwnCells; got != nil && got() != nil { + t.Fatal("a broken own sheet was seated") + } +} diff --git a/cmd/codeaf/poolindex.go b/cmd/codeaf/poolindex.go index 2bd8587fd2..2911eedeea 100644 --- a/cmd/codeaf/poolindex.go +++ b/cmd/codeaf/poolindex.go @@ -24,10 +24,12 @@ import ( "time" "github.com/Agent-Field/codeaf/internal/config" + "github.com/Agent-Field/codeaf/internal/crewpick" "github.com/Agent-Field/codeaf/internal/guard" "github.com/Agent-Field/codeaf/internal/pool/index" "github.com/Agent-Field/codeaf/internal/pool/poolcfg" "github.com/Agent-Field/codeaf/internal/pool/pull" + "github.com/Agent-Field/codeaf/internal/pool/record" "github.com/Agent-Field/codeaf/internal/trace" ) @@ -102,13 +104,41 @@ func refreshPoolIndex(ctx context.Context, profileDir string, cfg poolcfg.Config } } -// wirePoolIndex seats this process's index beside its catalog: it hands the -// reader to seat resolution (config.AutoIndex) and starts the refresh beside -// it. It is called once at start-up, from the same places config.AutoModels is -// set, so a tier row that says `auto` resolves against an index on every door -// that resolves a seat. +// wirePoolIndex seats this process's pool readings beside its catalog: it +// hands the index reader to seat resolution (config.AutoIndex), the install's +// own judged scores beside them (config.AutoOwnCells, read off the own sheet +// under the pool directory), and starts the refresh beside both. It is called +// once at start-up, from the same places config.AutoModels is set, so a tier +// row that says `auto` resolves against a prior on every door that resolves a +// seat. func wirePoolIndex(profileDir string) { cfg := config.ModelPoolAt(profileDir) config.AutoIndex = poolIndexFor(profileDir, cfg, time.Now) + config.AutoOwnCells = poolOwnCellsFor(profileDir, cfg) startPoolIndexRefresh(context.Background(), profileDir, cfg, poolPublicKeys) } + +// poolOwnCellsFor builds this process's one reader of the install's own judged +// scores. THE READ IS START-UP WORK AND NOTHING ON A RUN'S PATH, the same +// posture the index reader keeps: the own sheet under the profile's pool +// directory is read and parsed here, once, and the returned function answers +// the cells already in hand, so a pick does no disk and no decode per call. +// A mode that forbids reading answers nil — the same nothing the seam reads +// before any sheet exists — and so does a sheet that does not parse, which is +// said under the debug record's switch and read as absent rather than fatal: +// an own sheet is the install's own evidence, and a broken one is a loss, not +// a fault a pick should stop for. +func poolOwnCellsFor(profileDir string, cfg poolcfg.Config) func() []crewpick.Cell { + if !cfg.CanRead() { + return nil + } + sheet, err := record.LoadSheet(record.OwnSheetPath(config.ProfilePath(profileDir, "pool"))) + if err != nil { + if trace.Enabled() { + log.Printf("model pool: own sheet: %v", err) + } + return nil + } + cells := record.Cells(sheet) + return func() []crewpick.Cell { return cells } +} From 58b3d541d1bcd19fa0ebb9dd9e626d5d350aa341 Mon Sep 17 00:00:00 2001 From: santoshkumarradha Date: Thu, 17 Sep 2026 10:57:38 -0400 Subject: [PATCH 108/173] docs: the recorder change entry and the own sheet named in the pool section --- docs/changes/unreleased/1093-pool-recorder.md | 37 +++++++++++++++++++ .../manual/chat/running-from-the-terminal.md | 4 +- 2 files changed, 40 insertions(+), 1 deletion(-) create mode 100644 docs/changes/unreleased/1093-pool-recorder.md diff --git a/docs/changes/unreleased/1093-pool-recorder.md b/docs/changes/unreleased/1093-pool-recorder.md new file mode 100644 index 0000000000..db5612ea7e --- /dev/null +++ b/docs/changes/unreleased/1093-pool-recorder.md @@ -0,0 +1,37 @@ +--- +kind: added +title: A pure package that records a run's judged scores into an own sheet and outbox rows +pr: 1093 +surface: [engine] +invalidates: [] +--- + +`internal/pool/record` turns the scores `judge.Judge` answered into the two +records an install keeps of them. `Recorder.Record` observes every valid score +into the sheet it holds, under the `role_quality` metric the crew picker reads +seat quality from, and appends one row per score to its outbox when it holds +one; a nil outbox records locally only. A score whose role is not one of the +judged seats, or that is not on the 0-100 scale, is skipped and named in the +error beside the others that were recorded, and the sheet is observed whatever +the outbox does. + +- `SaveSheet` and `LoadSheet` keep that sheet at `own.json` under the pool + directory — a missing file is an empty sheet, a save is a temp-file rename + into a 0600 file beside a 0700 directory — and `record.Cells` reads the + sheet's plain `role_quality` cells back as the cells a prior reads, sorted + by seat then model. +- `config.AutoOwnCells` is the start-up seam beside `config.AutoIndex`, and + `autoPrior` folds the own cells into the index's prior seat by seat through + `crewpick.MergePriors`, the means combined by observation count, at a floor + of one rather than the index's min_installs: an install's own scores are its + own evidence. A nil seam changes nothing. +- `cmd/codeaf` reads the own sheet once at start-up beside the index + (`wirePoolIndex`); a sheet that does not parse is said under the debug + record's switch and read as absent, not as a fault a pick stops for. + `codeaf pool show` and `pool status` say what it holds — `own sheet: + cells, observations`, or `own sheet: none` — with the same counts under + `--json`'s `own`. +- The seat vocabulary a usage row's `seat` field carries grows a seventh + word, `judge`, between mastermind and talk: the seat a run's judge is + billed to, mapped from no tier and no role. No tier and no registered role + answer it; the word reaches a row through the billing door's seat argument. diff --git a/internal/manual/chat/running-from-the-terminal.md b/internal/manual/chat/running-from-the-terminal.md index a594ad8a4b..42e22ecf95 100644 --- a/internal/manual/chat/running-from-the-terminal.md +++ b/internal/manual/chat/running-from-the-terminal.md @@ -533,7 +533,9 @@ and the addresses in force with the word saying where each came from (`default`, `setting`, `env` or `ci`), then what index is cached and how old it is, or `no index cached yet · built-in seed of `. The binary carries a seed index of our own scored runs, read until a fresher signed -one is cached. `status` adds what is waiting to be sent +one is cached. Your install also keeps the scores its judge gave in `own.json` +under the pool directory — `show` and `status` say what that sheet holds — and +the crew reads them beside the index. `status` adds what is waiting to be sent and whether the mode allows sending and reading. `--json` prints the same answer as one object. Neither form touches the network. From ef004d81c7d3ebfbe62d8660f391b134a4386d34 Mon Sep 17 00:00:00 2001 From: santoshkumarradha Date: Thu, 17 Sep 2026 10:59:14 -0400 Subject: [PATCH 109/173] pool/record: a failed append is returned with the sheet observed all the same --- internal/pool/record/record_test.go | 37 +++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/internal/pool/record/record_test.go b/internal/pool/record/record_test.go index d93ab196c6..6508e8c266 100644 --- a/internal/pool/record/record_test.go +++ b/internal/pool/record/record_test.go @@ -235,6 +235,43 @@ func TestABadRoleIsSkippedAndNamedAndTheOthersLand(t *testing.T) { } } +// An outbox that cannot take a row costs the install nothing of its own +// evidence: every score is observed into the sheet all the same, and the +// first append error is what comes back. +func TestRecordObservesTheSheetWhateverTheOutboxDoes(t *testing.T) { + sheet := tally.New() + box, err := outbox.Open(filepath.Join(t.TempDir(), "outbox.jsonl")) + if err != nil { + t.Fatal(err) + } + if err := box.Close(); err != nil { + t.Fatal(err) + } + recorder := &Recorder{Sheet: sheet, Outbox: box} + err = recorder.Record([]judge.Score{ + {Role: judge.RoleWorker, Model: "a/one", Score: 80}, + {Role: judge.RoleHigh, Model: "b/two", Score: 66}, + {Role: judge.RoleMastermind, Model: "c/three", Score: 91}, + }, "z/fourth", "chat", "three", "2026-09-17") + if err == nil { + t.Fatal("a closed outbox recorded without a word") + } + for _, want := range []struct { + role judge.Role + model string + mean float64 + }{ + {judge.RoleWorker, "a/one", 80}, + {judge.RoleHigh, "b/two", 66}, + {judge.RoleMastermind, "c/three", 91}, + } { + cell, ok := sheet.Cell(Metric, string(want.role), want.model, nil) + if !ok || cell.N != 1 || cell.Mean() != want.mean { + t.Fatalf("the %s seat was not observed despite the failed append: %+v ok %v", want.role, cell, ok) + } + } +} + // A recorder with no outbox keeps the scores locally only, and says nothing. func TestANilOutboxRecordsLocallyOnly(t *testing.T) { sheet := tally.New() From e9b53494b22dbb28635f779196c9aba2f1da9189 Mon Sep 17 00:00:00 2001 From: santoshkumarradha Date: Thu, 17 Sep 2026 10:59:44 -0400 Subject: [PATCH 110/173] session: the tier test's failure word says the vocabulary's seven --- internal/session/usage_seat_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/session/usage_seat_test.go b/internal/session/usage_seat_test.go index 99a73ac23c..b7f608c782 100644 --- a/internal/session/usage_seat_test.go +++ b/internal/session/usage_seat_test.go @@ -45,7 +45,7 @@ func TestSeatOfTierMapsEveryTierAndNothingElse(t *testing.T) { t.Fatalf("SeatOfTier(%q) is false — a tier of the registry has no seat word", tier) } if !SeatValid(string(seat)) { - t.Fatalf("SeatOfTier(%q) wrote %q, which is not one of the six", tier, seat) + t.Fatalf("SeatOfTier(%q) wrote %q, which is not one of the seven", tier, seat) } } // Talk is the word for a conversation's own turns, and no tier governs From fecc3d124c6bd798459b4b830924e2edcebd7bb3 Mon Sep 17 00:00:00 2001 From: santoshkumarradha Date: Thu, 17 Sep 2026 11:23:08 -0400 Subject: [PATCH 111/173] docs: referee review of the model-pool paper Co-Authored-By: codeaf --- docs/design/model-pool/REVIEW.md | 67 ++++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 docs/design/model-pool/REVIEW.md diff --git a/docs/design/model-pool/REVIEW.md b/docs/design/model-pool/REVIEW.md new file mode 100644 index 0000000000..b91c6fa63d --- /dev/null +++ b/docs/design/model-pool/REVIEW.md @@ -0,0 +1,67 @@ +# Review of docs/design/model-pool/learn.tex + +## Summary + +The paper builds the crew picker in three layers: a catalog-only pick (a pareto front in bill and quality with a log-scale knee), a privacy-preserving measurement pool that replaces the catalog's proxy for quality, and a Thompson-sampling learner over role-level posteriors. The first layer is implemented and matches the prose in every particular checked: (1) is `crewpick.SeatCost` (crewpick.go:258), (2) is `quality` (crewpick.go:463) with the same pool-normalised scale, (3) is the shortlist at `Floor = 0.80` (crewpick.go:183, 454), (4)–(5) are `FrontWith`/`Presets`/`AtKnob`/`knee` (crewpick.go:315, 376, 388, 407) including the id tie order and the epsilon, the seat shapes and the vendor rule match `DefaultShapes` and the crew loop, and the Corollary's blend with `k = 30` is `blendQuality` with `PriorWeightAt = 30` (crewpick.go:479, 189). The judge rule of 5.1 matches `judge.Pick` (judge.go:159) with floor 60 (judge.go:45), the pool modes match `poolcfg`, the signed pull with version refusal matches `internal/pool/pull`, and the seed index matches Figure 3's construction exactly: its 14 cells are Beta(1,1) posterior means and sds of a no-major-defect bit times 100 (verified against `internal/pool/index/seed.json` and `data/seed-cells.csv`, e.g. worker/glm-5.3-flash n=62, mean 65.6, sd 5.9). Lemmas 1 and 2, Propositions 1, 2 and 5, the ICC formula of 5.4, estimators (17) and (18), and the steady-state Kalman relation of 5.5 are correct as mathematics. The central statistical claim is not: the capped counts of Proposition 4 are not the posterior its own proof derives, the drift estimator (14) returns half of what it estimates, and Proposition 3's closing sentence is false. Against the branch, the Status section overstates the wiring in six places, and two of the quoted experiment numbers do not match the committed tables. Major revision. + +## Defects, by severity + +1. **Proposition 4, eq (15)–(16) (learn.tex:325–335). The capped counts are not the posterior the proof derives.** The proof computes the catalog-and-pool precision about $\mu^u$ as $(k+n^{\mathrm{pool}})\ell/(k+n^{\mathrm{pool}}+\ell)$ and then claims this "splits into the two terms of (16) in proportion to $k$ and $n^{\mathrm{pool}}$". It does not: $k\ell/(k+\ell) + n\ell/(n+\ell) \ne (k+n)\ell/(k+n+\ell)$ for any positive $k, n, \ell$. The correct split under the proof's own model is $k\ell/(k+n^{\mathrm{pool}}+\ell)$ on the catalog and $n^{\mathrm{pool}}\ell/(k+n^{\mathrm{pool}}+\ell)$ on the pool. With $k=30$, $n^{\mathrm{pool}}=100$, $\ell=30$ the printed weights sum to 38.1 against the proof's 24.4, and as $k$ and $n^{\mathrm{pool}}$ grow the printed formula lets catalog and pool together approach $2\ell$ own rows, breaking the proof's own bound of $\ell$ and the text's claim after (16) that the pool's evidence is "worth at most $\ell$ own rows" (true only of the sum). A second error sits in the proof's first stage: precision $(k+n^{\mathrm{pool}})/\sigma^2$ treats pool rows as $\sigma^2$-noise observations of $\mu_{rm}$, but under the hierarchy (6) they are other installs' rows with marginal variance $\sigma^2+\tau_1^2$, so their contribution is $n^{\mathrm{pool}}/(\sigma^2+\tau_1^2)$ — the same inconsistency biases (19). Fix: state the posterior precision as one external term, $W = K\ell/(K+\ell)$ with $K = k + n^{\mathrm{pool}}\sigma^2/(\sigma^2+\tau_1^2)$ (or $K = k+n^{\mathrm{pool}}$ under the stated simplification), weight the catalog and pool as $k/K$ and $n^{\mathrm{pool}}/K$ of $W$, keep "external evidence $\le \ell$" for the sum, and make (19)'s noise term match. The Corollary is unaffected (it is the $\ell=\infty$ limit, which both forms share). + +2. **Eq (14) (learn.tex:264). The drift estimator carries a spurious factor of one half.** For adjacent-day means, $\operatorname{Var}(\bar y_d - \bar y_{d-1}) = \omega^2 + \sigma^2(1/n_d + 1/n_{d-1})$; the bracket subtracts exactly that noise, so (14) returns $\hat\omega^2/2$ and the filter forgets at half the rate the data show. The $\tfrac12$ is correct only for differences taken at lag two. Fix: drop it, or difference at lag 2 with the matching noise term. The steady-state relation in the same paragraph, $\alpha^2/(1-\alpha) = \bar n\,\omega^2/\sigma^2$, is correct. + +3. **Eq (13)/Proposition 2 (learn.tex:209–219) against `internal/pool/tally/tally.go`: the shipped sheet does not merge by the join.** `Sheet.Merge` adds ("adds other into s", tally.go:201; "Merging a sheet into itself doubles it", tally.go:199) — commutative and associative but not idempotent — and the cell address (metric, role, model, dims) carries no install key (tally.go:123). Proposition 2 itself is correct mathematics for per-install running totals; addition is safe only for pairwise-disjoint sheets, so a re-delivered or duplicated publication double-counts and claim (iii)'s "with any duplication" does not hold of the shipped merge. The abstract's "sufficient statistics that form a join-semilattice, so any number of installs and a relay can merge them in any order and reach the same state" is a design, not a description of anything on the branch, and the Status paragraph (learn.tex:548) puts "the outbox and the sheet" on the branch. Fix: either say the shipped sheet is additive and duplicate safety is the outbox's per-node sent markers (a guarantee that does not federate), or land the install-keyed store with the max-join before making the federation claims. + +4. **Status (learn.tex:548). The judge, the sheet and the outbox are on the branch as libraries with no call sites.** `internal/pool/judge` is imported by nothing (no caller of `judge.Judge` or `judge.Pick` outside the package), `internal/pool/tally` is imported by nothing, and `outbox.Append` has no caller — `cmd/codeaf/pool.go` only reads `Pending`. No run is judged, no row is written, no sheet accrues, so the closing sentence "Until the relay exists the pool is the seed index plus each install's own sheet" is false: on the branch the pool is the seed or cached index alone, blended through `config.autoPrior` (internal/config/auto.go:144). Fix: say the three packages exist unwired and that *learn* on the branch means catalog plus index. + +5. **"The relay's code is in this repository" (learn.tex:544, with the deploy sentence at 492). There is no `relay/` directory.** Status (learn.tex:548) itself lists the relay as "specified here and next to land"; sections 9 and "What the user controls" contradict it in the present tense. Fix: future tense, or land the directory. + +6. **"The signing key's public half is compiled into the binary" (learn.tex:495, 544). It is not.** `var poolPublicKeys []ed25519.PublicKey` is empty, with the comment "EMPTY ON THIS BRANCH" (cmd/codeaf/pool.go:37); `pool verify` refuses without `--key` (cmd/codeaf/pool.go, `verifyPool`), and the background refresh never starts because `startPoolIndexRefresh` returns when the key list is empty (cmd/codeaf/poolindex.go:77) — so "a refresh runs in the background" (learn.tex:489) is inert on this branch. Fix: say the mechanism is in place and the key ships beside the first published index, or compile a key. + +7. **"120 scored runs" (abstract, learn.tex:64; section 8, learn.tex:441; captions at 186 and 477). The number matches neither committed dataset where it is used.** The `runs` column of `data/measured-crews.csv` sums to 1224 across the 56 crews, and the seed's 14 cells carry n summing to 225 role-level scores (consistent with about 120 judged runs at 1.9 roles each — Figure 3's usage is the defensible one). But the abstract, the Figure 1 caption and section 8 attach the 120 to the 56-crew table, whose rows are backed by 1224 runs, and the section 8 experiments take that table as ground truth. Fix: say what the 120 counts (judged runs) and correct the three locations that attach it to the fitted-crew table. + +8. **Section 8 (learn.tex:460). "at four hundred runs the role-level learner's regret is about a third of the crew-level learner's on the same scores."** `tab-semibandit.tex` at $N=400$ reads 0.15 (role), 0.31 (crew, mean judge score), 0.54 (crew, clean bit). Against the learner the sentence names — fed the same scores — the ratio is about one half. A third holds only against the clean-bit learner, which is not "on the same scores"; the abstract's "factor of three" matches that other comparison. Fix: name the baseline and quote it; as written the sentence is wrong against its own table. + +9. **Proposition 3 (learn.tex:246). "With the single centring constraint, exactly one component is fully identified."** False when $G$ has $c \ge 2$ components: the kernel of the two-way design is $c$-dimensional and the single constraint $\sum_j n_{\cdot\cdot j}\beta_j = 0$ leaves an $(c{-}1)$-dimensional gauge that shifts $\mu$ in every component jointly (shifts weighted by $\sum_C N_C\delta_C = 0$), so no component's levels are pinned — only the within-component contrasts, as the preceding sentence correctly says. Identifying a component's levels needs one centring constraint per component. Note also that the alternating re-centring (learn.tex:235–239) is global, so on a disconnected $G$ its fixed point depends on the initialisation. Fix: one constraint per component, or state that identification holds only when $G$ is connected. + +10. **Section 8 (learn.tex:444). "the twelve chat-door crews" — the data has 23; `figures.py` keeps the first 12 in file order (figures.py:136, `[:12]`).** No criterion for the twelve is stated anywhere; which twelve they are sets the oracle ($1.94 all-in) and every number in `tab-regret.tex`. Fix: state the subset rule, or use all 23. + +11. **Section 5.4 (learn.tex:253). "A cell backed by fewer than three installs is published but not read" — nothing on the branch counts installs.** The index reader drops a cell when its n — an observation count — falls below the document's `min_installs` (internal/pool/index/index.go:265), `crewpick.PriorFromCells` applies the same test to a count (crewpick.go:231), and the shipped seed sets `min_installs` to 1 (internal/pool/index/seed.json:4); one install contributing three rows passes. The related claim in section 9 (learn.tex:498) that "the min-installs and robust-aggregation rules bound what a flood of fabricated nonces can move" is unsupported: the Huber rule's breakdown point of 1/2 is in installs, fabricated installs are unauthenticated, and a flood that outnumbers genuine installs passes the breakdown point while each nonce clears any rows-per-install cap at trivial cost. Fix: carry a distinct-install count per cell in the published document, set the shipped floor to it, and either analyse the flood bound or drop the claim. + +12. **Section 5.1 (learn.tex:199–201). The row fields exist nowhere; the shipped row is an envelope.** The outbox stores `{schema, day, nonce, payload}` with a fresh 16-byte nonce per row (internal/pool/outbox/outbox.go:49, 135) and an opaque payload that nothing builds; the per-profile install nonce of learn.tex:201 exists in no file; the sheet's cell is (metric, role, model, dims) with the triple (n, sum, sumsq) (internal/pool/tally/tally.go:123, 266), which can carry judge/day/size/door as dims but carries none today. Relatedly, Definition 2 (learn.tex:203) puts day inside the cell and section 5.5 says "The day is a cell dimension", while section 9 (learn.tex:492) says the store holds "one triple per (install, cell, day)" — the two spellings of cell differ. Fix: present the row schema as specified and add it to Status, and pick one spelling of the cell. + +13. **Abstract (learn.tex:64), two claims contradicted by the paper's own later sections.** "every constant except the user's price of a defect is an empirical-Bayes estimate" — Table 2 says $\lambda$ is the user's, $\phi=0.8$ and the judge floor 60 are structural, and $v_r$ is measured; only $\sigma$, $k$, $\ell_f$, $\omega^2$ and $\beta$ are estimated. "stay out, and still learns from its own runs" — with the pool off the code reads no index at all (cmd/codeaf/poolindex.go:50) and the paper itself says "with it off, only the catalog prior remains" (learn.tex:544). Fix both sentences. + +14. **"What the user controls" (learn.tex:540). The `explore` setting does not exist.** `internal/config` carries `model_pool` (settings.go:119), the pick words and the crew presets, but no `explore` key anywhere; nothing toggles a draw. Fix: mark the row "next to land", as Status does for the draw. + +15. **Section 4 (learn.tex:171–173). The working objective $E$ is neither derived form.** $E = c+\lambda t + H(1-p_{\mathrm{clean}})$ equals $E_H$ with $r$ set to 1 and $p$ replaced by $p_{\mathrm{clean}}$: non-deliveries are priced as shipped defects, and the rerun term $(1-r)(c_f+\lambda t_f)/r$ — and $E_0$'s $1/(rp)$ factor — are gone. For a cheap but flaky crew with $r$ well below 1, $E$ can rank it above what $E_0$ or $E_H$ would; the paper never states the surrogate. Fix: one sentence deriving $E$ as $E_H$ under $r=1$ (or adding the rerun term), and the regime where the surrogate is safe ($H$ large next to run cost, $r$ not far below 1). + +16. **Section 3 (learn.tex:110). The imputation sentence does not describe the implemented imputation.** `crewpick` uses the donor median only with three or more donors, averages the offers from every present index, caps the estimate at the pool's largest measured value of the missing index, and falls back to scale-proportional standing below three donors (crewpick.go:525, 582). The sentence describes none of this. Fix: describe the implemented rule, or mark the sentence as simplified. + +17. **Section 9 (learn.tex:489). "The loader reads the cached index if one is present and the embedded seed index otherwise."** The cache wins only when it is not older than the seed: `index.Fallback` compares the generated days and prefers the seed on an older cache (internal/pool/index/index.go:96–104). Fix: "the fresher of the cache and the seed by generated day." + +## Correct but overstated + +- Abstract (learn.tex:64): "nothing an install sends can be read back into what it was working on." Absence of text is not unlinkability: a row's (dollars, minutes, size, door, day, role, model) is a quasi-identifying tuple against anyone who knows the task schedule. Section 9's store-scoped claim (learn.tex:498) is the defensible form; the abstract goes further than it. +- Proposition 5 (learn.tex:420): "is exactly Thompson sampling over the crews' posterior costs." Exact only if the per-(role, model) posteriors are independent; 5.3's judge-effect fit and 5.5's drift model both induce cross-cell dependence that the independent draw ignores. It is Thompson sampling on a product approximation to the posterior. +- Section 8 (learn.tex:444): "Thompson pays a small exploration tax in the first twenty runs." `tab-regret.tex` puts Thompson at 0.98 and 0.97 at $N=10$ and 20 — the worst row of the table, above even the fixed table (0.91, 0.93). The tax is real; "small" is not. +- Section 8 (learn.tex:481): "reproduces every figure and table from the two CSV files it reads." Figure 5's constants (catalog 80, measured 65, $\sigma=15$) and Figure 1's iso-line anchors are hardcoded in `figures.py`. +- Sections 9 and "What the user controls" (learn.tex:489, 544): `pool status` prints a pending count ("pending %d", cmd/codeaf/pool.go:172), not the rows; `pool show` prints the index's generated day and counts, not its cells. +- Section 9 (learn.tex:495): "(Section 5.2's join makes a rollback detectable)." A rollback is detected by the version number the puller compares (`Puller.good`, internal/pool/pull/pull.go); the join makes old data harmless, not detectable. +- Section 5.1 (learn.tex:195): "The floor exists because a judge below the worker's class rubber-stamps." The rule is an absolute floor of 60 (judge.go:45), not a floor at the worker's class: a coding-61 judge over a coding-90 worker passes it. The rationale is relative, the rule absolute. +- Section 5.1 (learn.tex:195): "at present prices it is cents per run" — not checkable from anything in the repository. + +## What a reader would still want + +- The posterior under the full hierarchy (pool rows at variance $\sigma^2+\tau_1^2$): the corrected (16) and (19), and a first $\hat k$ computed from the seed's own 14 cells — the paper's data can produce one and never does. +- The regime in which (7) ranks crews as $E_0$ and $E_H$ do: a worked example with $r<1$ and a cheap, flaky crew. +- Experiments for the untested sections: the judge-effect fit of 5.3 (the seed carries a single judge id, so the shipped data cannot exercise it), the drift model of 5.5, and the settled probability $\pi^\ast$ of section 7 — no trajectory is shown for any of them. +- The pair and interaction term of 6.1: specified with an estimator, never simulated. +- The subset rule behind "the twelve chat-door crews" and the identity of the "fixed table row" (the second line of `data/measured-crews.csv`: glm-5.3-flash with qwen3.8-max, all-in regret \$0.96 against the subset oracle), with `tab-regret.tex`'s sensitivity to both choices. +- The fabricated-install flood analysed rather than asserted (defect 11): what the Huber rule bounds once fabricated installs are the majority, and the cost per nonce to get there. +- Instance-dependent gaps: the paper's own caption says a dozen crews sit within cents; the whole regret scale (\$0.96 for the table against a \$1.94 oracle all-in) is governed by gaps the paper never reports, so "a third" and "factor of three" are dataset-specific with no gap structure given. +- Where the day-one constants come from: $\sigma=15$ and $\ell_f=30$ in Table 2 have no derivation. + +## Verdict + +Major revision. The catalog half of the paper is sound and matches the code line for line, and the CRDT, fixed-effects and robustness designs are right as designs. But the central estimator is wrong as printed (defect 1), one moment estimator is off by a factor of two (defect 2), one proposition's closing claim is false (defect 9), and the paper's account of what is implemented overstates the branch in six places (defects 3–6, 12, 14). The experiments support the direction of every claim they are attached to, but two quoted numbers do not match the committed tables (defects 7–8). Fix the mathematics, reconcile the Status paragraph with the tree, and re-derive or re-run the affected numbers; the architecture is worth publishing once every claim is as accurate as the catalog section already is. From a66afc9a0ce0b5e9384d04f88bbe3d8c63dd0d13 Mon Sep 17 00:00:00 2001 From: santoshkumarradha Date: Thu, 17 Sep 2026 11:36:08 -0400 Subject: [PATCH 112/173] session: a landed node's record reaches Config.TaskLanded on its own goroutine, with the model the check ran on beside the worker's --- internal/session/session.go | 35 ++++++++++++++++++ internal/session/task_audit.go | 8 +++++ internal/session/task_run.go | 65 ++++++++++++++++++++++++++++++++++ 3 files changed, 108 insertions(+) diff --git a/internal/session/session.go b/internal/session/session.go index ec17fff1a4..a605e9b9ef 100644 --- a/internal/session/session.go +++ b/internal/session/session.go @@ -1077,6 +1077,29 @@ func (u Usage) CachedShare() (float64, bool) { // BaseURL are required. APIKey may be empty for a session opened before the // person has handed one over — the first-run setup's case — and every request // refuses until [Agent.SetAPIKey] lands it. +// TaskLanding is one landed node, handed whole to Config.TaskLanded. It is the +// record a caller outside this package needs to judge the work: what was asked, +// what came home, who ran it and who checked it, and what the run spent. The +// fields are copied from the node's own record at the moment the node reached a +// final state, so a reader that arrives late reads a fact rather than a +// half-open run. +type TaskLanding struct { + ID uint64 + State TaskState + Brief string + Deliverable string + Report string + Claim string + Ending string + Wrote []string + Changed int + Checks []string + Worker string + High string + CostUSD float64 + Tokens int +} + type Config struct { Workspace string // tools root here; all relative paths resolve inside it Model string @@ -1386,6 +1409,18 @@ type Config struct { // TaskProgressCheck is the test seam for leash checkpoints. Production uses // the node's ordinary read-only checker; a test may answer deterministically. TaskProgressCheck func(brief string, evidence []string) (working bool, reason string) + + // TaskLanded is called ONCE per landed node, on its own goroutine, after the + // node's row is in the project's index (task_run.go's [Agent.reportTaskNode]). + // It carries [TaskLanding]: the node's record as the landing left it, the + // worker's model and the model the checking pass ran on (empty when there was + // none). A final state only — running and queued nodes land nothing — and a + // call that never blocks the reporting path: the reporting goroutine hands the + // landing over and moves on, and a caller that is slow holds up nothing but + // its own goroutine. NIL IS OFF, which is what every caller that does not + // want the news hands in, and what this package then spends nothing on. + TaskLanded func(TaskLanding) + // TaskDeadline overrides one checkpoint interval. Zero keeps the one-hour // production interval and lets deadline behavior be tested without an hour. TaskDeadline time.Duration diff --git a/internal/session/task_audit.go b/internal/session/task_audit.go index 759e8f82c9..7271889d40 100644 --- a/internal/session/task_audit.go +++ b/internal/session/task_audit.go @@ -1241,6 +1241,14 @@ func (a *Agent) auditOnce(ctx context.Context, node *TaskNode, tree taskTree, gr } defer func() { _ = auditor.Close() + // The model the check ran on is read beside the spend it folds in, for + // the same pocket's reason: a landing that later reports who checked the + // work names the model that actually answered, failover included, and + // not the one that was asked for. The auditor's model is read from the + // agent itself rather than from `on`, which is the ASKED-FOR id. + node.graph.mu.Lock() + node.checkedOn = auditor.Model() + node.graph.mu.Unlock() // The audit is part of what the node cost, so it lands in the same // pocket the node's own spend does (task_run.go's foldTaskUsage): the // person asked for a task, not for a task and separately for a judge. diff --git a/internal/session/task_run.go b/internal/session/task_run.go index d6676ce5cb..7ea52478d8 100644 --- a/internal/session/task_run.go +++ b/internal/session/task_run.go @@ -92,6 +92,7 @@ import ( "github.com/Agent-Field/codeaf/internal/approval" "github.com/Agent-Field/codeaf/internal/effort" "github.com/Agent-Field/codeaf/internal/exec/bare" + "github.com/Agent-Field/codeaf/internal/guard" "github.com/Agent-Field/codeaf/internal/provider" "github.com/Agent-Field/codeaf/internal/roles" ) @@ -648,6 +649,12 @@ type TaskNode struct { // reaches the project index (task_index.go) and the node's own notice on the // way past. cost float64 + // checkedOn is the model the node's checking pass ran on, read off the + // auditor when its spend folds in (task_audit.go) and empty when there was + // none — a node that never reached a check, or one whose check could not + // start. It is a fact about the last check, not a setting, and it is read by + // the landing hook ([Agent.reportTaskNode]) beside the worker's own model. + checkedOn string // input, output, cacheRead and cacheWrite are that same accumulation in // TOKENS, folded in beside the dollars and kept on the checkpoint with them. // @@ -3805,6 +3812,63 @@ func (n *TaskNode) burned() int { // ── the world hearing about a node ────────────────────────────────────────── +// tellTaskLanded hands one landed node to the session's own landing reader +// (Config.TaskLanded), when a door wired one. It runs on its own goroutine +// through guard.Go, so a reader that is slow — a call to a model outside the +// crew takes as long as it takes — holds up nothing on the reporting path, +// which is already answering to the note delivery beside it. +// +// THE SNAPSHOT IS TAKEN HERE, on the reporting goroutine, under the node's own +// lock discipline (graph.mu, the room's spend read outside it as notice does): +// a landing built later on the reader's goroutine could read a node already +// re-armed, and the record the reader judged would not be the one that landed. +func (a *Agent) tellTaskLanded(node *TaskNode) { + hook := a.config.TaskLanded + if hook == nil { + return + } + // A landing is a FINAL state only — work that ended on an answer: done, + // failed, or ended where nobody could check it. A node still queued or + // running has no record to judge yet, and its own updates already travelled + // the reporting path above. + switch node.stateNow() { + case TaskDone, TaskFailed, TaskUnverified: + default: + return + } + landing := node.landing() + guard.Go("task landed", func() { hook(landing) }) +} + +// landing copies the node out from under the graph's lock, shaped for a reader +// outside this package. The spend is read BEFORE the graph lock is taken, the +// same order [TaskNode.notice] keeps. +func (n *TaskNode) landing() TaskLanding { + tokens := n.burned() + n.graph.mu.Lock() + defer n.graph.mu.Unlock() + wrote := make([]string, len(n.wrote)) + copy(wrote, n.wrote) + checks := make([]string, len(n.Checks)) + copy(checks, n.Checks) + return TaskLanding{ + ID: n.id, + State: n.state, + Brief: n.spec.brief, + Deliverable: n.spec.deliverable, + Report: n.report, + Claim: n.claim, + Ending: string(n.endingLocked()), + Wrote: wrote, + Changed: len(n.changed), + Checks: checks, + Worker: n.spec.model, + High: n.checkedOn, + CostUSD: n.cost, + Tokens: tokens, + } +} + // reportTaskNode is the graph's report hook: one event for a surface, a row in // the project's index, and — on a final state — one note for the model. // @@ -3833,6 +3897,7 @@ func (a *Agent) reportTaskNode(node *TaskNode) { return } a.recordTaskIndex(node) + a.tellTaskLanded(node) transcript := taskURI(node.journalPath()) note := landingNoteLead(notice) + taskNote(notice, transcript, a.settlePolicy(), a.addressLanding(notice)) // WHETHER IT IS WORTH A TURN OF ITS OWN depends on whether anybody is waiting From 62571b34814c4fc2d57e2ba63768d03c8e8aa498 Mon Sep 17 00:00:00 2001 From: santoshkumarradha Date: Thu, 17 Sep 2026 11:38:25 -0400 Subject: [PATCH 113/173] cmd/codeaf: a landed task is scored by a model outside the crew into the own sheet, the outbox and the judge seat --- cmd/codeaf/poolrecord.go | 225 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 225 insertions(+) create mode 100644 cmd/codeaf/poolrecord.go diff --git a/cmd/codeaf/poolrecord.go b/cmd/codeaf/poolrecord.go new file mode 100644 index 0000000000..6b8e62727c --- /dev/null +++ b/cmd/codeaf/poolrecord.go @@ -0,0 +1,225 @@ +// The Model Pool's judge, wired to the chat door's landings. +// +// When a task lands, a model outside the crew scores each seat the work ran +// on. The scores go into the install's own sheet under the pool directory — +// the one the crew picker already reads beside the index — and, when the +// pool's mode allows sending, into the outbox beside it. The pure halves live +// in internal/pool/judge and internal/pool/record; this file owns the two +// things they cannot: the seam in the engine's landing (session.Config's +// TaskLanded) and the one provider call the judge's questions ride, billed to +// the judge's own seat. +// +// QUIET BY DESIGN, for poolindex.go's reason: a landing that nobody could +// score is an ordinary state — a pool switched off, a catalog with nothing +// left to pick, a model that did not answer — and nothing the person is +// reading should move for it. Every error here is said only under the debug +// record's switch. +package main + +import ( + "context" + "errors" + "log" + "path/filepath" + "strings" + "time" + + "github.com/Agent-Field/agentfield/sdk/go/ai" + "github.com/Agent-Field/codeaf/internal/catalog" + "github.com/Agent-Field/codeaf/internal/config" + "github.com/Agent-Field/codeaf/internal/crewpick" + "github.com/Agent-Field/codeaf/internal/guard" + "github.com/Agent-Field/codeaf/internal/lane" + "github.com/Agent-Field/codeaf/internal/pool/judge" + "github.com/Agent-Field/codeaf/internal/pool/outbox" + "github.com/Agent-Field/codeaf/internal/pool/record" + "github.com/Agent-Field/codeaf/internal/provider" + "github.com/Agent-Field/codeaf/internal/session" + "github.com/Agent-Field/codeaf/internal/trace" +) + +// judgeTimeout bounds one landing's whole judging: every seat question rides +// the same context, so a slow model cannot hold one landing open forever. It +// is a bound on an errand nobody is waiting for, not on work the person asked +// for, and it is generous on purpose — the questions are one call each. +const judgeTimeout = 90 * time.Second + +// poolJudgeHook builds the session's landing reader. NIL IS OFF, for the pool +// index's reason: a mode that forbids reading runs no judge and writes +// nothing, and the door then hands the engine no hook at all. +func poolJudgeHook(settings config.Config, profileDir, workspace string, models func() []catalog.Model, ask func(model string) judge.Ask, now func() time.Time) func(session.TaskLanding) { + if !config.ModelPoolAt(profileDir).CanRead() { + return nil + } + return func(landing session.TaskLanding) { + defer guard.Recover("pool/judge") + poolJudgeLanding(settings, profileDir, models, ask, now, landing) + } +} + +// poolJudgeLanding scores one landed task and records what came back. The +// order is the sheet's own law: every score is observed before the rows are +// appended, the sheet is saved once, and the picker's own-cells seam is +// repointed at the new cells so the very next pick in this process reads them. +func poolJudgeLanding(settings config.Config, profileDir string, models func() []catalog.Model, ask func(model string) judge.Ask, now func() time.Time, landing session.TaskLanding) { + pool := config.ModelPoolAt(profileDir) + poolDir := config.ProfilePath(profileDir, "pool") + seats := map[judge.Role]string{ + judge.RoleWorker: landing.Worker, + } + if landing.High != "" { + seats[judge.RoleHigh] = landing.High + } + judgeID, ok := judge.Pick(models(), seats, judge.DefaultFloor) + if !ok { + if trace.Enabled() { + log.Printf("model pool: no judge to ask about task %d", landing.ID) + } + return + } + rec := judge.Record{ + Brief: landing.Brief, + Deliverable: landing.Deliverable, + Report: landing.Report, + Claim: landing.Claim, + Ending: landing.Ending, + Files: landing.Wrote, + Changed: landing.Changed, + Checks: landing.Checks, + Seats: seats, + Judge: judgeID, + CostUSD: landing.CostUSD, + } + // A landing is news, not a turn: nobody is waiting on the answer, and the + // one thing this context owes anybody is a bound on how long it holds the + // pool's own goroutine. + ctx, cancel := context.WithTimeout(context.Background(), judgeTimeout) + defer cancel() + scores, err := judge.Judge(ctx, ask(judgeID), rec) + // THE SCORES OBTAINED ARE RECORDED EVEN WHEN THE ERROR NAMES A SEAT: a + // judge that scored the worker but not the high seat scored the worker, + // and a seat the call failed on is the judge's evidence of that model too. + if err != nil && trace.Enabled() { + log.Printf("model pool: judging task %d: %v", landing.ID, err) + } + if len(scores) == 0 { + return + } + sheet, err := record.LoadSheet(record.OwnSheetPath(poolDir)) + if err != nil { + if trace.Enabled() { + log.Printf("model pool: own sheet: %v", err) + } + return + } + recorder := &record.Recorder{Sheet: sheet} + if pool.CanSend() { + ob, err := outbox.Open(outboxPath(poolDir)) + if err != nil { + if trace.Enabled() { + log.Printf("model pool: outbox: %v", err) + } + } else { + recorder.Outbox = ob + defer ob.Close() + } + } + day := now().UTC().Format("2006-01-02") + if err := recorder.Record(scores, judgeID, "task", poolSize(landing.Tokens), day); err != nil && trace.Enabled() { + log.Printf("model pool: record: %v", err) + } + if err := record.SaveSheet(record.OwnSheetPath(poolDir), sheet); err != nil { + if trace.Enabled() { + log.Printf("model pool: own sheet: %v", err) + } + return + } + // THE NEXT PICK IN THIS PROCESS READS THE NEW CELLS AT ONCE, the same way + // the picker reads them at start-up (poolindex.go's poolOwnCellsFor): a + // closing one is the install's own evidence and is never held to the + // index's min_installs. + cells := record.Cells(sheet) + config.AutoOwnCells = func() []crewpick.Cell { return cells } +} + +// poolSize is the day-row bucket a landing's token count answers: S under +// 200k tokens, M under a million, L past it. The size says how much work the +// bill was carrying, which is the one thing a score alone does not say. +func poolSize(tokens int) string { + switch { + case tokens < 200_000: + return "S" + case tokens < 1_000_000: + return "M" + default: + return "L" + } +} + +// outboxPath is the one path an install's measurements leave by, beside its +// own sheet under the pool directory. +func outboxPath(poolDir string) string { + return filepath.Join(poolDir, "outbox.jsonl") +} + +// poolJudgeAsk builds the maker of one judge's ask: a client per call, built +// from the model's own config, the answer read as the judge's plain string, +// and the call billed to the judge's own seat. +func poolJudgeAsk(settings config.Config, profileDir string) func(model string) judge.Ask { + return func(model string) judge.Ask { + return func(ctx context.Context, system, user string) (string, error) { + client, err := provider.NewClient(settings.ClientConfig(model)) + if err != nil { + return "", err + } + // The call says who it is for the way every errand does (chat.go's + // errandContext): the role names the seat the bill lands on, the tag + // names the rows in the model-call log. Streaming buys nothing — the + // answer is one object nobody is reading as it arrives. + ctx = provider.WithCallTag(provider.WithRole(settings.Context(ctx, "pool-judge"), lane.RoleJudge), "judge") + ctx = provider.WithoutStream(ctx) + messages := []ai.Message{ + judgeMessage("system", system), + judgeMessage("user", user), + } + response, err := client.CompleteWithMessages(ctx, messages) + if err != nil { + return "", err + } + if response == nil || strings.TrimSpace(response.Text()) == "" { + return "", errors.New("the model answered nothing") + } + recordPoolUsage(settings, model, response.Usage) + return response.Text(), nil + } + } +} + +// judgeMessage is one plain-text part in one role, the shape every +// single-question errand here builds its pair out of. +func judgeMessage(role, text string) ai.Message { + return ai.Message{Role: role, Content: []ai.ContentPart{{Type: "text", Text: text}}} +} + +// recordPoolUsage appends the call's one row to this machine's usage ledger. +// THE SEAT IS THE JUDGE'S, because the call was not a conversation's turn and +// no crew seat asked for it. The dollars are the provider's own receipt when +// one arrived, otherwise the model's published price over the tokens it +// counted — and zero when nobody published one, which reads as "nobody said", +// the same reading every other row's zero has. +func recordPoolUsage(settings config.Config, model string, used *ai.Usage) { + line := session.UsageLine{ + Seat: session.SeatJudge, + Model: model, + Calls: 1, + } + if used != nil { + line.Input, line.Output = used.PromptTokens, used.CompletionTokens + if used.Cost != nil { + line.USD = *used.Cost + } else if prompt, completion, known := settings.ClientConfig(model).ModelPrice(model); known { + line.USD = float64(used.PromptTokens)*prompt + float64(used.CompletionTokens)*completion + } + } + session.RecordUsage(session.UsageLedgerPath(), line) +} From 0a49bc805209fc230a44cbdff368e483816dde63 Mon Sep 17 00:00:00 2001 From: santoshkumarradha Date: Thu, 17 Sep 2026 11:39:04 -0400 Subject: [PATCH 114/173] cmd/codeaf: the chat door hands its landings to the pool's judge, off when the pool mode forbids reading --- cmd/codeaf/chatv3.go | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/cmd/codeaf/chatv3.go b/cmd/codeaf/chatv3.go index e2e3c6be2b..87ebff19bc 100644 --- a/cmd/codeaf/chatv3.go +++ b/cmd/codeaf/chatv3.go @@ -846,6 +846,15 @@ func openV3Launch(proc *v3Process, opts v3Options) (*v3Launch, error) { media = v3ImageGen(mediaSettings) } + // The Model Pool's judge reads every landing through the engine's one hook. + // It is built here, where the seats above are wired, so the answer to + // "may this process read the pool at all" is one posture for the whole + // door: a mode that forbids reading builds no hook, and a nil hook is the + // engine's own nothing. The ask is built once and a client is made from it + // per call, each billed to the judge's own seat. + taskLanded := poolJudgeHook(settings, settings.ProfileDir, workspace, + config.AutoModels, poolJudgeAsk(settings, settings.ProfileDir), time.Now) + cfg := session.Config{ Workspace: workspace, Model: chosen, @@ -854,6 +863,7 @@ func openV3Launch(proc *v3Process, opts v3Options) (*v3Launch, error) { Sources: settings.Sources, CompactEnabled: !opts.NoCompact, SessionFile: transcript, + TaskLanded: taskLanded, // The folder this conversation keeps everything in (Decision 26). It is // the zero Place for a launch opened on a flat legacy transcript, which // is what keeps that session deriving its sidecars the way it always did. From 72f0a852fdba9a6ca797747dc50cfeca89de0ce0 Mon Sep 17 00:00:00 2001 From: santoshkumarradha Date: Thu, 17 Sep 2026 11:49:30 -0400 Subject: [PATCH 115/173] docs: revise the model-pool paper after the referee's report Co-Authored-By: Claude Fable 5.1 --- docs/design/model-pool/REVIEW-RESPONSE.md | 39 +++++++++++ docs/design/model-pool/fig-cells.pdf | Bin 19197 -> 19197 bytes docs/design/model-pool/fig-front.pdf | Bin 24502 -> 24502 bytes docs/design/model-pool/fig-regret.pdf | Bin 19785 -> 19348 bytes docs/design/model-pool/fig-semibandit.pdf | Bin 17562 -> 17562 bytes docs/design/model-pool/fig-shrink.pdf | Bin 21230 -> 21230 bytes docs/design/model-pool/figures.py | 2 +- docs/design/model-pool/learn.pdf | Bin 350492 -> 354350 bytes docs/design/model-pool/learn.tex | 80 +++++++++++----------- docs/design/model-pool/tab-regret.tex | 8 +-- 10 files changed, 85 insertions(+), 44 deletions(-) create mode 100644 docs/design/model-pool/REVIEW-RESPONSE.md diff --git a/docs/design/model-pool/REVIEW-RESPONSE.md b/docs/design/model-pool/REVIEW-RESPONSE.md new file mode 100644 index 0000000000..2eaecc5136 --- /dev/null +++ b/docs/design/model-pool/REVIEW-RESPONSE.md @@ -0,0 +1,39 @@ +# Response to the referee + +The report in `REVIEW.md` was written against the draft at the head before this one. Each numbered defect is answered below with what changed in `learn.tex`; the section numbers are the revised document's. + +## Defects + +1. **Proposition 4, the split of the cap.** Corrected. The external count is now one capped quantity `W = Kℓ/(K+ℓ)` with `K = k + ñ`, `ñ = n_pool·ℓ/(ℓ+1)`, shared between catalog and pool as `k/K` and `ñ/K` of `W`; the proof integrates `μ_rm` out once and states the precision-weighted mean. The corollary's statement ("worth at most ℓ own rows") now applies to catalog and pool together, and the sentence that follows says which form the shipped pick is (`ℓ = ∞`, `b = 0`). +2. **Drift estimator.** The factor ½ is gone from (13); the difference of adjacent day means has variance `ω² + σ²(1/n_d + 1/n_{d-1})`. +3. **Additive local sheet versus max-join.** Section 5.2 now names both stores: the install's own sheet (`internal/pool/tally`) merges by addition because each row is folded once; the relay's store is keyed by (install, day, cell) and merges by the join. +4. **Status paragraph.** Rewritten for the head this document ships with (judge hook, install nonce, push, relay, mirror) and lists what is specified but not landed. +5. **Relay code.** `relay/` lands on this head; Section 9 describes it as present. +6. **Public key.** Compiled into the binary on this head; the private-relay setting overrides it. +7. **Run counts.** The crew table is "56 combinations fitted by a ridge model on 72 scored runs over nine tasks" (caption of Figure 1 and Section 8); the seed cells come from the ledger's 120 runs (Figure 6). The abstract no longer quotes a number. +8. **Role-level ratio.** Section 8 and the abstract now state both comparisons: the role-level learner sits at half the crew-level learner fed the same scores and a third of the one fed the clean bit. +9. **Proposition 3.** Restated: identified up to one constant per connected component, one centring constraint per component; proof names the kernel. +10. **Subset of crews.** The regret experiment now runs on all 23 chat-door crews; the table and figure were regenerated and the text quotes the new numbers. +11. **`min_installs`.** Section 5.4 says the relay publishes a distinct-install count per cell and that the reader's floor applies to it; the flood claim is reduced to what the design bounds (a minority of broken or hostile installs) and says what it does not (fabricated majorities). +12. **Row fields.** The row is (role, model, score, judge, door, size, day); the install nonce is on the batch; dollars and minutes are stated to stay in the install's ledger. The cell is spelled the same way in Definition 2, the join and the row. +13. **Abstract.** The constants sentence says what the user still sets (the price of a defect and of time); "stay out and still learn" is replaced by the three modes as they behave. +14. **`explore`.** Marked "specified, not on this head" in the settings table. +15. **`E` as a surrogate.** Section 4 states that (3) is `E_H` at `r = 1`, when the ranking agrees, and that the picker can use `E_H` directly where a delivered rate is measured. +16. **Imputation.** Section 3 describes the implemented rule (median ratio over at least three donors per present index, averaged, capped at the pool's maximum, scale-proportional standing below three donors). +17. **Loader.** Section 9 says the fresher of cache and seed is read, by generated day. + +## Overstated claims + +- Privacy in the abstract is now a statement of what a row contains, not of what can be inferred. +- Proposition 5 is stated for the product posterior and says which dependence the draw ignores. +- The exploration tax is stated as forty runs in which Thompson is no better than the table. +- `figures.py` is said to reproduce the figures from the CSVs and the constants at its head. +- `pool status` and `pool show` are described as they print. +- Rollback is caught by the version; the join makes a replayed old publication harmless. +- The judge floor is stated as absolute. +- "Cents per run" is replaced by "one short completion per role". + +## What the referee asked for and is not in this revision + +- A catalog bias term `b` is added to the hierarchy with its estimator, and the seed's own `b̂ = −15.7`, `τ̂₀² = 0` are reported with the reason the day-one `k = 30` stands. +- Experiments on judge effects, drift, the settled probability and the pair term, and a flood analysis, remain future work; the Status paragraph lists them among what is specified but not landed. diff --git a/docs/design/model-pool/fig-cells.pdf b/docs/design/model-pool/fig-cells.pdf index aadd12cde4a5b3899298e4bd757235594e61aea2..141f422c9074e64b6da60ecdf87f8a9c04700e9d 100644 GIT binary patch delta 21 ccmex6mGSRX#tjXwY=(x$mZlb)+g!_80b>~l*8l(j delta 21 ccmex6mGSRX#tjXwYzCGlhQ_9w+g!_80b>UT(f|Me diff --git a/docs/design/model-pool/fig-front.pdf b/docs/design/model-pool/fig-front.pdf index d9fada050038214021062d587598ae321d2cd4a4..04e9b904cd6ab2a03376ae8604a5e4aed00f4ca7 100644 GIT binary patch delta 21 dcmdnCpK;rM#tkzg*$fShEln*p&yN&m0{~#12bcf= delta 21 dcmdnCpK;rM#tkzg*$gaA42?}U&yN&m0{~!)2a^B* diff --git a/docs/design/model-pool/fig-regret.pdf b/docs/design/model-pool/fig-regret.pdf index 010cef0e1bf39d036c4c2cbe018de9d323691c7d..5ad4bd65f6d4762396cc5c015920d5f4883f3a0c 100644 GIT binary patch delta 4922 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 K8f|QCg7_a#6YL=X diff --git a/docs/design/model-pool/fig-semibandit.pdf b/docs/design/model-pool/fig-semibandit.pdf index 199fc415071e6e803d4fcd4a8f5ea38313f3a5b9..5567aed07e2a1bd574de496b72a2ecfaf20d0668 100644 GIT binary patch delta 21 dcmbQ$$vCT%al=%5HbX-bLqqe;v+e)0002}N2a*5) delta 21 dcmbQ$$vCT%al=%5HUmo&BNLO&v+e)0002~I2bur? diff --git a/docs/design/model-pool/fig-shrink.pdf b/docs/design/model-pool/fig-shrink.pdf index 449885a3acbcbaa780537710759169d9c38ab10f..5f27fde8339a2fbc061ea3b31a465e69a83cec60 100644 GIT binary patch delta 21 dcmaF2l=0nC#tmzI*$fShEln*pZ}zoj1psDJ2nPTF delta 21 dcmaF2l=0nC#tmzI*$gaA42?}UZ}zoj1psD12m$~A diff --git a/docs/design/model-pool/figures.py b/docs/design/model-pool/figures.py index a02e1f3d13..414950f01d 100644 --- a/docs/design/model-pool/figures.py +++ b/docs/design/model-pool/figures.py @@ -133,7 +133,7 @@ def simulate(strategy, N, seed, rows, oracle): def fig_regret(): - rows = [r for r in crews() if r["door"] == "task"][:12] + rows = [r for r in crews() if r["door"] == "task"] oracle = min(allin(r) for r in rows) Ns = [10, 20, 40, 70, 100, 150, 200, 300, 400] fig, (ax, ax2) = plt.subplots(1, 2, figsize=(6.2, 2.8)) diff --git a/docs/design/model-pool/learn.pdf b/docs/design/model-pool/learn.pdf index 3aefdd5ec2b114ae54b725565502e7f90623deee..1c5796b4a45a4827b8565a2689972dff33e41c8a 100644 GIT binary patch delta 101494 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 aHSTO`#@Pfa&~f+c))@`7wDdae4g5FHe(9qC delta 97221 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 +bPR(J9Fx+c-wKU~>d;M@3uvf)MOt~+ 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 diff --git a/docs/design/model-pool/learn.tex b/docs/design/model-pool/learn.tex index ec547ffbd9..935aa2ad3d 100644 --- a/docs/design/model-pool/learn.tex +++ b/docs/design/model-pool/learn.tex @@ -61,7 +61,7 @@ \begin{abstract} \noindent -An agent harness runs a task through a \emph{crew}: an assignment of one language model to each of $n$ roles (in our instance a worker that does the work, a checker that reads it and a planner that cuts it). The crew that pays best is neither the cheapest nor the strongest on paper; it is the one that minimises the expected cost of one clean delivery once failed runs, wall time and shipped defects are priced. We give that objective in closed form, show how a crew is computed from a published catalog of prices and benchmark indexes alone (a pareto front in bill and quality, with a scale-free knee), and then how measurements from real runs replace the catalog's proxy. Runs are scored role by role by a model outside the crew. Each score becomes a row that carries no text, no file name and no identity beyond a per-install nonce, so nothing an install sends can be read back into what it was working on; rows reduce to sufficient statistics that form a join-semilattice, so any number of installs and a relay can merge them in any order and reach the same state, the relay is a convenience rather than an authority, and the published index is signed so that every reader can check what it is blending in. Participation is a setting: an install can contribute, read only, or stay out, and still learns from its own runs. Judge severity is removed by a two-way fixed-effects fit on those statistics, drift by a local-level model whose forgetting rate is estimated rather than set, and heterogeneity between installs by a hierarchical prior whose variance is estimated by moments; every constant except the user's price of a defect is an empirical-Bayes estimate. The crew is chosen by Thompson sampling with semi-bandit feedback: because the objective is linear in role qualities and every role is scored separately, a run on one crew informs every crew that shares a model with it, and the sample complexity scales with the number of models rather than the number of crews. On our own 120 scored runs the learner beats a fixed table by a third of the remaining regret within a few hundred runs, and role-level feedback cuts the regret of a crew-level learner by a further factor of three at four hundred runs. +An agent harness runs a task through a \emph{crew}: an assignment of one language model to each of $n$ roles (in our instance a worker that does the work, a checker that reads it and a planner that cuts it). The crew that pays best is neither the cheapest nor the strongest on paper; it is the one that minimises the expected cost of one clean delivery once failed runs, wall time and shipped defects are priced. We give that objective in closed form, show how a crew is computed from a published catalog of prices and benchmark indexes alone (a pareto front in bill and quality, with a scale-free knee), and then how measurements from real runs replace the catalog's proxy. Runs are scored role by role by a model outside the crew. Each score becomes a row that carries no text, no file name, no price and no identity beyond a per-install nonce, so what an install sends is a handful of coarse fields about a model, not about the work; rows reduce to sufficient statistics that form a join-semilattice, so any number of installs and a relay can merge them in any order and reach the same state, the relay is a convenience rather than an authority, and the published index is signed so that every reader can check what it is blending in. Participation is a setting: an install can contribute, read and learn from its own runs without sending, or stay out. Judge severity is removed by a two-way fixed-effects fit on those statistics, drift by a local-level model whose forgetting rate is estimated rather than set, and heterogeneity between installs by a hierarchical prior whose variance is estimated by moments; every statistical constant is an empirical-Bayes estimate, and what remains for the user to set is the price of a defect and the price of time. The crew is chosen by Thompson sampling with semi-bandit feedback: because the objective is linear in role qualities and every role is scored separately, a run on one crew informs every crew that shares a model with it, and the sample complexity scales with the number of models rather than the number of crews. On a ground truth fitted to our own runs the learner beats a fixed table by a third of the remaining regret within a few hundred runs, and role-level feedback halves the regret of a crew-level learner fed the same scores at four hundred runs. \end{abstract} \vspace{4pt} @@ -107,7 +107,7 @@ \section{The crew computed from the catalog} \hat q_r(m) = 100 \sum_{i=1}^{3} w_{r,i}\,\frac{I_{m,i}}{\max_{m'\in\mods} I_{m',i}} . \label{eq:catq} \end{equation} -A missing index is imputed from the median ratio, over models publishing both, of the missing index to a present one; an imputed quality is flagged and loses ties. +A missing index is imputed from the models that publish it: for each index the model does publish, the median over at least three donor models of the ratio between the missing and the present index gives an offer, the offers are averaged, and the estimate is capped at the largest measured value of the missing index in the pool; with fewer than three donors the model takes the standing its present indexes give it on the pool's scale. An imputed quality is flagged and loses ties. \paragraph{Shortlists.} Role $r$'s shortlist is @@ -173,7 +173,7 @@ \section{The objective: the cost of one clean delivery} E(\kappa)=c(\kappa)+\lambda\,t(\kappa)+H\,\big(1-\pclean(\kappa)\big). \label{eq:E} \end{equation} -Figure~\ref{fig:front} draws our measured crews with the iso-lines of~\eqref{eq:E}. The knob $k$ and the price $H$ parametrise the same trade-off; $k$ is what a catalog admits, $H$ is what measurements admit. +Equation~\eqref{eq:E} is~\eqref{eq:EH} with $r$ set to one: a run that fails to deliver is priced as a shipped defect rather than rerun. The surrogate ranks crews as $E_H$ does when $H$ is large next to a run's cost and $r$ is not far below one, which is the regime of every crew on the front of Figure~\ref{fig:front}; when a role's delivered rate is measured, as the worker's is, the picker can use~\eqref{eq:EH} directly and nothing below changes. Figure~\ref{fig:front} draws our measured crews with the iso-lines of~\eqref{eq:E}. The knob $k$ and the price $H$ parametrise the same trade-off; $k$ is what a catalog admits, $H$ is what measurements admit. \begin{assumption}[Linearity in roles] \label{ass:linear} @@ -183,7 +183,7 @@ \section{The objective: the cost of one clean delivery} \begin{figure}[t] \centering\includegraphics{fig-front.pdf} -\caption{Our measured crews (56 combinations fitted from 120 scored runs): cost of one run against the probability that it delivers clean. Solid: the pareto front. Dashed: iso-cost lines of~\eqref{eq:E}; a larger $H$ tilts them so that quality buys more.} +\caption{Our measured crews: 56 (worker, checker or planner) combinations, each fitted by a ridge model on 72 scored runs over nine tasks; cost of one run against the probability that it delivers clean. Solid: the pareto front. Dashed: iso-cost lines of~\eqref{eq:E}; a larger $H$ tilts them so that quality buys more.} \label{fig:front} \end{figure} @@ -192,36 +192,36 @@ \section{The Model Pool: a decentralised measure of role quality} \subsection{Judged runs and rows} -After a run finishes, one model outside the crew reads the record (brief, deliverable, the checker's report and claim, the files touched) and answers one question per role with a score in $[0,100]$ and one sentence. The judge is chosen deterministically: the cheapest catalog model with coding index at least $60$ that can call tools, holds no role in the crew, and is not from the worker's vendor; a role the judge's own model holds is skipped. The floor exists because a judge below the worker's class rubber-stamps. The judge's calls are billed to a role of their own, so learning is never free by accident; at present prices it is cents per run. +After a run finishes, one model outside the crew reads the record (brief, deliverable, the checker's report and claim, the files touched) and answers one question per role with a score in $[0,100]$ and one sentence. The judge is chosen deterministically: the cheapest catalog model with coding index at least $60$ that can call tools, holds no role in the crew, and is not from the worker's vendor; a role the judge's own model holds is skipped. The floor is absolute, not relative to the worker; its reason is that a judge well below the class of the work it reads rubber-stamps. The judge's calls are billed to a role of their own, so learning is never free by accident; the cost is one short completion per role. One judged role of one run is one \emph{row} \[ -x=(\text{role},\ \text{model},\ \text{score},\ \text{dollars},\ \text{minutes},\ \text{size}\in\{S,M,L\},\ \text{door},\ \text{judge},\ \text{day},\ \text{install}), +x=(\text{role},\ \text{model},\ \text{score},\ \text{judge},\ \text{door},\ \text{size}\in\{S,M,L\},\ \text{day}), \] -where \emph{install} is a nonce drawn once per profile. No text of any kind is in a row. Rows wait in a local outbox, which can be inspected, until they are sent. +sent under an \emph{install} nonce drawn once per profile and carried on the batch, not in the row. No text, no price and no duration is in a row; dollars and minutes stay in the install's own ledger. Rows wait in a local outbox, which can be inspected, until they are sent. \subsection{Sufficient statistics and order-free merging} \begin{definition}[Cell, sheet] -A \emph{cell} is a tuple $\theta=(\text{role},\text{model},\text{judge},\text{day},\text{size},\text{door})$. A \emph{sheet} is a map from (install, cell) to a triple $T=(n,S,S^2)\in\N\times\R_{\ge0}\times\R_{\ge0}$, the count, sum and sum of squares of the scores an install has produced in that cell. +A \emph{cell} is a tuple $\theta=(\text{role},\text{model},\text{judge},\text{door},\text{size})$. A \emph{sheet} is a map from (install, day, cell) to a triple $T=(n,S,S^2)\in\N\times\R_{\ge0}\times\R_{\ge0}$, the count, sum and sum of squares of the scores an install has produced in that cell. \end{definition} Every quantity computed below is a function of the sheet. Sheets are merged by the join \begin{equation} -(A\sqcup B)(u,\theta)=\big(\max(n_A,n_B),\ \max(S_A,S_B),\ \max(S^2_A,S^2_B)\big)\quad\text{taken componentwise on the triples } A(u,\theta),\,B(u,\theta), +(A\sqcup B)(u,d,\theta)=\big(\max(n_A,n_B),\ \max(S_A,S_B),\ \max(S^2_A,S^2_B)\big)\quad\text{componentwise on } A(u,d,\theta),\,B(u,d,\theta), \label{eq:join} \end{equation} with a missing entry read as $(0,0,0)$. \begin{proposition}[The sheet is a state-based CRDT] \label{prop:crdt} -Let each install $u$ only ever publish its own entries $(u,\theta)$ and let every published triple be the running total of $u$'s rows in $\theta$. Then (i) $\sqcup$ is commutative, associative and idempotent; (ii) an install's triples are monotone non-decreasing in time, so a later publication dominates an earlier one under $\sqcup$; (iii) any two nodes that have received the same set of publications, in any order and with any duplication, hold the same sheet. +Let each install $u$ only ever publish its own entries $(u,d,\theta)$ and let every published triple be the running total of $u$'s rows in $\theta$ on day $d$. Then (i) $\sqcup$ is commutative, associative and idempotent; (ii) an install's triples are monotone non-decreasing in time, so a later publication dominates an earlier one under $\sqcup$; (iii) any two nodes that have received the same set of publications, in any order and with any duplication, hold the same sheet. \end{proposition} \begin{proof} (i) is the componentwise max on a product of totally ordered sets. (ii) Scores lie in $[0,100]$, so $n$, $S$ and $S^2$ each grow by a non-negative amount per row. (iii) follows from (i) and (ii): the join of a set of publications is their least upper bound, which does not depend on order or multiplicity, and by (ii) the least upper bound of an install's publications is its latest. \end{proof} -The cell means and pooled variances are functions of $\sum_u T(u,\theta)$, so by Proposition~\ref{prop:crdt} a relay and every install compute the same estimates from the same publications; there is no coordinator whose order of arrival matters, and a relay that is lost is rebuilt from any node that kept the publications. This is the sense in which the measure is decentralised: the relay is a convenience for bandwidth and signing, not a source of truth. +The cell means and pooled variances are functions of $\sum_u T(u,\theta)$, so by Proposition~\ref{prop:crdt} a relay and every install compute the same estimates from the same publications; there is no coordinator whose order of arrival matters, and a relay that is lost is rebuilt from any node that kept the publications. This is the sense in which the measure is decentralised: the relay is a convenience for bandwidth and signing, not a source of truth. Two stores exist. The relay's store is the sheet of Definition~2, keyed by install and day and merged by~\eqref{eq:join}. The install's own sheet (\code{internal/pool/tally}) is keyed by cell alone and merges by addition, which is safe there because an install folds each of its own rows in exactly once, as it is judged; it is the relay, which receives publications from many senders with retries, that needs the join. \subsection{Judges are not the same ruler} @@ -240,13 +240,13 @@ \subsection{Judges are not the same ruler} \begin{proposition}[Identifiability] \label{prop:ident} -Let $G$ be the bipartite graph with the judges on one side, the pairs $(r,m)$ on the other, and an edge where $n_{rmj}>0$. The parameters of~\eqref{eq:twoway} under the centring constraint are identified on a connected component of $G$; across components only the within-component contrasts are. +Let $G$ be the bipartite graph with the judges on one side, the pairs $(r,m)$ on the other, and an edge where $n_{rmj}>0$. The parameters of~\eqref{eq:twoway} are identified up to one additive constant per connected component of $G$: within a component every contrast is identified, and one centring constraint per component fixes its levels. When $G$ is connected the single constraint identifies everything. \end{proposition} \begin{proof} -The design matrix of a two-way additive model has rank equal to (number of vertices of $G$) minus (number of connected components); one linear constraint per component fixes the remaining freedom. With the single centring constraint, exactly one component is fully identified. +The design matrix of a two-way additive model has rank equal to (number of vertices of $G$) minus (number of connected components), and its kernel is spanned by the vectors that shift $\mu$ up and $\beta$ down by one constant on one component; one linear constraint per component removes them. With the single centring constraint, exactly one component is fully identified. \end{proof} -The judge rule of Section~5.1 makes $G$ connected quickly in practice: it sends most installs to the same few cheap judges. A model scored by a single judge so far has $\mu_{rm}$ equal to its own mean and the index says so. +The relay centres $\beta$ within each component. The judge rule of Section~5.1 makes $G$ connected quickly in practice: it sends most installs to the same few cheap judges. A model scored by a single judge so far has $\mu_{rm}$ equal to its own mean and the index says so. \subsection{Installs are not independent, and some are wrong} @@ -261,7 +261,7 @@ \subsection{Drift} \end{equation} The Kalman filter for this local-level model is, in steady state with constant $n_d=\bar n$, an exponentially weighted average whose forgetting factor $1-\alpha$ solves $\alpha^2/(1-\alpha)=\bar n\omega^2/\sigma^2$; the effective memory is about $1/\alpha$ days. The drift variance is estimated by moments from the day-to-day differences, \begin{equation} -\hat\omega^2=\tfrac12\Big[\widehat{\operatorname{Var}}(\bar y_d-\bar y_{d-1})-\sigma^2\big(\tfrac1{n_d}+\tfrac1{n_{d-1}}\big)\Big]_+, +\hat\omega^2=\Big[\widehat{\operatorname{Var}}(\bar y_d-\bar y_{d-1})-\sigma^2\big(\tfrac1{n_d}+\tfrac1{n_{d-1}}\big)\Big]_+, \label{eq:drift} \end{equation} pooled across cells. A cell with no evidence of drift ($\hat\omega^2=0$) keeps everything; a cell that drifts forgets at the rate the data show. The day is a cell dimension for exactly this reason. @@ -311,47 +311,48 @@ \section{Estimation with no tuned constant} \begin{assumption}[Hierarchy] \begin{equation} -\mu_{rm}\sim\mathcal N\big(\hat q_r(m),\,\tau_0^2\big),\qquad +\mu_{rm}\sim\mathcal N\big(\hat q_r(m)+b,\,\tau_0^2\big),\qquad \mu^{u}_{rm}\mid\mu_{rm}\sim\mathcal N\big(\mu_{rm},\,\tau_1^2\big),\qquad y\mid\mu^{u}_{rm}\sim\mathcal N\big(\mu^{u}_{rm},\,\sigma^2\big). \label{eq:hier} \end{equation} \end{assumption} -The first line says the catalog is an unbiased but noisy predictor of the population quality; the second that install $u$'s own quality for the model scatters around the population's; the third is judge noise after Section~5.3. The install cares about $\mu^u_{rm}$. +The first line says the catalog predicts the population quality up to one offset $b$ shared by every model (the catalog's scale is not the judge's) and a noise $\tau_0$; the second that install $u$'s own quality for the model scatters around the population's; the third is judge noise after Section~5.3. The install cares about $\mu^u_{rm}$. \begin{proposition}[Posterior for the install] \label{prop:post} -Under~\eqref{eq:hier}, with $k=\sigma^2/\tau_0^2$ and $\ell=\sigma^2/\tau_1^2$, the posterior of $\mu^u_{rm}$ is normal with +Under~\eqref{eq:hier}, with $k=\sigma^2/\tau_0^2$, $\ell=\sigma^2/\tau_1^2$ and $\tilde n=n^{\mathrm{pool}}\,\ell/(\ell+1)$ (pool rows are other installs' rows, so each carries variance $\sigma^2+\tau_1^2$ about $\mu_{rm}$), the posterior of $\mu^u_{rm}$ is normal with \begin{equation} -\E[\mu^u_{rm}\mid\cdot]=\frac{k^{\ast}\,\hat q_r(m) + n^{\ast}\,\bar y^{\mathrm{pool}}_{rm} + n^u\,\bar y^u_{rm}}{k^{\ast}+n^{\ast}+n^u},\qquad +\E[\mu^u_{rm}\mid\cdot]=\frac{k^{\ast}\,(\hat q_r(m)+b) + n^{\ast}\,\bar y^{\mathrm{pool}}_{rm} + n^u\,\bar y^u_{rm}}{k^{\ast}+n^{\ast}+n^u},\qquad \operatorname{Var}[\mu^u_{rm}\mid\cdot]=\frac{\sigma^2}{k^{\ast}+n^{\ast}+n^u}, \label{eq:three} \end{equation} -where the pool and the catalog enter with the \emph{capped} effective counts +where the catalog and the pool share one \emph{capped} external count $W$ in proportion to what each contributed, \begin{equation} -n^{\ast}=\frac{n^{\mathrm{pool}}\,\ell}{n^{\mathrm{pool}}+\ell}\ \le\ \ell,\qquad -k^{\ast}=\frac{k\,\ell}{k+\ell}\ \le\ \ell . +W=\frac{K\,\ell}{K+\ell}\ \le\ \ell,\qquad K=k+\tilde n,\qquad +k^{\ast}=\frac{k}{K}\,W,\qquad n^{\ast}=\frac{\tilde n}{K}\,W . \label{eq:cap} \end{equation} \end{proposition} \begin{proof} -Integrate out $\mu_{rm}$. The catalog and the pool inform $\mu^u_{rm}$ only through $\mu_{rm}$, whose posterior given them is normal with precision $(k+n^{\mathrm{pool}})/\sigma^2$; adding the between-install variance $\tau_1^2$ gives, for $\mu^u_{rm}$, a precision $1/(\sigma^2/(k+n^{\mathrm{pool}})+\tau_1^2)$. Writing that as an effective count over $\sigma^2$ gives $(k+n^{\mathrm{pool}})\ell/(k+n^{\mathrm{pool}}+\ell)$, which splits into the two terms of~\eqref{eq:cap} in proportion to $k$ and $n^{\mathrm{pool}}$; the own rows add precision $n^u/\sigma^2$ directly. Precision-weighting the three means gives~\eqref{eq:three}. +Integrate out $\mu_{rm}$. The catalog and the pool inform $\mu^u_{rm}$ only through $\mu_{rm}$, whose posterior given them is normal with precision $K/\sigma^2$, $K=k+\tilde n$, and mean the $k:\tilde n$ precision-weighted average of $\hat q_r(m)+b$ and $\bar y^{\mathrm{pool}}_{rm}$; adding the between-install variance $\tau_1^2$ gives, for $\mu^u_{rm}$, a precision $1/(\sigma^2/K+\tau_1^2)=W/\sigma^2$ with the same mean. The own rows add precision $n^u/\sigma^2$ directly. Precision-weighting the two gives~\eqref{eq:three}, with the external count $W$ split as $k^{\ast}+n^{\ast}=W$ in the ratio $k:\tilde n$. \end{proof} \begin{corollary} With $\tau_1=0$ ($\ell=\infty$) the posterior mean is $(1-w)\hat q_r(m)+w\bar y$ with $w=n/(n+k)$, $n=n^{\mathrm{pool}}+n^u$: the plain shrinkage blend, which is what the picker applies today with $k=30$. \end{corollary} -The cap~\eqref{eq:cap} is the point of the hierarchy. However many rows the pool holds, its evidence about \emph{this} install's quality is worth at most $\ell=\sigma^2/\tau_1^2$ own rows, because other installs' tasks are not this install's tasks; and the own rows are never capped. This replaces an ad hoc "own rows count triple" by a quantity with a meaning and an estimator. +The cap~\eqref{eq:cap} is the point of the hierarchy. However many rows the pool holds, everything external to this install, catalog and pool together, is worth at most $\ell=\sigma^2/\tau_1^2$ own rows, because other installs' tasks are not this install's tasks; and the own rows are never capped. This replaces an ad hoc "own rows count triple" by a quantity with a meaning and an estimator. The pick shipped today is the Corollary's form: the pool's and the install's cells are folded together by observation count against the catalog at $k=30$, which is~\eqref{eq:three} at $\ell=\infty$ and $b=0$. \paragraph{Estimating the constants.} Every constant in~\eqref{eq:three} is estimated from the sheet by moments; none is set by hand. \begin{align} \hat\sigma^2 &= \frac{\sum_{u,\theta}\big(S^2_{u\theta}-S_{u\theta}^2/n_{u\theta}\big)}{\sum_{u,\theta}(n_{u\theta}-1)} &&\text{pooled within-cell variance,}\label{eq:sigma}\\ \hat\tau_1^2 &= \Big[\,\widehat{\operatorname{Var}}_u\big(\bar y_{u\theta}\big)-\hat\sigma^2\,\overline{(1/n_{u\theta})}\,\Big]_+ &&\text{between-install variance, pooled over cells,}\label{eq:tau1}\\ -\hat\tau_0^2 &= \Big[\,\widehat{\operatorname{Var}}_{(r,m)}\big(\bar y^{\mathrm{pool}}_{rm}-\hat q_r(m)\big)-\overline{\hat\sigma^2/n^{\mathrm{pool}}_{rm}}\,\Big]_+ &&\text{how far the catalog misses,}\label{eq:tau0} +\hat b &= \overline{\bar y^{\mathrm{pool}}_{rm}-\hat q_r(m)} &&\text{the catalog's offset,}\label{eq:bias}\\ +\hat\tau_0^2 &= \Big[\,\widehat{\operatorname{Var}}_{(r,m)}\big(\bar y^{\mathrm{pool}}_{rm}-\hat q_r(m)\big)-\overline{(\hat\sigma^2+\hat\tau_1^2)/n^{\mathrm{pool}}_{rm}}\,\Big]_+ &&\text{how far the catalog misses,}\label{eq:tau0} \end{align} -each a within-minus-noise moment estimator of the kind used by Efron and Morris~\cite{efron1975}. Then $k=\hat\sigma^2/\hat\tau_0^2$ and $\ell=\hat\sigma^2/\hat\tau_1^2$, and $\hat\omega^2$ of~\eqref{eq:drift} sets the forgetting. These are published in the index beside the cells so that every install blends with the same constants, and re-estimated at every relay cycle. The relay withholds an estimate until its denominator has at least ten degrees of freedom and publishes the day-one values of Table~\ref{tab:const} until then. +each a within-minus-noise moment estimator of the kind used by Efron and Morris~\cite{efron1975}. Then $k=\hat\sigma^2/\hat\tau_0^2$ and $\ell=\hat\sigma^2/\hat\tau_1^2$, and $\hat\omega^2$ of~\eqref{eq:drift} sets the forgetting. On the seed index alone the estimator already says something: its thirteen cells with a published intelligence index give $\hat b=-15.7$ (the catalog's scale sits sixteen points above the judged one) and a variance of the residuals, 75, below the noise they carry, 231, so $\hat\tau_0^2$ clips to zero: thirteen cells of bit-valued scores ($\hat\sigma\approx44$) cannot resolve a catalog error under about nine points, and the day-one $k=30$ stands until judged scores, with their smaller $\sigma$, replace the bits. These are published in the index beside the cells so that every install blends with the same constants, and re-estimated at every relay cycle. The relay withholds an estimate until its denominator has at least ten degrees of freedom and publishes the day-one values of Table~\ref{tab:const} until then. \subsection{Covariates, and why no row is ever wasted} \label{sec:covariates} @@ -417,10 +418,10 @@ \section{Deciding: Thompson sampling with semi-bandit feedback} \end{algorithm} \begin{proposition}[Role draws are crew draws] -Under Assumption~\ref{ass:linear}, drawing $\tilde q_{rm}$ independently per (role, model) and minimising $\tilde E$ over $\crews$ is exactly Thompson sampling over the crews' posterior costs. +Under Assumption~\ref{ass:linear}, drawing $\tilde q_{rm}$ independently per (role, model) and minimising $\tilde E$ over $\crews$ is Thompson sampling over the crews' posterior costs under the product posterior on role qualities. \end{proposition} \begin{proof} -$\tilde E(\kappa)$ is an affine function of the vector $(\tilde q_{rm})$, so its joint law over $\crews$ is the push-forward of the posterior on role qualities; a sample of the latter is a sample of the former. +$\tilde E(\kappa)$ is an affine function of the vector $(\tilde q_{rm})$, so its joint law over $\crews$ is the push-forward of the posterior on role qualities; a sample of the latter is a sample of the former. The judge-effect fit of Section~5.3 and the drift model of Section~5.5 induce dependence between cells that the independent draw ignores; the draw is exact for the product approximation the index publishes. \end{proof} The structure that matters is the feedback. A run of crew $\kappa$ is judged role by role, so it yields one observation for each of its $n$ (role, model) pairs, and the posterior of every crew that shares a pair with $\kappa$ moves. This is a combinatorial bandit with semi-bandit feedback~\cite{chen2013,wen2015}: the unknowns are the $\sum_r|S_r|$ role qualities, not the $|\crews|$ crew values, and the regret of Thompson sampling scales with the former~\cite{wen2015}. Section~\ref{sec:exp} measures the difference on our runs. For Gaussian posteriors the Bayesian regret of Thompson sampling is $O(\sqrt{dT\log T})$ in the dimension $d$ of the unknowns over $T$ runs~\cite{russo2014}. @@ -438,10 +439,10 @@ \section{Deciding: Thompson sampling with semi-bandit feedback} \section{Experiments on our measured runs} \label{sec:exp} -Ground truth is the table of Figure~\ref{fig:front}: 56 crews with $\pclean$, dollars and minutes fitted from 120 scored runs of our own. Each replicate draws a catalog prior per arm as the truth plus $\mathcal N(0,0.15^2)$ noise, roughly the disagreement between indexes and measurements in Figure~\ref{fig:cells}, holds it with $k=30$, and runs $N$ tasks; a strategy picks, observes the outcome and pays~\eqref{eq:E} with $H=\$10$. Regret is the excess over the oracle crew, averaged over 200 replicates. +Ground truth is the table of Figure~\ref{fig:front}: 56 crews with $\pclean$, dollars and minutes fitted by a ridge model on 72 scored runs of our own over nine tasks. Each replicate draws a catalog prior per arm as the truth plus $\mathcal N(0,0.15^2)$ noise, roughly the disagreement between indexes and measurements in Figure~\ref{fig:cells}, holds it with $k=30$, and runs $N$ tasks; a strategy picks, observes the outcome and pays~\eqref{eq:E} with $H=\$10$. Regret is the excess over the oracle crew, averaged over 200 replicates. \paragraph{Crew-level learners.} -Table~\ref{tab:regret} and Figure~\ref{fig:regret} compare, on the twelve chat-door crews, a fixed table row, the catalog prior alone, $\varepsilon$-greedy and Thompson, all with one arm per crew. After 300 runs both learners are about \$0.30 per task under the fixed table, a third of the table's regret, and still falling. Thompson pays a small exploration tax in the first twenty runs; $k=30$ is what keeps it small. $\varepsilon$-greedy matches it here only because $\varepsilon=0.1$ happens to suit a dozen arms; it neither grows when a new model appears nor shrinks when the ranking is settled. +Table~\ref{tab:regret} and Figure~\ref{fig:regret} compare, on all 23 chat-door crews of the table, a fixed table row (the shipped balanced crew), the catalog prior alone, $\varepsilon$-greedy and Thompson, all with one arm per crew. After 400 runs Thompson is \$0.33 per task under the fixed table, a third of the table's regret, and still falling; $\varepsilon$-greedy is \$0.12 behind it. Thompson pays an exploration tax in its first forty runs, when it is no better than the table; $k=30$ is what bounds it. $\varepsilon$-greedy pays more, because a tenth of its runs go to a random crew among 23, and its rate neither grows when a new model appears nor shrinks when the ranking is settled. \begin{table}[h] \centering\small @@ -452,12 +453,12 @@ \section{Experiments on our measured runs} \begin{figure}[h] \centering\includegraphics{fig-regret.pdf} -\caption{Left: regret per task. Right: share of runs on a crew other than the current posterior-mean best. Thompson explores heavily while a dozen crews sit within cents of each other and less as the posteriors separate; $\varepsilon$-greedy explores at its fixed rate.} +\caption{Left: regret per task. Right: share of runs on a crew other than the current posterior-mean best. Thompson explores heavily while many crews sit within cents of each other and less as the posteriors separate; $\varepsilon$-greedy explores at its fixed rate.} \label{fig:regret} \end{figure} \paragraph{Role-level feedback.} -Figure~\ref{fig:semibandit} and Table~\ref{tab:semibandit} isolate the structure of Section~\ref{sec:decide}. The ground truth is three-role: a role quality per (role, model) read off the measured rows (a worker's delivered rate, a checker's and a planner's clean rate), combined by the linear rule of Assumption~\ref{ass:linear} over every crew the vendor rule admits, with dollars additive over roles. Three learners run Thompson sampling with the same prior noise and the same $H$ and differ only in what they see after a run: one arm per crew fed the run's clean-or-defect bit; one arm per crew fed the mean of the judge's role scores; one posterior per (role, model) fed each role's own score, which is what the judge of Section~5.1 provides. The last is the semi-bandit. Richer feedback per run helps (the second learner over the first), but the structure helps more: at four hundred runs the role-level learner's regret is about a third of the crew-level learner's on the same scores, because every run updates the crews that share a model with the one that ran, and there are sixteen unknowns behind 138 crews. +Figure~\ref{fig:semibandit} and Table~\ref{tab:semibandit} isolate the structure of Section~\ref{sec:decide}. The ground truth is three-role: a role quality per (role, model) read off the measured rows (a worker's delivered rate, a checker's and a planner's clean rate), combined by the linear rule of Assumption~\ref{ass:linear} over every crew the vendor rule admits, with dollars additive over roles. Three learners run Thompson sampling with the same prior noise and the same $H$ and differ only in what they see after a run: one arm per crew fed the run's clean-or-defect bit; one arm per crew fed the mean of the judge's role scores; one posterior per (role, model) fed each role's own score, which is what the judge of Section~5.1 provides. The last is the semi-bandit. Richer feedback per run helps: the crew-level learner fed the mean score sits at \$0.31 after four hundred runs against \$0.54 for the one fed only the clean bit. The structure helps again as much: the role-level learner, fed the same scores as the second, sits at \$0.15, half of it and a third of the first, because every run updates the crews that share a model with the one that ran, and there are sixteen unknowns behind 138 crews. \begin{table}[h] \centering\small @@ -478,7 +479,7 @@ \section{Experiments on our measured runs} \label{fig:cells} \end{figure} -The truth in these experiments is our own fitted table from one team's tasks, so the size of the gain is the shape of the effect and not a promise for every install. The script \code{figures.py} beside this document reproduces every figure and table from the two CSV files it reads. +The truth in these experiments is our own fitted table from one team's tasks, so the size of the gain is the shape of the effect and not a promise for every install; the regret scale is set by how close the crews sit, which the front of Figure~\ref{fig:front} shows and no bound here uses. The script \code{figures.py} beside this document reproduces every figure and table from the two CSV files it reads and the constants written at its head. \section{System architecture} \label{sec:arch} @@ -486,19 +487,20 @@ \section{System architecture} The statistics of Sections~\ref{sec:pool}--\ref{sec:decide} fix what the system computes; this section fixes where the bytes live, so that the privacy claims can be checked against the deployment rather than taken on trust. Figure~\ref{fig:arch} shows the four components. The design rule is that the relay holds nothing worth stealing and nothing that cannot be rebuilt. \paragraph{The binary.} -Everything that touches a task runs inside the install: the catalog read, the posterior, the draw, the run, the judge call, the own sheet and the outbox. The judge is an ordinary model call through the install's own provider credentials; the record it reads never leaves the machines the install already trusts with its work. The outbox is a newline-delimited file under the profile directory; \code{codeaf pool status} prints its pending rows, and a user who wants to see exactly what would be sent can read the file. The loader reads the cached index if one is present and the embedded seed index otherwise, once at start-up; a refresh runs in the background and takes effect at the next start, so a running task's picks never move under it. +Everything that touches a task runs inside the install: the catalog read, the posterior, the draw, the run, the judge call, the own sheet and the outbox. The judge is an ordinary model call through the install's own provider credentials; the record it reads never leaves the machines the install already trusts with its work. The outbox is a newline-delimited file under the profile directory; \code{codeaf pool status} prints its pending rows, and a user who wants to see exactly what would be sent can read the file. The loader reads the fresher of the cached index and the embedded seed index, by generated day, once at start-up; a refresh runs in the background and takes effect at the next start, so a running task's picks never move under it. \paragraph{The relay.} The reference relay is a single edge function with an object store behind it, deployable in minutes on a commodity edge platform (\code{wrangler deploy} from \code{relay/} in the repository; the runbook in that directory is ten steps). It exposes two routes. \code{POST /v1/rows} accepts a batch of rows, validates the schema, folds each row into the sender's triple for its cell and day, and discards the row: the store holds the sheet of Definition~2 and nothing else, one triple per (install, cell, day), so its size is bounded by installs times cells times days and not by traffic, and a row that has been folded cannot be recovered from it. \code{GET /index.json} serves the last published index and its detached signature. An hourly scheduled job merges the sheet, runs the judge-effect, robust-aggregation and drift estimates of Section~5, writes the index, signs it with an Ed25519 key held as a platform secret, and mirrors the pair of files to a public git branch so that they are browsable, diffable and forkable. The function keeps no request logs and stores no network address; the install nonce is the only identity in the store, and rate limiting is by nonce (a cap on rows per day) rather than by address. \paragraph{Keys and verification.} -The signing key's public half is compiled into the binary as a list, so a key can be rotated by shipping a binary that carries both; the private half exists only in the platform secret store. Verification is client-side: the binary checks the signature before it reads a single cell, refuses a version lower than the one it holds (Section~5.2's join makes a rollback detectable), and on any failure keeps the cached copy beside an error. Because verification is local, the index can be served from anywhere: the mirror, a static file host, or a directory on disk, which is what the \code{file://} address is for. +The signing key's public half is compiled into the binary as a list, so a key can be rotated by shipping a binary that carries both; the private half exists only in the platform secret store. Verification is client-side: the binary checks the signature before it reads a single cell, refuses a version lower than the one it holds (a rollback is caught by the version; the join of Section~5.2 makes a replayed old publication harmless), and on any failure keeps the cached copy beside an error. Because verification is local, the index can be served from anywhere: the mirror, a static file host, or a directory on disk, which is what the \code{file://} address is for. \paragraph{Trust and failure.} What a compromised relay learns is the sheet: counts, sums and sums of squares of scores per (install nonce, role, model, judge, day, size, door). It learns no task, no code, no prompt, no user, and no address. What a compromised relay can do is publish a bad index, which the signature stops unless the key is also taken; a taken key is retired by shipping the next key, and the min-installs and robust-aggregation rules bound what a flood of fabricated nonces can move in the meantime. What an unreachable relay costs is nothing at all: the cache, then the seed, answer every read, and the outbox waits. Any install can run the same function against its own store and point its \code{CODEAF\_MODEL\_POOL\_URL} and \code{\_SUBMIT\_URL} at it; two relays that receive the same publications produce the same sheet (Proposition~\ref{prop:crdt}), so a private relay and the public one can be federated later by a one-way copy of publications with no reconciliation step. \begin{figure}[H] \centering +\resizebox{\textwidth}{!}{% \begin{tikzpicture}[x=1cm,y=1cm, box/.style={draw,rounded corners=2pt,inner sep=4pt,font=\small,align=center,minimum height=8mm}, store/.style={box,fill=black!6}, @@ -524,7 +526,7 @@ \section{System architecture} \draw[arr] (mirror.west) -- node[lbl,below,pos=0.6]{pull, verify} (fn.east |- mirror.west); \draw[arr] ([xshift=-2.2cm]fn.south) |- node[lbl,pos=0.75,above]{cached index} (cache.east); \node[font=\scriptsize] at (8.0,0.2) {no request logs, no addresses stored, rows discarded after folding}; -\end{tikzpicture} +\end{tikzpicture}} \caption{Deployment. The relay is one edge function, one object store holding only the sheet, and one scheduled signing job; the install verifies every index locally and never blocks on the network. Any of the three network components can be replaced by a static file, a directory, or a self-hosted copy.} \label{fig:arch} \end{figure} @@ -537,15 +539,15 @@ \section{What the user controls} \code{crew} & frugal $\mid$ balanced $\mid$ max $\mid$ custom & how much to spend: $H$ and the knob $k$\\ \code{picked from} & table $\mid$ catalog $\mid$ learn & the rows we measured; recomputed from today's catalog; catalog plus pool plus own sheet\\ \code{model\_pool} & on $\mid$ read $\mid$ off & read the index and send rows (default); read only; neither\\ -\code{explore} & on $\mid$ off & Thompson draw (default) or the posterior mean; \emph{learn} only\\ +\code{explore} & on $\mid$ off & Thompson draw (default) or the posterior mean; \emph{learn} only; specified, not on this head\\ \bottomrule\end{tabular} \end{center} -With the pool set to \emph{read}, the own sheet still accrues and enters~\eqref{eq:three}; with it \emph{off}, only the catalog prior remains and the pick is the catalog's. The index is a JSON document signed with an Ed25519 key whose public half is in the binary; \code{codeaf pool verify} checks it, \code{pool show} prints it, \code{pool status} lists the rows waiting in the outbox. The binary carries a seed index of our own measured runs (Figure~\ref{fig:cells}) so that \emph{learn} has numbers on day one. The relay's code is in this repository; its addresses are settings, so an install can read from and send to a relay of its own, or to none. +With the pool set to \emph{read}, the own sheet still accrues and enters~\eqref{eq:three}; with it \emph{off}, only the catalog prior remains and the pick is the catalog's. The index is a JSON document signed with an Ed25519 key whose public half is in the binary; \code{codeaf pool verify} checks it, \code{pool show} prints its summary and each address with its source, \code{pool status} counts the rows waiting in the outbox and says whether the relay answered. The binary carries a seed index of our own scored runs (Figure~\ref{fig:cells}) so that \emph{learn} has numbers on day one. The relay's code is in this repository; its addresses are settings, so an install can read from and send to a relay of its own, or to none. \section{Status} -On the branch this document ships with: the catalog crew of Section~\ref{sec:catalog}; the shrinkage blend of the Corollary with $k=30$; the index document, its reader and signed pull; the outbox and the sheet; the judge and its deterministic choice; the own sheet, whose cells enter the prior beside the index's by observation count (the Corollary's form, $\ell=\infty$); the settings, including \code{picked from}; the seed index. Specified here and next to land: the engine hook that calls the judge when a task lands; the judge fixed effects, Huber aggregation and drift estimate in the relay; the hierarchical cap~\eqref{eq:cap} and the moment estimators~\eqref{eq:sigma}--\eqref{eq:tau0}; the Thompson draw with role-level updates and the settled probability $\pi^\ast$; the relay of Section~\ref{sec:arch}. Until the relay exists the pool is the seed index plus each install's own sheet, and nothing is uploaded. +On the head this document ships with: the catalog crew of Section~\ref{sec:catalog}; the shrinkage blend of the Corollary with $k=30$; the \code{picked from} row and its ladder; the index document, its reader and signed pull with the built-in key; the outbox, the install nonce and the push; the judge, its deterministic choice, and the hook that calls it when a chat task lands; the own sheet, whose cells enter the prior beside the index's by observation count; the settings; the seed index; the relay of Section~\ref{sec:arch} with its store keyed by install and day, the join, the judge-effect fit and the Huber aggregation; the mirror workflow. Specified here and next to land: the drift estimate~\eqref{eq:drift}; the hierarchical cap~\eqref{eq:cap} with the moment estimators~\eqref{eq:sigma}--\eqref{eq:tau0} in place of the count fold; the Thompson draw with role-level updates, the \code{explore} setting and the settled probability $\pi^\ast$; the pair term of Section~6.1; the headless doors' hook. The referee's report on an earlier draft, and what changed in answer to it, are beside this file. \begin{thebibliography}{9}\small \bibitem{thompson1933} W.~R. Thompson. On the likelihood that one unknown probability exceeds another in view of the evidence of two samples. \emph{Biometrika} 25:285--294, 1933. diff --git a/docs/design/model-pool/tab-regret.tex b/docs/design/model-pool/tab-regret.tex index 47a27626f2..3ce5cce56e 100644 --- a/docs/design/model-pool/tab-regret.tex +++ b/docs/design/model-pool/tab-regret.tex @@ -1,7 +1,7 @@ \begin{tabular}{lrrrrrrrrr}\toprule $N$ & 10 & 20 & 40 & 70 & 100 & 150 & 200 & 300 & 400\\\midrule -table & 0.91 & 0.93 & 0.96 & 0.94 & 0.94 & 0.93 & 0.94 & 0.96 & 0.97\\ -catalog & 0.82 & 0.86 & 0.88 & 0.87 & 0.85 & 0.83 & 0.83 & 0.84 & 0.86\\ -greedy & 0.72 & 0.79 & 0.79 & 0.78 & 0.76 & 0.72 & 0.69 & 0.64 & 0.62\\ -thompson & 0.98 & 0.97 & 0.90 & 0.86 & 0.81 & 0.75 & 0.70 & 0.65 & 0.59\\ +table & 1.01 & 0.95 & 0.95 & 0.93 & 0.95 & 0.93 & 0.94 & 0.96 & 0.97\\ +catalog & 0.99 & 0.94 & 0.94 & 0.91 & 0.92 & 0.88 & 0.88 & 0.90 & 0.91\\ +greedy & 1.20 & 1.10 & 1.01 & 0.99 & 0.94 & 0.90 & 0.86 & 0.80 & 0.76\\ +thompson & 0.97 & 0.97 & 0.93 & 0.87 & 0.88 & 0.84 & 0.79 & 0.71 & 0.64\\ \bottomrule\end{tabular} From b45a9b47db94f8c41228af69c9974648d6550504 Mon Sep 17 00:00:00 2001 From: santoshkumarradha Date: Thu, 17 Sep 2026 11:49:37 -0400 Subject: [PATCH 116/173] pool: the judge hook is tested at both doors and said in the manual and the change entry --- cmd/codeaf/poolrecord_test.go | 268 ++++++++++++++++++ .../unreleased/1093-pool-judge-hook.md | 19 ++ .../manual/chat/running-from-the-terminal.md | 11 + internal/session/task_landing_hook_test.go | 121 ++++++++ 4 files changed, 419 insertions(+) create mode 100644 cmd/codeaf/poolrecord_test.go create mode 100644 docs/changes/unreleased/1093-pool-judge-hook.md create mode 100644 internal/session/task_landing_hook_test.go diff --git a/cmd/codeaf/poolrecord_test.go b/cmd/codeaf/poolrecord_test.go new file mode 100644 index 0000000000..6ff003100f --- /dev/null +++ b/cmd/codeaf/poolrecord_test.go @@ -0,0 +1,268 @@ +package main + +// The Model Pool's judge, at the chat door: what a landing records, what it +// costs, and what it refuses to do. +// +// Every test here runs the real hook against a temp profile — a fake ask +// standing in for the provider call, a fake catalog standing in for the +// list — and reads back the three records the brief names: the own sheet, the +// outbox, and the usage ledger. The pool's mode is pinned from the +// environment, the same word `model_pool` resolves through in production. + +import ( + "context" + "os" + "path/filepath" + "testing" + "time" + + "github.com/Agent-Field/agentfield/sdk/go/ai" + "github.com/Agent-Field/codeaf/internal/catalog" + "github.com/Agent-Field/codeaf/internal/config" + "github.com/Agent-Field/codeaf/internal/pool/judge" + "github.com/Agent-Field/codeaf/internal/pool/record" + "github.com/Agent-Field/codeaf/internal/session" +) + +// poolTestCatalog is three rows: the two models the crew held, and one model +// outside it, dear enough at its published coding index to be picked and with +// a price and "tools" published, which is what Pick asks a candidate to carry. +func poolTestCatalog() []catalog.Model { + return []catalog.Model{ + {ID: "crew/worker", PromptPrice: 1, CompletionPrice: 2, CodingIndex: 80, Parameters: []string{"tools"}}, + {ID: "crew/high", PromptPrice: 1, CompletionPrice: 2, CodingIndex: 80, Parameters: []string{"tools"}}, + {ID: "other/judge", PromptPrice: 0.5, CompletionPrice: 1, CodingIndex: 90, Parameters: []string{"tools"}}, + } +} + +// poolTestAsk is the fake ask: it records the model it was asked for, bills +// the call exactly as the real ask bills one — the provider's own receipt, a +// fifth of a cent — and answers the one JSON object a seat's answer is. +func poolTestAsk(settings config.Config, asked *[]string) func(model string) judge.Ask { + receipt := 0.0002 + return func(model string) judge.Ask { + return func(context.Context, string, string) (string, error) { + *asked = append(*asked, model) + recordPoolUsage(settings, model, &ai.Usage{PromptTokens: 10, CompletionTokens: 20, Cost: &receipt}) + return `{"score": 88, "reason": "the delivered work does what the brief asked"}`, nil + } + } +} + +// poolTestLanding is one landed task with both judged seats held. +func poolTestLanding() session.TaskLanding { + return session.TaskLanding{ + ID: 7, + State: session.TaskDone, + Brief: "read ALPHA and say what it holds", + Deliverable: "the reading", + Report: "the report the node landed with", + Wrote: []string{"alpha.md"}, + Changed: 1, + Checks: []string{"go test ./..."}, + Worker: "crew/worker", + High: "crew/high", + Tokens: 150_000, + } +} + +// restoreOwnCells puts the picker's own-cells seam back after a test that +// repointed it, so no other test in the process reads this test's sheet. +func restoreOwnCells(t *testing.T) { + t.Helper() + previous := config.AutoOwnCells + t.Cleanup(func() { config.AutoOwnCells = previous }) +} + +// TestPoolJudgeHookScoresALandedTaskIntoItsOwnSheetAndAnswersTheNewCells runs +// one landing through the real hook and reads back the whole of it: one cell +// per held seat in the install's own sheet, the seam answering those cells at +// once, the judge's questions asked of no model the crew held, and the calls +// billed to the judge's seat. +func TestPoolJudgeHookScoresALandedTaskIntoItsOwnSheetAndAnswersTheNewCells(t *testing.T) { + t.Setenv("CODEAF_HOME", t.TempDir()) + t.Setenv("CODEAF_MODEL_POOL", "on") + t.Setenv("CODEAF_MODEL_POOL_SUBMIT_URL", "http://127.0.0.1:1/submit") + restoreOwnCells(t) + + profileDir := t.TempDir() + settings := config.Config{} + var asked []string + hook := poolJudgeHook(settings, profileDir, t.TempDir(), poolTestCatalog, poolTestAsk(settings, &asked), time.Now) + if hook == nil { + t.Fatal("a pool whose mode allows reading built no hook") + } + hook(poolTestLanding()) + + sheet, err := record.LoadSheet(record.OwnSheetPath(config.ProfilePath(profileDir, "pool"))) + if err != nil { + t.Fatalf("the own sheet: %v", err) + } + cells := record.Cells(sheet) + if len(cells) != 2 { + t.Fatalf("the sheet holds %d cells, want one per held seat: %+v", len(cells), cells) + } + seen := map[string]bool{} + for _, cell := range cells { + seen[cell.Role+"/"+cell.Model] = true + if cell.N != 1 { + t.Fatalf("the %s seat's cell holds %d readings, want 1", cell.Role, cell.N) + } + } + for _, seat := range []string{"worker/crew/worker", "high/crew/high"} { + if !seen[seat] { + t.Fatalf("the sheet holds no cell for %s: %+v", seat, cells) + } + } + + if config.AutoOwnCells == nil { + t.Fatal("the picker's own-cells seam was not repointed after the landing") + } + if answered := config.AutoOwnCells(); len(answered) != 2 { + t.Fatalf("the seam answers %d cells, want the 2 the sheet now holds", len(answered)) + } + + if len(asked) != 2 || asked[0] != "other/judge" || asked[1] != "other/judge" { + t.Fatalf("the judge asked %v, want only other/judge — never a model the crew held", asked) + } + + session.FlushUsage() + rows, err := session.ReadUsage(session.UsageLedgerPath(), time.Time{}) + if err != nil { + t.Fatalf("the usage ledger: %v", err) + } + if len(rows) != 2 { + t.Fatalf("the ledger holds %d rows, want one per seat question", len(rows)) + } + for _, row := range rows { + if row.Seat != session.SeatJudge { + t.Fatalf("a call was billed to seat %q, want judge", row.Seat) + } + if row.Model != "other/judge" { + t.Fatalf("a call was billed to model %q", row.Model) + } + if row.Input != 10 || row.Output != 20 { + t.Fatalf("a row carries %d in / %d out", row.Input, row.Output) + } + } +} + +// TestPoolJudgeHookAppendsOutboxRowsOnlyWhenTheModeSends walks the three +// postures one after another: a mode that sends appends one row per seat, a +// mode whose submit address was emptied sends nothing, and a read-only mode +// records locally only. +func TestPoolJudgeHookAppendsOutboxRowsOnlyWhenTheModeSends(t *testing.T) { + t.Setenv("CODEAF_HOME", t.TempDir()) + restoreOwnCells(t) + + profileDir := t.TempDir() + outboxFile := filepath.Join(config.ProfilePath(profileDir, "pool"), "outbox.jsonl") + settings := config.Config{} + var asked []string + models := poolTestCatalog + + hook := func() func(session.TaskLanding) { + return poolJudgeHook(settings, profileDir, t.TempDir(), models, poolTestAsk(settings, &asked), time.Now) + } + + // A pool that sends: one row per seat question, outbox beside the sheet. + t.Setenv("CODEAF_MODEL_POOL", "on") + t.Setenv("CODEAF_MODEL_POOL_SUBMIT_URL", "http://127.0.0.1:1/submit") + hook()(poolTestLanding()) + data, err := os.ReadFile(outboxFile) + if err != nil { + t.Fatalf("the outbox: %v", err) + } + if lines := countLines(data); lines != 2 { + t.Fatalf("the outbox holds %d rows, want 2", lines) + } + + // A submit address emptied on purpose: the sheet is still observed, and + // nothing waits to leave. + t.Setenv("CODEAF_MODEL_POOL_SUBMIT_URL", "") + os.Remove(outboxFile) + hook()(poolTestLanding()) + if _, err := os.Stat(outboxFile); !os.IsNotExist(err) { + t.Fatalf("a pool with no submit address wrote %s", outboxFile) + } + + // A read-only pool: the same, and the sheet is still kept. + t.Setenv("CODEAF_MODEL_POOL", "read") + hook()(poolTestLanding()) + if _, err := os.Stat(outboxFile); !os.IsNotExist(err) { + t.Fatalf("a read-only pool wrote %s", outboxFile) + } + sheet, err := record.LoadSheet(record.OwnSheetPath(config.ProfilePath(profileDir, "pool"))) + if err != nil { + t.Fatalf("the own sheet after a read-only landing: %v", err) + } + if got := len(record.Cells(sheet)); got != 2 { + t.Fatalf("the sheet holds %d cells after three landings, want 2 (one per seat, observed three times)", got) + } +} + +// TestPoolJudgeHookDoesNothingUnderModeOff is the switch: a pool whose mode +// forbids reading builds no hook at all, so a landing costs no call and +// writes nothing. +func TestPoolJudgeHookDoesNothingUnderModeOff(t *testing.T) { + t.Setenv("CODEAF_HOME", t.TempDir()) + t.Setenv("CODEAF_MODEL_POOL", "off") + restoreOwnCells(t) + + profileDir := t.TempDir() + settings := config.Config{} + var asked []string + if hook := poolJudgeHook(settings, profileDir, t.TempDir(), poolTestCatalog, poolTestAsk(settings, &asked), time.Now); hook != nil { + t.Fatal("a pool whose mode forbids reading built a hook anyway") + } + if _, err := os.Stat(record.OwnSheetPath(config.ProfilePath(profileDir, "pool"))); !os.IsNotExist(err) { + t.Fatal("a pool whose mode forbids reading wrote an own sheet") + } + if len(asked) != 0 { + t.Fatalf("a pool whose mode forbids reading asked %v", asked) + } +} + +// TestPoolJudgeAskBillsTheJudgeSeatFromTheModelsPrice pins the two readings +// the bill can take: the provider's own receipt when one arrived, and the +// model's published price over the tokens when none did — with zero meaning +// nobody published one, which reads as "nobody said" and never as free. +func TestPoolJudgeAskBillsTheJudgeSeatFromTheModelsPrice(t *testing.T) { + t.Setenv("CODEAF_HOME", t.TempDir()) + + // The receipt wins, whatever the price says. + receipt := 0.0007 + recordPoolUsage(config.Config{}, "other/judge", &ai.Usage{PromptTokens: 10, CompletionTokens: 20, Cost: &receipt}) + // No receipt and no published price: the tokens still happened, so the row + // carries them with no dollars — zero reads as "nobody said", never free. + recordPoolUsage(config.Config{}, "other/judge", &ai.Usage{PromptTokens: 10, CompletionTokens: 20}) + session.FlushUsage() + + rows, err := session.ReadUsage(session.UsageLedgerPath(), time.Time{}) + if err != nil { + t.Fatalf("the usage ledger: %v", err) + } + if len(rows) != 2 { + t.Fatalf("the ledger holds %d rows, want 2", len(rows)) + } + if rows[0].Seat != session.SeatJudge || rows[0].Model != "other/judge" { + t.Fatalf("the row names seat %q model %q", rows[0].Seat, rows[0].Model) + } + if rows[0].USD != receipt { + t.Fatalf("the row says $%v, want the provider's own $%v", rows[0].USD, receipt) + } + if rows[1].USD != 0 || rows[1].Input != 10 || rows[1].Output != 20 { + t.Fatalf("an unpriced row says $%v over %d/%d tokens", rows[1].USD, rows[1].Input, rows[1].Output) + } +} + +// countLines counts the newlines a row-per-line file holds. +func countLines(data []byte) int { + lines := 0 + for _, b := range data { + if b == '\n' { + lines++ + } + } + return lines +} diff --git a/docs/changes/unreleased/1093-pool-judge-hook.md b/docs/changes/unreleased/1093-pool-judge-hook.md new file mode 100644 index 0000000000..b02abdaef4 --- /dev/null +++ b/docs/changes/unreleased/1093-pool-judge-hook.md @@ -0,0 +1,19 @@ +--- +kind: added +title: a landed task is scored by a model outside the crew, into the own sheet, the outbox and the judge seat +pr: 1093 +surface: [chat, engine] +invalidates: + - "The Model Pool's own sheet (`own.json` under the pool directory) could only hold scores a machine put there by hand. After a task lands in a chat-door conversation, a judge model outside the crew now scores each seat the work ran on, the scores are observed into that sheet, and the picker reads the new cells in the same process at once (`config.AutoOwnCells` repointed after every landing)." + - "The outbox (`outbox.jsonl` beside the sheet) gained the same rows when `model_pool` is `on` and a submit address is set; `read` keeps them local and `off` asks no judge at all." + - "The judge's provider calls are billed to the `judge` seat in the usage ledger, with the dollars from the model's published price when the provider sent no receipt of its own." +--- + +The engine grew the one seam it was missing: `session.Config.TaskLanded`, called +once per landed node on its own goroutine through `guard.Go`, carrying the +node's record as the landing left it plus the worker's model and the model its +checking pass ran on (`session.TaskLanding`). The chat door wires it to the +pool's judge (`cmd/codeaf/poolrecord.go`): pick a judge outside the crew +(`judge.Pick`), ask one question per held seat (`judge.Judge`), record what came +back (`record.Recorder`), and hand the new cells to the picker. A mode that +forbids reading builds no hook, and a nil hook costs the engine nothing. diff --git a/internal/manual/chat/running-from-the-terminal.md b/internal/manual/chat/running-from-the-terminal.md index 42e22ecf95..b3bd800382 100644 --- a/internal/manual/chat/running-from-the-terminal.md +++ b/internal/manual/chat/running-from-the-terminal.md @@ -539,6 +539,17 @@ the crew reads them beside the index. `status` adds what is waiting to be sent and whether the mode allows sending and reading. `--json` prints the same answer as one object. Neither form touches the network. +**The scores start here.** In a conversation, after a task lands, a model +outside the crew is asked to score each seat the work ran on — the worker that +carried it, and the seat that checked it when there was one. The scores stay +in your install's own sheet (`own.json`) and feed the very next crew pick; +nothing else reads them. With `model_pool` set to `on` the same scores also +wait in `outbox.jsonl` beside the sheet, to leave with the pool's other +measurements; `read` keeps them local, and `off` asks no judge at all and +writes nothing. The call itself is billed to the `judge` seat, so it shows up +in the spend pages beside the crew seats rather than inside a task's own +cost. + `verify` fetches a fresh index and checks its detached ed25519 signature, then prints the version whose signature checked out: diff --git a/internal/session/task_landing_hook_test.go b/internal/session/task_landing_hook_test.go new file mode 100644 index 0000000000..c50d47efd1 --- /dev/null +++ b/internal/session/task_landing_hook_test.go @@ -0,0 +1,121 @@ +package session + +// THE LANDING HOOK: what a door outside this package reads when a task lands. +// +// A landed node hands its own record to Config.TaskLanded exactly once, on a +// goroutine of its own; a node still queued or running hands over nothing, and +// a door that wired no hook at all costs nothing. Every test here drives the +// real graph — admit, the frontier, [Agent.reportTaskNode] — and scripts only +// the work itself, which is the part a test may not have. + +import ( + "testing" + "time" +) + +// TestALandedTaskCallsTheLandingHookOnceWithTheRecordsFields pins the whole +// shape of one landing: the hook is called once, with the node's own record — +// the brief and the deliverable it was admitted under, the report it landed +// with, what it wrote and how many paths it changed, the checks it declared — +// beside the worker's model and the model its check ran on. +func TestALandedTaskCallsTheLandingHookOnceWithTheRecordsFields(t *testing.T) { + agent, _ := newTestAgent(t, &scriptedCompleter{}, nil) + landings := make(chan TaskLanding, 4) + agent.config.TaskLanded = func(landing TaskLanding) { landings <- landing } + + graph := agent.graph() + release := make(chan struct{}) + graph.run = func(node *TaskNode) { + <-release + node.graph.mu.Lock() + node.wrote = []string{"alpha.md"} + node.changed = []string{"alpha.md"} + node.Checks = []string{"go test ./..."} + node.checkedOn = "test/high" + node.graph.mu.Unlock() + node.finish("the report the node landed with", []string{"alpha.md"}, "", "") + node.graph.complete(node, TaskDone) + } + id := graph.reserve() + graph.admit(id, taskSpec{ + title: "read the law", + brief: "read ALPHA and say what it holds", + deliverable: "the reading", + acceptance: "a reader can check it", + model: "test/worker", + }) + node := graph.node(id) + + // THE NODE IS RUNNING FIRST, and a running node has no record to hand + // over: the start announcement travels the same reporting path and must + // reach the hook with nothing. + deadline := time.Now().Add(5 * time.Second) + for node.stateNow() != TaskRunning && time.Now().Before(deadline) { + time.Sleep(time.Millisecond) + } + if node.stateNow() != TaskRunning { + t.Fatalf("the node never started running; it is %s", node.stateNow()) + } + select { + case landing := <-landings: + t.Fatalf("a running node called the landing hook with %+v", landing) + case <-time.After(50 * time.Millisecond): + } + + close(release) + waitDoneNode(t, node) + + select { + case landing := <-landings: + if landing.ID != id { + t.Fatalf("the landing names node %d, want %d", landing.ID, id) + } + if landing.State != TaskDone { + t.Fatalf("the landing's state is %s, want done", landing.State) + } + if landing.Brief != "read ALPHA and say what it holds" { + t.Fatalf("the landing's brief is %q", landing.Brief) + } + if landing.Deliverable != "the reading" { + t.Fatalf("the landing's deliverable is %q", landing.Deliverable) + } + if landing.Report != "the report the node landed with" { + t.Fatalf("the landing's report is %q", landing.Report) + } + if len(landing.Wrote) != 1 || landing.Wrote[0] != "alpha.md" { + t.Fatalf("the landing's wrote is %v", landing.Wrote) + } + if landing.Changed != 1 { + t.Fatalf("the landing's changed is %d, want 1", landing.Changed) + } + if len(landing.Checks) != 1 || landing.Checks[0] != "go test ./..." { + t.Fatalf("the landing's checks are %v", landing.Checks) + } + if landing.Worker != "test/worker" { + t.Fatalf("the landing's worker is %q, want test/worker", landing.Worker) + } + if landing.High != "test/high" { + t.Fatalf("the landing's high is %q, want test/high", landing.High) + } + case <-time.After(5 * time.Second): + t.Fatal("a landed task never called the landing hook") + } +} + +// TestANilLandingHookIsFine is the ordinary door: a session that wired no +// reader lands its nodes exactly as before, and nothing anywhere notices. +func TestANilLandingHookIsFine(t *testing.T) { + agent, _ := newTestAgent(t, &scriptedCompleter{}, nil) + if agent.config.TaskLanded != nil { + t.Fatal("a session built with no landing hook wired one anyway") + } + + graph := agent.graph() + graph.run = func(node *TaskNode) { + node.finish("done", nil, "", "") + node.graph.complete(node, TaskDone) + } + id := graph.reserve() + graph.admit(id, taskSpec{title: "a node with no reader", brief: "b", acceptance: "a"}) + waitDoneNode(t, graph.node(id)) +} From a26996ebbac834190b2ffc8f247724e54b8c8fe3 Mon Sep 17 00:00:00 2001 From: santoshkumarradha Date: Thu, 17 Sep 2026 12:01:42 -0400 Subject: [PATCH 117/173] docs: the paper is Pareto Crewing; the help page fits its screen again Co-Authored-By: Claude Fable 5.1 --- cmd/codeaf/main.go | 3 +-- docs/design/model-pool/Makefile | 10 +++++----- docs/design/model-pool/REVIEW-RESPONSE.md | 2 +- docs/design/model-pool/figures.py | 2 +- .../{learn.pdf => pareto-crewing.pdf} | Bin 354350 -> 355375 bytes .../{learn.tex => pareto-crewing.tex} | 12 ++++++------ 6 files changed, 14 insertions(+), 15 deletions(-) rename docs/design/model-pool/{learn.pdf => pareto-crewing.pdf} (77%) rename docs/design/model-pool/{learn.tex => pareto-crewing.tex} (94%) diff --git a/cmd/codeaf/main.go b/cmd/codeaf/main.go index 32355f98f2..6eae8ed539 100644 --- a/cmd/codeaf/main.go +++ b/cmd/codeaf/main.go @@ -375,8 +375,7 @@ Look at what happened — read-only, no key, nothing spent codeaf logs [--tail 40] [--follow] [--path] [--json] [--run id] [--call id] [--tag t] [--model m] [--node n] [--body id] every model call codeaf made — what was asked, which lane answered, what - came back; --json prints the rows as on disk, --body one call's bodies. - The filters are exact and combine; CODEAF_CALL_LOG=off is off + came back; --json rows as on disk, --body one call; CODEAF_CALL_LOG=off is off codeaf models [--refresh] the models this machine will use, and what each has been measured at codeaf pool [show|status|verify] [--json] [--key key] diff --git a/docs/design/model-pool/Makefile b/docs/design/model-pool/Makefile index b9815e3d3b..8be7b44a5d 100644 --- a/docs/design/model-pool/Makefile +++ b/docs/design/model-pool/Makefile @@ -1,17 +1,17 @@ -# Builds learn.pdf: the figures from data/*.csv, then the paper. +# Builds pareto-crewing.pdf: the figures from data/*.csv, then the paper. # make figures + pdf # make clean remove build products but keep the pdf -all: learn.pdf +all: pareto-crewing.pdf fig-front.pdf fig-cells.pdf fig-shrink.pdf fig-regret.pdf tab-regret.tex: figures.py data/measured-crews.csv data/seed-cells.csv python3 figures.py -learn.pdf: learn.tex fig-front.pdf fig-cells.pdf fig-shrink.pdf fig-regret.pdf tab-regret.tex - latexmk -pdf -interaction=nonstopmode -halt-on-error learn.tex +pareto-crewing.pdf: pareto-crewing.tex fig-front.pdf fig-cells.pdf fig-shrink.pdf fig-regret.pdf tab-regret.tex + latexmk -pdf -interaction=nonstopmode -halt-on-error pareto-crewing.tex clean: - latexmk -c learn.tex + latexmk -c pareto-crewing.tex rm -f learn.bbl .PHONY: all clean diff --git a/docs/design/model-pool/REVIEW-RESPONSE.md b/docs/design/model-pool/REVIEW-RESPONSE.md index 2eaecc5136..0495b95948 100644 --- a/docs/design/model-pool/REVIEW-RESPONSE.md +++ b/docs/design/model-pool/REVIEW-RESPONSE.md @@ -1,6 +1,6 @@ # Response to the referee -The report in `REVIEW.md` was written against the draft at the head before this one. Each numbered defect is answered below with what changed in `learn.tex`; the section numbers are the revised document's. +The report in `REVIEW.md` was written against the draft at the head before this one. The paper was `learn.tex` then and is `pareto-crewing.tex` now; each numbered defect is answered below with what changed; the section numbers are the revised document's. ## Defects diff --git a/docs/design/model-pool/figures.py b/docs/design/model-pool/figures.py index 414950f01d..9e22f22dac 100644 --- a/docs/design/model-pool/figures.py +++ b/docs/design/model-pool/figures.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -"""Draws the figures in learn.tex from data/*.csv. Standard library, numpy and matplotlib only. +"""Draws the figures in pareto-crewing.tex from data/*.csv. Standard library, numpy and matplotlib only. python3 figures.py # writes fig-*.pdf and tab-*.tex beside this file diff --git a/docs/design/model-pool/learn.pdf b/docs/design/model-pool/pareto-crewing.pdf similarity index 77% rename from docs/design/model-pool/learn.pdf rename to docs/design/model-pool/pareto-crewing.pdf index 1c5796b4a45a4827b8565a2689972dff33e41c8a..c1e25dc4a12a5e081ec3c3797bf82ead6c828b13 100644 GIT binary patch delta 82072 zcmV(^K-Isl&=s%P6%i#+L`E$!E;kA#(A34)3OF&55fcMBF*B3VwJ3jmT1%7THV(e` zukbB$Ra+}c)GMhyOkP{b9+FMEY7dhfWQ*NaMwT2srl|4t9KuRUk7s?nr_Y!FPk%CsFX)bT2si;fJS(zaJFzj&_RH(J(yUoh}a_ z|BQAuwEw(|LRFT#SKI5d)2RyMJcIXk_xrP(%Mr&$jAb=IRAUn!}~9O-t>QsV?Iq z(ziRrVU__gNm%NFh^e}ox>Y=jsc6N{u8iZ)6|Zz{PRNW;qkJ$ICZTgJ^m6LQy-I`W z@HaAPhOJI-^yPnf=(|ZaElUi2%L5%b?%Pz2pR7=9PjIK=G%OVCCoaOGRJ5Ppt8VJY zGmZT>k~?22p6a)%K3A3za2(2x1WqU(`2jl<`?~2zB)>5&Q}f{`;-3z6TJ8=>6sBpy z3!3X>E=M6Zj^gZHa!J}i46-=0!Ul5`FjW$!AaV-@#}R)-<22oyL;EVI!boXW6BT9f zFWsd&50e6xxLD!MH41NHg;xg?9tR4rPJ`31kITTUg?E|zqwUn-S?i2X7GADMTyrus zhm}2SWyv5ZUHYesRBBHup&g}?x<2nU&@|AWiihK&(+b^&fIFfEU7ct_uf8*M&|wta za45+_4QGF0IpjzxSmfXZ#qp6|VNOdXFbXB(b5om+HeUqQv7aY;J@@-04PNPwopR(4 z74rTwel`9opa}bRPk;a5!h=X=C@oYg5b7n?zNjLfV*qBMeYzBvyTPNl;%DrrYC$PNVu( zBz%7h%U}121}XZ4+Ghq~e#IQBds>Z)nUc<6w-fucWB&sU32S~23yg~yR7K+||`u2&gdn`eKN zPuR-H-3E3Gf_F})MsWb`{_j331OEDgMCyR&7Gnr>9{+;5n#G)=W>~I7Az;A`QwkMk zfW0hJPHjJ0s?ykVIDn=escl3Ho{wE;1 zcb4y`YO31a0y)LzEB08ybFx~+Akcpu$tfV1PrPndlYAg8Za^#U4%kp6o`nbOtn(<$ za!qVN+ha*Xu@(HS$#YV3!^Za5*h7XECPxRp!_D&Gky>9tz1Tie|M66{=I}fiL;pW@ z#-Q4Be>WWnd`CSz-fJJ z_J9{gf#LS#zr5}9JV3#Oeg1|b&nrbN!7zF}^#h%&^6={sE!o=|)hcXN)T)R}D)0b0 zpokH01+_2KiM)YXv6N)N51#8{0(eyea!AtP78n+$s>P_T1%$byi!e_aF`fu4GlLs( zS{-}O$>6MC2*EGS2s&E!FkOFjF`Lglz@-QzMHv#~wpXZjy7bJ4( zt6?u!u7&G;J;#GeI<%~0|pPd>8 zOFH0vPYrpr<}}JO!lRX06jH^u-tZ#M!XjaC^H<+=htXV`wwf?D5S!YIJ#$I{?s%41 zDRvgnJC(HwRANU5(^lRjCpu5?p)eJUTp(ZUqSBa6*=l_dsG*kSot+(gOejtpH&9*V zjk#wY?+S;4U^Fj%6O(_XM7M=0_AkK#66Op;nU13a@O53okp&JDsGp?*^`SdGVe~wm z`JAjN*47l~5Wy4XX@9PrCh4ty+)3uwp)5JJm6endJH zL_)^}?wwH=Jj&n`HL;GuTO-ero)EQkH3gK|+JO<&K*_H-*cyK+E5^+gWV-LxLo0fW z2E?&@fiOZxaUevjZ2(f`$OA9Pd_B&S72}-t$0rCdnZn)ePp2 z2G0X{ZWyy!$<7u#sy#Tueg~qw#thX!n%JBKPl<=N@`b2Q_<*d6vw$-IjRc^YH?D6t zzFzgxTO{`lcVaG)atGxl8HiKCD6e8!GnZ>40J~}r-&O?0 zl9bRhULfA9*`~}?PwPn@FTs#2nfCvspAf56BnCXARJ(>DgQKG@-cPx6X=FzMY6`2?7 zWr10f28&Ua8rzr%3%2hGMpA|N0if87DCt-H&@V#br*B<(Zw$VT= z1;W$fXz<6G^~aht4o7pWLjTAJ#uA<@pZ&N%w+I*DuyIY;I&fEh>O?CSC}TJaafF~uu^N#xZPCXaZhnEFB7#?;s)x>|jDtfGwxHuYcUIgS0MWp>XOXc5{*@_$N-L>&RNx=GX=wYF!633xQiYzAfZ4=J z@JFy1$EK?$iDO_QpO3ir41E=gnCE{=MlK$H_uKbRdw}Qs_b(6hVgV8i{BfvcJMUFk zGdH&O3$iy=R_~yc4ilZ((r&Z^4#R1cRJv2BR8bWCgb@Td&K^Q?8<)IuHeRkWS7D~r zE{>wGNLgGya8hhjbaAO;c&puBJoVUCKN8QLCVoXGfgtZ0preI#+!aeVqbz?cLwa7# zoSNFOJ|7MHCmwhcT)Zt4@{m`+nTSiz9#u?7BRRg?f(Wxl6@oK?-=K^IBuOJs@R)=v$kfifwlm(~$`6*?DXE(LKHhAyW;IcxmF zJ?h3rLj&hwN{i2UB{q#*_1k}R7O$Kw8lmzmEHz8X+yUk^NjqjX%yqs^$_X0f2UHe&_hRt_%y@Exi5W(xaQUF-@tG(uw zSL~wa4d6p2Wg7mw8j&m5kZW3M8JJflXep&9V9z|=p_@(8q z&1g;`FKMrLf+1vY$D=@X`E4EabmH_G26l2y zhuIl9!w}w7(F#;6y;FY>B}4^tV5vss=u6s7^aW^OZYOu!0mR|dpnEe*soVF(IOP6S zjO_iZ7~TZ75}tbN`?x=Mmo)GehJ{eERR1$C=i=X=(6{#|GSA89au)*QDomWTJlF-s zR~8^tJ`b>flMIzyt?*oclyDLoQE#Fd=SAG*3acD8njke}C$WD~%fy)z4!IV%nJj&1 zv1Mv5&SK7^@2Sp-<4!|AI$yL-M<-rFZkCWM(v+tWWUt$n%q7qjuC5iqafLdi+i7vemypz^4>}<^ z;W7(TUF-ndmOg)2iMhObxG5pTw_*Ioi}=-5WPlu{XX-Wuaa(ou?Yh8!XE~+djfVQ2 zf;U>!&Q$>hvogZ|=elfe$8=nD!u)>=J=q`G0{@KJyhoAK1$Bv0 z)|k1MoU1PFTE5H!Bg!b-<*owYktmnNmU||irROf*|w zATS_rVrrKud;tstG&neyvAhB)f5kl8lH{=-di5Ws735ozc+P}{^eY$}L2#_-q?fPMtK%;Nxe(BT07EcSd__w=%y}$eCKXa}Z zS(+Q87Wa<}D;74-QkKEnX7M5U{!|`M)uCPf?fx&&SMaE>%ynvV2~$zmf2CkaaVpz- zxe}Tse>{~-$&%;l@Pwo9zP-Qu<&Hydw&1jmg5h?t-rs%r8(VCk{Vxla@;qNWyI%W+ z(mWN`!1HeL!`*-1F~4y(001_X!={A6O=T)oS}v@HCShr3B6S-GqZT}6!tWzDsa9Fv zeGlEgS*|3v$);SFhxSzLf2yY3V3SGqWzn2ZZplSAJQO zG)u0NwrDGOP)S>@8)|R2h2PlYQszm0TCRAW?5U|hBmjc*tcN{ke`aBoO>>}!gFVb# zt{w^pw$M8HQ*eDR7chvQ#^X3V+UWWc${koJqp{Yz#LW=}! zG}OJ?08AD33LPF#^`4&EZAqQh_2GaB{JX;`enPb47vIJ7%dXbtZbu)V0C<+1isRM+ z&LfIjptv+C)~C8@=(#$0bnbSIpQt!& z=wSo2fsxgtpK%LccBTLLfP(YMkD#Ajy)Jeu;4*;n@5nUejy~+`O}QH}_=<~^3k|=> z)|#Y1-j)ImILSRHe1q&NvZVApwl7-bCv1AI+ATdDYW&TUfA)NEG6GFNEgDfcZp9gp zp?}3aughcW2O#C}m6cN6#s$@AtS=9A%m#ZzHp}E$UYH?F1t0 z$DP6hZ9qB-YN8-9nCzr1n*CVUzQ5ygiHt+hcUcq=;GvJ&06qH9(9t&iL9rDPhrO}BW*m%QSAN(kS z^z2tXe;+q6%Fi6>8S3S=Q4BK7xGg-k2&tX6a!QFFNm)2`*uI2U^d+xkBUVv>Gi!P^ zB|xOh{ju_USv%4}+Zy?PTLKetU?SI4`|kDmcNo?zIqW;fnQMVto;sA3u41Vf7yTq481D9(57CWQMA(FblC-0sZ>Uw zgA;jB2DB4+E`$hy28prchZ1>)R6~qNny7b_BJ~eb>5QeCCi)ZFXm#+GP=fL1z4zCT z|KL6$Qr;t*AXB2d0{Wxl5dk1O>FnT$h9~L8_J-4IDU<&!z2_;^VDEn8g2S$bjCcQj ze`>E0tU`cG2}~3Xc_<0YV(CG0(EG-wL@NW}fDxjZm6+NIXvWO&c-pT+e?7eu5c^b0 z6%RQf%A9!5AeZxjfY&r1SfM0zfQEWof?@=qXld3F;~ zTrdb&%y3}>)?UQRaU6~2Q-f>bndPRSv>a|=)Sfn#J-)mQZnMDp2S>dmXrIh@r(w*fjo!X{=U%m$?sfeH)*aZEN1 z>V{w~2(lx1n&7Oq3znpjGdXNFI9IP44p;T(5?(;+~#L0~h;UT1^NIW#G>qAKCq2r6rfiV=$LU8|S zzc1Smq)_;MDtDATy2judQ&ncX70G8D8-t2b2+I0_iz700-6=%zkX2X1dq2TFz0k5vXWrem>ePk zCFU8uiux4NI5J0tsKC+Of_LE`_XQ=Xe70c^&WMk$@hiHbIQT#maTfL}Q4|MtZ%13{ zb>0YWHD?f4+#tI29J-R6y#>c_T#RLJ;exj=CT6YuZC^@(f9pJA1W(BeuX37x5aKCN z=kO1rS^N3uC%z#0Mv+Y-m+N7esq5Ds@rrcq%0e3Mq!wReE9cL^du5`(lcZ2GZZcBbL1Cxask%037 zf474-n&;A&D|mj{6>VVf$0cZ$`rI?}c-tUIyRw)?E_3d|!HfOAK(YNP$ds%5(OCBr zNk&UR{N1)hT5QYD61&i8L5yq`QJI-RKxX- z0Z;qtaBj=F%#^}Rit;+;ep}8UGc%#6e`${LHrZ8_S8)l--1;5BNS9#qjRiFwPc?C| zdk)}V%}iR{{Ak1L>Fm2UniADM>iFuGirkNVS^5e`2f1}A> zMFa(###6h&+&vFf;KY-)7@dbH0mRF_Y45UklqEMo>d_<5O==MEcrkF7%oOZMK`{mU(2vO-TSUD=8cgIPgO|sd^x@H?kuk%_8>baQWG?O< zfPu=)@+-m*sB#s1Gm6xDMP~XMe_2<*5m(Pf)y%%{rlb-jo>UK^Ah+^WzERPgN%SB3 z$!GUhqj0*?DvdeYvZMmqDUORchx3$&Q-`V_QfTn;!*XPk5d=6e@Ccjkr!mG+8uot`?u>>R(|O`S1zYPwG4 z1A6}N;^My<7t&7a9F!=m!SvQ@QtXuhBVqx85RCF{D4t_I$NwRRqT>Kf%m-*9yAkNl zoWlq#ft`tFfjnlWmO!M^f4&4Iif1E~h?%7nn>LsnuWSs_KZ&~xh$Kd9y%sDEme1PT zhx#U!Yyj7+sRv&s#TzjrQxEdG!g~YeCO{%phTjB8<b9P z1S*sD5S*prB?S@46T>R3`RoAr-_m*TJd}Ncre#?8lnuqU({EMVe{_n|>C92t^T>?Uv9kGL36k|`10+2xVA%9Blr}~-_|_UVBS{}I8_MR(2tYI(=1E=W!jt2Mh?7* zSrubrZ{l;Rn5n?K&wCAzVKU&-1zr-#hEB?`CT<0a)H;~6mcLDXceQpr+urstfL47@`~6MHR|N)sZ^-}DZrwyJQNwu zJCJfDnaF9w`G?0Sl4H3Y#z~nwhPrZ$qQUdj+2uJ(wAC<~$=#WZ+rz&2b&?#`-70|H z0-KK2hz1`Qk%Vf5L4V<@=W(7&4`e;9aH0u@GB`85c=x<@hG z<6trY%307JMWm)?>26Y+;ax^4@~-<_zFp?N`d@TfEsl1OGl41!;LhUvD;OU&jnBC0 zND0xace$&5=Tz#;%WozW!uacMg?HhP=LgVlTR#O|Ov$?oLUhxvpwKD~s|>9^o}Omk z)MYc6{oUvuf8M^_RD}-!+_edxFdAnufDl5;)XI?cZs}ZTR~FqJI(JOu?mXh-rY5~@ zZ4WOrJ3qcZP|AEQpzG(ZTwyIic^Gx-u@2qH(8QmQr_!ff;Uy*tygp$LO!=d#jA|2M zlGo%j2>1 z-9yT;8@4OLEin_O@TFM&1()t-+V1Q}gNR^}*4N*Sz?EVye`b|DPuxAzY`#QSF&9+5$X7JKm9Gd} z5_5NAj1jMPcd?FEePMr>V&wkH$CxTifGkb3aAxNmBblcReSg!JqP)uM80D>)tMyi? z2>#d`;t2H_d1DZkD@oCdX)V z<~DGvpHn9h=>@=-@GTOvYBdS_a|cJuY+VW6**3GPu;=p|G*FIf}`0tnfq%ZeNMC+@$LQH z{{f?h0JD?vkP`tim*FM?69hFkFffzRwJ3k>Tw9adMs|MJuV50#3s#9p-_ek2E3q9b z<#ir5v8wjLTBk51W(F1sj0w=A$*<2jeF3^b0Hj9JRw|X38L-ip)7_`f`Oc+5R(C71 z`nMO~zJ2jeZzz>3o#sl3)!W-u##ULbQ=;M5ZuN8W=VSSBtmFIR$Via6vwh_)*B{C^4+mqbCNt(^&Pf;@%`HuzrCPPo2;lEM?mvzwLQG}`LATP zgYrMENJ{g3^=N7xRzlL0WeT46tDj!{#|sh>&azbw#4zOvm+%t`4K?3Z`~6Ex$Zu;Z zlT)#;x~H|2i7UN5VsTSfWwtI8;m|*CGLpPw8J+6r z=Z(seU#6Q0j?I{Bmc=hQ)5#xQJ1K%!9M**7D~e0ycIh2w(pQCbfHlKqxLt`<<~Rvx zA*8MP%@mQ)7l$Vbla_9>-%t0K!R~BmD*VWk-Kmsny$Wivna=1`U-2r5G^c+m?CUuT zo%4leOiYgq+bZq;Y$ORcYSiAmWD2;7DxG{iLxKJh9Fk3Qh^A@Auo!<`-snuPSjuSjEUw8G=l;j}iPK9HA2(8jWU!c$2>4@K z!$^dAeMM6x@E=Ph(D3#EeLYs){p4A3Z9u<)ij3P7Q-cTNP{LD9^SIHeu5Q;nPnzR; zL-XXY-Uz{x8`Q2(_IX#{u7OF5(;n2>CI_+c_P+GA$ylTainQhjEoIc`@&XKQvLdMt4=2MIi14<}X_~|F5L_Z~W(b{ByE#>zLzj$P!xAt+N<|(mbx{M^ zJmbmxHJtIXd_bNs1qJ>S=13r(w!a?l%i6xKx^_tlalp`oi)eqWuP@NY6@x2X8`TQNsU?YM^u|Mq+f@A^l%|R{vP}}l{V*!6N+m?%ZD9?eLbxg{yGboi9 zATkZRGO4eD#2NrsQ0v!ADhQ(PA!b!?f1wX05XAubPEl$6PED5++$a`#UeMo!@u4e;7ZLof`__U zQy40$+irhBZ9{39P7|Hyxq)A%a&z-*3Cv+-q$|3$|9>j|d4M0njb~+RFxtbhxhZZc z^FwibZ;PsW=SDUaL11{F!5R+-NKG%c4CU%bj71EcsUh zqU(RHQxjDiiDM2&VUN1~DU^H>1)}Fwd~EFh_5}2+Jy1}VMXBwW2cX6X zUt4%;y?oyKErG7fx~*;oqPR5DLP(DP(jb6sG(b|WK zqL$K*r|v7en^s;~0!h*yirzKp7=ZY7C&_=YJQP)Ju%E%Orh~?D%+Lv(&CRYl;F>gb zv3I@g8{e+#=EP!P9!L%I(N3KI2-}I!OnM|LllQ=5b21J<&<}<+5fm<+_YBK;Jk})` zM{`(jJtEKXVcB05 z&%F3BGjmF>nK)v`pw`zj3$BxwBb+v#Bfh)=H4l1XH`$5rp_gBwKu6GqBHB4Q?!n-I zId*Q$n+6e!g)QSwtkgR?;b^gW@c#B_ks1oRdKwFG62)Z3Qzyy3p7He(bq;?yvS&aJ z7r)BUv70Z<+!7J_em$rTSw@?~Nrus}v4lbQXcEIbgw>0-w6F#=aE z=mD6e(*v@fV>qgPx?D2I0P3UO&kDn^odX%x}PIZDSWlBIY0iG6) zyDy%uFA+x_@BH}SFm@$f@oK?A%DFxV2ysN4OHds^n3N>V2p@;FC7A6#tgT2&GS)}6 zT{Jr+mMdKPl>~{;{ZV=j>9Em+PlAX!rV?jN^_f-?WS`X2Fe?%^6r+C*422EnE@o6s zjRx>2<6~hn7Yfrt+Tf1#!JR&r-~vQOw-ZJ@4<;GHI0+t_wzB3f2MIr;VYrEsNb(a{ z$)&AX8SD9X6R5FvdW9OmQ#2+7q1%Yik#?4XIb0X2X*2Zph+~zHZsgXMR`u^zAy>w1 z!dIsnExRTEaR$;~=J9_@EV%wfmSlf7%^v6?2l<#qOcKp0gkn^uQpLmGq6vU86K+7u z(-Pf6f(;?!_At0m0{BGC;R3Me$tA)x9B`GzJzJaUn)L)r5f&4;T<0qC#-AY=pk;0) z*}M~kSs^;75`-E;qhn}4VmM63F{uIEC)L0K0QPB`+cVBQLq2~4z#0=MmxMeR@M8`+ zcb#7;?fGXTA(mudoGP7Pr#$Gop@ zyK!7afRa_zMJ&Ao{akjFIa8j2&9p`Jzi)f9*c$Im=iHCG>K&oHJeE+abzVKj@|Yc{ zGI>yhU+|%8mF58Tg;Xi?`3P^lb#8+U=;^fBC-hsIBo2QYoNo_hS8ZS6;;0}v0q-6h zIs!snM-SEuPm|6%>YZNzz0z&$BRCXyb=94AHgrKJJgo1HqtkSI@7>D*VTT4s2;CAU z)IQD!V%E3C=@1Nq1G7CL=_0{ZesV7*tZIR6j+IG;B$k7=HxX7>~)I$I6BJ|UMRq}YEuV!E$jKVOW zhv+KfGnklM&PSU{xtNq6My!cT{yZZBXgmeiV|qeaun&ETKHee)SeW~naadSxsH z$*~d~{%7hENXFwj?-k}WTM}uR=9it-B2}DSmlJ;(W%7*7M*>2cT{a&FMcI$W<;>(3 zM#IErea+mweiLK;Hcl(V(K$#~hhJY!v5PR;VG~qyKF!vNnesP`_MUu{#WG3x(8dnP zGijaE>G5Cbs3*7KcfFq7nmTiaFnyUFN4$CJ%m;JHb;Mi<+*v%d|( z#bJxn-MuSwT#X4tAmfdog;LMI$Uc9rThsuxISM?kTYh-J5r9Vw_d|(c8JCh|I;I8= z3oovDkQvl-+?e=kF=dod*nd#5oa$8jWUzOHv+3x$;OaU+!awzr#?=?5qIQ^UOEKa# zM1q$BgLDS7vBjton4y=hh)}X<{?8nBu=???%)|j4+tcVrj~95B*gW|!Be{RRn-(sh zcI4#|$45T!jw}p4yG|@;*DcSuon#+1*RfA#h~UNn9VU62Grr;=0+GcR0y>ldP}X!5 z(TOPG#0F70MYnW+Wd_cqwAyB6uL&rqB`$?C0BtL6WT2XgXGD?=tS)Y^*DyX@>$%dC zi*kCnnnGdH`HJNt%?b5NA69?1>&nAJ=iDC-*ove|s}y}7`0Nk{rV^gEV7j(GarP>> z0ZakRNyj5^sL<1zIwYHEW^zHJ#;-?N()Ox=Eo(;aFc_DCBh=3?ELkOuIao{WOSag>rZ=M zQs4H_TPOWK!rSUlw&#PWc#t0bPa8gkW$l+X<?@@ny_uDLZ+PL}~6C-s8+QTLoVUiRp0NIKJbF$|QE+ z|1GhSHdsmgKUVs}Sm_*caGr7nqL<~V%oZUBs6@JqIG6(K515iMVyZ6O7NIH6sev9c zSdR;dOkW(44N!7S$>(CkTwSLt&onKwXLn=Bnnh`p@rFN{hsDhns~d``lC=9 zI$81nrJ-wv5t)Bdro+&TkiC8s?!puN_pS=>T`2}M2fuDgck|eF(D|NXR0IX*cTh}m zkMp3Uu?e*2{8!ek>*vE1%3&`icpGwBIBxtnu^<$YX& zr%dyBUfvBSd#E^jnM~<4%SX+hcRuJR3TgvMXPo_1%{zYu^Zu5n#5A91Ca(ux8{sP0 zj+AEJmpbCO{?deF;g5E)Jx7eueA*>z#MtW*{v422j36J1s)s24riV8i$mj-`T-Z#$ zDD32dTXQBLOhFq(0pS?^$Q0hhDMA5iJKJ;aKa7y20%QMp`Bns<1b_*70izuThY}Yb z@kWEGWBh-t0z-Cq<;6E`qAoIfj|imKOZ(G#O}jAP|4Ss<#0foRy)Z*6*l z5=sAo3s~4gYs;)F0Ui!b-E@{A6t`h9;kL&-Y5viNWI9qfY=(Fis&<}qd zYIt+M9UNi!y@1{kCh{`~*nf?}lHiCrmY%ra7XNt)8pWW3asj&5=bklpc$L#$NY@NT zsEst%!EI}16&`wL+vUUufOz=bw{V~<99-fe7#yzpQfMaP5q3@K$a}nZ*XWnR16-#9 zk=BUFOGCp#!w(*}tK7i&X#*;p*=m1RueLtw&i*oNq3zXv_}S;O0F3DY1-v$ZLKq`i zvrFIs&0YLbT13$|mp;}5_$s5*v#?o>K;Cc>6Mi6S#*~UjTgiRrA!pHzb*Sfi@rGFJ z{?BFV!NakU#U{qG<->#ueOJ-tDas=L$CRiS^cEflq% zK*Ey3{yR2%({~?kfZ^P!Rek;l^WSQ~PlvVkzyCJEOXmV;`j-_qbD=6HQf+-!=-c?7CG{Z*| z0%tg|#=W(eLEc!7kp9|kkh6aZ+}}DG6zzjCL#MV=@}A$Vi@I_3)dtPI+9)jKxpaf=T16 zznY1u<-<<0l;gM6SQIVuEEaznWJw-NWim1|wk+kMvC$8#dEe%tI2LY$8?G%QLbx}k zu;^Iq%1wi5#1f_itbP4Utd@Euwa3Sw539)1wdyf;1Nf=cs*7ZiA3stodW=}~)y?1T zZa(^wa~1h*dV7-c;!g*W`Ly+v?DSzMKmJE4Qlr?#{;4!3^| z{$|3NVZ(STFA`DYXuST|q7{miHoi1H{%v*b$ssHgE>@nHT)b#49%(N7xb)1D3bG7Thk=F zy=H0rx~;1cE>P)l*EBu{hll5fu5EvY1TC{NOO_{67HF2wP5)#|?+HAv`gT8462hhi zL-hFQ^p%WxH_6>Kzry#1W}42e!@KIpF_l<2CPWV`m->p^f?7jW4|E=Ab;75<4Q~vI z_0~nf#gO^n#o_X^JD8T9;Htgq@5E^M#mFn*bdkE#SyJR_Km(qn3e2Q*!c%{VX8N@6 zFDIwp*`4fs8ay60xbenS}(=7ha))x1B zCEYSX%d+L56nXp!to8Nybje@}=u!YY==h-w-uYrNSFF+Dj-%Lh8}GJ%kyXV(1@%$1*5Y z+?}^IkuVcZJ6aGN+`kXXBb6Tu0HkDIWfu}{aNoD?AEC@78U(V|K{NsB%>D7AC7gu& z#v7ZB#_le`26Xe`Bxl`c_talQ%xuY}oI|=I&Qqy}0Udu|p{jaLoZ+AIs-^-~8mb2J zd_8f0<$|EEO1nT_?W-HfK{VXfnzCQb=)fntayJsR19^shxWx&?x?ONOFTGu#5Xv9` z>QDv=7SNiGIAcwO99jkA!Q$Ffr`s`|&TvH(BFTe+=rHLmLC7Eh(l2y)8EPyUwssHw zvx_jS6wH*bHowcGDif9rHrHbMr-vDa~3cu!PI;gt^#o= z4OM7GR>m7?VbvPML=LA5DzZw8E)OAGfotPcK(c?W9kfkUz=0TqVodpuuG^gLSdqE6 zW&nUdf4?wwONlE)1U%C9xfyLaX11zymP6(^3u{^Y%QYZ~1zfBg1!=IZ|6@m!>gAAYFsJU*oOB3(#UI@$V0PQRqP;0Z$?0bsO&2Ve;q24(Gu3P(0_o8N@E8Q`f2hu9TYBFq zg70`h(=9^6G}j#Zw!RGZcG5A}YVJikgUBK;>TGR>3Uwms+l3ZtWH!zaHYi;UBQlvj z6s|#zP*am_)Rb|@9l^3S!S2D-4pm)S@-nocTDyjtD^hi%FQniLloWch zejQ5uhM2_6b*=NM8QD|d#Fh>?IMY&7z#NZ|&YI!X+OV^3j2cS~f-4Z=$@^6rCII0& z*EYa^ybdsrHOR6Vl&7BI3&|7iScY^aHycXRK-$9eWAt-8RFl6kO#KMLNA_8d9(+V!(SZ%qX9Jz=@OF zf576>LC=RsTm53UEh~yV7Oo>Il+~3`>ybqLun#HL)kWjWfw@YT}WQn+}<_*dfz;B8TCrnFoi_m zDk7w!o$+jzVtE=khtr_0M2@)1oJZqm1d`A|aLcgED)Al6fJB2(ZOpdSdS-|4x662c zOu+oD%QGPdAS?SdLxUk$n5x(*Cw4N+IWTKmziYq2H~^C|Ue(pM%j< zVqE8+O*L%lP`8k!X#dM_@MrvVOABXz9S?uCwDj;!-ON*&NcvO+(x0Jy3z$H>G8c2tU=%o!g-34~re!zgm*kO6%; zwfZXR1a+U2r66Vuu1T$gjA{vg$j^rUQr`FthXmJyVs5$bES6UOsCahOA2aq+=U&6Q zJ(t8DI53bE!5Cj{eEL#(!nKa%)Q%I|UlO>~fOXR}{5~QJJ}!WYejzmk%C}col5!1F zc4eWE1tf}b;Y?QWC?OV~J>D>(x751dr(-8&)tKRl2HLXNwq((ItHffl+!xMJ9~&^o;;}!n1*K!?9tUd2;3lXJLZ_ zPv)I3EqM;wQAA_=e(H0tzec-2+Da4C$8DgypmP!JRSGuHXic?P^2OcF{{Ub$iRhE@ zkP`wlGn2qZc>yw$I!8DRGaxV^QZGg_ATc#Gmtk%J6BjQ{Fd#8CH6Sn`QVK6kF(5HD zHXtw{Qj?7RRHkVPP0Ti>t zZ6E=YuWb{P_HLw;<8Bm_u5YT7nr{@7Y;f_DSa1`waB*`1lU#8Ww_p|lJpq&Aau%~n zbSVLoHgpr0juZleMT^j)}0h330 z6|EzP8|ZB z0h9cH6_y_Y_>-=I6_e0{aFe})6PJ)60@{;4gBF*7Ap)_JErb=9Z`J{Qlk|iYw_c9{ zUICMag%y?`0wv%71dJ26{FDLJ0h8p66_*F!0k)IQjuw-zkGhkaj}w!8kcpFNkQ28(FabmX zlLL_zm*6Y{=aXEL6}Qgd0gC~XSd$jB4wZ!gllqhsm!9PTMw5}27MH#@0pyeHmKC%6 zmy`jM=$8|>UO5340h5iH6_d@H@RPcl6O+)Jmy^Al6O($Jt&?Y*6q7rhyOSoJ6t@sQ z0Sp0?^_>-$FgpU$mpN4d4zmlO1p$*mVJHtXG72wDWo~D5Xdp8%H!zdYwJ3kpT34^z zMize8ub8*k*fpoOzzE>Dq#)iHxlUWh54;j}uVP7*DavJk{eEXiQJ3n0wGI}L$QjPz z>17HH6D%+-SY%R2CKZz!H_fE?Ogg3nZk`#T(PT#BCYg(ZDZ{)H17-0ZURMEkXPE*X z87$L4r0@n_kj81I9qtj72&;dKKnS$LFau)IJ7x^|UkY{!2*EQ^LG$Dl?)gBS)~&?c{fVv=n=m zks9zXOJ1YND8vOi9vq?p3NDcXu!oEi4&x)alqHiuxzdv9G!R?E0tuY7EMPYY{-g5r zBtq{reTaP!FX`QUzQJeNFZV)F)aZ203{6knw*j#=j|FmW)4P_m;)5 zo|N-U-e+HZHMsYO_6~plhY;dBy3FZP)1{(INtY2_R&*J5qmyoQe*Yj5wM*oRE=7lC zgDVlfIgCs1;(AG!+YaF$!ibK^s(bu?XDqQjqsyX`tnT1Pcj3o~#xbF30A_vF#(C(1SHTeIj2L9u6G<3|5H!y}gcN>QA_$|=+CVd${q2htU`f-nQ&Gu)pVP~dEAAZ=6 z@`l&LtVprT%dZ{o{M@p%>N`qxB%{=O=2J&>CFKESZOtWC`|-TO(p|8h7=JdYUlXE- z4+nqz<;O+A_>26e81TpCye{VTl3@`EzybfMSeC15SS)Gzo}N4}Mw9&O@&kKC4{XHD z$YafKWESG<0q|5nfaPNY{b)Wff$kM;77%5C((U!2M?66d_|@vVPW>My^EU(jby!O-7)pVbJsK2?Y^SWTkZ`go8gC`}ypGkk9 z3+-#qXg4I7uH#v7>y81K?>dTwkUHRjzo^RLRRLXt@RMhZzbrn~+dj1Az|n6zaI~N1 zzzuJ{;mXH!i;cLkQA2E`jOLO)u4T-OryU*#Z-`7Qms#4xaqy0yk2;vWoR=#lvS=Lh z@b(}KQ_5!n&Sx!dVJ{bps$7)I$(?`gm&*jb@rZILK*OUAZBm&`O9Y(JS@8NKEBbP* z7%egjKlM^&Q5j~m&zz$jQw?SLqqcDu)CgR`(uOTEfdD(wn~b(S9rB`->L?rjf{9R? z%t^%@w4kJ?&1A6QX=5}~`glGL7c`2EAg`E##|q>T;~ca?o6U;YJ3nBM9ix9cDKu5DCAZKvb7!)u%Wokk4R14>ja{}=3B%F;7;p$Hi$1F^!Kc3zS+Dc1o>rpH^y3!z`V}r@LuOt7%KC*($A% zt0J$F!u*A}BOyUvGi ziuq`b;(kj2S>XxU@7|Lyl~7JZ>5*ci4G)C*LWnQ!_w_{PMUKjsMKUyh`NNOD|NQ+w zo)mxPzpSo`YBGikOsZx5_%^Sw<{0oF^0p&`ZrtNZu^d*D1<6Je#-ye$2@)B={VSgp z2e1Rf*Gc{Cf%kucJ<#wOMUEk*q$EiIkiD7^2!dav?73+Hg!mz^Sf+0(5(7`?!*Vp4 z->{9W>qm&1$#`7AtmFr;AQfLWG2!^AoKEwKFADew^kD>Fko~8{xNch2!UvL_{F0ie|mbzC&XQs*mVde zk2%C%)R%vC2&ewCLkK?2XV;^gkEeYFI=!nvvtNN59W)Fd34m#SvrM6NQu0U3 zz19*g(QK(AsL^SOE>N3WqFt82BYEdhlP+Xz8Cd5P8Flx7a)ylNZhsxVc-}Hb*#^Cq zx3_=vsbbTF4r0q20koV^j!~lJtx0Z5w0zZQw(O+Tgl@|WvK4*v*mPECw#45tC0few z(|4GuNfcVjl<&}s>rg;WrBC+2K=;R71g_Tgb!`6B4mu=w-QL&m2mkBwyZBo?+t2R) zy}koW^V1`+4vuZ%F znZ_(4G+>5lo%v+`{$Ky_k=JKs+r8E0Z_T#O`E|A(Pl;HYfl|3aUv47EX+z+zFR|oC zrZe7R@$V^PVIAWzOR<>_{7?xk;)4`+ zzjXBJkdyeplHk1E{Qm+ug?i7I;Sm87m$5zt6$3CgHIvb`D1X&lOOxEjk-qa+^v#%v zX{g=>h}J=C%GN4;@U9qAY>L0H-v}PMl^C;+U zH@oAjcYkG@Jv{$m!*X7hn~$HdezU zVTq#ee!Y=8t9zh_WuJ(uf821EovZfLocxnrcYHjb>VG}m-PMNH8gJ@{U zH*rJaPa&6wE^~W1^5Ry3eyVifk)yL^xHXRCC4ZcPH46EK+%fQyX|}WqY4fruUd$k| zz@V~N9i6kB!P@`-F$srFn54J_VMQzLs1r-8Wx2K*g_$e<5?0Eb;q4kLQyNg*UO<5W zm$3yST)c$cA}>riXLqW~DrYcbs>ytb^<3r#D2!DZO?ubX?dho*yg}{S6(c3CnMfml zi+_D%FSc(hg0_`~x?+dzXIWqI#n$(;)WDJ?mGH+=2ENMjoRz{mE$^G3n$tt#T93^z zAXIQSuyu*o?(52X%W7DrSf;LMP8G}8;$p6>gu_a{ppy-qW)#K@oppj`c}p-u5n!Q9LOPnZ1K!}Mr|ML1 z@2Y{Iipu`*-*4WAeVx{J~NOI#1gQA(X# zneyOG=%81aCY8*!=4r34J$&+n{EBoqpa^%5=et8QJP!8tWKjGJa0uS7|_DxG?okz-Cw z;1+oEc&O?6&i|fpZy)?kkI*qYw=F4rc-@iA9q19D>nma)EQAWGNC{n(gJ@yGYJktP zPQSY`DDIvx5q26^>7O?3Re|I-mw(KpgHC(3N{kc=LY1_e(_ffzUwyhdrPjHeuR}gJ zpJCv2XPqEeQNVdH2!3WX56z_GE3(bNf$K6I>h#R~ii@0Sn|5kEi@{?b&tPsE(!^VQ zT7t13x?SU*qRa(3O$617T2I#*^X}7Ztqrz&z<3@PIUKB z{Tem^?r1Z1wU?TG+`~5+s$;kUD`ql34u$R)mxikcFm;1F6l!E`mFxGIB1f>@Q6=kG~T;+huf>qq1T0Ic6 zKR7_rPnka%rR7GEM$6vTA6%Z(c`BWGC8&|5{aCmA*o<(p45RUN zua4)#sX+^hxqn+Q61TzX`mT3qqd(DgYCw44j##Cc6V0)`;sgzxKk)GoLIZ`nG&A$M zWVtqi*fMA9ltyksMi^XuJxq2TtcNJ$&r(?|+4ijR5_UV3P6@mCO+?xn9Z^ z6#xV=mCbz)=*^N{FxUvdU0Bx|Cq&?}(tRQJPeSTC2+ zf?cgsdGBDmyDt~7vQw`-z59f#>Odj04ZC9FT~QQ_ z&w5w;eG}rmLMIp~B4eoT-Sl)9j4TdS@2c}KPU?H%cBnO%fS9}FekVy^4}0&hV}G>k z&#f^_641!Bp?X5Wg7(|FB5F^nhjD7PI~}mWCwkF=od{rTL^>(*`TKeACHS=u-ewKP zqwmR>8X`X=7%XO%(vLw1=`_j=@#=zSH4EUwaeP z)WdBcwDP34b-f=J4({_QKh6P|X@9^z7>#VeECO~WGy?&QJ3nXH4~{b|n?M(G*bj#Z zCYGJ%bp~U4pg$i%3TGDk04QXjgdU0gxn4WzFIo2e%4tDSQ2KTPF~hD4kT4WK^Z+AQ zhKBw5c6n&41Cly|e?6=fA=vEqYlp3&e~iFJ8CwB@1<%Um@k*8XvW=f_?0?972j^z2 zKap?Rx?n8)*s}#0>zf$VJ%^25T99^31Z;Q1DA~(#^;kQ4u{T74` z^yl}B2@}K+#U^SE5SXp(37Dau77yG(puw!ibC%1;zY47K%jSY;?tjoDMH!ehOoBF; zg86Q_e+j);cq*53CA7ZJPJaU_Q2cn^qq8#TL<$`XQ8)XZ577-EmE_(m2b2BywrIxBf4p;Lkw0>X&9EQ%|LguW)N z>%JbWD>umm9<`}?otIMH>q){C^%M`N=Mqx_^+}`)S=0fOr4boSMD)J@!w2FA&~%Mm1r`R}3h#{W$nnM8>M!+8BAn>LlYhp~&pdOf(24WZ%}Kpd zPP10!hY{5CE1sK{QBssJc>&8$I6r1ic9^gp(tr=M9V9fz>M%#k(ts+2V0C!gqQp+D6IUuZDkB31xCDj=4#&nU4-s{9tWV^ZR=mQ1llQwY8i zs{vJR15jTxfPcVLl*lQG>9>cnjE0TgEA=TbvY@o4Ne$x6B{G@TM!rnwN>o{CFt|~w*@PO6-w|c<%`9Ke}9?$*q6!ARpXKRY6-mrY_b$( zTpTLITA%^(EDF|(cSYt>zE}-4#uN|(T<2qiLtX?!{>@rfe1Y%#a;?)Z)w&l1b=T~x z1T(Ck*EY~J(6aI++E#!EFjuurT2NghUZ!oIfmZ*IluMR*0b(*&u9fUa;X^UL7>dE7 z87{?=H-GNy?QSfXUGb=+!4nLSXps)^({pi_0OIAWQ+4nBIKl^)U3AGgyc-C=){g)# zOaamPn0v&x57ps*OVLUFNRN(9+Z>w#@h09@_+#Bs1mOr35|8e+haUSL(8G=%VSW<3 zxO^r)xI1(^OsV~HOC{8JTqna>7d8NJ+w9MH?tkR^UmWL(5jbcFE1WerEtZ=YWb6; zF@IJEAB;XDA^kmjwir_gn;SmscjxQ;rqV39xoaLPs$V@dREkH>Kj4XitHP=teVW=8 zhp~{fKHZe#ATI7xQyE|2r!OKbGZ4@Ec3v2UHGx&rBNLt}J?6PB%NY|^56A_kX)z@m zzB|&(?%SnZ*XoSpsJAR~VN5!GSYcR=FMlm5B55;N)EWw5eO)DowmnwR+)@^U^iGDN zO)UBYMjF}#mOWu>@T2ur8wl(vWSLI*TL8gq8jL!N)Eg@POx9qjY&(c1E5LiD)VgG~ z3~?K1H`aK9={(j`&1;Y4jsv~#?iWpbn4u_KB~>y-nhLx^pJWzXw@iCiejD8E&3|YH zDCYpy?U8oTmwgkBPWEwYLnfUP@$iHaT3DLvimY(KE{3F6ZM~xv@5h3r*obMFD`uuj z30J>3mib)ul?4d3Oh;I=CGo6WYKXjN9lxf+#!xWY#MP2wPT2H8!c>F?D?O2Y8E}MC zTA~^A*jEr%Q%RMteR43t`!MOb%6~IIYBMWfyC4b&|5#u8N(G{>Lp}Fc;N+KZ7{b8{ zmYA>DpJJ7*bN~|02-D48D4WH#fmuD&V|$XDWQB=9h6B=s;WQ`=_RlK19Y4Xqs#^m% zQ1CL>QpWWOT4nnhODO32SOrA)(SLvv7xO815M?e8qST~5WEF8JcM#<+AAc%e=+{^6 zFaBE=k2ib^LMHSfKzlykg|A}5^fWL&G#LVlVJs=UR!6GfnN3$6ZTTv@l(u;x=KVq~ zBml?ra6xVONFWwbG*Q-{^oG-KFHAKSfbYX2FYp6SavMXv__&;kJbV(%aTebO`1M$| zKIvXAeA}%;blUd{%)PYAA_ zekhD2LE@9XKRq`imX}Ct(_VY3ENEv*!4db28mmW3U-}O>XIO#bza9`WK#Mw@d-&~p zP2W?DFAhyxQ{9=IX6U^=fcIqa;t}Y!uaB-Wmv(HRhp>-FXVX;nGk*?5wF2~lF3=1e z0!>L|79Gz73T`R$TH`!XZH39gu~l2oEiNx)Zj4=UX;V4H{W+?$PhIeDs5p&Kp8tmn z+yK_pksSj#5ZB}3t>Z!XT5iSH!Y~RjSv=gcVX1+z6l*M^K6obS!ay{3CGstgGRdAiM=$~*{9c_SSA{U}ar2T*a|OMEdfzkSvJn zwI)02PXL#b679amwjV>?5NT!j&H!@@OW0-lA1^}4Tw;Ye7+f3vuZm8(eE0U%{{o&w zxb+HUZe(+Ga+l~40So~%m$4%O69hLfH8_*ewJCpHd5>GkasOYRf(Q`g4X}La`>y~su`C40XV0(tdfkujaipXw+ky4*1MiLe7jq`>3((GJRZ0E^Ud#G{{n3Vui8ql zd*gp4hKfyZ1nXAE&G~S%6qm9`g*(};gqpwUkk zq2(*qOYbyBB{2SHT+an#cEM$@h2kDlz2bkEN;4w|;0{3GqZu~?(4&>3uA(4iQtV*TzQqT(8cn-660!39SvH9(gIylcV30C}2aHBUxpfURrnmkpxY z(+FPIBN&FJf{A*j3RdGw6~zNvk#Ph0^LR zG@Oajs-&EgRw!HnMM%;ry-31%INfmFoi|A+5638yj{pP-1{WA_1^8R0+3|m{+a&3I zINWV^fbk2tO?XvkHFBFsFO(O-72rKO1wLDDZIDr5ubk(q0BMmhgLKZ=0_5Ic#~B?Y z)Cp~B2*;=zBhaE3=(2!!Ex7d0b1a8RGA;$QuCQKK7z)ENjQ3S+FNq8u4HFkI#BAVr!NdSGb!20VXO-M7N>se#@s zf${IB`zcA?#CSgyb`4Ce_fuZDvTkKRNm6Vk>>YfN&T?9IhzE?RsDLC~@RqpdUJOv- zVPLce@4$Jbj1d*cL$?{Y79_0&xXjR91FgE}NtV?t37Y}5t}$QMSW42#LFE;wBsFWu zW|wIOP=#dyRyY<|)ir-jP>p}V!?>qv0xRy0HWiczycH53#$V`^NT+-9qL)_UgPU_o zaE2q79tBG5RdACmg9lQe zQbGM#2X8rOHZrpnh%7JHK?M%bWdZM6@GwB0W?9XX5gK6Y8vB1`jVS(LgDI&iu)?%L z86~TkYFLHq0IpH3prwDAXPD-apHaMON-Ivc;w2Rs(Z$mBn!QlEfDN2+MJgLEjh~Y) zrFG9>%dLbjs*}t6-P7H>8=<>Ra(aExK#k9Ecsif9@N$DzKi{Vh>lC|<#WEvP;Zlob zBDYX`&Eu6EP6&Tv#6PJZ#wI#q!ug)ThPnz&+6p0_4<2HH5{L=MBg`XY6%EYv7~KU= zvI1+>TgKtt>{>9qLJaE`!$jIpbW1jSK&imi4FLrlbe5wTWIcie=Me;`YMu&|F$!a5 zXhDD1Kr^A$EQKLc98IdQA~fSP<@5>8e9aTOf-nifKnrP1^-6wC~d=pt)-M;&Z{7zWu6LFOWpu(M6UuDT8Ehio?P-Y zsmY4eYodQA5yNqwWY4}Z;DV3PQ>G`%o|p@`a`L&95H46@RSfx|S(y&TvcPvS6`+|`riN39Zn%^|r^6f>Rw?#3yp7xPz?OglcqQ+K`slNDyX7sdxyFoP*%f~Jr%uCR7mxfTeQ(rF*# zn+gu;vD%3kGSzTwSxvcudNwS()eH_xjadP%TS5w)w4&s2vY1dcTuOn)qMfb*X57Na z1H90(8^JT-^>mp-Q#?+_z9u^(H)-uLu{3{Cu9HoDK}NwNd?(k0L@62HDRa*dMJg5K zTIwX%$IUKe5k8QQ`h)3^8{uC!dHV3COKGmb$qipO-@+~0tk0+Tm9_)tv}OB6o0MJ)R^X5HPljywF%h@g?Q&Z9F6HFjp6NS}bI3SHG#}Wo7fFJ|sESW*k|2L9 zMzt1Y$Gt(Ahh6Ao`TM)DD0k2Osvzrr*f97_n&|2vZd5je=>;C#657O8xf)JDrge}# zirKW%4a`8RY9D=w0W5T{4P|0>Ef_l2KnpMGsYv4+W)T!tcopEfrK7+@SctVuN$U(I zm^VH~ka@NSm@!+&0W!Q0vKzrO(ba!!g*lTP&W_JObw)16hhxVF4pZ2MJe;At)bjx% z!MY%PQS**ZXq6j9zXw6zgs5LSKTX#Q-zO4L(NGx->)~e7@tcKa;vCkPhzZUP0EV*6 z1lX16c|?BOEHdFu5p%Zqjxvw}tCYr-J$FKoNrFFyO%9DOq^Do4lHR6ql@x!RybSub z%wKn<@iMp0qb>7r^|H;ct2y>wOqg)atzDNYea`DX}jntetDQwrR}q2QTd ziaf3JR$eE#ZT1-vOc_e-?co0ef~AD}&m0|EUn96}_Bj!3E$pW-G418xKnLHh0>k$~ z;2`8cd_rf?WmBy`Cmv8-p3Q$r#e)H?G`t+mqE&^(VTR(88X7sXtmPtnnP~laN(Z8S z=Oq&(=`g-WxaMPZg+%lb(i_uI4lkN}HIgKrSrxuqHvYfMu3+`w4Syw~Y^E5a ziOq7Wc)oW|{_=jkxyaRxI*fgVg!_Q{H#5Qnhf8vbURq%qz;p^C?OQWDH zhfMoa6n2iui}R(-wBvtV$PCd_$e9rsihQ}H@CnYeL0iFG@VX3CDW~L71;3($C)4aC<2GjahDyjpbj;a?gJ?X`2>fjC0 zvf4Sx$<1=B3zd5jx{o@Hb!)eF%)e7YmEuDzAdPT_|Jc>e62MTj=^kgy~@z3a? zsn(y5L~zZBA*ZbL#hWeoq}&$GdjYRiYqiaXP4A@PqU91}blM3#}=x z;*o#vx958dm!?f#H4$!f_d4Sl4_J6c^kI{#GU0=ji^AY#5ePVotFXur`KW^~P{yZl5bjZ5A>Ha0pY&eZr?M5~E7*rKR4dt6Z{sf4?oB1AaNJ&QHhHd6UF= zb#9D{ro&({5`tM8v7D1MI`Qm=*KjlGYCtnlpY4CRTN(5M?~O+wXI!|i6Iy>oBdG0DZBM1s95~6i7Iys=}&FxtQV>e@^uKx7tPPd%_`|bG$(FX}G4}QaBJnll?J)8;_@y9Tq$wmQyhs|n#YL-GFZqnSew13Nl zndG^G@ce92?t7=xW>s12>1=<$`<6>JwMaGlX>M(0i$gN0qFZC2?(lHC-ER`=l|qNP zB(Kf7*a?)Ax-`YhGST+~V0Tz&B?Vx$bb)!M_?GxFc#VeK#oMQ-bW@8%{JV@EFTpSxT`TL;c=TwN4cW>=bbk^~8Ijf9AIS`Cj;B=`ezk)kRTzKpw>QRhsryqf z*Cx8n*`w7JV<-?7iJP$m=ZaoN5TnVQ)}8)pX5q>#RIwNquI{xQZwr+d1D{Uo)oydu ze!&^1v{p0ZSE+{`YciR&@P*Q}zME0qoHV@9)BU*Zl*__@VH}`Tp{HkzijcDV<(FSy zECtr8cTBYuCFxdquabZ76a4d78?`4%UF}j6Z2RC};q&^CRmi?osfvpbXEw9EkONZb z3n~D$MfwuOn(y7-rVn&3^qMQWSlBRm-QZ#FHxVOxm~m;z#UCnU`v9itO&Q zKD>oW{r;{hD3(yt+Yv(qmC7jI&hxyDX}QsfG3G*=UVtmju-kuxMbMTU6Z0j= zsfr5`_2vnl&N+WPJLl+db=T*mYyv<=r(8Xq_@>ZLr_k@~BNZAx6F7*4G(Kc`0}Fpi zSAPReu8ShN+7CsN4u(1gJNTH^)s{4$>-P9WeDu8gr_D%e6|9*y?QB=fF`qWas%OSG zUZ{MALg3YNE!&B26T+)V0j)MXp%{h&e3z?BRkysR)9HVH4fabr_HzhHs_Ij|?Ud!; zl|oXolUL`a^dcKEbSB=A;?sB*E)kj$&&DyF%WA;J3*(Jc;6n_UpVC(tN0I4DO{3Pc zScK80rg|6(A>*`|C5DPD#p}E8CqrAyLj@tUEhWGLo&P?~4jW3}2airpSz+6UClH2K zKbp2V5>bE5L2GKI&OM8MyW;*(cL79LWu6oB#Q8Ey`SVo zGlcFLy*g6Qg^j!9+`5^8>0X4GBmN4LzhyFnsIL3x8#?;99_voio3XRD+f6Z5y53>` zsc=s=%qphiZ%*auRkOM1@(l&$l#G;2y(GktFkyfHLjo!M$1DonCYzS55#7?uD+GJp z{o9P#dG%tkn|AJ`@-M-}heD(OT{-pSL`=yk-5zx>lGC}&!c1}}GMTvuldoF!Y1=pSJY$mXpyR#UgddqM7{jw6aD(=9GV# zm}*{7Bow@swq5CTrkPGqeYuskEpaP=51WU>aC!kiJP!8V>XwdnoAimHbhr5!;lDz) zeO~O9gH<1k{oQa)F9z5wX!4Q2PnYnmO=q#~9XZ-Ew5M~vkgA58z`4%JO+@DphX?4S z)Ha_vRp+r4g3I$^nZL&0wt@G@%|3rT>=uw#6m&@{Oeyah{eF^V8Zq#sOsZaUIbqb> zEkT#!d1KUzu~78_FswkRBw_g}g!_Of7M))1gQsirD|S;0xD5B^=ntjXLc4#x@!^bj zauZ*5bSN75^oX7~$zS4>&YmlglzloLJlzmNJ&X>10 zLy6BQ;t`a>)C*x0J?hm6IK7yo31J-x5cEJ2l^V(G{<$PiS=*_Ir3rxywl9`CS4vFL z8LpVBkD-)8DlM*(svYbm4(HZiXGi(_;Xhlj%}f zmbO!ZuX@QTpJ98)-Sw0MRSzqYsjQT)1a3kJS6zfIY3ZtP1WkIQQ~ej6T4qhZ3dK@G z#x~j*ahb%;=QO%an7^Y^cwx{KQH+P`0c~N&DAhbKbZJ7pUKD;S`*<`ANczZsI~HQ>B8b z^?66wf59BvVlktjPo-4_CDlAJI#H3XGk59ovDN|;7tnRlwm6tjwrc50%$AqYsv zUUW8$G30yu7O}6NqefUsCsIwPu*MQZl}gR+NN6NII1Am?ben(Kkj!;|KYh-xO`EO# zaynG-^r~QE#pfz%Z1;E5UX&8Esl=KY^rVNQw^3@cszs@mFh5Zopno9geOm44@V*HJ z1iU@L+wn-diAM33;swe}B0OoDf6US(7bbxV1h7pE`0Ilisqsc%Me%QwnKzZ?(5a4YP(d*JM z(xvp^5tDwfoI|}&tq@ml&-vW)*A#1(;#9ad|Dk(b7lXoSd5L$eD4hb-mGo zR1q0H;b6mg0KDxSV}5x$Jw1@xeGJvQ#g5DzM`X9s|C>aHyJ9)Ub-hr3<)6C1^v14ne+faOg|8&- zFS_vdV-}v?I{0)tCIzCVqX@)wI-Bk;%8$q|(GwB*YWy3ZUB0x1TlSnrQqqIM0AS9yfQ}_z0ZK4mN=g zk59WCz`53c#|{mi9#fA){C>z2A1H}@+FU&y49|vfQPaeqJ(d&n-(0D?go`nEar)bQ zsm1ZGs=SFcISHN*KwWee>T27;gQTp!*7@Q@U=(~v6PPGXDtiri%54J0k~NucfdLX9 z+B30AF7{4Y2$}EX$XL#d@B72{bhVIHD2F|!8_cGE?O@NfYIG)>tNSfyZ>E>93-!?9 zKt>G2oHmHh?=poUqXB4LkbdnLSjEd)^#or(gOt=zLbmNwFTsKC!UYc#e<7EAkV^oD zGJ2WsAp-Hd^l=5x@cvI}HI9d;{oOc>$Y(RdHMU_psIa66Dtn7$>(1a?vr;jK8fT#y~aKj5zrgQpA?!jUdg4WUtdwh zNcci$NxpZkB#i1VKYsn{zX0G5Rrim12#7_lhCy(f7M-ClN-r#e&?@% z^`f4LnReX=R~un@wQE@szQXho!Ci$hgC+-B3@}1tL{Sg^?ar*DyQ;gY0g%*6ej1=r zT}M{l`Be>By3j*-Bf}5(D2htGCU+9=qq`c7MA0 z;mvQLEu%?WZe^=1e}}Go(;G&b^|3o0Ztj>Q%{RyHhLh&QcK;sVe)a8}SHFKnp*2}i zKaPOc_3Gj2)!QG)Y6H)Ivm!0EcJ;xvdRhrdTc#C!-mU)q)xW(WVd5lP0MjcAlVS=d zMO(s>F(mu~zrT5MYq_DaIXoZw?P+`1!z?v##JIAtleh2_e`~%clE9+imHvi`=F}Z; z?x=0HhvSX={oM_>P5g3t?0kpYeUHPO*1H{aKx6mm9N#vrqexr^{4QiRydn^k@hWG%Ejs~Y4$zivOE;! z&HK_wwgqOKjkLNWZe4e}rGok&$Qg=qDOyX|l%z{URHS9xOm~p63L%BPo{*yDhX0jO z@*HmpB3t+^Bg=ZfSrP^T3)RZz@BD9Z0YZ*soKl!Ae^8hXKjjprtr8|(_w(VfD{i}N ziOh*S>~+2!FdX+g0O|lfXj+++TId4a!dFQ}%W)m|Ve%rr#LnyxD4EtUG1onD?WWw6wM6R+CeA@V#fr5; z`NrG8EPI?8%<*Zx^AB!eLk>o3XyDZ_6^OoOJW@dUa*F6gW#-)Y(+b zmdkOuQp?hE^U;yDxLa*tpx@oV+zsp&q41iUnW*SVZpwNRM8g?0&=kgZ`g$CCTmiJo zf2=zU$;%={z<5nWh}IMuX?|NIq$H;KyAmP6f6`OFE*eKmRmRU%g)DTL*6<7Sdeys^ zbwwalMJ?Zf!j3)gr5>1$dov+A1+}HiWY3AIS}Xa~P^q#p1*-qLq_}u%IM|7nK$I}C ziuoo0*AnTt)=GN;*J9v-yJ3_%t5p+Hf8-JbtWW7oVNB_uLHC+V=|CeLCc4wnp`@C^ zs);u9Rp${CHQ>cbaMr15A!M6GO;Va6YNFgiOEE-EK1#|(l-X7g@RW#YwUlS16at0T zm~&D}LHph%4XxH+W3^GTs&x*xKb4WG$}%F+TV@uyuDBZw7_`0loLptIs^Y zhc&qJhxftxhtRI%93FzV)j#Ipf5E=12%+ov`MKMa=IDR#{usPX=g{q=L-%la+1I>c zCdMdUPGrMEWD=(9W07a)D#B8HmMJbO1SB6G*Qa7=a@e1a+xr(kw@oo=VA14(x59u) zRM^_vip(lqanHPEPh|FcA_&X@77_!=sp(Np`6#7Z1$?$3n~Z_L*(KS;f6gGRS}AE- zCUr7{asUhd{(=BdQw@H>#G#r{V0-0YxJ9`ih>UzcKIR4~fUYe|<~1X)6xA#NwW+en zej;ok9LGK@=?aJuzP`b%OPCzX$4Dd})F{c#MdIPp6#T z!9nEdB^=^|(FF#!;{dVRBI~ zr8DYt)i75!8KVWwA#L;KG0bzcZSck0Hu&I+1j0nSCeU=eyAi~f5lHj2m??&hR^YB& zvN-RwUY!c5E*cy~a7$!9_F=!no&FdXcRakjUoIFJ)^c&__NY(V9&Sy`)ntrYKgAD^ zhkn?BLQw~ryx;!ee};qo-48qV6gH~&Db1|Dq?luWH^x5hq4Osog#WNTp=lQy{DSUb z+oLzI#JdVSG0n3+>vmDAwz;EXwemS@szdX=>L%rx6#l1vQMk19ol0ZoM3Bw2Z=LJicBM1ad>CM^G#%av6Z zBN>*{^ND^{f6{7HwiNZQTkU3p6?_OeXs}=Bs6BR!QfU6%VF;w@L<_?~K)RVbiZIJmj26?R9 zgI{PTLx-8K{WfGqXEedww#KOZ#cB=kZ1GXn>SHihe?c!QIm=5+&T_#Va-tU{U6gqW zyc$-hj4v=%J}~g4E6fd*wWMr1Gj*h9>QGJHme=KCn`_NH2Y0_vg%VbC4aMNf(_pquf%ym=fdx*U9>kPtcgZ>TBy;3RK0F^O`C zvt&SDf1;a_4)t|e^k^~5KV?MC&E?pKvUf#WBh*>4C-h^P_1j(f$7`K3r?P|HBqkZE!%O@T_wHGl!Fr6z;ssl9ZL=ud;Un6-9w zTpAf<7$0`ALucZT&Z`FPe?GM~coOe< zt*@1qX(guu!%%Zst&;MK%l*rV;&O?ul&eTu))b6nWn$*(%Ylm^-rgS05^3&u8|T6j zdoCF=e-~t`Efvx+n*=LjWRE+#aqAScl_oTu3oNY`T9B64yL`r!5xjO(m2N^}YJQ6(CN&2nl}e_qdA z3~5?!0}3)PV(!+_!uF5H?fyrX(_X*70X_&#?swgOa~tb}+q5hg@n>V%Fz?(q{xO0i z*U#YpM1Z3&J8sq=ONg~4%RY-;%#sP>MH}=sTJ);_X28w~kYkKjsLsL#hDF?r#FP<; zcv;>$FH@v_BM3$T|76WQEhAVTe>~Xd1Dm6~7VZ5Jx^e;YvC4wJ5`fo#|qnds)Lac7XScl!l16LV#sA5vidC?zFkHOK$ zAEC3Ue+^w8V%KqrE-Ry4R^~RQcu49XG2u$djxL32#aMjDEcn4E2Txede+LV7JEX)t zU*TjuaI=XKfn!!^^K|gpp~XB3!bnU$r6pg3Rke&|m2?EidjN;3%hD0*a}e-+Hd2Pw zUCvSsyTpuX1uQ!1 zcv*l)9^2u>kb$z;hF6XqXxWD@b`ZK=hk+jt5B|f?&W!BiR6p5J7<%4yKl4xSG2{6$ z^z8rq9`)(VdVkuU!Vj_NgG~xF0`JpC6M`;lNUgF!B=^B5SUOMff9=!y1fm6GY%#PF zK|x}E4>Uha@JIixf809Ii|gYAIzLVpzB(d}X&ic%PD}*(JgaC@zv1Xri*YOR6R1N9?#)k8T*{l zeIHbzPdbl?o4AwGe_2{QnHJ|ZiFI=n(&@|F#?-9C_!G09Bwm(hAk?BxB=z{qG_;PD z8jsY>l9HkvoVt*FF(PeChu+8voWNGVC>{&nJJz0OupSQk{^UKR{i$y&K?DR|&9|A6 z9N+dF7G%%lU1k`e(|PO}56E6xhP*355cxg+UyZo!Ve9A1WM(!W%%??2;dC z+%YDcfApg7I0-l$Frv-(l#AjJ!g<~noU$RgRrKL`%c((oDU%Z@?h1wgtnbwKiUJ>LJKNFf1@BqEI0smy23US5CBnG$=LGS_@8crp>e~6>#vZ)S@y& z3KJ4o)kVGVDhD3A^Lyo-5wpt}Uc z!kt>uFdR=jOf^g{heo8I zz{+Al0z`_=j~iqwx6*+X)L^+C2m~SdGZ08cf1AXlLR3L6{2HA&bl)&gK!mE6%Ih7I z>K$0OL9i+O6?{SQ+#&ivq4=`sZ=Q2yrxpyfiv_R30v{5-cp3$ip`Ie%b43hHDj71O6b$Mog3YT=Xe6#mfZ#^s#gG-u#M?~#FV z_(iGHx1oGeRBRA48H*EUOrE!*_EIZK>r(gA9AN$8LZ^S?l3;1CWDB!j<*veYK1HsV zfo)o;Au7`#M`H2ZgkoSS&63?`ct#(Qe-;WNi^1us+I>9Td95!)oNZtra`yZs5lOQJ zbhFmis+{5a2zgfLEP~t=Q;E;vW%R7WGncXc%kA{4)PZA9dPBokOwC#I`CpA=gcZQL{nB<9J&cXMLWhy)u7&3*{x>U0@e=%Nr z+U{TayZvG7a{ShY6qIk1=ZxJWSJtxe=m|XdaqF_S@aU!Q!lxlS1)sltcXT;aYeIe( zp8b;2DfbBs8{n=W*|5;a|_2qzb_RzI}BP1L2f_sg|t>b(B!e8wv<@d7B<{r&cI=Mz`Emo8uj`iH|Y;tFDoXHDu9<2)*3yB`kGq7>am(8Uzyu~~b+61uz_FYKtecHofc$!e+ExyVC18r$JC9lkG4 zA|Uya2}}_&4E!%nrp!kgZ_D7rrrRGN5)6)v!xZ!h?hgL$x^)J3>Gtu>aHT>QL89IW zA?g8~sR(=+L4biP`bnTWmF*!OAG!df{Os zoU|gAWgvRI@#v3l4u}*QuJAl{5dk+${lg<#^p6pn5pN5{lOXu&hY1cOD9EuG;c#{9 z{?u(M?lMh6BPHeWZlUGPhu4iWAKq7vK_G&IM9FZ` zx6!wOfA*;7olhE&Xu8=PfEB=XzRk3Izx`8C_xCRWEe1oo40qCM&}p{`xJ?gRtVDC0 z;_7rNmbvoBL-GSk4wpeix7$Q8-o{gMstMeUKq7doyETGa1@)>7Ji3>Fz=dz3@mww< z8;a&JI92!gHRJ<2LU3M6*(j zf6x>guSPg*ULH;pHR!nFVbH9*XbwO=cHVx{@lfjk5|aL#1q)IF$ZEEk&?e{~cY)3(Cd92M{U{qIt5%3XDcLcx;0aD5W9 zPRhY2&XbuNgFB;f@~~>{PS-#uL$yto(Qt?WhaI;jxvI}!QdXQTUU5L}Cr-CmlfxrM zmFf*%*=Sf^&f<8b9Ju5EjF;@OWjsY0Ru^$wmQ-R*R84YLOPx}Jcri#qPgzbLe-e=v zR_ldi4&KMM-L{BAFT)seow2aE@&#NbzzG>GooqoZhSC|snAFc6a}F%?A@EcR`VeD= zA~=k+3CsW7+(n<&1s7kGh7Abx{4qVIuzBzk(Vw+ zMZ4sf%P!A`B;^l@h;&SyI86!Pf5Q2j;gnEv%%xCOp+tLFgO8|Em1~0I7sFMF{#n0h zcB#Ni&TQVRtdt*gHUG`5%;K6^qExq*%6yb*?$v_I!uoOAX2LP)Wg%3x1hi__{5Hk# zOT1qo{m&>s7m<=l$JoQTD+a&;y4xj(Gj^fv5OY0rU-`>O2I@j+2D03Qe{Y)*=WTP` zzL{LZLXrasG9IbOWx+}ymtT@Ujgdr<;QWA%KkracyuL2)>)h212unqzT=ELem~$t7 zO#2JQWD}2S`Gd+Zp>R-{=_s-A$vC2JI+?Ju`M-k9jp5JHDlga%o#CZc2H8wXmlhL&(f7M*;kL1R2|9*eP z7!Y7CutT$-By|oDIgt%ph~rq7IKZ8paA!EXyyK9gA?H@|hy3lS>Z&HYIh?(d37C4y>RgZg-By|``KNPzr?TX^=^nB7tFPa@`u>%G z*4akTJQ}*^o89r%+wZc?9-jYUlcl08H_xusaifh$Wp3fR-F*A%-(F=A;o59UAVyjw zCQHkr;K2)c-Nt^U{!X(c~f8KykW%8k}PxOAg6It?9 zH)p#2h|R0^Qq%48Q{7GN1hk`@rwW?GkX=0t^kj#FKUJ`HZIgOPX9mtRzuhWplHVVA zKpIRY`@ZMNtM<`dW=S(X9qC!a6Cig*@{F$qydLZBp1Pm=JB5pr#xL%0?vIN(de06w z8PMh!sDjs-fLhq`G9m z{tEU>=;YUH`x3H3<|4IAemui=ES7t?_ys)UzSPa*f77^VQu9iW)mZOeT9?j%-n@+G ze4Hr_*Oe7wqf#q!T9$Nsxy_v5Z=s4o3tm)5GKNlUYsw`yAuD9ZwM&vMZ17mMpi z9D}`Qp(=|&sFY4~UC_Y9jmim0G7e-ks_`Yp2w+SuR6Nej4bOkdhEHS)sZ$B;jOIAC z7bdXlf5#`ZXhf}JgD#WdRPUOqZHC&fgMS;pFjC5nZWbyE127v{n?EAM>+}xngJk=V zc~-;4GxqMOBeI&O7%!wVJV}2U`8gZpee*68sy`K&(lxe0+ypH>qBnz~> z1VaM-a`0%b{ku%c^#zv3kZ76DScu&OEq`(6C>FOg(aWHKNzed<^q}g2VI-MT1Y(AcXyY5UPlE$10` zf7OXDKHP!0g6kX>B$0c;5qnP+J0Z9NWvM=p4ms+*wGRjI<|LF(G$0~N3&k7SccdaB zdE-@MUyT)CfkN)H~77KQo!5r#Bk2#(qCEWWnn_3@hSQ zRAWRC;sqVx1zhKWdyIbJj+XBJ6FdbWu!T-b&~C@jOZTB8G}osk8t?`jLCrm)^?^(pV_~NxSm(Wyb8J)5&%{R@ z0!ILbtCARw@0$+$eROi#HXzS6Q!+UoH&_ca?th$sTfm%L;=SX|u5a6V2OD(H3QeX% zdo|O^@Ko0$YT7RmrVjM*ecyC+e?`9vqJaL?56#&0{6g}3?s4fVXE<;!dh$^3s>@JQ z_llnFY6p?vip<7jc>F-X)2_vp^V5nRju)^tB5Cii4_vU)|K-U^84g@Aoe#XM8vW-F zpsqVcT1yGCc&~{|9v#~m%!Gs^1|*Nn)386Yb3=E1KAT3`?E=9{)4 zU1yZO7zH5W)na3HnP!&lR-ir()-dFFehST_x5DSppzZ=_106j;^2;!^jA;;>rFIU+ zgl5NKuV7q;pdl`*@%0ake|q|Z?}Tn*Ap_nrE0YSzK=;o`Q4Io3kI!yDFrdfs#li@ROT z1A_Ig&z=&Vf2fXp<%G0CoADzJS25L0N85CD<-6lJK*W)|lHYJ`e;0lhkXI_KW$^1U zIw0IhJ<+QI&Zv14h87&x>&1o~`y;s?-0#rNdXRjs7d?1gW^!tV80sAQ&^$7<1;chZ z@^!<%r?%SF@zz&UlYO&u_C0XgGLIdM2$pX!HQ{mN?~(to#bEq~FTwPfoaqINxjSRF zfbtypBq#R6n~HuOf6ut={&KqE)5|hTz8Z$h@x;az`Uv_NK&zd$z^G^7G!G2o%rlI^ zOgXaF!66{;FHbf3rht%n6L*)k;&;zz5Bf8?IN!covzVrkF4sdLA&#VZNF59b4?%x5 z=JjB|#mm2S?_K_-e?--(aV*ZqoNwWOe`h2+8hiA48|^qoSKs3R z7jUK{8y}z?OklGc>89a_RoL8%YtJKB4;)T;=jqnpu=RAtmJL$Sz)AB8FY_$TJ&1j3 zK)B9qTS<$RKvz9H<86H&=s`1r?3pYh98u8ZYcYopA&L?Lv3Ra`G8e1C+!o~L*K1+4 z5UI+{B92}ee?g~O6q}&cJ1H&Ln!?;1x6tL_dLZ5Fgf~a+vfo>mmoogGBDorzKgZF z-)_N?vLvda2xcnMB2y@cSt(*cj0R8;!Ihh!6$!Y#IjWLwR3pTT;w=jaqnhHSbz-X$ z#XHN@e^R_->9WuV(}B4aQ{p6}l*1n}KP^KOu#7Gf9u^(JLk!N?Q38@3< z3XdLYhwI>qu&669_qt2A#L=OVEerF2*?K-2q%^Ckdf9oElie}kvE zuBMzu>7vFL0Gl=z45*6P^!^Ur<~MY*^!^iIP3J1r=y$>qaOnW^C(~Q3aaN*NeoRjpYQfL=ARsqYqQ==fP5C))- z4=a=~YlZ<25%bNqOhtJOf_qKPm{DMTJdDYv$4 zT5b+@THBc6{lmc8L0MXt7+@~<#8TM0*;fwzv7UA(pLeHN&w;`7Ngu4HQ=j|_ALNmj z2cI|zhc3ZM+7&$}JJn4GmVg`R`f%aYz>_~4yt%|O%+NY}SD)(ue3TP(f0~UWAcRYh zI6~vZAc;ZOxBanRnmD?zIjcM!l5)FU&+y<3K*7H~S0kqh!c3Mf@Hlre@jfL4(GLXY zz#cM^Vm6YrAKkv^BO@lZt=FYe7WoUulS<6F(!TS@QB&%&8RK*+_?foxDK!wL6&Zf( z-hFdpI;rCm97=-CDcYr{f56i(bZ#9Ls7wVmajgyrzsfh-mZ^m(XHl<{m-$?;1Gpx0 z29N4>s7@y;8iMTrV+rOI0xyncrroR%Flq>mlVe>Gg+eRLg2ab3fE{v~>{-b`uC|mE zGhSY?T<5yvh?ikhYVxaPXs~84F9S9{T80FSp|wm4%P8frRwM;>f2#yFet+3Ny8N!=4$*{!04?$MG@dg{r4cUPMazpbe#yQ9LRdLZ1v?KCu$&2TWC2(Va3q@C zC6EqC0Me}p7dl3suzuvX%sju9wu#EtRhwpbQd<0}v`Re^%*Ed@$#P7Fy)&9GgAkXL#a|f8Q;+OFa16Q=ba+@Ja7# z?9qwzkb&vcr7!j{*>v{Owrr?f^}<{zFf@PjCOkzey8Lk><)$)a zs*K&B;258pKrJu$sQgAv!nTlB-jYVJ3~7!shN{Y%)2{8CO;v7J_ATBJE1epJBC_W* zy2H32(VJROf0rW5X(=jHpv<4vnXK#mvIy-{6abcQ9KoV{7Om-;aAh?C%l>0c-b`v$ zq-W)g)ftx>nwl*v@Nfmbn$ay#SYDur7kGncgAg|)`s;P;phRpPzFMUkOq(e}s?SqA zi3f;yoWcNRfC#G|mT{aC81#dA$n!Mg3JLI~!^Nozf0#W!?Z&G)RfTuK@OWG8wsGpk z4{R;vK&igq(v4i+yAjd=*&>qp?1sUym{Te?zl5Q;64I?bxPJ?cx~^7<8E@U$8a&75#i6viRPtD_#cXHCDU-T3>1|RQai; z9-HnmA{xueDVKA#G<+=zoqU0(4eRI(rJrbGc=r7|O>%N53!4Pdj-h%vDb008!{z`OIe@We@uC=*}*;nHRPa2Kv=PhL}D+PFH_>gHn$zxke=C+=|O ze;A4GaGcP5mmsweJIGSOE-~b(kEg~fM%8YA!#>_;{NtJp|Hv04h1EWFTLl7lx;(Va z@Z>wpbsSTNrrXo`K^$}XUlIm^2F*&R?V@nh!-l5TSJLLFkbt@V8S6a32Yu;tv*6x< z=+n@5Q%Q+?&`uHWOwv#vo9zRH<312Se^7pQ|4?xO!;Q0~Aa5#B@-6$?-!+^*2n>u! zPCZvjGQ0biPwq7Yr~s*>fv=D67I}!Gm>Dis3SuMiQRH@w}N8%GSlbAYtBsatUJn!4M^f>S*}meHAGf5`R; z0ec)K=H!jfo1)ktsEXhc45q*=MuRz2Y33jym|O|#&LpGu_cY<8H>ZY$l53zYR2L`E zfi*`=C`9K!5M6p& z3mLISjC^e!Qe>gRH^`3iQIx1||mgRLs%ms9O2#f3JQQKeF?kSh(rN>S*cqC-`Z;@%uBOOY>GmBf0URCcByF^*I zFASJp{n6Vpg|=+6V=8&O2y1iq%}dl>Eawmq7-?0+t>S}22!H@xE$EO}e*>fR-vQ?U)s?jQ9sn(!~`3ks*56zKMV`2#LoRg_0Y5~ zyrehP7}7RZpiO-q*z?}ge=U_WpW6Oh%@t|QRLj<%NCI8C8i7R3AN_zJ&$Dp{s+h+w zy1M&_jWa+|$q_{2 z8CKnm?T=jI>eq|=e{jabdae|J9>zP>Pw)j|A9nx%#8W={ zCS$gk^u)iJET#Zmt}Q*ZvRbFwq2YkU{>~lDIeYdZkhIEux*B!<=2J7~5NIhKccP;_ z6M7?ULT`k1$@oo&r^-E?PL%ZR`)o@;Usd;;CsFX-$#(_~(FdVBJ{Bh}5QTWf}QG5FSaBX8kX;Bh< zq}>F2e^W#dgs;lT@)%Hnow*$jfKU7DdOxjh!r@R1B*H-hP`XT|2XBoxVa!eZ=*M<^ z9(}lUyV***e-w+hbEW)8P9VJ;`$*7mgh1h}w@Vf!Sh=5Qgvu0v%M0HHuLcrPUGpbd z=ZXd(a*Y;o?`r{w8(a}*-M3a;tASCp@b17l=+CJm20Ujo8&CGlCn!`o8iss*bEh3U zU52Nv&*G!UrcX`I`x`sF3ewyM3oACa1dOTp`x1_=e{>mZ-fNDxrZE2Go6*!ks;;fZ zv0j!Pd`7?MKMmkFo&0fG&fjT5TT0Q{q~7{sg!#jyF&_5YdN-pKld46P)--E$%mObc zki6zm0b>-rJ3eJO&jB4xYNm|J#aEemx9F3dRCOk}>I?(Nx^1VrwK&2<_RW6PQ#;8e z&h*&fSXU{p7oq8*5f2|#iE^#U#PM)az_8AQFTeIx;QUxfs?&+8M}NR{)WqiNq|zb_ z#oygXS;mzTnawflnW+`SJtq;RR4Jg3%@+KF5^MvCWvRsf0$1<5P?s?*0TY)=8vzsq zGdVUelhL&(f3;gnliaouzUx=aA*pbyc4#~a(u%60IFZYCm6O<;%0Z8WyTjE|njyy| zM{DJf-=1zXKmuHHRxVqY9X>#y(dhpA>jsu>-fr0D>nDG{dh*2!&h;iwi!4){tJ{qc z8&l*d%i(Ufd71pZtBzgW3>UAjzJ<1e2W@4c)2xute-$;o5G*OXYG^OELbK$nuDXyc zxv!hI`1O;&UOoBg35V8f!)Y7^pPS9i;mOO_Y_o&s-)>mSi(+$cTOBq^^Hi7&uJ@ZC zpZwzqbAtQdsXxwHs${;=S)Pgl`~2dYgV_9~4Zfo33KL9$rA3jsE((@1%{N-JG}C5t zb$~g&f2iM{x(m*d3g^TV`Q?%#UT$S3k{|CnXvC7bd0)Q05SqSWNqbA*wvFqgYxnNe zPuTUT+}FcLdUe~i2YNN!xz;!3Q108e^z`V4XghlOifNWJ_{-@YbT4&se<3nhiJg1q zdpcBQf9k3obp?0L)JdkXlS?-{C4$*`6)RPwe_E?pL0%um#QM;A@@!dE!sLl$_%71E zGBPcgjC(&XkI7S2%ogl+MINrt$;0&nc{Ee0&St%*wwV_oFH&PbVp)J4=hWir&MobA z**HblJ^kL_l|!_)3|hB`W8XG(x9dvlFrnG7=5eF%6rehhS*YM?ra(blr8&z81D@~Swp~5kIV0wi%BSyM+E;&T zoDPol4Fbn|x;r(ys`IO^AAGg1O0axb%qE)fgNY&I(Qd7l7AF)jGbuB1?`VgTJ50fgW835WhbjOje2(bS zU&6I8$@OVRcsS6z`wOX(7T}4*RAymrQgFLm^SSXQg@At-2Hr~j$dsg#sbn*ae>o|K zvVTwV_4R+xI9F>b?Ih`MCnu{omnwKxCi@uC6)o047>FKKc+cH z8#&OOYydemPw5s~gR6W%e>yw1sQjm%Fr7T(emf&pzOzSc7&u;GO)Icr`6*CQd2$zE z#_BjM9zgxJShS2JW)(xF;$R+IQRK;YOGtnir5Y_~%|BZp zeM|tA2ee8+*bm@=112g3MxiI5Wo*e}WeSZq+n^<17E1^bbTS)Hrv@?t2vo)oC%In2 zIW$uk4vY@6!zkoM)63uk14b!4nwCqQsWi(Tr_EUkENv6C`lT_>1_Br+lYByErNZ?K z5uRr}SogRuSY76&e|+$rQUQ$5@VG z+ZLETx|r_5eeK#o>R5GW^M6?YOce5$f8qf8$i8kZGQ0@mF5vcRrWjIpL_530K!B&d2GgTfV+^lXjZ;SXcX<>x?KN zUR?LCfbao3FKxlgCZAK1ip#2juW9ZpeX>Ywy`*q#NZHJ+aHTUK(e)$s6F{C^$Gtv# ztSBPkF<0?Nn0>h^OD2*-yQ}uDCJRy=%ew2`D=G>Wf1arp%LNpCPIEx`0XFsl3j)}J z7gojaeq4Wh=PV7b`n$5Tl|lHnoCKh&`+B%LTR>3G%){Bed8~v1jWYo5=%Yi6A`^Y@ z!JZY2esyQ92p47)aAA>^uw_`=%}sl_uA6dbop#DS{ASu#SFJ^58BNMQ?W;q*onC3Q z5#=4rf7WsA)0a_QLl~eZOvU4>xnPhzrVNcGY;_rTltbnTxpve7t=WSpKfEM>4z ze^Im7Hg*-^u7yr=`f>YD{Y;`={fco}byaiYo;dKw?HCp45(px?AQhr*>aWEZ9eX>l zN?u?DZQaYS%TwRiw9h3Lpth$MP85;+xuhUwQEU8;mVx33^6o{qSFoP^mVJO@(gJoxx?X}&!Dg&T>xmUfWtTK2MbQkf zvm8rdUUFMA0sWZON;8nbOeVO8xy&Rb4?dmGzQ{rnI`_5FK9tG<%w|SXH?#HK)rYF- z9l2Or8_fZ&Ocr+~mju`b9;5l@6~0=YUZT8 zj&M*IkSPk%cP6aX`vh@Ej%Pve-QMho$N8GfOz(Y3#F2tYC-YV6F!-?o@@jyT%0oA zt>7X)#$W`AX86>?E{CG!03t>#7oNLA2*zUSIlROIJS23{9Dcu4{mxRJn`pD(W|?US zp`%TL-*)lBO-C@K3@|p5-RaV1N1>v;AB;*2)O_@ZYv8)Cd@H9vWL}g#f4jw7FaRLv zhB@omwflY9IdVxwcqBz-|K1U$nI*u>W7l4n;5M|(nWH1fP3is3o>mi{Kmt^cNefud zN0$y|I-u*+ID;t+QTFFJe(PQbyKX2%TQ{EF<6)VQxL_s%WH6d1>V<0$r4e@o{*tjg zZjPx6*A5vm(Xjcha=X9Le+dzH{y_jiG|DaOJ+468?ZBmgiJeV88BaN6P0{CTTiD^S zoFgv9-w_mWyAbdLEW{o8@n?_veS17hce%kcF={T(ay@sgejAp9=J721q{#`UQ#lu- zZu~(e0yxNM_obhqW6i-ar|>`tA60XIS9R5_<)kNIpsJ&Y;SW-sf5_^pe)f^|(5G~5 zkGoKSac5q5!pCvvgc)5>;&B_tmB&y7%+ZIfKz1M1l;1j2v~}h(h$&Dm1<|gAr=5Ra zHgSho7_k?Kzn64*e^*bt+f$_VrB z0|nrtL9I@3Ae7Q2e^~x}TVn-OK3v0vixhC>XQ>L0CGfEJA3OjP8@AvD8Ol_4%Y&13 zVXt{|4cvcM`F<~5jj#0rm9AMyKPM_(+5RB7B~Hq|ay@v%qd8y#1RQjTa0|7-vNX?w z;)&;nuI5=T9hMr8n2ohJxW02o0O^pQv*J~1I7xsAAhGC-e-ix57F~HEtsU@!mK*PT zs)rBzYPZS{1t%M@?X7L&RYb`+aR7W?HvK(XwXKihP@pj~UVKX|652OReFaclUC=Cx z1b26LcMl%io#5{70Tu`l++lHd4esu)!CeCchv4LG^8N4quL`KCIWy;;%hpzHZ_n-K zX+n@%3o;_BObT1V$_-^dC2GU;W-;YC$t)Je%(hhz`Emgf=^9fL^UXKVP!y8*UMcMB zjdK6Tsy9I0wx0UK9F0CX84xW#0A?t}1-C0!H8LN0y@vIKe@o207PYu=Qm8(?MnLOu z$8|XmRE!xRUp69inzA1@sdG+SD8$_{9fxG9(N*bU6jOHoJvc^3%*vvYlKM6-E0hxw z&cGyYkp9i)EIr$3CXoJMgTy030C-Br8}S6-hs1IF*-T!Tjq--E0Q87*|KeKuN+ccy zAt9f8^EgUf#;Nj~WCU*pUR%Q$6a?+dBBrqjKprzK$#*j$Vl=p9Y3>a&+|fIewsWb6 zh^?`*MJN&eG+Sf8_00prlx|6%otCnTxRNIOD;1M4n&&Zu5&yBvlhdIysmYwxYF~{w zL^0;Afj|IXGzd&Gz%!da6ur-L~Y z^^@%Ls0r(o#*NU=2o{6M;X1wN^VfV2Q0VwUjnLVzg81?rMc~t_`;}ltGQ`y^?0j_``xcC!pPX!ny|ot59uTKKa;=m!bO9a#J$YipMlo|S=v->&K<5ZawvtQE z6f7KmZiEg61;E;zu#Z~`cYG7UU5gRKOA! z0+xArT)sznmJOfV_FvidkNu(`T5-b2yY-n#Mn}`%$!=?4 z(;H=af5uVB9hZF&ee+_Z>mEJ40LV*zqr=1XCtz&16o_4nCN3V*cN=Oiu&*-taPkM$ zgU)>LpI$*y%d0OGCp$+H*Bc)SC;R_1yXR#8XLfJdu=9qA1qz`D;IzPF0Ac{3k1PP} zmP$+j0RTkL2KKRcc5wxOg4n@HuowMD04RV1oRoE^1AvIRz{yKI02~0+#04Hd`ya1* z6aYw(2fPCIr)L9z%6Y)$dA`j6PysIh`>#WNFi4IcT>aOg9^@_nE(Ckj3jjdgg5cy| zlX?)j5IFhIr5>~k4*%NJgIlAKo|hP^-r7qKj(Vz8Yk#75s(E|YzMGIV*Te{ z|96XB8p!`~tOwWdv@o&(v;mM@yh(LIm>>{4;24sN543v(-gI^c5b^J#e~$p--~xOF zaQ{0%19D0T0KBg%FC;c7_Yfcjj?wwRG1#^K5cF3?0&H8K2yW#DsTP4ZGWu&>5AOYY zWw3kw1*k|AfDN{5!1M2_EsSLV zH$W0jE(%Cm6F`j2`@cr^>}>xeD~9yeT?u(H{ExMF9QN0^FP4)THJh;o%jG-`5_rdk z4`DM1WTUCo$m!QtYq#Cl^2|^B6P6p*sBPIeg+&Wpp7RI;uRmFvbYc&~=tq5re_%nh zaz@tUd#%E*@ir;Enno-QRVHDLf%~9MDg_r{u86UUXA*-sWTk(tbQ}U{3v6D>S5fgW z_qSKyH+tJDR5Iku5LKtq!68DZi$>~+4KtF)f<%~xOOg*kI^gm(cIZ6|;c?iri-L3% z>e#;$8}+_i0l*bF@eH$x=gw(&_ppH zIiry3Dt}((t5+G78qb3P0q5k;h<_bpaAO$Kb|dsJs4Qt`7Kr7BJf!Pc(TUoLeaW?E zjt-nDj<&y|ux7x4M9^UB{moT?3?CZ5s*PS%5(Wd)Xb(1c#qWy2W0OXdiW1+$qz&e^ z3BiFR7=mg<<6;``Ltd=h!HzLC!%?*hFp%VG#Er+W8yXA$iChOB%R&mWbf*~J`@~ii zo~||Ua}eX2KRPu!Y+N@=vU1g4(iK0WpazRCwN)06vN4u6y^G#E(as42VKv4$O2fLO zA94zr+QcD^AjOIxRdkeu%;mc~%Hw>WhSV~x7}kgjpaP+)e-5iy)IvJh_M0JJzXq)P z58VL_1!HwndR5syL7B&Z^fVrL~0w^o1@JC9qRiL$d7BQ?AJ61T6j_jgWbz?0)Bd24+0PC2Ra&*p+_PG z4j3$klUCNz8sfJG;)uZCp8*4)B-zqrAyG^{16*&OwfG^ZVA9#S)(@khB|}kV36fyX z+|l14C_WtOhh^a>L&?fLj(^X(Pqcrn%TiH3roA0{U-h1cUyka*tJT^xrLhOToPYaq z`}V{82fR9IG;p!u$zGHBPc(GKy44I)Aks#SQ}2q4|5vS`#rH6KNm7SehNFdN1G0<# zq3mK2eU0E|7HDST@>&dG)t>jWsY__&rssDol*7%T4&10G^B$$ zqr34)Jt$X9n@=q(so{FG6scH_kE3K;s?;U@@65c|h=C&MKs4-*AY%n|VqOkh;j~?`vMa?j5s+J~IpdWE~_Uwm)@9B*D~^fsoex)!vZBd}MWd z*eDRanZ50cNp-@M#?9=G)}bkD+hJNzUHx@xIoNNg2da^}+;F{3;O zj;NtE<1n-SeF|Ju$K1m!$fJGh)CDiLzMQko+~EU6PucdFAFz9)k#vRtZ6BK@#1Sxv z)s}#XlIY(A^Og?3n2s=t&7UVMeSmT#R~k{6E1oIn-KkMEX3G^1;t8`O((#?XZ8uQt zSo3rMq&n%J5NbcK)WmT5J1%P(&2}89C`6@sCa7`{8k?qykw8d9!pn?9{W|m-%TId% zBtc+!mCtO|pYdy+krQm1kXXsUh5LS`EQbPmwOUd=_kF=nB2=Nen5BRl!N8EsHOoHY z+mw2&WK}&fzMP@J;S67Hwv3K|7cTn|spM4+6 z>vmM3q*BTATh!BJa-)1Q8zgg@g<@Z_Q-xGsT59vgcJyVsaH#0CXqP@^!+ekzQ#L-mb1iIsS5bTJV8WJh9YfrHgMi659u zZ+-+at#rVc6S^Xeva$GxPD4nLiwm&>A4uY3zSEjUrKwS$lEaAN&z}gPpD&oCjFA#?k>vUrQrA-vD``Y?y$d?DIKAJC)MaB8YzTsB)_$OaF%IEPy z4o{7`aw=~559&-dg|mZZqVde4?o6ZZ)S`&g|NoyCmZnSB7ZzX(_-{>mfOZ1qUcPBt z3;CHLu3D@{>rkIP#fDWk{4E99I2*U=s+zp4pN|r+K^$7V;K+jNc0T`i(S{X@I)LP%@018>;Bg#^m&Gr z8#Q!d^z5Q9`oLmc#zd&#WBKSs&BFLgmpOzBbSMz2M@&+dSX%#^I&@u2D6J7y*G}-4 z3c{@}3TOuek8eJF#_QygkhnUn4O4+7ju0xO7kgIedsDtdN^#S;pxwLUwrR^|Gx6lC zFht?v#gjB?6efr-pJ%m_vVUB^-r0}&jkFQlVwx#(vj7)mYm~Xe7Im{&L2iQw9d{Ox z16Db)@fqt7D5~*1Z0Mkyep>W*!%gqD&!QdV-_qj@mIG9#&3xxwbfzhCrsz?})5E=a zL%$$QI>rpu41R85-SQ`LG(aAzfp*t!mi-ggJ9*;F&E~e|Z)u#mrl#M3fXaWj+^hjw zzWhw4wFF$Q$9EPsaNqyoN8bJH(kURAUaM5%G$;tG1v(<#^KKIO98kS@G9<`vL?m3M z#mATHo|&6XOccqxKeq1Z`MqXO%nkx?d!G&1^w9K!C%7LYPA}wD<)z2wtpzso0~NxK z$G(O3bzb+E_W|WN;LD5Q!^`9G?VBUJFtl1p)FJTnQ^S`lpn6+l;wuZo15R-rkbFwx7vu5kOG`9slZI38bkzpn4>b_&Y!&)4g|$Ws-H?G z?hgmD5FS>;e)Qx?CsnYL-m9-YlfJ4v*){ogO(SZUuWh^9p`>O-tO6zRSoOZnd}RCa zY!P_+re7V4Yfy>;VDRAcUoJnq`>N-8*`9t{B0a*$ohj}N$(j{mjYV6AshUVo$!lAH zg7r~T`cy|T!s}iuyG)&1-OZ!qT?Mg8gVCdAtNfv{{JDYR(;J7d4l(PQOV(OG=oUCr z@)Kgwp78xM?9CJF3~&oPH&1piy?S*KmJS;tNiv0C0ptH)Gp;>gdrpL$HK+FoeX;xtb!AKEmJ&>Y(Njtp?S zvB6>S%U%0D&lP#(RCvh8i?vktE`V3{8)ePp?;$g+&oAm&pVyuEH6Nsww$?sM)iogs z``aa5gmf(Z(&yV=%UV9J9^=INoMhMI(art(&AFQ!iQ3=Jdu+2!tLWhdO{@ck&A@t! zS3u_yvf4YZ4tor$)qx<8zuZ^V0)dt)7GZv$J(lA7zHd?(W7mL|)J^0guvBC5A`u~EIB_iV zI}<7H;D!q|ar$}kHXeOO==Z(!;5V?89wGe&tH-lBOIG)F)(-q0;v*GUrEn8SLRAp) zVx*uV49cZKU45o=Q}`UmUb1;e$fG;fi-trdPwxTJw~~NOIZ5^FIwDNkcMNkk`&J%z z*j{(8IaWtPm}I}mX>Ue`Fr1Z5=W?P z``sJ<-C!nm`-e=#@9(Hh27~SYF}}wpe;Hp6p8tIAvk461_M5N$fuVs#;Yd~3*Xr}c z&+#R)@p*J>6W!1tq0wf~>KtbNME@@jmx2TMYROAm5-+Bz6#QA2tOCk8i9$~&7fz^N z+wXKCvg1|tTC}KfvgAnx3=N$jEe1Cq-4aS73 zza4D=YnSE^P97v0)O?;dJe6BtF1~&15V?e|n#L46wq7x$THoJ%5xFsk-3H3uC}?Qj z{L&)EbdaP$MBJCSIos^HyushAmkF6pZk{)Dtf&gEMagz;!tu!|74Z|`miS`dekZ#r8WAuBO#xoJ89Wjj4$eBZ1;y$%^#<5(SQBoxC_y* zHi0MqNi~&&Q*Y|}bwW}b&?)68Ov8g|kx(U7!kGH<8}VR5&T=N*rC#d&07A;nT znG5-wOIE`^yo=Lt6m{&O-%CE;Dc`x80)4-HJUNJ~ve_GTV#R_lkvil?{!PqM)f#w+ zoia<%Z6kO*E&>QkIJvAxTP}JJsJ`XuDY3pZiECVb2ruZYvtees;B5I_+^@f&nr)?* zA!(uYJ#@k@`BZ@}yQu$n&67epAJuS)?Iu9%@nz3mgEh zk*f5T^?zz`kCyd1*AdAlgsirng_=87bPM=YY@HFy)3{k9X7SRfM&UsNSvl%#EP7E6 zPlB`|DXn~k&+~}n-STj^@$fC%Xxu7bOxxOoLp{}PeCsNpPr4HKWwAkIk9^6`4dH+5d5l^rq)pvB4x&- z4@5_RYph&xsb>`6a^PoYj^fMI3K~4AYU;sBGOs4G(oyGm+-tv~YH2Bf#e4e3S2v@r zK7y3;&J45bFOD}D0fQI@Hi$Y7N0mK9q|QltB>9ubbzf;a>3?&)=^e;dB?GZ^ZE(5r zmK$mOTM~I3Iz6%?2(jq8U;q!Dx-0JM(L$^HXVS0awB?i}sQ15n&5+W1*N#(~=%Yrj zGrX3tbUiss3KxUui!HIQ$$Mv2W*SKk=|DaI*{ql#cZCuJOCUe%lc7`%gt&U1)Ri^ad6 z^}@;zP3TKj&YT%siem&2&}2zJAMBN03&iH2Ubk7L-Sx`+8S@eR^h;=0h)?z7ODK)B zs5X-7*BqP-WS8w6rDVQQcXiTJM|md-=;SD;8bw-r>X z65Hv|-H$hFue!*N8qkq~0y5D~&}~|2O1_W&B5tVeF)Gq%p}ETSk{aH}Hyyy48kYVI zHI|7>BIni}0_5#z!@qV+`(fGV`=m71v?x&_Y8>Mtuhq-R6DKEd{cK9uqHpFU4A9b9 zY2j?3tSsZ9^QIC{!i!T*p3u#LY)59od>s7*yh-GkmKWDE=753uh15#Bi@&kW4KIdZ z;)-+i!LEC*f$U2R8lbqM*PG<>Pa-UO!8qA*_PH(t_92>%NS#?2>qw5i(5Ur-{n^CN z1r#kP8R`cjc2Z&B?@o^!Fn*#M#(RrKY zK(E`NEZP0L&07Ppoidw;y($8jh1 zwuKxuVV1TPX^ds?w(;47tMIq>RQj3E&VJ?sXJDz9IhafXoUmxKcgNC1Cm^m5RsqQS z7k(um%9BxVk;2Qy)}k)yMM{&)tgUIX*s6_5WWfIU>lR~B6iNp0nm1av_mw_j0Zxlh zB(W`5(*%cdBnn4~Y^Hy855|X}@zLYPQisw2}D=x@TRLCn!2!wn!+PC=8{%V zf!@v^2I_a*37J5d!rasM;Jj0HpxWr=wmvVt1xzZFrD1*EXCGIG znP$X3&fi2D@f!;hi@0AwNwsu*HsUHQnuzRF9}QCvna#lR;KjFHPA)f3sxNdOIa_~G zC?BKPKV|Vc^hs)_k3zoUgWpQyEh40vHxKU~-DaYkNCo4Zqtp5A%w&6suz%1iu__|; z(es@mfq622=7e(U*bxRySm4jg7h((XnSGZ(p1*~;7@_(>W3${EJe&pfKt+%lT6cS)`NKNnZmoOhd1nVpr2fB|D zdqml{JC(IMlzzernf3p&vowpYbV$Rf06p%<41`5J#nRPwQ27cUbusHkkt%j5^BY?34}bu@I_w;?)KbD$K|P!rzQ z?Ata1vd5@J2?%{k_2BdBDILGt2I`fUF=^l{l|(w zHiGp9)%_!xKZNw!l*^+S@J=xkW~_m7fMm);F#$&YuDGeJoFCfh;XJf=jor{+qP;%6 zT>)UAqz5iF>ejHP?Vq&zaQn-cvuZfu>gzs`X!=4awd(9Lmr87UOjt-E?3P|YPACl4 zi(9stYDyr9uNU>Q%$1xA)h*mB>_{hw6K}Tu4Bp7Qa^u9GSR2s8p6Q0sgG( z)TZx!G{1w9IU4BCUe?EI#6XBY@$Q7;XS2M&fD1d3UtBWkmugXKwOr28Matpl*o%Rw z%=HZvxBxJehFsjhP96cUJ#5$b6~4QTF!HnPXGY6=r4gR(pY&=<{k&F~;%dpuAJgqY z*xDQ#EZ81dr*imGU9g1F9f2RC3S977-Hp%h%jYO7uv+im?Z9C!F}Zt2363w(p{)DS zi(bufD}d4^PSKU#?(9f7K;V_A1u)|L<|;p=@NfJ^Q_PiZL|=M;<2j(9vBMBuqh zQE|sd-g^YAgRaYaA;qX?2FH8iyDTK2-DwfRd;Y{6V<=4?Cx4hp_XsTp4b1iJVr0Og z>Q43F0zXpnAh*D*Z{}09_3de?Ty`4jh8da*|mh&1ev4#4%+Rl2vn9-CMc2TiJAqQ{! z9v-ato7%NV`SU)F#D@(@L7p8)GfBmUHmu8#VeQeCTBhr;D*98JnE<<4oy`TFZm-2n z{oSUXU3cWC$FYLrp;wO_| z>)zZQ{gkif{eP}RvCHMlqnhQ9GnUOD23b^7P`}^jDJ-ev$qdB^x68*=Uj2dGVWsH* z59{PO0Q~-PF#t#$5W6FQ3Wz0}$o#-W)u|sK9<=NS!2FL#^Y#Z*TVS3{7y$Yc0KjRGbEgG>R0F|< zu~?8rpx7XA^uIKkbTF6!%JZdeX!c3~fYQH$d%>g{D*$933c&u4R4WSucYsN?Adp)) zI0Ex$g&@SgWY~ZFSx?0O@MkVjf61g^M^HdCxa=R11}6J(K!N0tb)c&laL#E75CCe8 z1z`Q<(To8g`MCcj(H216@!&nT5&&ob&_n_NGwEYQB;S}+1zr}+1|$POblHD7y}zs&XetN1j+_U$1U=;fus~?}01)Uc58U0J52yx}=7V!z z3ILrTs{(L~Tp<7!0OBnKp9oC=5)lAWE&}fvS`6?9fLx2g1=yuv_6!7B3J$?+Sp)!z zosH|CVn&LtV$wk~a@S1l&kCY~nzgw|iu=PZYQv6S@>#z$SnNm_nJ*eUZObXky*#r- zNCP1x<<>cC^ey?Y;33Jbu6kuX<2uYA_BXdlF1FtDG@2Rr8D^#~b{A@7yreXTW#NI` z9dUE4glpCm?U#uSlB68N=(Y?02 zfMAj4?BE|`N^|@lnsGb!pDy;D#%h9|a=m1$Dr_GPw6eWu)Lrxr*e*OtFW!AX-}i3T zC)5R^n&vwwYE9M4t7jpn(eniq&62R9RBnQ0p(c`5U24RJszSDZMfPmm^o_i5OwG$s z8v{A{nB{EpC@H@}eqi{j%#fcDt3gx9l~C*ZWNUmE(}VNEL40;Y_@bcs9^ubeJ=cxZ zsgcGtji~sW_^JE14^CG;qmXCwg^jnh6&_w8C0{j!W)lyad#ZIcuf!p)wTRs-+bv?f zGXM`Ia=VhMrv6l_YdT7A!6k5pf~*>+ZWx$b$tc1Ef)RLVFobbHeD~C zB;4}wt=(x>emt;!0$}MDc(dlqTVj~bbw6OPn-^)JWlL;o@B$awjW@Lt(f9L$+h4i-%$&YW-1*jTDJssI(0;~mK`AVkxIAaM6fcR|K^Bj=; z)4KAP6e%J8+^kkEg5l@Ucf~@>pY6U`KjWf^ZP=Cpg`v4!@wDP`Q9LmQ(gwUip+Sl#!J3` zXY3a^&czuG)!%9wmYR3Y1k<1~1G1_RyEb>7stu#%ZWplYN*zvE?s|709hU`=%denZ z`3rKsL#BlPp!2&z14VK{aM8MG)*vk0%{BTZxo+am7n2+Zi70lYjlA;GB!}*G!J%(k z7-UaT*hbdGxI1jeh~4R&TN*SrW^gLlkhV&J8Tsfw2h$;!L}TgSfCE>kP-<`=Zx|W^ z>msO*ID;&BZAznrr_XV{B)^FQuR2*HNAW*@A`PqYuK ztTzwZW)eV+IX70C({4{};;z~R6G0H+Qih2WS$mj;0#2aycAJ}l{#njq zfF6;&A*kiDJx@Iq1ue8B8x~zX_>ChMi%BIYTMKO3&98WSP+?a^=eY7fT0_&hRU_8D zo`&<|2wdex)7IE%qn}l!hOZS!u%vz&FdKcv`_4x(?y^@umX3QakoB9Zay}*u zxzc2E=v$ncTm12;$2@)bYBtfFYjJdKLDGFBp zpivz^Jy9o!NnFG?Anid%FQc0#+O`xyHWh^93=5v+M6+mmkOV#SI=-tC!WH5JRX{LF zrFdfTK8HH_Fg+5t)yT$tp0E+o*;%+y?>i;beK<2o!@ih>dD2<}yC6z_qEIaU*pKmit?wBm9a8TYk#!@f`X zK}ITF&ySr4w!0Dx%$oXI{B0*IKhD#GZe)IFON3ws6-IrZ{Imnx=i?jYWQRHs)8!6@ z(Pt$KM;ycLM`A%v9MYIM_?sNCB(F`_M&=bQA zmm#Zkj;$xhaV%nyN2Rpz!<@CZ3!&Olcm8VK_pEyd+6pt5i?& zkT=1*qQl}xVtQrQyGAQVjRgEQ0fTdE^Cl6MX<(E~!F;d&&n@Jg&++Uf;qiIVGhZNz z9<}!TJ8Q=|d{N15h%(9%#8XlvFyzLAvV2;_p~VzkU4TfK+%xJ8Fm@o6@Q5qE|^hq*XxM_Da^nzPVcOx`91yvk;G{8!(5x za6Oot1}98@)vs@BqMm6or_4XQ3==~o;U@};#dF3SU^{(Ur(s*38Her5g*`qKCovc0 zS(Xp1>;ReP&!HDnPxzWG{pR`fNtFqocl{i*W`tSYdiY$`-7e-LWRuVYmFt}lnFaIeNszvPUd2S3T0v2Vdt6F$uys=vNa z+MkOBjiIdtM(h zEhCWb!Ka@cmdFJQMG$SPBT1uD?B;qNs`( z6oySx4`_|Sc zvHl|`z~}fo-?Dz)VUwrMVLjdUiyCs%v;>``pZuxv1Uj_S2H{>*qdZhbK|0y_`a00U ze<5bKu#;i3V)dJ~n+CyXM4t>SA27eYQu45mMc#?5u-MK+9WXE_pF8D$1>Nk`Id2+c zDi|1R@N&pJ0N1?tDIfT48^n{@DGawNs3Yn=-Cz9qUI?=(VQaLrW`1mu8e!K7QE@7ZZxXnZO9l>NpLGZL>Evhep-QA+h)oHX(IXSk$ zf>6W+mC=Mt^^TpebNX-z-BRWiO78ain2X+F-89cg(_5H@M6I9#^ADm<#L;Un!j;5M zKBR%;bNM9W^Agu;am3F`G{mx3j)j39Mf?lemcAf(qf1lY&DV%@I%v2NlqtNVO*IU6>?7+g*Dc)(;k&2^uvhiPD6!{deWoDP$H_h-DPRvQ+K&*CpvL4 zMC1f$hweEp2I5!rAsCP91oh4>&R0V}eC~vokQQ2ybgrm3ttpsSV{i!5;NLGN`NurL z(mlRcT!b_BxrI;#I74Pa`&5DAl4}jwujSj* z1&|EB@4vBU9%<)7Ir%=@U6}{??SCN>+nM}=)EZgCS7p2B_mO1t&mbJ_{7((eC?TDR zKWs_gncxM)qe{H06kNgYKlK@3`NI{{ZeF3nI(g z*<2EF8Tm0=N{iMSpK0@WBHPzQ>Wk(MeSfZiqzo4` zc+te9G&;#uH+O?Smy?QC+ZO$Y7%!RufdD>`Qww-9mx~_+hKTnqxvhXNu%JCY@F*d3 zFQ6O%BIx}O92S6<`u;B@jOYiWlm8$g+Tj0%gr-Aa!1E6h<_?3S|AmA)BmX}ntQZCN z{s##?#{VxQY?}ag{F|-}nF23^rz`mZp!jKU5{wQDK-)6_tp7oW$+Q214nNIUZ~?&{{s$suKv9R_ z+&>U`a0u=OgUBQRX#4Ms=6@hE^e6Z({(;E46L2LMM5cloPr(rwL{@R>#8T`u0#o$bGIhMXFStud7CB3J-@;JythwC9l_?PRy#- zKS+-!I+cDFH_+Y`6ILSGlcysre3C7S^*bLpaNs%VB$=Z1(-(WKntg@(VhoqQ$@ipE zR4fr@R{-h!TP?y`GON($_7ft)Di}^aZ>AapFyHzJN1pw19)7yrjWm1MePPQA|5%HO zDO^RYU2fy0%{>)plw`f_MXQ$_VLaVY^AH^|QcQdyt6dy*)$jM2S-sV=zSD7=`v)jz zetb|5Czgx-q^UzALHWY9`JvnO$WRUzbw~}XK?4{vH4^1;dd>NnX96enhc3q3dGnlN zj+7eQn!?>p76*+lZOB2P0K6x!oE3YZUsD1Fi-MT#>mg4eQ_L21dS`Voj_QQ5i0<9m zOHFC6MU(W5cE>f%=Y#Z(J$wkY64@+S(W1FJ2#4$_76^K@d?_5=*U`e`AJ*IOlm}fQ zsIuV4w;XlkiDP6Kqsl%sxuXxADb`R^VwZ)v8LcL+*jHY|7dBPs>G6439Fp-yt*y@S zYz8g)3Tg#(l#5h}AvILPQUC=g24?P{Tu>geA$|@hR_as)vv^&!hfPYGKt&ft43m52 zb;kO07ywG-)p)X1+&tMKTd-VM`eX#nJqLkoV(=3F*5}?QjBJs?WzXHk_hzu@sDQ;h zK@rYs5nr3v>xa$MHx!z%DX7FkVdtr#MnUhYitC(iWfvc*+jq73UB!DP8JO^E*!J^K zr0;dGYyw63-2!>p=Q>_xEUk;pRVuQtXI)T4gVbb}^`XPjEO_GB?A6K#y@OP$Ev3L2 zUb1YR0aQ2TT;);SPxq2j6|`B~^FL8bBr`b_g>QC!h30uD?2VEm*z5C1NoN;(B@)Xq z*<95*gx%`zTpWM38bSFAsvQqfR2t0CD0Q+Y{?wpUMWFdr>+$jPiW#EVdAeeu(ijoY{22Hdr9WejAmTTO=ZbQ*RRFbdwO-Eu9-VPLbXPPJZF3+b zmn_X4=YVWUOxhhoF3zkvttnOyB(J6shhwh`Id?_;W@=>Ji|X=SaR9c%QyU#v>)T3k zvBpnj(owaBUipEuo#I1x`NFRp>?;Pdr2;2K{|ae)#c(Ji6G493*-z4G#>}d}e)%Mf z&I-hr*A1e1>XCLgNZ1_eKkXT3W63SYE(ITTDI=xwPqxE3^{p&NGs*acAE*!(?{Uo) zO^NXJ>T4%_8t5ltcs_{Kh$CA73WBuu+8CxVw#0AHLt)cggQiQ=)seo2_te}|nZ=@F z6FW2*wR&WJqL~QDYj8LEXxK7C1Wl2wAnG9%tNrS+zrAYcoeO)Ay0wrbLm`wwC&e$33*)m z*vr!jxA41rWRXat_T%@tax?ewp2vZ`MtG1^IjHLW8lm!ODqKp93h+s+2fiWGKSZI( zULFLr6P)lZWxfaQYMBI4WXcY&)s&Od@j?wAL0J~PR&GH9Rirlab3f{Td)CN&avqA< zE{#br!_NSbI}4K7qav#VO-B4K;U7TxdWjr2uu4e~Whm-Pn5JRks>=DTqvG0fL>(nr zOzPef5WyVqx9E52VF_48Xg?b=tBX5~LPNWVV)s25GWx|sZCM-2gHwwvqt&10P58CZ zxi0Sx&FKYFuc6HhC2ZiQcAy&U#@RB?Coe|dg~a@c*pfDligA>Ia5V1v!rs9$>TMBj zeLq(Pe=Z|;Y%-l`onG4Xelnp2i8Jsk2jxKOpaJi)JFP}@YY25DVg_u}Wq)S&tw5;< zxPp$zkP_lDx$S1%1?A&28fA}3!c(p}S~^8E7Hj=7rZR5ezM0!+)5`q$G2mgKnjYyk z^_WiZ$<{Ht1LY0^xWTM7ZcnIR@4+cEW}#@K9G(Qd`w*&*qMfm!Tp9tUS>u?1;~ z+o_)lb#|L{&=);VN^(|ZGsH_5<%Y12$@F2*Jqwo0_i2=DcM3RfT=N|G2$3Mjsh(snwm|r7XOdbRRy=2wdmVTVO27O!zd0FZ=}-8CFh7G%FF*7D7K+t{9xOgK z+4RW~fPQVet^T|^7cBi`uX*a?too4$aqkxU&uaT;0K92Ri3|N--y# z5UkT#q*rHlSAOUFL7U?!U4lWZMW1XA^Ab!T4OGXPZ52>e7eB>_r_qhGDJ|@CJ#-vp zub%HxdAUIm?8FYtoiSyr$xE zN>Xv~uO!haURVB4YBuYxKC5lrNmlIPoH-MCf^4hpeL)Dq)r;psf*7(ECeQqHJEbK^ z)RZ&(^Ws1^NwN&95vdHwpv^@k0rCYuK9W^dYtnc74A?~O6SkDy7uhyX4jFVj=*Uj? zql)ld%9DfXAj2HrKfjY6&sZbTo4*)IiF)C;5cf1u3}Jd;K6*mi%KL4lI|XoiMe;ko zQBL)5^?7pV(i!q--GU2l^+4nM{DUQFlng!I8PoBW!*iUTui{TXf~` zSy;MAp^SVl1C^vket&KzO7+Vlb#P)i7$Zf1W6U>%%56c;$ed1E-sO+M&x|r}hhei^ zaZyx%c+|+{aXAo+y94e|Q)k78^oqAXStp3gvgOZizu!+($CBS!Qg~!V3b`W+`j^`j zB=Q00#(3wrRA&zv@ok@vT8?|(@4O}KBl8lrKiPl!v-X<%Vfij@gQeUj}S?a)G7u@0{jj)uNbUhi9kd}c&? zJ`GAwNlk?Brq{7!+1o-@1cnGUxr20W>dzUVkl8?MtB6q|_MHlY8R>Wi;h&?Q&6 z?LeD+DVIwx}yl$TgaCI;qiI7|d?8CEcS^;`!oYU(Tah`53e? zmF^g!82hqU#{&%L`AP^t`9c+Eq@~%MAtVHWf-+6qn;i8b)%JKh#p~qM{hZ3)R2Hdc z*XI4Dh0V|8w1SSY(*gG~6)4z@I^qvBe0%&|+k42=huy^B&ODm}lrXq%TVMjD z6__LFZ!kGR+7kyEARsA9Y@f}xk$&uUQsY>L*R0ZjNm7Ur)jBP}64#XdL(i1&2wi$W zJzLjBYC&W^>GFU&>Q|6SH4$L^O;*Zaax5FKVX+@#~LrE|B+|RJU#ZTZW4&-3J!D z0kP8ap)p^Yo}ba*$W!QB^`Ku{U`KDyyPaau4qcxT4tOQLy=p+7l>$A1j4v&K&CwJvICpF8Yk2q8|>WgH7Dl zS^i+Y#-}fRudUHe6^==Y;#?1>lS?!C_f?Mkmq5#TlE4V_k#}_+GODECGHxJ=fsJ7k z3aF!2Ui2qXjI?J(oo^9~wZ%b9i#?iG_@k9~$8g;&R6yB!Cu1q6)-0!QDe)6VIdTkT zRmRTj+S81z^ufxd? z@j&jcEwVk=o5KeJiZwJHi||<+=Z6opG9vm#Ya~re@H;cBq)f{w%}pyPg?N|HvoenO zR6Zba9RewcXyYfG(|+=4m=6=tmTCzm-e;jLd*n}S+jem;rmXyt#ScqMM*9@!WZ}B) z^qIDA$=5MsDst?w#-Cy~*v!&4AKN<|V836oEwJi<+H&r>?r1<;ileN3;9cbLDO%d` zz|{VaN!0!ulZ-Q8x?bY$Ar|&7l=pB^pb}hys14AE8kVJ2%{(~8!e;bSL_rllvomB; z?|yx=IA<`o0%%_0n;{flmUc#ewhI+9COxABBARt%o}wHu#1q=0H${)uxNpv?xWPqD zhiuG~pw9);00qTO@l&Kw`fhpBrF%tCl`a5OQHD-bOFgD|&jEjopo&>uKBPXEDjeXr}$p)>vh6={5IPmV=yXkKIS#h#F9*P8^do~%K&`2nELEaK6q1^ zyQ)31d(DOWt8~zL1w-qw%dbmsqFsMWb&}KF5J*tU*C(cfml)9}lC6C4vMQ~BI<2f< zyfu4}e=MV7TU!BqSI^7ldI#;j(L3)5JOQ20!+z-U<~%3GZ3Y-dj4t0!i@3)`UG{SY z2c_$Td~Y3Fv!v-moTU?)X=_$B9MfZT)bA8x*npd^?uApP%FGHZdS&>EPnO!R@CyRd zBHy;=a1f2HkM6{*oSclZf??=%5F`5gBv$m!22S*qWXiq{KGST<=dUg#o6r}>Stvl9 zbk31&+7Bj0=ekjWlI^psin;}|v(A>e3E$x!iHD#1^E=AUfBSb_&!13v@O`}Y>QoC2 zf|>2tgyn6p-i8spp7J5e!oo}hz_!4h1fK>xfDG~}XQYK9Vd{mqY^)%zN-ZvPC4(l1zNIIM zE8d+}w{-q0t;E8Fk9?P$CZe00P!G3pfEjTWy8}luiz&YgcScsl^E;}r_`4|nyU;9NtNiU@f@1*0B7PA4RaP4>nD|3L58TboeY`_tmFh&>v{ zU#3Gle3`y`bheA&?XW&TE`|xsS;7J^DBiIrc~*(YPuyEpL9{SWZh4ZI6?jIpx!|(4 z{h%(4BF|PMS}|&po}?Kt-9QZthCyHY0QQn>MbBb%grNBRrGg<_A@l7&vyk4a4u7D4V2968QlASUyUoB7@E& zF$*e2B(EC!#plN{CgPjkLC3=qt*TdJ&M;WV8Cz6H>FQYtC#p_c%rXlMD;rlYg@^CI z)s55}s&`JXtt?M*DgE~FD21A?#^c!!1z=6l=#g`MKj|12*)^s64o6a6@u*qk)M3cP z+EH`N;00mM;O+kX1x6VCd+l^D@b@KMXYgpjFWoBV#C~Zvd*adQ#! z4Nd-D<#rk{UB*1}?hlAvYwBWTyJ`K7a!RYdEG1|P2hGr1f&q%sAVh_DeQ0V^Lw@Yi z)|STj=lz8HV(&HOjjsCKU~7h_hN{8hhihJk9)YSr^9Rj1hXxP9imBDNXRhchiXW6T zwp=Ha%M`pxE)?4{6KEDw`p&J%O%%nTnl1VCPz-9|k08p&_T))2Apd;79N^3Zq>y4= z#89MjMMCtuhy@y(z|taA?kB+<`;baT7h9P4MOb)v2cF!?X=EdGW1z*5h1W-sSso<# zaR;}p_^MwyI5njXr8aH3!+cSUiIDU`Nm#>?Jij8#9f)!R9k$A%cV~1qbnjTDb;4ie zTrWIC#L_Wu(yl~Bj&b-ad#;0?(XjHP+ST{i@P(pJH_28?(S0Di@UqKzdX;q z>wZ7KKbf6mlB_jrCX<|;vo|)c>-B7Y{s1kFk(%~Ef#v`}O3Z^#hf$)xD(FDRv%$`J zdq=rvfF4#x3*pGnE~Pcn*08+{Q?!nya!bpFNso=-nI`fPAPzQw7tXlw4WTC4^oyH> z1g6;+GUhQ|T6nQVV1&d1I59=bLwS{HZ4A}9IoX71yTR=8u|GuO0&m?+)YE}q}$rkxU+2of&+hHsU>T?j^eBY>59mdx% zNfch5gA&(Mx|^1#mdclu3FBnnik$ySum9k|M8n(-7NJSma+9Hn;)FE6S@9Oo>8ZOt ze}@Hje!k8{B#p`8ac;maAwH6H!n#kmdRZ$`thna7IbTJ!7cSUmTV>hl`?#3JCtaph*k9kx%j8fu%{3h+0)w`ze}B-^<) z75$ZUjxJwbUVI;UUy1&C;=vywYPU^2S`MqRlt;PMB4#6W&Ohc8SR*23cPJc$C}+sy z?Ilik`i9}WH`{8>RjC1WL7c?3{hMr%%~iVP+X|$2bVN?UDTh=3tYbFrG zk;AyBCeEZQ5M+YzWmRB!f80)eoOg9Q)#tW2A6i2| zwpA}k4*tG!EC5=+H`!#^uxUmf!D_oT5MtdHfSHu*q?d8;@kD&OU2JuZaW)w z)2EwBBD?P96iMDtSdK|%4}HeXDL2s^+l;DXF2!ZQWmoKWEONz$al?_o0xT{x%Z*Wk z8wtxixZ}?K)7jJF)uyI1;SgwQ&hO_C)vp~dYDPzDWm_j zZ#9biIsvUp1nJSrXUuqeYuf04uo3dpqdWOKqV=KuXMU6Z>7~JvjV|xC+90}o7~$3f zxqiAZ`_IbXKclx6=1*tl!NKH)l0WDgww*=vuG00O(irP(vCO6YOnCq!)TuzBsk`>1 z`|p@XhhotZS(CQ>dDs?#+JA}=R)1mH@ezgO-729T6b`Na({fK^BnnT5XPo`ckFeV0 zRkq^%I~oat*e-t}>yKlkc&ms1z%I^&8AVa-za*L?$=~RN>3`owzsDWa%ze;luGuT6D7M){ZDw}a2g6Lcq0axT~ zVK(k4GTAh15hf~h84cPphUXvG^Qp7H5v&d@QhmM)x|WRzdH;3_gfbe_TP*XJTYm)~ zz1l+-yY3p-+sLC$8ubR4oV~MNb_`E=Nt(#4Xk-ALQz53a1KWJ8dd@#)T;=cjsKX*2 z^nAiT+;;E^LtuvsGRBKqFB z^7)8)JlkX;NI|cG8h0I5ZSB&5=Ah`X$SB(mWc?#$^g?XQ!6UhqlSna0?K3_KVW-AsO4%7hSoF2~8gS7{jZIf^EmOhN;Pz zWaYks`j?S`xLkcj&EjN(WaGNg*Chn39Q^d$pJp9s)F~f{F4Yqo6m$?ZI|XI63XZ4K zU0zm!;m@;TS5rsUi#k#nZa*S1NZu=^Kkh8fNx=kxGV>lL-spZVrn2lqi}vdhnm5)) z-(0gv6y+?bSC>Y&zJ;~lY81t989mNBO9EOkcNhB&c{jhkT%Gk^61^x2o-6O+9_rBY%z0MV;6-BO7g|y0tcDzsDceCg zs@>#W%^Siygwlxb%(2zj+~pa+#5Bq_7$Gv?C=w@~1-GHt{vP-#OnG1xu{&BDIN$~+ zd+pO8Z5M-V{9Np^IDHoBo`nN-vY7*SCdB?B*d9zabk^)4RXafxvODcE(#qV zdlUXxvi_^_>22DNrqcGcbPc zU}gCb6Km^tag61xa2xK5q(zRq_Fc_>(EVhHLHEgtv?E|n4hWCa)fEcfd7bM4CPk;O zFa+g~kuU}Pp$X)9M9eMA)=*1CvzS=N5V>w$LyDjx8LSp56+{#*UH3TY{XlulL}D!A zdysuI5SUTNO8M3dl7Gr%WkjRh`Yt`r1u@7TwiOi)o#%lldR*|J-xkgoGB#IX7&VqF zE=j(Qv&6S{H*f4OwaL;u^xY@`A=!o9zIkW&@bK>B4EQlb%w0ETLp2nGTEiS0Q8@1Q zyi%L-RI0PS(8t%y$m}hKXhx3QI2{Q`R{&v1cVot&enuwLp#-Zon=#(s0jVkxDS4^XsbriP)!d-qiJI}cu z?%W6)47)sO56m(h@d9MGY0l>gY&S-uy@9C=cWcf%w>lvP>eIt0DTK~Mu zyXU>Ii_n2F1f?>kG_EQjU#etnYENOeDr#a?kJmgQTHgCYCudIpKXmoLb@P0RCX;RnR+5cG#i@(y}^u5y6vp!Vgnj^FIx^?ugcX}2q{ z8aE_<9aRvQ-W#MSL?OPMzF*>aqnqi?+J!{M7C#jKAd z8_5z21HD^cX9)8`!i|F9zO$jde zo%Wr0-QdW4A6#gu=Xf$!GUFx8;f7$i1j`pYi)0@Ly>XMtd&qY@eGhW9Oc@0h%@R)4 z$G>jDWM!o2Z3_SbRbLh_U&ilI?DT&xp4S%7FmW6zC*KmD%mPRCIwoasRg*^nYH#)a zUdSGXLP2{u3}RZ(w_S+IByA6l&LOq{0sPi(-^^=H7Tc|07Z1O%aAa!&2VR=VCnd5? z;S=c#msjd9tK8VsR)vMCe+MGynufQs@95W+Sg5HA@pgdjT%Oe9A_&U%u>HpkK@Pn> z8rDlvdCRIEasG#Nd7#A^bX`QsoTdBzM)&pAr@{9mHMmIeA<(0XMX5)uQTn?_!u!pP zPn+19c`qP$ca#6EeU1U+FWF}-^oLIoO~RdBx0}$R-0<1sLZqx)uD`DueDxY!k2B_N zI~4m%Go2wEHPib76dlzfTfOszF51EU@;NHUd)5-JIOddLop87(Qvc>2>`c ztHpoKcX$hL$>f&u5c0Tqmecq!HX^5?kerx;Huf_&+(qU4M|KFEK}iYJ$Je1JY($C* z3K%*l*MFGGFu<9)|HtNEq_OF=%8B6nXqeZm;#T{YD4+a+K_<~fyVJ#`$>4O)KOQF( zOU_ZEwUhnxP1t{Suqu)NjDi#t1xnBxM6}?$c)=zmRZ61a6i%W_d?~$L!PqMGp-NRm z5T?DZB>YVtN5;Z}zPFDDXj7&@?KfG2fR|ie=-yHcc6WGrMBDNK{t#gejz&@;(sh{7SA3lgr z$oIfuA_O3SV?x&(Vpl^+Z%0|lBgKHbSUW-Mqd5@=e2k%s3txz{vxvHi1}M+&?&?Pz z#4isANZhZGEq0e813dNLQ+^lnrX^w5pODgBV9HIz+Gs`xs z{bZ@6B_f>t$>dj9g$t0^y6cww$GB(^fuX4-1KOd$Y~?BF?NBRGd=}_h5NY=Sjh2T5 zTqkz@?;btjis@xE7e7>4jSYsI=@0jK{f}jd`g?!boxqdL*YK@wBdx9d7FGuPMnbS4 zXo4r#4X6;TB+B}Z7$=JG8igljuLDIEhkZ%=@r=zvt4>sq;&W0*Lf1fYu;3$af zy~c{Rs&IL9#i!(O4s9^y>aw3r{ee5NOvlcIWPA&% zifhyYv+1^qw*=$(8#oJHnpn7RluGN3JS+g1teEuNip%b@u# zR+lyt7^hURou(2~f-y5&kJlj7An-vT*R3@T4kt)L4}HA6*S$;|Ei?RNkRM*uR9&1Px~E&rs0ui)58Np@Pr>c{cA^2@SdSk*IUOTDgiM^nRO{1uf@jy7vmX;d^3^9s=%!WYBx2hUu#F zh{b)-j(iZKJ6;Qbq+$I=3{_FYaTeos#XN!Qc12b>;!YiN=@FY8ehG;XIL&3?XAri< zA}JCXJ5{v%3e6@z*tIn}RaXp|(lS)# zs$ZP(m9um}>6$VVg;hjiC7-Q{j(RNSnC+YQR2}((r8^lQXH1~CYHz+k;hUnHw+6@Z zEp?~FfUPJsV9XH@)G0(42DU0;s>|)tK&k76pCPzIh)@Yr;MvimV|Y;raj5%oFL30n zFthV}YTHTHn~Go(^5%*6Od`{0uV{zM9(<&>nKTxxxqKQ}e`r*(UsJD};aWS@o*YSz zyn#XxKiVdc1kk@mZky;?w2}Ef<%NFI568P(ZEMPQpxjJ!`vx*amWrZo9qsO0++D~D zx|`<)WK~DTR?u=&I$x(1!ZkT!%X7$Q8fwEFRd}h5{ zVnsS~hE(3{7brziF=4$+&^UL^B*s$hN-P}zY~cdT3z@Q%hBu|#?{?jaADc7FQ! zx8$-&Wk^}%wK5MlMWxdXMOFD6xVs+E4Kr@O?LG1(4A$9JqfkXA3ErLdU#{91Ta67*Ac1a z_w@jS1#=SeSJj8_IO%@i*ScTRQzb#ze`e2DFe8DtWJG45A86*8U=mvo+|fuW^E=$hg2;b#s?7!5!t zCT>5;SNvUB>%+&(&Q7l3jW%FPR<*(ekbR=im(*hLtF#6pbDGwbm@sD(zRmQ(hs`x6JB`I%I1 zlnoU=cnFOxu!Tnj-8#dG_`Pnf?&r$oD;DUj8CSW|PS4uwkNa1+mho0KN9OYAOK!I< zSd+)Dudcz1n%IvStrA?V9E+l4Z>;dW{t0Cq(#-t{c)dNd(IOXlsB3GnssM(DU+0ME zqymgKDuzoUKPk8T9QLdu46G=b z0Fj$rsjS=Gt#hP-t=4S3E|=+xTs&!m(6*{UoBqR_u2FTVzV4GvqBxJ1>0d#?4XrJw zJi$&MX2lW`kdmABMqt7Kh6~KQTw)QQZag=(9l{Q`X^yKJ7;0jv#krK6lP~`1j5lxo z(;iY$eBp^ycadJw%;s|VeapTW6C^rK#}6OfUK`$@**{VzjAB?P3^^d>KJ6`R-g*6& z>dT4!S^Fx1ED_(y-V|?2{HRZhA7OzoIX-%&quG_Y+jMxQ1NuWw$_W78J6wPC`kKDJ z5~6OtW=+-nYO~aZIDGJ-U6&e{+{a{7f}}rGO477xCo~oJ>2YSG#{! zg7|1ediDQ=p2`;Tx$|VYvSZf=3(Yj`!oLnylI#M^J9H1a^Hd_ zGlr85Ec4%HUv12pg8i8yo{uj6=?>fwuOp)SzPu}<0l_n7hw2hYHHPk$tR;Gf=X`)> zHCIo0b)!4nENA3C^7!F`ku^F7$j?qYqY7jO>wA~O%i!BT5;Y=?Fq}u2y&-K=f2&%4 z>dNEkMFN)Kl}0ODy7H?ge7Zvuab_;%4_h_C&bx5Of4|K}BhK3B&r7sJ`Tk^3dhpeo zg~E*K8th}S{;^HTlUi$^SgPw-SvJ_`0v1_ozarfE2mIbPcTJt(wA3xwx8L?(JN)|% zALknGxoiFg6Sr%gfucZlfdM0hFYJlH@2F3qt{yINfn&+)pJc!GkMHh({zL;3TY>f56-00qUUdDx;@0e)3^pwq?Z*I5011k}G0 z$b>TMe+{z9%3gchN87}q z{}F((V}M0u?27uh#byHkS4!U-*L%gA;a`J_t$wKT0)d1Hgj5wKs$oQ8smxpT zc44vTRT1~dchlcOkVtjm5A*?oANwP>1%{7w0U`FV&?;(MOVNR@O}@(b;tJaPa`w~F za@i?HG5E-}{5Ets;d_oz_UuIRX8Vdg_4>f;Z@^kpP5me&3dPpym57aFGH{*JVSJ8;v}~~VS&?73 z(kH4IGQ3bXPTCWkJi%6klp{%)I-dq?p{J7uLW5DEcreUlfrT{9B})1~P$xwylJdwJm^?t*Q)2SS<~>+33Z5zBU-deU6V*04<9~-ct>P`XfW_1Mw$g zl$t|3W{UV6SCZUu@y9^S&+5@5fKzg#7sn+lY37rH-qi(r4d=4k z68lb&-RR1Bzzg6yX}d&HWh@kB@gpW4SN;aLxBl{`bxk=et{-t`CDK?j!sAuPQ*O)a zhuY+rHj12b@UO_2Pz8~H*vu8I>uTBDFl+tJs0|u4M(yXG_*|(hoH(WjC8l6nvU8EOwGJVtL?uA?@UXL#G+rNO$-G znTNC^VkU-O?6uZBwpOr%xYV!gFi@P}H;DHt>6l&kAZnrI;d*{&1I(x{@m$g1Pd8GN z#cb3{GW(W(f&-pvf!gAU0MIPh=6wOmOvpvy%^~BjA$dFxAV^vxy~U#n`{uFq2xz{8OYJ6hMc-u?!^J^M7i-j-47NJ{*s~( zi2@MInG8>is+}X9r%oLsZuf`o8L{V$QcjrbH^?1rPwdxI63$#i&|0eugk9DwK1G{4 z);>>*ix8T=kIlXwwVnPua)Pf4ppNJDo9eW756gygzuw`)6scCW+8v=MC^$dlFSX|M zci6&yuj@rH6Sea`^YgE@3p_ogDD1U8TT{C$9cYd+G#kwAQKXN-aQP^MH%f@3u^-GIdrrA!)te{24S3ck*sQvLh||4M?x zV5caSL0~rT8Gw(bryNs2qNi*bfI~D7K7)tVq)gI5qBs92gNX1-@nnI-X#Svp+yF~q zWB-Cz)R3ECDd(JD4KAr6o0?PUA!)%<5_!HF%rin3fu+<5e!*xK$YQWWv^?}?N%k+4 zG9eC$(ZmIr0G5I#^VLZ)4@RjA_G?JMws3q?hJ9Fpi+@MhuJ&>}ls)D>h132s0#Ru6`%YoOaGb zr}>6BtUwwGrmvSD)nC9rlQym>Z$Pf2n~^F?-liM@FqWF|Fd{G{&0O`V#5-&LFv_Cf zETD*Xan3_KwJ4pC7xi)2Ml%oMuhouk(Ub0@OwhwfXH-w%1G9^urwPi7T%%;Km4Q(- zvK0KIr@9xUKkY4HWLa2bCE|qY96z#_z2{!WfSB)Nrh)e|u0mcl-D!{GqYAvl^35f& z^Td+?M-dNMC?F(JP2q(eK{e^rwUGKel}Z(bG7w@ZspM{~4%pf#DVPk9wPuzsX{e$r zp>bg2GwP~n_GI=Vb+iApX=%`-Cw0QngD2BN=$tu|#lt3(1#6R?Ar$lL3#7oZoVG*{ zMZv{L)g}3P|B3r0HMmH4k^cLqBx5{_CZER+80K{|*i#(SQeDuK6n$Xw5gRv|@3tEt}Zc=&+dc?J>md7V-uP9pLDigzCj zP^aBKwzA@P5-)D#sd_ga>wPP`Nr==3jUiB^hQtEGD zR^I=*&o4KhrFj1@0s{ghYHA@ME0pll5}HXRTeU(F(q7Da%$VFkh?Gr0PI)= z-p5m7YYurF%EL_dDj5AOH5AB0I9I|Ffo!za>oDZg+_Y^ofv(=w*kmQ}HSD{qq7L6E zxUNQ^s0l9Kbm(oH`J%K)I5M$w+0KeEBDG?Q` ziNcv7bCK8qI8GcBY@@?fDdgCksc_^&W+0a?#~T71Kj)N;V_uCl{zz(mF)0`ZH7PwD zFQO7mJM3;;jj$cZi>y%03!h1717X!Q{xQfbnADPoVUNVi9)2B1|Kk$Xi}vbxbg2S& zyrbopz_#2C(6e2yG@?xcoEBhoXcCIuxcq%?>a+jVKT;y*_=lTp{>p!tmWQF(XU;p= z-WPFP1X70n94;zs+)LWs68iI!&GILNRfT`N8eBjU4kSM_3R(fCfer+x@>`?iujrr* zCn3>bY6(?QR!K`@XTLqN=*OOw=L*PcBU*zz;*`(|<(PXsI>_q?z%dL^eMQ!y9L)g@j8G6E6o_5fhw*9f8zIKtDtp4%@_k#yNy1 zSCD;jv_NAxmHw&ce*~AwBQN|CZeQFJPd25s_Zy#*XC^W6MVVDGYYTl^*mDTyN44;x za>&PQ3XM<*H*MtuKs;Pe3_maJ(Z5Dg$B6?u^s5GfPyNH)B(9s8FClG4r&jXY-)j!I zhEODu2z{&w<6YHEGZGZ-C;GHzf0?=Epgzbvf9r5J$wORid&+HZ%<0EHf@)v)$z_;S zT$%(6EOI=FQxfQZs`jo0J#lyE6=dtrGM^mFWN}Z%+~9knzz3B{kV8{gymgTRWg3~J zgpb&SUT6jSGW81w4qXZ{eB~-GkE%ti6Xn0mK}ps)sTvP89>%4?l&vkjpRo+MyfsANZV0nVZ;jr}iix%EFSQDdc5nK0&8f??fN> zUV|~#A;C*=0O^FI2aoLzTM|MaL6Bm%y$6I9YZ5nW@j(z?tthF_4P${sG*C4Ta!^9W zL<}}!B3m_*l4-wXdOUy)^ky6L8Fj)e$X&^(lcOi9PX>azMX8W#nrJa2@sOAS`P~CL z9%=n6ve4QI^T#E1{U-;S#DuwdmGDgGNX zkqXFe-DlWU6ko&ccV+IArXd|_+#s<08i}|KkiYd0Fj245)jXU-w@><#i6PNZmRwNv zcj>&g$o!$F{0k3~l)Zt2$tW^7t#bePQewvRt(nH5R^jd71Gob=2;MgK7R7ON zZ*K*S_o;QgB&Z=c}n+W;rliB<{tgZ;VW0|E2j%mGf^1G6OFpxsXh@1F|j3Bu5# zIEscm-AKJ`G9++PNP4?z65M^*_&D*jB-@g+qCo}7b>lt zrR5X0`Z-tcW6KHI{9gCl*MTWQUG&{u9-linfPnURRa5wb{^aDsX2Ry>w!)S`hdDj9 zfE!P6>|O6ezTUaLF~1DaB&X8Y$w^!5Ag{Y?HMdO8xmo1NPVdPt70SmT9vQ>S(S1JE zr^GuS>_k)c!TSnd{fh*m*~oiG9=Ylh$0oW{XSFZpr|gSCvWU$t)-K1cQJ?!BP4g~K z;HZ44i}z60ybG->p-Z*vM$PJ!PN?z0>6cLCLp`=opw_5}L(CyQ_9wbnS9;j?yB z&ftr7Mb4ln{eqI20Oo14B~F6CS4UD^o@aE~QIlQ&?TNLk?!V)>9F&Ltr$Q)gx{KCZ zLRb5HLY-07ej5M1r-MN}!0*sEo}fwf`84E4v(mrm`+oIcsm$&aSqv-E`q7>D>xNI= zMm*tg9r2{y`s(_0h$*`h2G6z4It+hK@c}HNO;HuZRBh zzS%1wa@!oWTXon#sMT1y3W!Y1?a8QRvC$S!hm`xe5F>7QB{Ts__h&n?j?L6eZszmf zTPkP1JucR-FaAP0WG^54=IN8K(X}n;tyz8e8o6He*3`~4>&aQoXa>)}bRk3D{;M-x zb9KVlRZMA>a##M0$x1oftKX~+ zcSu>WRnw*;mkhDJerCsde(MtSS z|JlIcb__J>Xu$V%=#Muo3c?81JdVw0&UNM#g7nc9_DbHv)ryJEbEcuL*?;t@XCf-*TpQ!>cDiU5g%dsdgs zW^w&pz|M$qr|h9+nnCd%^c?Mb^fA|okK^uq*5`%dJ>6H% zu1#;9Mv~v#@nPFg$=6%03vg6Ru^aW&w%y$1(`7n9|F_SXXFq0mxeHGmuQV6Bnk<-h z{HHW8;&SIw`$egaj2ZjsHR#DKuq5Iz@ z$bncjec!yU)f2p#&i1M?)^#6!qA;b;_KHRQhf53{U(d(AW+|oDvu@hz2NOzkusCv% zw0e*$+OD`A=y_4r;z**BUc{>`KaVPFx32e%fPo|WAU;tk*ygCVy>LAHFu|2#Z;T(~}5R zIj-;2U_^kSLoKiOTh(*01-4(Ep~mk34%sYef(n{ECuu1Gbkjf*j?v6g(qf}1CbZN= zmnq+VSF!xXpVTyLNayz}<50s3!@$X8#ARi2CBu?fKlh(m8^T~kgIYD2n}B!OJ+%bU z`!(59`Gh`>2=;G^T+u%_T`H!$u;02SG7g9G*5e~Ch0dQaqcMsm%~sKrBsU(M&wh7H z`wJEwe(zlGB}aSOIjpx2OntQC*wnXsrixp8oOn5pkj@YzpsLIH85eR1`2wWM6QQeK zW1@>n`3(pioQbfyg|hRHJd(5ul9T^{PDe@8N&knE_>Rs_AP2NvUGaU#8iVdPC#;rdJPhKKDH<6dbYOe(>OQuF%_UgS6{g|xBt`B3z z%8zXwhDtScbsO(=@;3B>E(84T-au3a%b3BW*9ep^-_zmZ72EK{$lY#E0 zEPt2516nr74*Bz5)9aO%m#&oNGg*m;_+;M7mENe=7t^ofve_Gr(*W-`Hacmtcl-?$ z^nF9FE94F3U1%mfuOxw{kW3x$O==^gUE4t#XY7Al@kSK{x}S&dwjzHPnot#HhhYx*aSVlUDju3XS3=HZNj2Yw zk=>?}rki6ZVIHttECKwK*;0syEKONb1Zh(37KCb1<8!Lz?f*_tui7Zoh{)9bnxrrB zZ6Wg@iEG)zz73)O*dhLjO=%mKO;`v&-PcBqL=Qo~pb z&pWBUk*Pi`eO(sILI*s?H-j+G(49m%1ITCgv5N$IIDmUYSIeU(SGikBxDX445dUh@ z*lWN= z@6l3ViN{AX7oMGK*0nT6%=8=oG%0(3r1^8dptO^P@BuoDZ@ha@5L--V?nxdan_dtj zCcmcXNopj)o{S66>FRucN}|6OvfBooF*(K3k4$&wwhg6xnaAH6=u3Kuk^lVvj z%l@kcG7We%3$;@AamS4I!AFYrDRW|fNP|LlOt%)h!n^R^^Uo}b#U){iIcOV zsgccpke%@_I8KiL2&Dc8r6m7=!~@v%;h5AcJxz%@IoRQtG>LUth}no)z6w?C?3}*} zS^h%{4aX#7Yi9SAWBuQpII%9j1Sh*BC#!@wI}aPDC>I+uizu78gak7)I~z9(kGKe% zAo2hIE&2%mXO?8)Vo7oM4oU+MELz%Y&f^SSzpm{elLuLolhFGIq0}X!G?Am!#bwcx z6I+pzt6S+p4=(74bBaiyfY$D^I-{&%H)dI$v-+r1@~&VO>#P(sGS1Vw(l1z=^Hwso zO>aJK51LPUIDQ^xdrjVEdWGj74a{@b;SK%3ZyHkh9Y)XlooI$OvY-X1j87IpxF5i^ zpb%kYM=LK$@`J4jSz^^eH%fqkHI_Rr7N)I*9Y~(eLa-d5MAMabgDXTIBW((^g)%{- zBi4}Og0Bmoplph}P#-4I5U)tKW!yv66{!fhU>~MBa54~f;~oDYV}@E@%!mG!QfNpt zgP5V?k!r}eAz`55Q34tw%`gX|EaQP_S1R7_-FCqeDk0VH-oAX$W^)Wh`%c%P56?++ zH!5#DZ=mlZJ~BGFv-wfCg4s-2)qj|pE(7e8zzZZ}=Itui~_7lMlFw%lQ-2!lJkF~jw$gO^K{J?ZU zka&5V`ToHyInHJsD1oT-OtEK6tPP%)x&)K)qh5k+U-o%MBWDE@+h1j{%JWQT32v2s z<)xUBP|iOm^19RbRm4HRti@N{`x(`tzvgKaeaDI`94`lpujQ#2-@z$wsa0%^RmuCE z;W|2YuyQfd&Hx~@&4AoQZ;u!_hab2gd1_ypqk7N?^RYDk>>lt>8ORzM?3Hymifggz zZV7EC`1sS3l0hVcB?XXQyei}Gc0CfI54{eCgE=@Bx#yCz>bfm0#_k{(Wd1hEjS(%A zFweg0_6#fZV5-ofKYy5UoFzmRLCcF}Y!%XzH!w*Dee`Uh(Z!v>4~y^W;F z6y6R}r4Dq=Z32_PDlHHufIOjAM)41T!4N055et{F<3Hre7X*Vur4ADofq*0ZstcsI z?}Lo#5{3+-JYda9r{n^o!KJR5hEo^L1q1trQtCHtnfzR;I%|7RMFjcZ7H}~CTL(zj z3#SL$Y-!8qLyr=(>pGh*nJX1R&{80<lm{6^OlR*Wa^RA)gGyePh^H&eCO9CIpun0mzN zu64&4^g3|TE&()RFHu9`*eQS+d=n!IYm8?aFcOC zR6nA!$27}Y&y;z)METdyVzui zX%<1DY99bU9r&|45a_Y9+8p+!#j@Og)<-tDGov|RX0@Dbvwg;X6mm_%8Ow2!?q*fqA_k zJ`HMTm*{Etsr)8#Sk>vI-7zNCEhXDI#qjAa>3#M6n&k1xBH$?%IB$IezlK$N-79=z zYkq4`d^1(oGtv~23x@xgw)#QgEKF4qFlYw6k26_@XjlYN)L zvTJ9ykkz^Ou^A^|k}A$P4g6WDbiZMYcU390df!2?G06NQ8&m%z@c_y4y*ia!vp)7V zecO~p!2{(Gkdyz4yc}SA2uqWlY5CEWx(;4;(Nd$2!QvQipyc4tlv=W=rk9UpsJ8-n zD86i|QY))|QiP*<9yBF!FJ|ojz*?oE__uMT*?qf3=m(aw*#FHg_tUlx`mvwZC<~4o zDYGk3c=Q+Nx81fuDlM2zj`}9y9Ac;pf!GPeIB!6~Vz=XaJtq;R2wY(wd#d&I@S&h= z`XCjOyH&*CVOvL%?R8{IXOhwNsi8aF+zvo^t&vQzzu-={n&27P!7-okSJRVjDB*)| zNVhflZ5^v4$vpgKBHcX+j#Q<Iv z^cC+X-V@f5KF9>(7BhP?-SJsq)!u@5Rxcj2L+4Aji@zu!z{N(idUCqU0sDj#2F zlI&h~$?o3|-#TJ3%|j zSXITYK0kc?BiS|3{_`%0MOE#-*iq-5%tf3Q3f{Zjhll?@Bz%N>Pdb_?ccO?>kz0CG zQO0Fa>t~}Q(X2?6>$Qj9Yhs>S849}1~ZeW{fyku z^(#IOm(fgyIzG4k%Ofq`nA-I7sJnJD4KW>uP$`!-S`s587XthRdTJj3x?uODV2>(oDsrt@qIL zX>uLrZ0~c+{GCjHV*w04q`>e>5D_1AKM&K1rv3-lxt=v|@tbZ=+ENcZ6asje0{Mbx zzQe(E=6?PWDQ`{3CwljjopzA1%&XlY6R}A6)E~@cHs_-|Mw+DRO=?L_fNLsMR!kC* zYFJ9fNmT|E94EM>dcM0xs2U_w6E)1Iak##mTrbIIRtBVev|aVMPpim$c}tH!M&IraSpnkV zsG6qsr9XSJNc!N}8+!3#2((j+mqj3Hpr_IGUdZU=*I?rEkWb7cnWvzV)tX8Fe2dtC zvvA{al59ACI}V*;X8oSzN!ycHy>TCK!I>y%)^N2_0+I!s7RX@KcH5%q@nxUMXc#}C zBp4p-G$W6ifi_jB6n;XE6Reh(ZW&^UAw62wQo&lI(aprva1$#q6QiOFTtI(IMSY$9 zL^!;N;aW=>JrPL?gn;ebGVdO9K>sq(0?V9>M%j6Pb#`54(b13rp|94qrXaN~oPMN6 z;sdYU8c#&APjE*lL9~;JqG|s>;k<_4G$eg#7GOEFP_S6RI0LCBIXM_Lh zhSofP_8GNDx1HTuT1`ir*iVl%lFV|~(bwFbc^=6EG)<@Wa&fa8^Na!tN11q-HRv$x z!k(x$4ax&pkN@pe6>;wi5=mMNlbB^l=kZTii%&pJtV&4?$KNm|5wVoSle!z&jiqoz z39gIwKR;62bQ<=VA{k60{K(yhriYbM;Tr9K{bK8IzFcR&G6M@g>2f`S|2(<=TF<&0 zUhqS(`AP#$@S3bvDadnAVhSGV6YtwMBpyhMpU~k|GZ)ItvmuWG#mFj&CBPxe2DCkz z%+Lj{f^4Ehx}8yNdulqi9UD$2M@D}kB)I}0m|yp*h#sl+3uqU;@xoKBJ9Bs*4cp*< zPrWgy_B`G%2lCE_J>b0zK?WO4$5n|oh>1d;iNyf{9PD(x@L(ee63_?O4&niuLg8-X zo4r&HUtLCf^Iu>0MG>J~!a09KiRYaXhe!k*a~SDfg|+U~eMC$4O7E^!V5^{3L0l41 zfRYBVj{xMTeWA|D8(0-fiHhE95{H0)0N&Mr9FjEn4aSA3Vlk>~0b%Z_GR~D#6aqiY z;MTC^?gzHc23P$$Kdzg9PlW8)!}R)WHr*Fq%{wpWIC>U9#IZBVm2Ev5;-Um4S-BWk z3G5bJ*TFhOAcriGT?tGWZ#(g}U%U$<2*G|doHm)>d$q~!bjSP1yoePzcFU)K6P)4s z+7bDr%HZh+Jx+$&kR^Zz-J(ahkx5`ahYfc-Gv47>5z=wb58-m4748T8D}wdP%C zKKgWp{Z-vESdtN+dv3|0we{q-aXea?MMtVLtv9@oDpr!g&7VWtA0~5dJ3Yh7#@W=t z*)y*c;Ek^i`=;Ik2Dh>{fx_8;(b04|Sfr-=1Q%*-Kt#Tonk@Y1WpDNeff^gPy|=TY z2^7ouY&?KIyEo>ZdAuteSstsh2u+O2iEfKh>?ffD5@yinda#s{tLs}sP8g?Yhgd8g z>S;JVVfJ-C@jY2nEUhWFK)G>q3{46B5AJWhhXn7jA_rl$8)7d_dElpikgvd8qH)0t ziU1+B+~Y?o!TXfXs94e~hq~ZVMW3jNl@#7u_nhc?>_9moAm!E`te^x^e#ObwN?AE> zFCf%Izn-e;F&U7@{srO)nWT{;Vr>MFDo-A`L1ygnI$Jp|=%Bo8&o2~?Vn{^a)VBpl>2BJo2=Wo_qu++1PWPEs`pupRbMA z2psBe(4#)Gw9sYwh5MVcrH8J$p~hpdxTdC=;*A5Lz@Q(k-Blf<$Qr z@^RsGlesH7Ng9`exOHo_*tRr$kmuQXSk|(|W^}_IyeQlfG>E8w!q&~D?4 z!QEQQRxlAZCT?0;V&@8`AOwQKMF#Cz&mI*xEw(m=OqM{Id|Ux50SYmjap0KS;N>`1 zy#+E^CE_w?<{S-P2e8~QX0w8=7CR~fI3it;ernCAM^ePLc6iEpXe(Zb>Kq@CRjG)?ZMZ7yPgM&Tk$w)n9Y@zi^Su|`Koa*vy zr)NA?tq~D?Q_NP7o+*l(6vj>5Oauj+El~q~(*nvyWbrM3haeQl%3Y?axXKvmt>bTw zY?xGRwGskS(9`#FsS2h-*fR~>18(xtv1%wE4?sI0g2NF=r+k5m=uDNOiRp?*5ApIX*JKG#<Jgj0z0Qu5g???kCHNILRH5W5;aC zFBJJQvU3n1S~Z6!rRng+Li3)+hCaP;st+#RyW?*adQJi1LK}W z$QJz1>h59_h8!@bvR7|ewB?V;W06%i{7DwEh+Q5z%<>1UF4A3EVFru2Y>N~2V6U9@ zMGM_b-!A#+Wu1=)S>;aWGl9mqLvuRxa4lJXwxoZs&!&HDyLA2%m_tq&`@iQH#$E-F zTUD-%11!Knz-@z$H#i@E zBq9p|F|ydDX#z{J`*|0XRBSnGnq0|5a|*8UX9;9KEU^W(jp>bg%QQR#FtnhJhhkx7 zavM+UdI+$mZ44{x(XjX7i8qnM>#`tE`2r{sQRnHV;xe@^gLhl*aI8tn*tr;5h@x;s z*)ufam2ya>vb0VV?By=lgWJ>%_k*5)kw#a@5yP#YTFUV#SFo(CmqqO*`8@jwaw#kN z5OcWWGzM0MW;U%Z_Mx=x6?RgexF27wThR$y$oBi<>sKRTOEQ zRxDi?Gg!11ay?mWM6wj-mmK$hIyW`rQ6>zVzK3^!%Ye{y$ypliW*jbr%={8`Us_I zQ%|&2t59_udh){Q>V>EaZ`2D>XnUVU9nDGu#G)d4TiK$MHjWl;k%-A^w|u|7~J9^xx} z$x`7x`%c>1Ise`B@37Jg$V-flL%z zoQ8OD^}`X4K~cpH3dpe~EK1sj70uX?Bh8ost_oYe0$rH-wAdYg)>ABe-4stOZhfP8 zg6$+xxXKDnP?O#|Fuu|cu}_<6`B) zM!}&BSd1VQizO~wX0w$k9^h)L!5<=EAA2k99Lg(S-txH_zwMzmu9C)KrM{fDa*P^X z1O@2LiCC&0UD5PTcwLMW$molUS6@)n19jH*I82lC2ulTNYZ=z5;>1?&y0^RV-o=|mk|yE69hFg zIXRcXQ~@Y|-8^fP+sJX>^(&Z1*}P@dmg|H2!OlmlU$WAyWn6R-90`19+1U{1zG&-i$A=1@lUTPQ45n7N{Pjr_Y2Jy zx-dC0@N2VpoBjDz9Z&V4UH;|GAE7U0QD3fkE)9c!schGOK2=LjvWF#;*?GBQl4S2s z&E7q|{HfklhjoSXymb#^PYwLa4HYeBgP`6w6bZGE@k+qjYwQ)`B=HNz@Ksh(Vl8fx;C70Q{ zI<|hmwsJ2o=jya%B>VMJ2qYc8Jia%}5ugjf(EDswuzU%jX zDQE_;Qki`~MyuMkK74TRPGuX$ox+0yfZH9EZU?K4pUN|5H2bk>eE)ky<&z}|?a5Jp zz**5;<%D_$**E*@(4GfewW7?j-ijKapl3ZX=meB5#DLON=OVBk!ojj*y++RX0smAd z*Ou%xHfY>ye58O%*}BI1?Eb&mHv8i_bUl~*L8-I$5TX#ohWqLanj!S6{X&~&eZiBJ2fbq!RODPJ zhLSx}9SZb9C!83FOhQQZxq?@SM5Rvr9shG|+-0;J;-21wiU*1OzEk31-Ym7){`_z2F9 zU*HU&qI{U@ zqr1UmffD>!jZ!N^ZD+WX=wPokKP)fmk7ClfH%}sHI<1C(ZV5r(0TdIbOb59WAqz~C zGifG}-8gJZhuaZsPcUEGDsdISc?IiFq%)v4ZpY9vVA`nLe1eBUrD$n7$_6W;v#-Xr zreHXX-PmrfNKW+NSd>dp?Vp_A0GK~)D=?E!?uYXp=H!e(EYlgkx&eidFow`{(Au#Z z&9=?XZ-uvi)@i4a}8%;a!@yp_7hP2qRctr@lveN=$aJvn#wumKMre&s`Th+ZX}G6~$;sro;mnV~s_#X-Rlo)Q9&%mdmN%_&58c!5$;fW^55JHtQjOBcHGL5Mw=CEn@Aw`h~%;Dg%{ zt7>OAMSwyFQy>MUmwN-e#gt-PQHA7^Q{>7D@*EmJb1`C`!v)V>j7@6&+g6pxB^@w= z-^nZca(w^5*ApPnC5V5m)#uwk_64_(f5F{r@HlI@cgLrNnTF8sD7DWCs82G8aFalshac+JY48mL^C=5;2D->??s{(C4WWRZT zeg)xD@Gl@-)GsGoK97_T&z2G}BnQ(rrJ)U%0}9R}(?t|pz(C;HHKIjOgPFYB)y{bK zc`~EHeBOYc;D312`q0+v5PrulojYzPdxRyRfZy5Y-)t?vpelLJVjY(}W4k4zaLDM$ zA;VrIHqp^1XI|%94}Nxc3UIpRVASk??oje9i|bj#Gz@z($w)6kCCJGXwJwrB5g*px zl}O0Zd3sl|Fm9&232@@Tn^VuK3{=Wf%tyq6WKeQ?4|4bMWwBQ%zTy z(BM}>V^wYv7m!9Spc{Sq!TmZ6vYbAnStV&w7`|5O@ z>D&8i*C083es@zH!CyWHdOQORG6#k$NQr|q!3U?%MhBUYtLz0hfaUpzsp$hTdDuvv zT@H1dMkXFG`wRT=TNizpOq4KxOsPZro#?mz% zPmSYZ`y9}p4NYnUegN=)dOQX%K}g1Fps#M3n1<)X=ydKB;LaFad0hYqM-^o=fP?Lt z3ov9J1Gol1X_!-5tVj+GE0q(iABPXuCYRv;%;5v{-&MZuK|Dt6!f#|N7&F%E zjj=#Urs>Sd2SV8*z6mL+y%bZy3{kF%10S41l%WF37WTO}o640f(X76&Heoy5|ELg8 zW%9TE#|8FayWTbdcFetR1cbw3P_MRH6T zSQ@NFCVdR#JJfESMYwLx?LaHJRiY9>=}H7YErF)_goTcO$o$#5&YiAtr_{lUO5Htm z$Fj5fdK+~@(~0qFmA7d2-^B%i6ZWGX7dUvJurAZ9s?kF)6lf4_D+mQv!o{a^DCFqh zBr7`f&e*hf#>@>bcOnVHuq<}Mm4)q569ok<=lU@u5!4Jw!X}myWL#TPtY1&bKT10T zo5TidJryl~3TjX5+j)JJbCTdSspr9v$tjgXJrCy<6us2)CPFM1>Lxy-7}w4e?-}UZ zv{N=UlF*Ki$Aebjq>NTXfY-$yQxbtJQKU;vMh3w4=GJV_l!O^`y8D<{nu0ONq?F~<=rk6eK(zuns;$XX6x<`g*=RW&=xhOd z%&=a-^)x-eeK)`+iB&{zvQfg|0S9`3Oi|%j-6}EbHUxZl;kXs$>1g5zoMO!#{O1KMQ_N*hB$%DjT&GYXP`k<fDMR0mr<;%oRg{(JlQHlFkB2 zQy`Pc7zk(PfTWR}n%sGnYXi=94hN{~J{4?#=Y_u!z&f!OyE@35ZnFuRb={p7bdJtv zA8wP86mmbk$zAU|qf&M^bt0?Xm%-TAA@B*N|K%P8+}2OwE~^yXogv#=mCmt>ohs#4 zA5QOP-!#=Znf)&5i~ho=e4kA+z8=u_v$wFYkf1sYI`veCZcfw0uaBq7$6Mh?yeaVW0|vl^&r=y! zp9qsQ?+E2x*$TS7-E96<7;qY?A_Hm|W#yxj`FhOTx=_yA2aL{%D6V39i+E6w2HYvjwLBo z#|vSgYjp`u8P;%4c;|}3Z+oc^ymtXj_P&yi&p3=sjT?M>RpiDL*i77X#hXP{=Xim!V}9N?-ZKwT$j+b}(;YshO_P6~sDKU)CK3kl57C z7^T>g?OpJF3h{*vUgsnC7e9u7yFwV8rEwNEddf5cQ;eYT4<4a1&oGS!aVuiWy+zKH zdbT4WJ|k}=Zn@xzcRQNc=3vAawEVknS4rKgkd8OC7=t79<5Y4`&-p3OtburPF=PRz zxD_b8v*c0@L<`}<`A!4j{WofF!%3Ug#UJ;T)h77Zb?`yS*2Yr9yV5^@mF}O{SNQm_ zt;3x${JAa3SN$CR0 zZZw!G3@`gQ1gf@MWws`BK@A)V<_oaDnea*_dxn^momjdW1Xes7^H2paHIr~y3mpyy zr=!jW-l|~kz80Mg6ahPb#uv68p?wB-wkR9DP~v-eZ4s5HkC%e8nIo?B6rl<}GmMFG z;qunkh@jRWH)0J_v0E^b|Mn=w!p`JGCHKJQq+Qf4rGw3!n4h`S^s(%)rIuU1iS z+7-F(FLmk(h9uA86~P}Fj8B6=G-t3c7+>kGn@)tegfoNem2x#BMqn-uPoQ%_%{=br z=Y05@8k64h`x04?FC}M&dLTsA7>L;HR}&DJ&%q&joI?z?6#&05hX|n)4k^dyQ}R(9 z!dSu~zaLjxoaq05XDm{fal%P30Zz!6j_3 z53{xOU~*&RJXwGGpxa`9(DHq%WJab7^Qfz33RY$*0DhBRhhUTc9?ldJlEMkWgm65^ zEy{!MQ^>r3@U_bK&dHit1Y^EZ#$#@iX`C#IWPd+8E+VR)CaTH)VRrOnm3Neyz}l3{ zIDg|2xCP*3QGUId($fV@$;jg_qF{8rh$(FYq+IjNeaECaI+tDBT34<`^l2;n^{~Yx z0=};2zj?tWtjk5b)UR&U{3*Q0m_QeQBCo=VYlI4g%d23&Zn5`t$UU<2y@EBbsnazeGaTSZ9)=b-(?-< zq&|3m7edHnl~=bwV*)@+$CN&wayfKWc+w|qM4zEM&woIRGtA|xWa4jb0mC_1Qal-R zD*g8xLMkVL27ZUyYtet&!>5=m`Rt8+Tl43RB@fMvVRk=>-be-7=A8;!^L#@31eqdi`xM}{kIt;FYT=9JI$b(am z$#vRob2-BbV_#+KzkA9U2a`*IJ5%Jwn8^3+hP-_9;{O1T0Nj&tgcJcYlW}Vk12r=_ zli{-|f9+gZljJsXexF}aj*!=^h%TBq3Cvhw&&rZR>x~VqH4*zDtsLwsb~ksb$QH?( zBlgcb69);fNLKfZB>TY+GtB~VBoc|lmj{?_?l;-yKVJO)&5M8jhGcT1QY|Ilyt&)t zbdzh9W(s~CHa{fawdG@5)&2IbZ~g#v2@UG9f1IU4QRvE4{i-duEKA;RsYtr*j*2X~ zYnzjO`ubgUDC>QR{k-`4&5NI35NMZeh#irqQsBSF!%{B)p|HCFriPoF7hPWYH9e+Grq)N+gSnRJQN)g4-D2SU=z<%4Sx-r>ZUGle{kVg(lz*(0$Xr#KiVJNzBm>*9!aXMa5I5& zpwF;{(vU5FF^CeW0?~F zh!b{PXsFMVQ(1K9wmcZI$di9>PE}phmJ$vtX#e&PZbD6c#6A4c7B}s|h*m%WA7m9D z8~e9A1N}02u)mNguk2%qoxQ_}f0UL7A8l`YuP;ok!WN!8I~Y&)eo3I~vhJ$ektk-B zOi}mY43O|O@e_olxgar*zFCqO&s0Ha3K}wMbYoaQ=opepj@;)^(aY3BWmnT`zsl*u z?o=F)H}=^HK<4?LC2e^ss=BK0?N8f5<0NM61or0cP@Rw`OBf2;aIIZf0e z9R)>dkqXW&wf_mr38Ik;OB6o51sPk?aVkr`HKNHuG}rcfj%6$!YnI^wZA1gV)r7#? zE!DX6NEztE?xMb%}Av=9M8%f9DnEgErV0)=qk#&)#oq^2HP#7=*xML z;t~|1Ifn01%gY25)VK}-V*mswz1h)JFlia{%^Z7kSTDT^YQ>zMf8`+8I+aq!^xn*~ zf@gVhDn6`DAJ;-5KGLf*r@5NUy!fy%b3&dnaX25siT`ea;VKE#(Ci%XpwmyB>WokdfJLC>)114L>gwj2UBW#3MOS{hZzz2tT`{*{ zA8AmF7x*ByU>R7LlII&KRLX%F6Eju_HJGt7OS7B~R;;sLLt>dk)^8-&z*zYlmR>A$ z#%3|Zl2C~Yf1&z?xCp>lsCkqX12|v~Gz4A4fa#`QH8&cdqnu5Z4Hff5D=rX_iA=zM zh697|;0i(p)h9ED1HQ99g2$$-thLKP!7m&z0D8=T--D4{+M1Oyp5JXLwUZ`$f3A$xgs;vu8g?r{n&qJUg^oF~ zyoEW>ll&j%#e)M}1pSytLc$i9&xuNfjEB8L3!rH&AEj z0T}ezHNsSu<}#0awkFe4_z8w0Fp~&S77i3H9D@vKS>hyJ_JL5VM3;^PE(eLdK=u=X zL+2Yne-@yZDDMYE@oAd73(33yJ|n)G5-8S`JlOA33AvbH_C#&ZelZeaO$N59MEm2K zy@)DYL`ro$J>_(*SJB?5ou-sOl|PsLisjP~wx!~8v%hqoaBr6~4(E{E2+aWhl5xv~ z9uxrcz*iAh`gK+ioitB46-W3ZW|7`+EO$($e^Su@dvZ6Sl9<(d66Dao0pw866IG@F zzU4ZW!dH=RR3x`;vEPCodb_10DSKP!J*ek8_77W!t4dxwH|9fi*T=3y3Y!YJ2DX|R z&HK+Ub$7O8$y2bI_IUgsyTK~9wtLe%@8hBRF(a&OG5*pyryk8nEDl6!#L^kN;zCz4 zf7P4=GS+|%lqLI(H_mO00Uezd`-E=Gx^ZUvzUfY7U+rHab7T;kfOn4$9RZ=Pqett6 zCrNMp^v*4SPU$Xo5uA$qy6Vpd8@V7e4%WBE&uO}S@ZRNsuv3F0gkFjF4`po^EP{pt z!v`62%4};k%>Im|^8~s4(G`utz!Nhee<(x@L-QQH;yj51H|D*K7S#t|Z-4Gx%a}Lt z%6moerJW-bcEzc*MRx{n?tAEoYxD&IE;6DBwa~r03jIJT!QhW3`f>sG!6={_d4}jJ zXA77(OXgvTb+gR2Auxwo{bAG^zvj*}DuBdOKvySj-Y#OAN^VE@6s}uaB4^)Qe^$M( z`iE6iM^n%o8^OR<b1u!!shRR;4ECOVki`l~ z`Pjw&#|v$($^3Z01K0@YV4v%~f5kxRM2Qco3!@%ESO?>n9D_}Fo1IiC1uG3~5SWMa z7;tpc0}>xJ^B9W;h4a#ehew}l)Ag|~ZEF9@MVj##86(Xf7d9UeqmtygG`7D44e0^=rfBe_5Kr_Kq6T ztHXiiOm*Do&>WA2IkS)rqt|^tgoW#y#~pliZB)v@Y@3jJs_JuJb~kp|I9UJ+jB+zi zVXnd2!5~4uWRt1AzD607I;8-qqE!5x#E8 z%qPjLW;00?I#QLho-dgS7HY}`iL-uzujWRg7;MY0kHpd05>!f8oosQ|2?hy?9+brC zgxrOLV!=s+I2c`P|7GB{%_cBQz)+d=6>3uMl-8#^*I*{ew|BOyfAXD4=s?GlOb$pU7ht5GE|LtifhHH7mUkZjfbU)btk^-~%fv0~ny2J3QS z^)C{ulm9lF>#KR?0>_THKH{+DFTEoxLof1)YWg(xOxp?ithr8oGGhQY4(L$9QcW4` z0-X{ta#sR6gaS}jWD?PdC}Ek6q4G?pxn!1OuW?Tbd)jOye^j8bjEeDV;S50AQW+g6 z=i(WWCIhPrpe;f}*aw>GXL)i_PY*dc8G%V_5J*gsQ2x*d&h~wIdhDJ1!vR~KRB4r> z?*pG5!oW>X2-zs7VEl8pjSDlV;}A$ogjT4pRZ! zF>!&%Q}A9Nf9vaMPG#>?{9KNjo4Luqt0QE>H3%4)P_}66f@Ek9fAJs#`kyy^3M<+-HsjLf^T}yG zA9g=MoJ@)|_l~jpd@8}LYuYVTp7p#T0Mgv2P<#K|B6`}n`a4X>picXG+yrARWTW6K z)EVCmE~JBprFRCD?)%S(MZ)3s?Og=C0Uo6R2O z(rEr&l)J%ZkJj8AC;xkBN&P=s`c2T%CFo!*1?90}fGBdU*FXmxiew#gPzm^5@(m*} zZpJ<4m?Ms)Op_RV45vJ_`+->IAfVzVW2si ze?BcFC@7fhf-_7HkqMSk#o~E+N1PbI;u2c`|AT&*H2<{yK|WJa8&JC7?k95DE$BHR zGJ%Oc(M(>AvNl0hRDmwzE-pKjxZ&1>6X9n&**-;$5k2peHEQhD1c8oFE5?wIMKxd) zf7QdA4%y@im{?g%wyNyJieqyjBuwBme~LoFG5irJyor;90@!x3=FBam&S4QjMG2h> z>MRJWCvvY~ltbZA;vyv8Y%q0E6>iT5tCw7N z?{Rf(-Z{MON~;A`QuP=v5xKehK+W9;nvHiK<}=U;Z1If5vkbU1MP!UG=VIvOe+%Mc z&;u7arv@ZM1GuS%+2tce3cwv0dQpQfNOsY)fHQ=^=yN^0Z1ZSSa)688RQy6@(`dO% z7l^81C>oaa_clR6^28l8hP|iGmRV;4Je-=k>8(IWW&>l=+vxD%;@zgm+-||gKx`@- z{E&KVGPv$R_Q5{oi5t>u3s=ADe_c7+la8qMunjla@_9Vlf1G!nl8I}5KPMV!HiWjo zmkZllsa2;)7#5d+E@4?C65WLD9toN2NN`sz%TyH}%q~fC2N*&Qia6s;GPk9g|G`ss!MbmeDSqec~PebTnWXx_JO2N{L^UU`1b{7D(iQ)BmQZ!)+=wLx79GM+}+3PrA z%@q%?qQY1X%3BWRN;?mfF04h4-XGj_Upr-I^3uZ{3MNP@U~5LTm-{7Pd`LQ}ZlgtG zmtdpjN?mrD&vyw*M(IiQe{1hk10fR$~>kA~<7NBt3fdofPTYBPx+x(Zg zXautrU7)V>IcUv2-srUV(iMdPYNL}?aOs-bV-LNw-FgB906m^Zf8%_Lmuuz1FM(z} z8sN~Bj=1*^?k@dWkbtR_XJks^wSg&Ku>pY(n^kUD{JH_*&TzGytF529x4(=;D0{mf zeh&F80AvP80j~|D;Koc=^cs9XgO`$+)vvEif&swF$?P0#ULz1U-o%6-h?+5};=Qfp zKJ=hzbY>`Chyse-;M-2-sqZxNX*!_ZA^>>O2c{dg2SL< z=;<3Is=7b7t_pc&!Klg2G$n=o(>6!b_YiV`;oOFmLk*S$`%tx@WXA6%1paUkjt6TqgQ~GIA^gqVAa50GkIvm8N7CMPjRCaM&x2gVE$ zm@De;^;}Dlf5mUsfMpZ4dASf_)8m@blmX<8TDe8>5*L3gWKA^-X_7Rf&XyEkL z6^k1_7Hu(l=(;i@iFrEQXHBCXeKa)dLLwPotl=YRMj`E*Cai0qb zlE)EStd}K0i{2TESAj`ij-Jn>_&XB?VB)rlc5kL8O6HgwS<_Pe_9kF(&+ozu~VeniS zS27&IFYU*mhK55nB_ANv#Meit1mNj(UvuvFf6Zz9^2ebH(l@3bHUb)Myag5|3S@sL z=DP|0%1~;@-vHKWf4Cqj6n8-z*Ti$wneZQw{jMRD2_!G+KfWU+M9!{j(Fyimh z$BrJ$`q0H&Xbx6{pJ$}hhShc~_?}~&Ui9^w7ykoZr6ewsabpt!G?(E~0TTl=GBKCH zQ~@b})mlq)+%^)v>sQR~C7db^f+rtw<&d?VR8qT1HrCC_fir|G#vF1&a;$iN{dNNl z4oQ)+rL9WUUNj^DG`bsKKOw$(x8a-buKsaz_2svM$D5R8ajZ8tx0^(6k}PFBg|}k! zHu|}>r?#y7?Qb_fz+53mb5)|4NhOeSQ(v}!cB^=Fzm+EHwmWHfblWyZ|MdDpS=jnu zk;nToQ?lGj7DjaL%MjHaAC*?%VQz z-MQTg9$6GfMdGU^mAu`lIE{XSnJ`I4N1J!&))xNl01A>BDo97&S6esL#D45oZutFH z>8NSnZzb%~ZdDdtcfgzo#m*&g}&=$~`{gOqtx7Vq$XI7#MXQ`pK6H?{vwB&uOSc{)&t9xS%EEx)t6OI`+-zsxLp&zY7h(y_#CcOrF4*(t?;1Q@q-BWAl397MfwiUorI3aI1J&j^;LsYqocePIu_ z?pxrf^MK}Lc!Rlm1>&#MQ&ZVN6ca=twE%;HWyUxL`DQ0$@BxBYV}h9b;aoU~h5cHO zXFQR*81J;z=n(~KXV7~FdY7@I&$2WLeo(Mji;V*BD+55jx$V8FYcH#R8RHz@(2DC4 zKopGJ+XIXYrV8KAeFx6hzj(O91awA90B@eG&5Ld}qqShQh5v}S+mlPMx7U6G83{rz z-&gi}zC-X@V7!QU^oI`&w>El%ErdC2RaJy=}jmd&| zTmrQHQ5^EBLKoxwj5B_8NMMU@6P*RD&E{Qkldbunq9b8&MubU*Pf-p1DQbu@E92F-j}D7EgW{TfF0-1Oyga`Wp2Ljn(Ex!>x+y*S7Q_z z&a@yDwYGxf$IC?AHxnr|s>6770E&XpWj}bPyld*IazhAzB;9CI1f@rgB51ZEx4XBM z*T>u~R(Tinbvqjyj)<6f;G9u2B0BH)aDbrqokwPVJax?HlQ%)U3vWt!zAm`3;{G$go1pfU@5`@Hvw26?q1+LrT~t#^Oh zL`3A6f5bQwVvx@>`hiSf(RugsU}woNObD(!9Vs|YSt1RkP3Jbhq^!aDmGvk~)s8E4 zB;X0ic@|9twPQEQkTqsr5N_!!g_ywsE263|$+tT#f8$Q;l2WK~I-tPw7oj{gq?9E3 z#l>K+Dmkok&w1G|%1|{BrHKBy-@85jyrzYBr9Yo=w6t_@=P4(^hb7e60q#%s(A(nL z`{Q#Bh44@Y4|4Mm6S;#kTPqk9hafQ&jFJk*H)lX!d??8ffJToakY$z~3m9JADlX``pY|020P^8@iZCbUD`R$$h)42(n$-i~VV zV}9}vp$76O9yuuja|nU1o|<(rfl-ku7Ar}qF+h`g2?-Sw5TJ84+2t8(>jTLryL+p&4$w ze;ZA67}%*y3=6oJat(&^<4T&Ov9-xp7D|;tq!<>Ccm+hkhn-+LGsFd#W=X8KyLf1& z%sMjy;0T8^rtxG>1mxjdJ(-g!nn&=Hv0(^^304rcsiLr(V8H{SiavJdBSF%I5E!K| zRAs_QM~&OoMfTCaK+t8~H0WX{435mke-GT{-8`BKFzq;mQ%JOm^C472Wj${nqa{&! zrCnJyDPCS*3cw-m65kM(L&q6ww{su%m{i)Ax{A8Px99p0Lfxe%>qB?hMKtF7$#3wv zn+1Cax0l{}*m0B<43vIvmRoV_{ub>+L&}zI zcB7fc{Ld9uKItHd6E(6-nJ|$WZ^Q#DLqMDm0qZ265hi;5fF&-H&RFu0^5Rh$7V9bD zXOv=C;;Cn9C+4HrFqJ|jt^)a7e+9_}1x+H)l)B)IgY5r*N}Bq2P?8o31Eet106LKt zs)WQKn1vRfqe=zAAzoMIlm~>{XK;Yc{ax%@&YxDdWSF!~wLP_D%{Xj0HRQ#dSwK(& z2FI3+9=)ldI=U~r&_1hIe3X35L&oZVac}&y-P;Pzb}H46xZ&hU&R0I$fB8Pi3K9>b zEaajPsuIXWGC7S|;nHh}tjDtJsErHNY28nPHa4wkcDItnP9xF7s_I!cU;H`c6n|P- z;}puCQh;ZMW#OM8e)c!Rr=}Z9aJ^oF`}W|_)_3X3v~f=LFr{g|-R<)(bnIXL^6KUZ z<@&7t!gBqecGOhI{~>|Qf9Gzy(xh7~dz3OPQ41$a(ibRnxh5zn0qB&{^t+9gl zKG{^u&TL`7BLDd4hQ=^86GDAzx0thZwg&1GE!@Av(7vvraWU)_NOOqUCYw;|Ke4+O zl5vwv)|8}Nh|$M$fA8;A!ThlDj5BcWU2`biSHxO?=NO*}&Yx+f8ht6d?htp@G{=rlc!^s17PqSW4u0hAf}hRb?k*(|2pguF5-Yp-PF6n9$Lq7 zPC?9(Cwk@4()E2uclNoTQRSZ7)_--zbO@by_)s)Q{J%RlBBvX8f7lfr{oi2#JlHw@ z0u-|qUcdp9IFl$3GC2w_Ol59obZ8(mFg7`t z!Bhb#f7M!9kK0BPe%G()x7gU2eP4kQ!135|f_O8?IM+IU;7HVrVo8)K%46@Z->*oD z8cNg=SnKQp61$pRth=fY(lEgS(}G1Ng=A7OsnKXAy=T%fCD3?gga*irMkASvf+@qi z5(8!N93EEzt+PyljtrJ*5K?#o4@l!Q(++I}e+iWJO&wK60+M8ddN zOdFw?hiJxV=7Dd#W%y|a6(N%L7JbijfVw_FWMu{(qDwFfni&;63yzH97&V{)Ok-fZ ze<;yH=0Ghv5~3St@f=zYtZ1Q{vV-WD6`%)FRz?Fv1s%zXu(;KZ0K=>iWM7!U7y{6S z%HYgevOF*>G?Z|kbs`KTM6ixfX~;!5LE>Sr=?)(9o&c~6iiQFJ_$WqZBQb*0oP?3# zFQ>_4GQyx<4J1739HCGYcsLDChHc?+e+f!nU|dpUNJsvM`N&36IoOvXL3tRM1clJv zQ@m;51Z@TqHuM1&B)&dM0)P`O0TBR%gPcL2BMsTn6Chz>NdTD|K9_K1KmY+S08AW6 zL{v~@S%hTJWC7SEQ0zzqQ*dNRa0!9p9v+VxrjQ7!=mMUy5oSP?kkjD`8YA7LfAD7T z_%Y*GjDMTI${D|450=@moaFOFK4f2iJ$Uek_6~plhY;dBy3FZP(xsqFPL~l~R&*J* zqmy=Y{%|i7waes+E?G-vL#twZv!9mUrS*y~w=Kaxgb}TfMSJ^hXRH!?MwdmaS=r)` z?&FWUmYCfiD-W_+Z};~L?zMabK+j+xSav4<_Rl_qIdsbzmK z&APT(w!v5Qd$?otqAP9(`&~j65tuo~awPM*)n(d5jB4QJy#Sv}Hp_ypeQ%9$tsMnLPq_QQC|qm_i45#f4qiN#JVXFaoG zhg;e;N^QUWp7wiUCF|DA(N+oCnG!d1?bh;X*&h^JWn$5tm^MsKe+i<^d^&5f9<^(r zE!RwT5`Nq*!lUk!l?~qRH(=P4Dj&Laqv$5hR`FQ&ub(=1Ry+PaVZm^4Lf)@b+%vI$ z>ot9SJKq(GXl(J_%b--3q?lIQZT4L2t z=QVbWg8jt!^GW%Je+WH(Jm9ZBE;7b1)0=F-pXT#2o0m)M0SR%yf6A8msu*TV+M8E* zUS^|7`c3|Uy%xA(BUVP({RbnH5Z??yrvL#Ko9Ca*=Q+r|rjr8V4AiRebrb3)R?VA1 zN22P{0l!*ZmsS6V$^7krf0GxZtf+X4H~aWTX-s>_IW~8O4w>F?7Jq_t}6yM}4Sl z^qfx9!n&H!Nm{UNC$Qkx9gR+CJC=ny*C7MHEb`%1e+IXJ@x^n-Uu7T4?IN&MmgvxR zt1iX(Np&fRN}gCtAM5fI{W%Mg(E=$S=qwKb66xqOagGj65img?OhR|Y;0HTvz*Zva z1%kqx!~{BY#5PVHt_A*_70Dz{(lMw;`qObUq(DkWgPL&I7~yDU271KnXC; zB_hy4e>#?odqD#jnhb71R{iAV8tM3{!7c*eky z{7x=T;}R9!VLa(A)UnMZWsVY+h#cvU zxKSGTFwjdKmlzfQhC-|qO#^xk(}^+tQ$hA_m7co_OO4z?c(rn%lu#m{B*z)&B!#GQ^g_$S ze;p{H%7nzCRLOu6=zGN&Pi|I4mJHLy+2?8D5{;Ok`C8EXVU6ufq&LiaXQuy(O|WGU zs1a1(T$D^j6j@YelEX{;nT=u~jX@4KMllcuQ^aXTIb*<5UUt24z18cE!tiDX#S$!G*NvkjV8mhBgdRk;@nHTKA%d}if^Kv@5 zX76=^G9|xcw`I9le#QB$9ZT}!=3%AGXf@2bk1a-HcAXC2X7kaS#KVRGlKF!C^Wfqu zEG1Baq{p-wZFnTi7eajTuxkf$e;+DKy38s^@>f6n`1{Y_|KlS2GyP?Cl@*gQTs$e3 z<#8aR#@M zpI<{OzN};3@ligVrUhSQhymEc2)-cyPP1`YL&fCg7WXw@Ojk=j%xANd-+o-&W^;OI zl8>-&=Ks!$oagh5mv=cYZ;LGBV}unSPu^#I2}P{qSw+Z6PLtvkn|}Tb<$=FIspXd} zdYpTHK#Vpks`9gK;mmrs8D>Dj)R5cfl3Hz1rk6cD|n&l?a<|K)%X ze45U#M=2jqw+v`@4QTrfxUoSk@PP!FrZ>wfwT^23WI3#BO@zo|OJLHdUMn06u!i0om~Hxf`n zA>tb$ZU8ny+yZO_f2b3a@}lk|g+e31TP8yz0Jw#Q`3zvgm>z2w=d}B2z&S*S4bug{ zhG|*9vlpzf9c8*!@AM4!zz-Mbu}Mv@i4hWY#Ie`DSXyaU*9CJqf}s&^Eq z>r_BZmGot;*!;DlP;2{GwO4kzmH z$4~fMIL{eDAJ*0_B18KccGx*-wZ?T79I; zX%n~aEdf0NlW%Jkw`|A(wgHo~Y!$cQ3<56!lYVX$mu$!ZCAXl<0g(Zd6mJ!mP%;65 zlOS*vlPPcclW=hpvo&x!0h1+i6qi6X0il!Da}~E=iUFMgljC$1mvB4*CzsDR0nw9Y zbrqK$jR9_xRdyDWe0AHC9(NO$kd6Veldg9amyH+#eUl-16}KN50$u@=E_xN0a6AF{ zlTmvWw>upI4gr(!dli>n9s-J!Onnu%{T>2G0h20z7MJzh0mzdKe-)EIe$SKmfD^Yj zLIKqQlfi)&lMI2klaYcGlh%Q|lP!Z2lca))lZJ#7vp|DJ0h5A-6PJJA0q2t-hZeVd z-~oyOlevf$w=UxWgaMNji58R3h)0tUiW8U2oB`#NwTl(EpymOR0h8d26}N4l0Tuz1 zi9oE>H*QXwhN&cDB^u5PlMBBimN;J%E=Ws{5u zHXk=^^B-^i@czvg|IV4&Q1uc`A+DhzuOAUlEtVQ{z4y z;kxwY(LYsp+$3(E_K|*1=esRW9uD=c9O|~|@2R+q61F)v< zfSgT#c+M(I3;4B2*q={+rF){0r}nU?rvPd;LJdz(6~L{NA+==WX#oCg1y2i}dzwWO zY}?1U8 z8cO;(b?yGV8{D@?Y8=|3JRA_z7iwrbz$qPnoPZM=`t)uqjdpZ0LGt~s?T8{IQZn_2 zQ@TRDO0qN;e6~T6$%(J0K|gJQfS9Lj9e#l^!Yq&BS)OplmY}dpsik4T87L!CEoWzN zPrK~fC1}RYWqF#jEFSswt>toQDQ33ZwPo@mWgvnsh~(Bvg-P>#wh}VUm9;dN3+EMo zt(Ad1M1k_S(J2TR5>A%>#R@=q5wIdxDFYClRNNt3JrS}$f)rp$*WO7D!rV4Jph$U% zNV;j8ZBur$fxp6b0QEdcKI&P!^xMp{jDDf#Sb%Vk9kB{CCzwNdg;RTdKx067{q9cTDb2*ZDM06c<&L)I zXk`wq@H@DNLpfCaB-cTDFdk5@QYy9j)Drgx!4`$qRtb11OsoptdnK}~K-Ai&V^w-c z9I{w)YTHBQo_|KlD-jIT>35P640LdNYWo`TL=D1Wk$$@BPzJrngQyVsBuwZ8T!(<>h}u#n z0)o>fmD~`)p;|r<_N+2b>Dg#RWQilS`e>=nHhBTF*gqs-G~A382%YgM%o^< zgVNf~RX9|M!fvn7Jl@HI1Tnlk0RErr{qRI{&}l$>Uw>@s z<|FWkWQ7y&er^z&?YZ;QSz>_Sfym%IJORQq1B8JYEI}2*`mLLP)E=BO|KJ@wE|q+) z1L+x)3J1T6Z;W86(u5b1h;V` zleniZL>WXr0HzhN(EJb(XUNx%++44HNW)|65f}*ie?&%^%@-;m1?CfciZ+f)u<&9t z!jcjy&5ewDk0e`vT>u*5tHq5yHz9K7bO}e0!4a@LRfV1#auQU*rS+F-koGzr13q}w zHRa(8)Ky;~94||9IarxzDpg<^&X-0bAaiG%;VJQAeo7f))$s*9JLL&a<$)aL%%4dX zo{37T{&IA=-`C#D%XMS{1z_~$qg|ffyoAN6s$F^R$3@+LC_E0erVlh&+}>y=UOnuc zwhm2Q=4iN>Op)n4^<)AI<>MJ7n+nHv=%kjr;eZ`p=tB!K!dqganG>5me>?9T`F`a* zw^@h5+bokHH!6wjM<*2v=t2&^!#tWY7xtEy17BwuH zOfE#Rn-4TBXUQ;IWeN^-=YtR6%sd}32^lb^1-6AWpCq%Qn*0HED>5aJrNnG2 zi+o^G&~8g?u(}5jlfbTeWfPN3106OQVHnCm4NO|FL!UOWF%i<4&~4W`YoROahO9>E zEtCb<-S1Rpd8)yTP=PhE=EQ^h4kTf{_;{-`DlCk$Uc8}RUr%x&A^{fT?E_d7cxQ@na&nG?kQmzZj&;c6 zLKoPXI%xbiWAr{;$XUzesy1)RQ{eg|5D3*H)#QwKS$0b2ctw@P2*KGOl zc$V_4SUSB6X`2Tw%1pU|Pw|SCXKED&!Db^>C-w5;dSr?jgP|97d&hIpGE#~XCe2~{ z5$4Cx$qqx-eeCpMxPyr1SRUpkvobn0+7WT(h$Jw&$7o^S9*}^^8rol4YUN>naG|aS z#y1}qc^Hf%mV=Q5j+`_%*w~2ByGN3+HjC{6g zVzOl`0T~MXwZ)~E)&y_Ms2M7M#APZTln9uvU_{6w1v7Y-j@LRL)A2=?f+`xb@iq%T z4}fPNq)_T-sno?knR?9YsOPHah*h9~hZLpu>RLi<_ly!oo zh05azez@gi2>@Qs29=LigQMkOql-2;hi^TZuGIr;7n+>&2Xov7(p zH}$dZ(bUA(5`QwAh9C=nLixa>{p^g6eFt{Kj+!t&i9>8W6B`~5?G7VqpD+!PE1Rm5 zKF@_6z^<+LXUsX-@fW#ViNy;hr=4QXzixINn8=9TICVG1g=#=YIVJuk@NH6WjNbcr zVLKZ>8sOX5_9k2Ej1zxFub6iTqf8^defKl;FuN*<-C`)KD|di@ul0$DWt(qNWXvF16|e#aQRRlz&)MvxAD4|d6>Z!)t3FVRfS0YR9U*Y z>ZgPL6_UrGe_%e<8_yd0;DYSqXq=6G!d9lbEO>dSPnf>Mpez=C(J?^#M@%T#60Gv% z!qm1hjQODTa950flUSEeIdZT8bpPu*0C`sT^QtgtKb3WdNa(MYJ!L%2ieiR@)#SLq zC@n^0U2;6v%evb}U4zW{8ZK3lrvg0PXnDUwzZ+Lqk_XggK>X&8=UQ?`v91odC*u^< zkdE)bO(usIEB*kHh7N%x&)6HZXmwQv0(x>;#0&lwb6~cA3_6^7tP2&t5iwXOru3*0 zAa%Klp|!DU>0LJ9ZY=Nw(Rr*K-bm;eiWN=O8Z!~^SIBc9eiJVdM*^?FCs_uXTgIbH zcMNv*dK3c`ZUE_aqEocg&^j0`?c>qnOC`k|u!?q%`gN>8 zgPjf}UwWKji0d;$9J}(ta;m3tg-_N@a5_vntn$RoTF_bu!r?Pn{`}kD4 zD!;C5KGBm8+NpDS~OItw{I)#tkw5WW> z!yO-5${`UiI_30mEI+y8jcZH07l89 z+%i=(&I_(R0YKurCWqHo<=}^Uk#j(Q5-Z^n0p2P6lFUh4>wu-rRYS=h4XR^G3+b>N z%zd}oLiyT497~lc@bpL;$li^z4LdYQpQ`diuT$E~UHEwGmn3b4lVP>Vo}S0bZeQ}L z13vS!IY&TKajpfzmo>u+D!;2Y9b}qw9i!+Ukgg66Kr@jFk;h9+3V6=vGE3Eem=JDf z_2ZE+bs50r$V9WRue>{dX}9OMwlL&DL2W1J{1Uqc#jm0;Q{1U)an1WY_p>Pl%(TtnG^ zas*j_zK=fbfI=V2-uG|z(N|}Gkhw}gT=mg?aC%^HngD3p3<_hrJCuFz!LX*86}DC|U7_tv0BL8oI8qAdqjfigPlgvFXJf8j6+*QH{(dxI z#joZ(&KbPB0SoARGoQMnvc-{*`B>4;=arJ{S_jjkDxUYjUSvqH9fYb$K{Xf48| zR?^#2!tT^g|5#>W)Mz46j*?_Bff zJ`~ zMV!&zUo|c;11(+qa3+E4dbIRgQa5naTd(|H=!;c~Mu&pBpU+Mz*`S_m$;fsvc|gj& zD}U?9n`;M9qKcz`Yi9Dq4W~MKcR$jb#5zC>SlP-rOn!TQsJg2~jRlZE77zNDHM>ub zaUXYR;i&DLCDd4X4#vx_JX{ZB=LNXdc|7GGR*Fp7m4eqISrOYDrMu8;ne0U6V^!{4 zF-Q?q?OiM>W%8eI-(4I9oK>1L74OJVws)_!rqmku7nG2HQpecUxpC1=6-N@{TQb7_ zZ-Qu*Ux@*!^u;c0K6*}7_8N}+C*xSpP``MLd z^4GioekjR*s|_QQauV35uG-Z-%(9KDB}duqaZ|%zk>Ue_$=VndbX;@)rPH*v>J3AR`ocXQF1f0^@8A3%+0dEM3T1AS)Qu1VGc%KcZW97BGMB(q0V#jw zJ!y|)w~^oHSLj>Rfc2L5;cS3))=r%40UI7~u$W1(8cEYFM=f$qUd&NzQ8MJVoJ zs#hGQ(#*&nxC0>IqZv0n&_gSST}5Z4;JgR$PD>8`SF=|Mw#dL;2~1t{=!-mj#-azU z3WOIWat1~eOzQ$_VMH#ddzz#G6&Pk{iD3^+o%0lVhD+MPXm{o63YvaHAaSa8MxMVQ43-x=m`l)%pwk8%#I1rV!Pr;QDqx>}x> zyTdK=B4MdKQjT?A`_SOw2wcnN+tuduN#Nh@ehu8M=ieiXZ%&Dg^MWEa1u|ck$cFXi z<-%`YhlO=s;j)U4T)WN)uZXTvc`1n}88EhFQuR<`O4GrV&h${uG&Fy-*5TGEZG|a8 zypuo$hdb3n7a6>(!TkVfnnf{BLa2wWO6(UEqT1sKUY8>n22BMM{({iN+rGJ_Brr%rNIU$f+t#Ma!tG6KGOyE`}&*^e{DN;IUIkhe-^Qiqm1o8k zKzry!bhg}D;!(g}InPxF;wHfi(m7)@kb46s&S>ILC$uRc97feJ0&aSSE;4vmgLD5h z$D*Gk;gUhC0_#PAp)eeV@xJISjDU86nSPEER3O=dOB@Sm?w)@p>E{?wFmh$Fyq>hR z_ynojDeI9VCFsbjI8rzVCrJj5bZ~~+9GRLSM}qn#=6Zd1xPLeu_JOz7$7N9a$cwTw zHpx;KS}%{ty|*_DZtVPiyAPIx0A@3twlFl#Vmsy$sZJ!r;}0#118@;D)o~Wb{xLR^ zx@ZKWD2GZ82(Ew9agZX;E6VZS0FBlBt+2c`(3=Hd{D*cwC9#_ro~KN%o~Y${N*kB= zEuSZeg3Zv`fe+GIj@u6G0m@VqKx8hkmbm6#^ibhJ!DtV(1IjC9j3_`JWSarg0=YE< z7YVwnphfpI$)cDgV$*|GCFY9~ONla>L|%qU6tf0(cAkHx2NhUmV1Z+X6obU_sr%p#mWS)(Wx@<5wCb(rGXQ-~d<&AKaKxlA5Jf#G^sFKkkPtHEcX&sfc#Q zIJVq}O(r8{dm^vF;ouL3j4GO$20R4NcZ)`7K5k`2{t0Yc5oHHp9; zy2#*N4eoyjNYgBec@jcBY*k{vC=rD}aKMz*1z5qfLK!8CnMzoJYY#3_Eugu7o~NJY zlAeLR%9R$BuGvc}RHCzm>omKPy8s(7#ubWeFlqc4cPXtq2IAF9utjw=dB1yH-=br- ziAJv{2~^n(hsW~?oU|a-&v)^|DtUDSwoJ$rxRifv8JC+^J7w`o4n_!LNI$87jZHAb zg!3H(9qI}&N-J>jJXwetiXbK&4=@kTtEgb6gV7yXk_A|+&N2>ieOH6QDuiL(te;35 zSfWIo-J_IYt7-umOft*i46+|@A@W2$+w>?^jF<7R z_znX&OxTi35$3!i7R~b%u#)q7aK(BBIIn-TpQ)$GB~7E4ES5UOdJ=p%&Li*HD;+Md z5ju?Y1l|*44p(ZHS_R%izWWf(+tb}oI{4@X7%XoMs>`NmLWaZK76pGz zIU5+Y8Zzm>&^+GZnrK03uWHF&ixRe4Ki+Z{Wm~f+!dw5>UZU9dUFQwV_6%hpw?mL<$$VyT9w$wcXFH$MuYpEk&A2&OSBK&|h>Zfg& z8{EHnGY|1*9>ZL{kxO4UZ^10utj;kRfyaS!JOiKP$nP+2_T}TdO$@J*7Wgy%CqcIR z5Qtmub}1~pjxo9SXZ+5?95R2*5!449*oz=RBve7CI!Hj9QS3!Nac{uQgHz~a{`-5{ zl=V};D&TeB9~k&enqcaH->59P=^0LD2_9mrTnR@Y)0%h>eKxIh1v4O3wGTE#4;H%9 z1|u=M8VoYmfE1pUQ=!B+%z{%`;FW=^dL0=~ZXxVt3|c2J!o2apgG_(3RltPVDh!a| zc_F(2JT1DIEfc3n;bi#qt4_!T|8QvdfWbt^kW&ujTFVCzA?*V0iyAk4s8wnd>>hCX z8mxY4{8U{pY@e`*Vhx4DupZ1zOut!>Oq_!@#>J4a0|0|jW(4fA_dHmB-7FB{O=fd8 z`wkonRT4OdRK zn%&KQIs1AxYpxe_6(I0MzhLxJgi-#91y{{Jq6K3J+=D^E(}L0Sw9Z>Oz2LgpN3>v! zP{P?J`-c`RC7A!rVbXeP!F98bX~EWl{uCyJy&M?OWZM;Buzi2PaS*a!d_LR3Y5m*&uLLifDaJ6kYq1mr6s!ONDwS|E05JG4 zJ8ovL-d`9E;EI21kg^L4){MQuS!aYlz}~W*9>7;(W<7 z^*9$igX_u58DSXoe7QyU35;n2Z3S~=br}$)9D_#%d_o2{+kZ@EXD<{nG+;rS-2Z=) ztBAb8ngSG$iesc&g0S)2V>OmD7Phl_Y&d3H1&UP+tX&xpK?&|js#5ZLwvJM>cq8~Uf4}d)(~#19pjwTEVa52xfibc zpuK5Rk})x!I+9vNihBby3Eaq5q+1RDnDm4nxyZz=O zb~oIdLBES!523&4@y}bcr~eL%?H94yeo>U$M{Hf0$%1pM69yOxhPwS`A1hm+ zWngA#v_72AluLupSZknwnd!^+6j}y~BA}<}&j*7X_PZOQ=0AjQ=ezCxHhm<;4Q*G) z!|Aj+#i7oz-{biMc3d9U^`y(Vf+)o?QNh3tPS+c-yxGmPM!$*&`hD1*?_jt%ZM1)? zap6YKUne|6f`unU?>Dh36SE}|wTH)dyX|Uu-X8W5f+MHZ#BshmZ1-^m?*qbL9S;wO z(-ymhA4K@G+^>sqPj_jWW!$io$t3!sFfsG*iMk%T7O0l&On}vrGAc7ZU?~L_9vB=% zle<_m9Zjh4r%y5gWui9{u%r*+r|5s9^mS0l{`%t$u+-+^JgAQvPz@`lmisi{yT|Qr zo!sM1N5^q^b0wS>ID1}eLG`O(XXAnvsO4DH>R`0qfDO;s{PQDboAL?qa6*KM$4vBh zYLt(%0m<%YC0Ib8u4DA@c9i!fclHZoY?4$WNZn|2@<~WZHkjPIgM0 zdbYPrn7*F2NAQh;UH!7J{{@+0!J|b(rensF=Jl~+S3a>qz^minmQ@qKiwG~n3Raul z?q@ej&Ev|~DaEMfixz%ag>fu~ACT}Vge0bRl&?G8Zvrp4oJ~~oP_;igH|iKRAm}ez zGy}&}yo_d4Nooc(JCfwa`(qiT7xZ_=U~LW5%F^;~dr!49x)WMHh05o3#=Ao5!<*I~0^-2c2m1GucYuh^9+(_U}F z&f58|MO1J(9Uee1&EtU(E!^ODEK(?%r9%EoT!1-IB}t~Hgc@rLH4HuQzY#+U|C~+1 zn!+|`4c0BaoLI2e^Z$PyVLPv`X1gXLuvPvVOgx2~|Gs?c+5CNzPqBvBUB#zTw|PUi zRRlG8nhK5m$OmFDkukVBJ#t#Kkf8!bq)M~OC_xF1h2vl{;F9j?DiJCYIMM&M+})F| zv@iqXY8S4%!7uM`q@N$+7cj6-q0#QJTJ9F8e^jA=;D@BOp!I*2V$Ke2$nYB&g$hjM z<@$&6`|`XAU!%PXJy(Zhb>^$jZFTKLXTZU%7WG1PT}cC%52>o9!I1m(lt|LP-LKdU zyz}6dt1*w{N*l2ur|1Dp*Zp~m_ILDjkW@Fq&o|rKJ9MGqMpx!1IFSmgs&H7cyJ9KT zGTWVSvOj>G9zTD+zkvgFD7=#MIr>ug8yX0!^Wj+86dc@H?(3!OumfSrjdGr6e91Jkt@l#N2TRUPUK*6U6WKetAyy&hH8b z{r+$dGU!mi^34q)oq^q4_w2NzU6iGMJnNA3RW z1`Lwrv3-gsEgf_RUhdNiyuEylRa*H>l{7Kz3Os*!C|rMz!zQnB*udA)`oekwB`Kx^ zDjfQzyb&mGM9Vm=Mo^uHeU+VZEuxjW#03dxA{X(?2Ux%^=`iiiu*Q7&28#yYJH5dI zQrG$X0t*Ft2ObZMB(NyhVPeBo{4IeQG#)M;@E3SZ76Z<5+CG(7|*bn+@B6PU=CR!Pw^-HHlc(os^L={I|s6{@Y`_m7fC%l91goA z=$8~RkHOTze1Hk`RB6icbxNxa<2?rGB^#P=_*|=_D+-LhC9W z&@dJ*bbzSrk`WsT{ZMv6I=trYhKLf8i#YY>;Z#y2Ut<6*NJ)yH2f8hXB<aaz!<>F0`H?SQ0VFNGTpO_84t_mwBI~BhlMt;EQAZx*ITd&PTPO< zE=58&Qh{=n9GUC9L+pP@IH&QBo8{@S@6t{ALNJ zX(YZn1fr|zHgX`DoBvBQ2G?mLE3f;rwx{6nX^Fm7{Uk``x7hhVw{0jTMk9$OG4NiO zgL$fD6`N9QVS1;EpnpQ{eO&Ig=O1cMK!B|$cs(9UH&F>*neXj`+yU>TYW{yoJ)%U+ z-EhRG2YEzeD7(B> z$v><5{G#b+f))1M1z=eGngw`EEIQ2%16c}sf!vj>Ie&(y=rk0F%lGGWpXVENHFJg% ztegJ@aaN{4rMt$)$4j-CNkV^B79R)tJiSR3x0EHJIYkk?WD4dBKIjrni%&-EW<3(> z#0T{7LV61k`vOEH;9Vh7sgbO=F0Y8!$=#qq3V)1K&W&#Wb~fF5{OojkyhpM7fM>hc zBO}>}WVU>{-5i(4Sp9&CF&*^btSDs%Sk?9%dcFEtxizKh+*(b<3zvTg-(71*)mt_! z_#e#D>v(F~2Mm$wMbYwleW0JE5AfHXo8LB#OF@rh4qZcJ zaViYAU4pX~m31c!56|Aze!>tX^xI8EIefQH)FG`#9u@nKL1rQ}7vR%+Yk7%J%E&*2#USK=7% zvf>z=4lK)h>Hgw7OUC^ae1`j8FxpSL@Af(Sj`v+&zK$k8l$KYx8uOG=}US} z;kTfo;iuks2I#|av)+c609-P#5qx-f+$9Ij)iyRYczlRG4&nPDO?qWz^8ZXPN%!&k^D%9VQ}Hedv$zXRNsWUoCr@jP!|k_WI`5&glI+Lo3)_+ zX@*EGFeD98Qo?_q)pCccm9aPxK>Ncv00IR85}sV3R3V#(-Z2y*Qwg99MUC)%f7qTj zlkTN*gi?iCTN!qoF4XrfOx;s-WKG*J;B;(EY}>Xcwr$%^reoW-Z6_1knAo-^P9}ez z_dEGddR6V>>XW^8;kt2gnOpSa`!eJM-OEQxo1;(DT-N=wdgX13`yhqJxj7VgMwLSb zdaOfd^Mquqnw5LE8yxdYdfH@zx8-a?_`*ta( zykt0=bmi?BEUHA07XbqlXIipa&y2huvdMFv>*sZ}EFE`x-{PZ~QgfsIpNS3}I8a!g zAp>GHAW}mharVC11DgObjlXg=qv1G_hh1mRa=y(Cmmg=69P3$k`!s@yasBBYXX*keRvfcqY@Fmc zFCqGRQ>ts$-Knl7OGA`gOarBrdA^2@l5yR(UiZ642%*=~PAY?D) zSw0V~Ky9FQR4e6^awH{oVm;xpS~B?*P)A{O!D1Fv%EH>~l(+H3Zj+NPOLGqYvDda& zjrYN1MZULjA6q;k-57-DwdAC=Vte}z09(sZenaz9Bw5Fm0}`Jn1#Yw~#kxJ!e&XDb zfEKM7a<6tJTpKhilovG7OGE~V|CtPEa=q1mRQPcv)lWc!Nos-GWa^)egPPb%Pqt?Y z-6=QzGur&u`h=&AFK_;jK-pFW}Vr(BPx*t>Iw7Hm3Y`{fLQys!pK44&W zcT)VbxsdCwxcRziT||Ty8vBwmux)eE--`Xy1v}qoYEh(xXtZI+MT*6O z5t%UR>FvT_Sv}4{e1WBKc8b;_;E_e4z4$`ancLpfZuI_dwt?F`PEZT(ijaAeNF^SN zPnDBonM$_{o!)H0gVGQ5GMhUgX5iS#bR@2Lf=EOfSYBlZie?$7){(^!`o4Y#;+I-d zYda?6k7uHkx}@h?`kR#V;8CE&5KT^_8-kHx#b0a7!6n(+`toBh+1NU69PZ%*tii!B z*xb>B*(2`fr1}>Fm)0t-?uTaXSwqJV4Qr~>{83|APbu~fI6Th?J@Eyg5Ky$2^Olyj zzPy3T9t;V7c#y%&+eX3YH}>pOZ7LE{mK)so?fdb&rB|X|n?mc>`E5I4Q+&udz85DK ziGZ%NuxpUcy^y-e_hP4**v_G7?E{V{b=c=?yWcaGqYmc*B*8LaFDgiAKDt})FHhI$ z^omf}Hl9w0i~+ppziUN!l|W)qFSzP$=o{5Id_`iOs?R0BaKo)Z8!Hh`JMFQ8KWP0kaGzbt zUEa2KFr7Uxp3m4on={{SCd!RQOhOyDFZbm~vWg;kBF#Iqo?J2o@GQu;Sg%f_TU153 zu>DNIJl*WG2-yO5aVTnsd!I)${~lkoU;?_fLwdt4-^v-fuD;4IBr9cZo}~F0Q~-Ng z0{6EV#=*s$CS3WS7^bI{bin2O>&Bp9E6qqO*>WH9B>dJhc54mV=gKi`1%4y4c4UszxME zU9->W)GCrLL3f3J-xnh|-rwr08uB=Iy8RwjIN`TxIi}*MT=X)hug|8t2qUKna=(mJ z4y)-n33aQ=VpQo)#0O8@A8s7}UC9Z{P$iZ?cTpR8>xPw3y-dhy!p>A*T)S{a+?E@7JrU`3_4zxyP5@9Q|7#kpBRGr{rzdzR z1O(g4u_odRvU9`*kt4_?bO@d}AFMY~XjbYeB8DF(4M^m{lzkUQG(x3qYvXyJ%O z8qbY;1lFEqUDr=^Yaf=NSe5NyHWGo}d5+(-id*cs6dZf|wwaN15mK`+q@-|HZ#RTK zxgNoRBUG9GIRt$o$ymNR=aJ5(B~Sa)@7jrY%J**k3q7#ua`AOJN~G^zj+xWvQpNm> zf1?bX`g4CGF#z3#nj!VlJ<(LlLcz`LHcqip131<%gDf$!uq~jM-o~(7R%3dJDN6`B z9+M5v<+zJ+DW0)BJ$O)2TB7r%(u|TyH(LN)6L*QISP7FBs7Iv5 z0=tnHCD2(I=ORm@XCFV*0};MHv!f$7VNdB8rT12@tB2v~h4V0_^92~yUPKuAip?a+&6)>vPa0p01i8@`jMj`}1DSrm2k4>yC0PkA5iJCOz?>@Wwt%zKNVCn_ zutNRJm*siT*f6`p>EfdUNp7N9Ot9Kc*$EuFj}FyL?r7ohikpT_Xk*q9N}c>sjuDN( zq(#i=ajyKQVbE^LcDS|zJ&FppiRRcjHxFdpkyd|D@vN^e4o9P+9nTfrpwVqO5m+ON z@5cL^LFXU545O<(fLHGaP-|2wl@(-|Sp_*bg1omb*ZK=uE`RW4R}=&_alvrcIRDx( zU#s|Lc+gnW$Ac6E1OFqf=nG>L3w+|e=j+VOb3Cy0XEqAb<8b#T^9aM?bs`eN3p?ZC zAi3|bGwq{VrNrO;XkOLaYU3Rc08rk)mObAF#QRj=GDv>ka0uc-e6z|le~lU$8;xbI z<-s7EXEA?9`L!*-;jii34V)Si#8=~4E=egQZ9=m+dy%GLypiwfFiygd0-{#XwjdW9 z{O_C22cu;#u<|?|*XjkMKjUSv2(^Kd2IqMj$H{b9a@g$(pjX9BW}ks*sX&iPz)aFDXLDvDXT^G(R zH4;5ghYJu}B?f?|-V~T7r3&|ap`lfstQ4D2F1Xas{?g9q3a7yqdY3_~!QY^$h$BTd z8|N?cMLO;%d6EM-{)93KpTIN7hRL%~^tA);!vYWi)3f}hW5_9dsv0an(`yX9Fsnoj zMmZ!C18%!b1kRKuRA%B@L3iDS238MM8RH%K2BiEX<^yDO7K#z$OhOP@kXlDN=|k61 zn?D}v-)$M+Umr%bajiUgQ}|*uZo`mqNz(2NMJ&xY`syebG~g8Z9whtARiH(o zaXizO64q^te#(T5R_|2J!tX+z;T6}7_76Czwhyyk^CV4%WECLH(yh0HaEGeK#SKem zQ9BHdY?x8&OOfQ}7W?Gs*j$EEntkt@1(g^w#3(vLvMrK4bA5fBs15oEr;;_!#FT9B z#<}K}xV4M6pnU*<6p#R_gd~nL#W2N2$=jvOz(g0-O85YFpV008k-e~Bu)37vXpwqo zZsc$T85$cM19ZI=7NZr&#}W3-P^-tk2|}ot)YU3*>F{}i6gQ1%@&R6xDZp1NZ(5o( z<9G0k=+yswj(w}JB|+ci_n6SBnUJv5cp)>{YaBRKjo^(*2Ye&>)ns@=HrBx61xSk6 zg2=t9JP96(#@xNWZ*B>y@kVTAxpi(~vvN_8A@E1^9K$cw+ibcPc#W}$lk=;N?ZPb$ zFcvZfIiT>r+wll@;YXGr967iw0^Y&Up?uuV19AEAa~_jWs8Ib)+^)7E@ONEe=IEdKndUIcd7U50L^oKbTV_;(g@#!5-#th?)rh%rM;}kb(@7>y* zyFn79v$sUT_8t5+dP5xL)?+cGU-@e=aS_$}?&}-CubpPfZvP}Ua<;14V2fdd0)Heg z{V&yNqX5{g^&d9lhD)C{+x`ZpgIBx}T|VU*-qIO-gGX_wEJD!$19M&K{Gi zN#N;nE$?5-xLCw80Ru;(zO#v5Dfq*e)|0;acYwPb3_eHx)0GoTz{!Ypq>=ikiC!MV z>rI7ErBQ0$t3%keVp-0!3nM@1wBKrSO6?OUOwf>X@au!f_Uz0Cdy0l`$N;Rd! z%r@M18q_lZE`J;Y6_2=90FsV^4cT|5(SU5SdNL&>F{>_NGBKmH7c$i=A0KnQb{4H9 ziE#!|UH9}}>;bIq5*-yhTPb<`1EU}^(<3+^iaL=dYLYl9cUL7qL~k{L?6Pqw9LW6B z#l0wbf}Clhcw3U1cL_J8&)Gjrq$7crG1C!#baoY+Q0l?v)~s`bZ-t2-v58qjloal; zxr6-;mit7L&YRen*&W+PTXudrEHNG2TtqcThgf2ir=0sI&9qU}$eLig`J3bLwj(bTsln{Dbo9Nx||JtwejyrPlHmv{y{0qwxKr7g?s6&k-l15Pck!Up=tqyL(bkI3;__zp7aw@AB?5IcQM)%^8--^tNp$ zg^JUA!YjyW8B=IgLHvGryB-nt0n?5M;oiu8q6hwPCRl|&L>?$;0-?`PTDKv(B|zQT^s??gBzul!?PH$&7QtuA{&aM(OxKFd7JpP0nJ}kMBKtY&|+rg9t zv6X-{^@retN+pG=WJyckIfAgIdJkz;sD%oTG_ZfW3_M%>J@oeurWfeO>;(y2b=^@G zcg#$}nipkZLd=_Wdy8$PY>GtKq|h-AO?PqIicDo}DYVr({U>iW^Fe3^6VKHr!h=&KSsmL?qFu z9m;WIAfriXSd(^YVg}YNbEV(ivs*KUY&?+LXI?{0!#=gaCWDS`R*#td^Du2GoB1XF@vb3NC*3b^Jl4= zG&Mx4>*@5WJDU3qe5?^qe)*i@U>-8cz>2(bSS_vcJ4@BD-~j~!pl)z6+d?ExdP@~V zePIQFE2MKIKzK)*nonEztz2BXF~p%Nl)qU#4>ZM_kq8J zqjoap19mUmdyi$A?=rOwDP#=e#y==Vg|VPb0#domp#3L}P zi}SJU17)~PQv2}hS=R`WlK%KzDKwXd=!?@t?9l{Y1O<&{pfIrTL!9FLhGB54lw#d>E@#zn zX@sK-kv!aF)X47O_~3;6b3+PIAyxb5T`}%WK!8H!v0^Skd{(z#?)fIhZk{7G@inI$DP`Jf^KK z2Gy@RO7{bPt>B9q2}|BbODp2sGRv(`svWpHIYSH=BHMCT&>RhKW+)kUy79PDr4Nfl z1OXWj9hXt~4Q-Vasl|$cwz{t-)X-`8yp^q7LE8m!L}V7{*K>HGCe2i0Z3XAX+9wTl ze6_(0ZRN2V1^6I8Shc2&XL`Ezqf+OjQ~P9;OM1d&?|P96^)nL~!MV~!D|TKTt+i*x z^e@pP_(`AAc@g)#P_+5f>~!h~i4P157lFh#m_sJ%q+^g{A(A_t2-ZU>gR;qRjvj)l zGfTpDFV?u#!BKQ5zIKR7D?KO+Gtfp3<&~wPKllH5>~R__?V9RHfJ2t~l&5AV&nY$P znkMBp-kK$;;aw z6E=+3*5J#zmFu7_#fG?Z$!Tn1QV6^G&V!e-GlSN*x+tI?)3P{B?hf_}xbDLIh<8$h z@Kbe%0&a9l!|I=ldL}Wbm5WA{bBbs_gBa2z3q-T~521e!&F4(GquoPoai6_iA*@E7 z*Or*82BFZyck=&qBTdIvi&WVQrveIjQs0AYAH>vsy;Kfr6HBwO2@NTNa>{-Vl4}2r zE8q#TKHlU;_I(^I{t=9Jm>%gE;-(h&!Hkrqb%A*TjVUw{nQsPE|F{S!>fc;vah?v%Ix^*qezrDS9FM{b`!$@xQ zr*&XoeY~3mOxB|mR`-R1b@qgXsSteTS;V{C=PlxATbVVJR7%A^MY>+?$D2<|!a*-< z3W+P*Cv@s;OT3+nYc!sRZhGM7U)6Mi8V?>R8UR5QC1V_}QPT;jJa`gvqaJ5k<=Aqt zy)Qu55l*mU5#Hc?3svz!3mj0J3Fn_;PS-uxbaxEuYm$_#KjjTV)N7S7>~QQ2CYusm zyZl2Ho(}w)o)43n-+r)EA(@uZNSGWBlhpP7;hJqeIF{Z$$k{GP3VYvksQ6PL zts6pf*RU@ei*B)}OO{bLskBehaMz}AGNw79>zoJKLY()q9;K=XhM<+cMukYB-5km4 z9EIt2`nreB6i=nW;M`F@z-*ogN_Wh)cq~o47_;WxzdVrPGq(_{=p4d0iHBz#s-1l~ z1)Om!?MHPR&)?;sy#a3ef8U*@ygH|>Rl%<2wT~8l3Khfk=!E1k5~-F&>God5N37-z zu4&Mjkroit>E7~B5p?NnlH%qwZjSFY;;=|9`;G##j~M?2_qX=NTX@*lOs{;CCJMp< zRV^vA0w_zZwa}U`=Lh$=GM;(W;AoUk*nK7LBHLW#H$&3su~D8N&}=A0BPR{_OboQh#rdp{{U0} z$5U#&8yscjuyXCtcfT}{dM3l=K%9;2d~k!KqQ%v4G)p^JeA>;zDQv%&RK(UcSalbJ zB<5;|`q6rkdp!sC*E9%9-rdvC+;&ffcMla2-MciwoZtS%4_W%EZOIKw{~Tbu;t~er z0%4S8eh2dXU~A>-ziuDJ{`0TPWkthG=EQJ#vA5Qo1`V=F?Crm3yC1$YX%+$=bl*@EqNa_&@lv##u_8N@1S8jE2MJ z<+N2=tmVe;%bsJr4!Mj*nvIoV#cmfuR6lq=VD|RHmqU12488`#)$T4$GPHWpBMs*$xRekmp=}u_~`qOT!}f z)?ENCV3FUz76r&?sqhzbsSTg^<|RgTzl}bw+$cJsG~z}g79dR*58r0Ht8uUDLv`cL zbM-{^GS5)o9+Wg%PM=OU{w#9U>4Js;o=W7%DJb`*VZVH& zk{qi_JMBkz=2L*dQGa1)uu0@@IxK0q5jv1&z%(z$E)cAWs(rh8DWU5mx#u)jB^@{; zbe-6t?_$6F=(A|7yqjX@Eggp}ADij}M1;d~(9L9%I`VluhdFsDG12eXv)=<}#ykWK z_0!j>I-F2bEb-ZCoqyCpvn0jCqQBlI=e*ebCcR&6L+k{}fBZnIB#mG_Sjao8J zo>DMK_xXAVy4atg}&eZgIq?a8_M*J?*n7J8CIn<9$Pg^;0Ux%P=}UE;Jh&epr1?Ess_+ z;1zHXjLbL&&8P{RzmZR5hvHkgLCYpa0m2qFZ!YcCE9lAQkzU6b7y^@!T{#*k+QtBw znJiwI{B*CT%|`#X$squ9{CiK2J{I>DPA?@D9d#2QwoC)WDjyLWHGnPY-WUSyrE+_Yp_MRRc9E)r z^8Es-TAq=<7CL+deD8t7WUF9;j&C{JT!b`bobKBW0h9+@Bxv977&H@q1$`TMZ%S9P zeV_f1+5Y~)#aC=mVc=B^98|-uz{hwPPW?gTv6sYTp^roCa;*QK;DBn4J%JgCRn0g> z3Jka}l|_Y2#b9OB%VkYbkOMFMb5zSg*b`8`f4S3)SU_oj+B$Xrx5c#w0#*$l&E0KK%8Asp zmN9%yBAjSN2I!=Wh^5afBG}+DS@UasUMY*DlTN0~%h&>kxi^+U(;|y8u8NK0}?-n$Bx*4@aj7 z0cqSnyIh*oyS=q!R30y4*t*yAx=g&^o>aPQ|6MI zy}rNp?9$KYJ_IbA!?D)Ks8 z;MZ=b58`i<9)r#n7Ei3!%2{6P$>dnMt?zXZhJ?VjMadx?vU~C?`3z%^jA3mkb6|8X z3)REm%!`rlW7}kP;*3V*K^x37d$`)Jwe;uXBiF2JI{xav8Sh{(JN|*q>QTq(=o}@B z1|5BWq$zIA23Vp|fk)5hpAzqidExW)6~mJ%ei_;HVcT1G9XIl4b{@4!H0Z{IRX15z zCDVGkZxwyiwi~lVh@ZwbqEo;wk#G#5ZN;1I|N;4r#IGK3A#*N3Pl|j zde=?-q@qJwhi06?ui%Es>1W773q9HV-X)#uuMk&A`P=?}`Ifg$tGb^TOxAAgciVUx zX(4^6viRNSOOC~75=h&C5q9ZK*d$&@?~BxAE*5dmr_Ok+sJy6z3u z%6uVBLmb7^W#u`7{Wgd%GQ1Ub!zo7J$qPkc{TJZ#^OKK!z5XJ#bW&d&Q@>P>156#! zK7UJpK}IIVZH+(5Mk4tl?AjZfV*EX9T*yO7LC$!ySv2_=Lf;yOm*~vfTDW~iC;KX* zSrZ0n=nd1?PEn)2pa7%eTTxIP@hXlF=|-9?0f;`Rfsd?ybtLEhO@vnGb5f=JL#eU6^1j%YR-L`CnEq zH5ToDP+KRGhItoA-Gn7m-2+r3K~Hb#+%1hVTQvArStJ4aeI8F~z(mUSTe~x0=eUN?0&AitT46gOsRi6p@hUj-R>?lW2Mats#iyex6w+3%#UeZdGd|706=y()E zxlJ}BI-ssQVzx3G9R27(qS7MRK=$leM?Vr!L<44oF)Pma6C^km+=*Q)f1D2nQ?xB^ zMZo*j{VQXHpG3Zv6*-qkg`7USCHH!N2eU+-*F{3NpMHG-{u4>6N&sx3UCn+_SJf`> z?PHoBcHlybsGQ^9=kXEwfcrIbw-SQ4@s*R)IKw z3`m%`r&)|z1PQNYi((vuhLK+Z3qr+pS7jPd<`*h?7Tr*>U+5j4k0q$TFuF+%SmzNJ zl(P0X8~lY33pw|L4vDLxCyoPC0xfHs=$3`_cHqVeXow^4q-6w+V)UevrkyxWgxlSt zYvK!YHSR7t3g`B*^yD=xC%4Pn>-N5)2+ZQZs4mZJJUhVgJ0Uc(GaME82X8#c7YDhV zrPx;k_J?qak)=Zh?wY!J7E~WES@8N!T5CScpztSMsFix@h9NCfJMjFkF2j>ODCvrL3}E~bxVYKgLUnBqHP zqq^}_pnF;RpQ!PQY@!}n)RJ8bM7x=)04Dme+xM5NIFHh#bW>v94fqVEM4;H917CuTAmLVPSEFs|{pqU0&l9NL99ID1+pg!#kYWkLh;bhCx0t5T z1W&{OGygLa?H}@Mj3;Y$AVB_qo?9s;2`Vd3w^T$+sq@t^Y1Y^xBI)X8K|DE0V45h* z=LorU=ZxhdvXcHULQEQTODqS`ktpax;-E#YR#Rv<<5K9++zuL9g&R<}=#wrmY9y0R zG~wM!kmjt!$$zCulb6=F^3RN6`h3E}(77-~5@0l1;uO-Xa6ISE3xKI;s*-Mm87086Q;CU5&=XB-gW>N&vAIlwpbSCWJ5$KmgO!)0BzlAi^ErT43{V`&sP)) zdKpVVz=c-9ZhCfs5c_k7DU3ilF(#{eB9UXF`gW2y6T(zMbU~%^`f0tZeJZsD=8T;; zk-9iLjhLx~+MiFe)*)LPVLd(%E80p`qTIK=FzE5=@Mje~5Mb%ew^N*})m&^dkX~$) zVfNKX<`AmIKHubM;Xz>@9%|2uy1CO=rx}f*tht__Zht#dh!D8xJio)B14Ul?XD@45 z%|z&YQZ0_%rJB@OErduGiCKMdmXw>E=@!X<39Ko5!qemv_mUuG{mx2OYfnd*(BR_A zJTqWCIVDVz3K+j&TndHnc1ev;ts@(pyVJmh;Sy!OW)xTo)oZ#)!C}jDT9*_4{#6rk zsP_{z5j1}5^BE;#ATlC(>aG89a}C1($90$tI_L5toe{m^pdQ1(by20^)fB{m#E$o{ zAz+ zO7pR)9(Q%-Ei5!L7(d&whVu(UU&Fv~%?LZc;6>rj-^H9R%pjQzE%Is$ANE3|LV{U6ee3fYO$g~&K(8~eh_s#3s-bEq7d~$ zsa)f!a08Ql!up*){acj+8h&`xQU+wz_2z`EruRq_hn2>I>6BYSHY?A=XW~pKl$Dc~ z)I_T?we3dIoK{#w%R1R6R|_YmhS{92@EG(y3TH?fSJwNsCNtA9oCdIe^`=IL*b$)#;v@SPkhVuC*V`f)XX zst_>gpJG8am+4iKZvOs#fN~ailA^Q`5?2)lX2z1{i$_D2U%SJx;SME#dXb$6@dX)0 zaD(~iX3Yh!bkknk%iW6G-IN@3=QlBbO<>(Q?pRi?dMGb^3r0TpYegBffQhlt`}zlN z)*TRI!v?~)CLJKD7vEE}H+_%1t?|{NDvSn@tYC?ny)nH5G?Ij$%phs`%G&(%ZUCLi zXhamclywvGge9f!FWU zA~8iSTPgTs4$S{>?e8xqcZ9uWKrKd&#aj>6??JVR+$~j9@Ft-NTE`%Y9mMLhnT?9E zo4m6?-HGcmULFp;(SYB?2$?)3aE{jAsAvMZ{mI4e@D|NVW87I$q;okVXZt)A%Lzbe z`HYBWw#UhiC<;(Wfn?4L<;8-op7t{1Ku&gv1S09N+nabR&xF)o9X^m$2i-U7uI#C5 zoI3GJ*S&d=rv1%uhbAH-on# z~#EFHQxlJDh;DrXp!p)w95{cG!O%8|# z09cX&LeSdcC;>QtB;qjiwmwKuCjclb(|@6wP@4$N_yJ(7Of3H?-fbB$pp^gRSljyP zzN-V6(wJ!gB5ec=03<-l0s!s*5cTi2|2gFUpQHX?Z~vWe{m)Vl{BE_k2|!Dtad`Ug z`Q0J_y@}J2^Lt3#ecl|0x0vh z*kg?P4XdwxS50!V`9E9zyCVC4l22MJ7O3xk1>f`dKVtpA4!@^>_FoszX)ELajJAAS z(9r*l`VEq!xAEhH!hh#jk^uUs|AW^7tZB&q&ES9bdYU)ocb#u*{SP3mf(n54jjI0y zq%qO}(Eq2Zrv=b{7yIl2q5x^Vbl(p#BmqHb8}#3Q$^V4)G&)8AdeTg|c$y>AcjuK}>{G$%3fk1?@11j&c*bHuXmsUJ_nS3exL_%CerEyUQ5%>r5{rukYWY&d$zr4J3w* zo{fgBHpYLqZ^MGX!Hl(||8(`9Uz=veY5(c!?Yx)UWWaBW+Mw-VYhwi%*s2R|XGWm{ z%z?w2;P|YpyC###y$O~N?eZzmx?lB>@OXS2eN$|ChaL60d%VCD?Ae`*yK2u;Dk&Xv ze?N%gbXdCvnf+=up=ONSYO-_Nq@Gquz;3s92YTHRGHN!16IFpU1`9W8Y9Edu&8QUZ zN(|a0T&H}=A3>0=C9!_tX3eLM;Y7L$vB zj+E@KW!r$qda$ne*Z-mE-P~^WtFRGfQd^JBNhY7yR@Li&uk~?qKQ{2YFM*p`uqP3i zGe6pE2Uc2e{~(F$5tdIY#+-E;N@mcw5o2MGO<2H+6F`SzLKX-$N29K5On22n1rRYG zHD%9!Zm(*5ZU1N~^NySR+P$8+0;C^#kADw=o>p{eQZX**jvtAbCG6;) zwm=N_(LuSR+c)fxZY?XcqXSPme;+tLi2eNIe8guK7=DP?ZXO6w(ha4gT_(~%xUL_- zr%*q2#1afuuR!A=IDjh6xF9RuG^-c?zH-fruxQrhlUo_+I$zZ?4I{;-HPPtliY7VU z!Q1NTM7?+Y+CZ}bn8`%bx_1=l7Nu8@x!!un34T~243{NV|3d-MwgGI>#WgS)%kt4O zy;VQQl}VH?O{C`%SB*Wo?Vsmd`jV;g;kvo;1T;qA^6M9A+|ey>+Aeg7B_z^KH$kM) zn$FFZSqrK|?R%V9TGqh^gQ}>MUt*#b6!aLu8T~}SK5xYvNj0~C1(T5J;27=Xk6W(q zr4-3HtYXjY={TWN_yt5>8!^wMdv)r`OI($!#$!?Kj5ewAnNdMBM+3sue_FTQ$<*23jpgmD4Pw1)GRpYhZEWa^1>hoJ@yq(d#rah*}4F6~#e&^fadt~y< zggcw*(a(O52cw|ZY=G0o@6^#{Sde8{rK`OV)Ffuqya!fYfB=bQ|GC*AB!batGXP4} zF1q&}hscrAKqCYCQ_Z-swCnNrBTtXK?gbz6SrI@Hp|kC+zSe~`*8GsDWKOz#z#>_7 zNZp+5wJKvaBUG{Fi(!rOMf-)ZG{!*mFW6}|w_ntWruEe^4*N5plG^091Cu0kBgZE> zg5zlcKi+~gHGuu{&AblxNPcJeHDDlr+;;;SDGoP(o!J+#5RUHo6iZ?C=*_KW;Hm>}@GgIOQ!M2K-_R{o~U8<{!`Qs+lS|L`lPek}!IF@wt z#}!~r?Ir`DOD(R;^Hl=*m?M&wjo+=I3GhiM^o5+>y#z)g;DSm&IW!R0OD<(a+G#PK z$)5C>;?@3a70?~|$3e4;q;IDL$iM%+7k>OZgStn|e*(;q-1u==m=+WbCOf`Q56ZSXA zVg!|8p^-`frLRdG2zs9y-H(sX9~g#vtdj>M%aPf%4?47amo5~J`uP8_XZ^ZrqWWne z)ZjUwvnhTKSBbzNQaWNbXQi52{nEunDdqs9b2tL-byhA7??`FXW_{3sz58w*#<{Wk z?giq-{fX!8LIQS6)y0)Hxw%{-cz$v88R@_?QfA2?_m;Zv@~M9=L;D*!5;#bNs`NpA5*f^VP-K+8!Ccg^#yt$)rz#^ZLWMsWWoD)@z_XKK>`9AYEt}e6 zk2+SaKDxNqhn4ewp{)^UZyOUb&r&=BiKe2TIYM%(SF66xQQWWS2l9Obm-2HWaa#q} zd?t3MtgCJ&eZy%@9`O8Z1W#|8NSs`QW~spAQh#P=MJEj`7X-kt3{p5)PA-ilq0*{R zAIKc5g0GMrzjHsr3+m*NpNGweY`}BbzC6b|ES=Zq623xE5^?#2cMV#>g*z z7BCL9mw-x?G76K8B&5p9(#!U~0;uuc;0hHK9vp-migY|Uyn`jDaAZro(|e6H%H zXcn;Fa%8e;?kgWn7~nYyfzj2L=L6Lkxh+4jqfzszQ&8q{s$nOR3S7D!Goac%cZ>$c z8tbU<4Ef%R$fgeo4B$#|(t|QAYj5o5!aAtHRz2-Z%C>lN!ph!9dCd|qy*K_aqVldC z&y&0ZAUJlkyBYA!c#mEVgBuhT*R;rPT3X~_dVAtAclE%ECl_UjvcyxzR+k2dmr%Tl*+tC)KBdTID|M0l*W&d5`ee9VVgSGvYnvk4KX?b*SGD_sd zk-$_WgXzH|$o6>qa{^Z$sU1cMIXDAeDA3TE)OUu35C#ul-`QJ&VLy@Q7@nL$CEdr;&03hg-Z(DueJI17O}NUW z%7Q~LDm&3!{L0smM*^bh8%I4};s|)y2+g9+T}Zf6)I%`RMeThye?7&LA=HLaI?_R| zv}MCMYH%a~>AQ+0rv{flJ!yYK#E<(c5W4ZWOE$9VIllGb?m6mLqwS5zg4fzMLPC^B z58-C;O@?q78AEUu5(>wu=Ogy#~{8@QV%Gm}|C-S^HM z!>P5u&T#IRgD*moMJlm03}pHWbEeYG7IbDIu~|JWr9fH{VV?1=*#??0mm8}rDxM__ z@i+bKprQ^V#5D(vTQH><8IURm zp-I~2{G>JD)egBCfn*($z%ED#!DE`~M2u!{PR!cNSEV1@v#S<* zF^I5r60l$Vj7`HEn^DwZWpuK*QRn>oB(f~Vba>EFJ~YLI9S6colASuSe~`K9g6j>3 zadaY-URo976uW^>2GxGKar#;7zD6DR8IMCSjnwS;2o78`CxSu1dZ2u(GCH z`+6RNcY|3k5ASacENB$2o9@JlQ-&#PDM2#icaSg{HSbs;jXE(4!_x&V2&9VQTH5_t zKHuGb^#-yV0UGHaITv(TtLpDiile`p@b*Hf?|92Via;8}Ei=57eqc^G9S@M#CW4$G z6amNn9I8-7|E4wzTFB5$Cf`fypc~g#yqPfnr`bPzSr2qZqO-!z(^B*(BoW6>WJtVI`3Pd?F7m25PSz!5nu_di_O>aHh303Y{{jj^QE*FK># zO%{Yg{k}Vy2Ligzqu5SBvlICeHWqa*y#+k3fd+w$V3V3Hu*qzsv^^U*f{vWn0|S(V zko8TrAFd$d30t0T-{7`o-GYDQdEaXZc?%?Pn!5V`(K7*)!6H40Bl;y#7ZgcH7D!?S zechnS1I}<7&O}g$`a1B4c8%0Px{J9PSI+HCqnl*@Q*f}eE0PARW?TzRS_ULsGXi)8 zTjB)O`42<__JME3S7Ag2QC?-qud16lJJn@L#!0XYmP&dARfssH%efP+UTQI?c0e!J zD4|n3u_^a#XW}v}VxWYdHLr!M-<2lKbg3qel^X&I(N5(J{fBjKp&}m*$1$o+ZeHcL z(w_Yq?9{!j1#i8Wle45YSeV3LfChYRESa0>Q{-n;cwxKw!E}HEUY$*SKS7}aJ~p3K z0 zysogkf_(r#HkPbrkRjqR1;s?-E^Boa$ z!Zbhqodgt4;ps?|geFE}`i?c^P4U&qKsmhb0X$`?+Evg3v!n4Kz)2>2qM#6^d$)r< zAI!CSJ&LI8aQA(J$bJ8g*lcjJMdWQEO2u4gT}Vg*tXfAzkQzP`}k=<~8%ace4C})Lg;d zRpY5a%huw+AA>|`N;M;$OHAdOxs|Ac2#4(=4#f0>O9|7}gB}^+NOc#~~lLGgA z-D#uH5Wm0B^=2|XRbSYKVtcqu6cqRMP>)`YzQ^JNA_T5lyd@K0+h~>-LYO6R50uS9 zQ=)Ml$)NowXg17yFJO;f4ieRYhkNIo1>$TBvEhQf6b8)vUQ=H!OU^$wMa&Ca-Rd5v z^mCXYiqB!P!>!eDo7vXUEOG`Zu=O(Lxd&UZLIi%PeppWyv0#CB0R<%GfTp-3k@8a{ggPMV!ZALE zWKQ$?y|%XiLyh=u6mfW*+kFObAnL!_1C%Ru-|HAw= z7Kyqd=(8{iR(g0i2A=M>mX1PS?3lxHO6b3?cMElVjFuyJdLnYJz4XT6PsBvCnh=V> z?ZYbl+}0Qu9jF^M7w|7a2@5dGIYr{T&sNY*jo@Y?Sny}kFD)|vb95&I7}l}9T{+mD zjCS_%Z9IJwVZ1+f@Ipr?$yGq5bB)GER}gMzDd|jg)aG^&12T%U*=|32{)~e$&nPZ; zY~)|#AMEt~7K*nJv;1j;O4^s!^7P9ZKfh74F5%!@fHg8wue@xl!?hw6&jQD}xqtLL zp_zT_jX?p6?DR@9B_~`g@cDi*C#E_-sjA-wgR0gzq_U-6J+ya8Y}IW$r6$nKpnDEM z>ULJm^--zK6qu#~^IgfVdZlTmiDLB;vt3=C+tNO1TRSqSLHz6xTQoIH9RX@8N-y%* zR}IDGrkox%oM5Q%fkEild3~f7HvW~69w)QttxYy18l}@aKu0Tcm&mKEb2>CoieRIB zTBpT)fn&7S)Kgl3pW2_a?jiw&8Op&PYQmh^pb;sP0T@-NLXzCA%r_HrPnav<f-mCVB3|LVIqI6zyfkhJLh;1o@4%GNHnUUuYM99*3Lx#p1(=h*CYn)U2)+L7%> z3WALon5j-D<|+_mKO?;nsifAI7Whl9(BojR^X!(UCHM1|=sC;f8CW?$Fe4Yaw7U(Q z)AUt#+CfWDo;!lGcKq2R;tE||meU%pav63C7?U#dOb_QYmGlZnI@F2uN5P*(S!&JP z?2!dOiR^xw0&l)``@v?EIf-b@ebtht`4SS=EgjT1Gnb-kPB1rLW$|~8#t?PtOF=18 z^br1{INXG$Zcr?97#<6O(ht~3<*{#-1*yaa&Wp&Cm3s#9B_`kX~iWWKP45Kp~`1)44XzxwjG6kv7Dj zWv$rm836CgEWRXfR*SJz`7A2?bS_T<-9}*D~b`Y-oQ+17QQ(guFAqpM{rWP?r^CQ)4G=0tSZNu|G8 zLv>?kqUbXRH9H>oG0p%XO^kWbVa}k1B*2tyksY*HrcyZXDD+tB>)jEc&JNIw;j&kmg zdIx-6nE30aE#lJM=(NKsPzz-*ar@o8v#yFDI&|H4aaHPQM@0x#L0U*v@2_z!!d)?` zHIF~WUV*Q%q8|u#<4c3c*FpN8x$3eeo3%-~)rJR|k>7>%(r2{QTP{~V9?L(xk8T$SM0A%J3Q;Uuhtlq&``w>$?2r|-8qbpirW3RWzST_!RV=ma(@?M_r z;cNIItdSXHDDcC^i14zT%;YDS=w1l$g6W)@oR^&9DFo<9bvPHifc?IpP!VAt)iM?6jUlyOq| zcn53=ltPF^&|VCTnusKiKgmW^<@Yhu6)b*^N<8x?-Y05em{^pV3@%38`$x(U2IvhW)O z|1$v&jg!_e_-X9=$wm75E?MOnUPL$@k8%%7l&zWfUsStA(o_4=DP^dI|@vy`PW!g$kHHc zM?ZL4vc&I;RIytW=rn^g4Xo6$X|#^ASwSV^xmR#uLW^-ypU1)Cq_MycJktY+c@YvJ z#z@dgE5Cb;U_&O=S88&){u0!7$Xp*K=G1@FQqbY}(`IO}wX*=&e(9j*E-%(OAM}<5 z0rselLuuhbKb0?$sQqZmRKvCMa(|X7TY4WEvbNeO+zvX;nQ|o~6<$?Ci)!Bx+TR$R zj3m4q$z3K^w-9?f2Nh6z{H*CA z4G#&WW{cHrmt#L`*KCHbx_+*mw%LyqLp`mMQyib=Uz9kP-f{6s4DE&=|$ z9=Nr!i3G7go$^PVV#~-8|IHPs-^YF_u4kh*7CP9|u@Ij5iLuZK1(WVqov@hSVkYhL z*kT$Xdeow)cI|*3lhen)-=U9*NJ#$F!*Lcof6w^tTZ=Q;9IYdP!5jbYPm?TnnqjjU z;auM2nIUoC$>7|HC(BWB-4|0}EnN{QNq+MePRLz!Q$c_mHumfYGmJ(Y6bE(;q{$FC zQ5}^B)vd}72__zOiqoZ;2LIp)dTSv%|}8sd}w{AiYU zPh#_7Yy@<|S$a<96ZXpLkg|!_+P^S=`ag)OqJ&`Zn%`e@nWvU4IoLieO4))t?q*&b zKjs$Vt@G{n9!Gj2#A15)SrpkiIJa-Tyw$FW9=|rz~&}SPPR6 z$JW!zDJ(}7IW=yBk)S{Jc``;170WhOt7Y*8vcYeT4ZBI8#~B-~b||^cK;Nd)d9uQX z3i$SI?!qi;qQ?ZW=d8!d{y59%8F=HbZ1;_$DS_6V^FjJt_xv;x7%#Df1Q6J!4xA01 zmXeO`CjL#bmEaSO@B1x3E=N;>Fliq;Ria-cRXwFioS1~ybT8GB-9B|zS>=&O)g{Ld zEafBdsF?ku)!Y`lH7vHz-9xe%*bie-j2+9b{Kg<6NaCpSGp5TiAwT!{UXNi}r&)SnW z-U`nr;KngsN7jaK>*}*3IYX>KEr@vzRMb*5Qq)s48&@CKA9wj(W}K7d;oee>#2FEc z{FB&($DAOnu}#kAkTOSh-Drrk+NCPu$O`5`nEsHi!g!vFSq@Ko*FlUVZxO5Q6cs8u z$!mK~A$&z)#!J9Dn|fU5ncX0r&EOk}sVc#5@Q!E;B?PR6H+?jPQa!Y~bDNHW`qK8W zf+|`OONxgycLSq&E0+lEjY>pM~Uc^t+r zt>S8Bxfu721Gy6jN=_G@v8-R8WWJd-!=Nxeu%HCb0`MO_3&(%;4!;bQ()Nc@x@YRl zGqkL$#*5xmJX$^6FkKmIiN8G>8#|KmW7-b>lpZf>ZhE_pa48@=aBIG$09iD}$rb~+ zZvceg3m=)^-ZssyuDgLFSz}d2yt3BM$p#FKjkSonee0rcDgPXuW-OP@czZVgK{p#y zH#eq7)k4>MlmJQAfr(bH_p&EJYfe!%ulxKtZV&tAF56EXOLy&Wx1U$fLa68Z!|rlA z-+xc~ly{)@G&rH%i&_8N@y-31TIrIhQXF?}W z5Qeo6(1X&2f1>L6TtXxv37|=-o3iUaA{$L8~r;ie} z=#P-(2w$Sw`>RvIdEcnyO*cV4%eUNMt=HLwDk^7Y>eSUgF-MJ?Xan+Emg)ZemAe@E zAgP3RR^7boSUS-VYvbS+BqPiVOl#7ZMDE^Hc%G)OJA=BxIz^&7xIDtSf6JtNMTJZi z574F>01?0WNP*PQsvC%f*b1=e9Y^!TF?2x!>r@S$o^lbsFY^&A6^@lyCV_Am!-ifI z<7|#g3etGP*v725&JwWoTPrPZY|VjcKI% zwmdC0WZLrq5b7_xJ2@gO@p6-Y4^Nqu8Msl6Wvbrw4rki#ilC^}1RW)#ndXt9AJyRe zS5mcivp`ZoVdRa2gr@R511Rln{XpRD?sNc3u)VYJYmg|arRQnN#@5WtVC}P$Lh=$n zl>;&zDc{oe#^wy0nf&UOYfja~`|B*`ev+>DU@h6r>#XVM$4`nbCo9cF+Ae35;siq- z&!Nsj92-*H(%L(*Iyq$x-F7RbsA@yJ#!SznPaymZI{4sjP^OQ7;Kc$$F255o-Ux2j zxbJsB_opL9ld5?qRt_xkt$A^b&$4lOW=_OL!6io#{cr1Wp>V}7 z5rM&kc4O@O?ENgB_Bg{79OQlWC;4vl2k(k!R-hwE%10JyJ}-Iz<( zH$Zc@KyEqj6g0^I*PA&w`<+AX3y$)BTX~`>`GNwu8JiSUdb|g+Q!Oib`^43Skc*AV zol>gb2*z(KSGjXg*xw5gYpWy~a7(*AOkRq{n23$%yBflKR=1{P1rN7ry9nNCZd##RN-5b^J(n7shHRs`N)B({9 zQ|l7&pR1I~EB2i1m#Et!m9hR{w}BkjYA7WF&gc~g4w;G5ayn20dEyH}74E3K;I{=KgEq5ca=COJ=xz#6LGNXl-6Y z_(`RZC0DHMF?bDhQGZ4;e{*50Is^V%XQh!8Jkn~~Y$^XBdiOBAAPDyvyYmO9#yyV0 z+|V$P_)|)v3SAxaFF9@UlXqjOL&g4w{^C!Fpgf7jXu88@-xl@0#?e0*XF5dbuVU*X z6WNJGNuFBtK;c7eL-M(|tJ57k)dbvRO9Z`B5(7AR01@$L$43g%B~KqH*|zTdE?TNe9W~W-S0898mI{H z1(lEUZ2Yf5C$kOSA>k$@s%~WcV?hFNXfb;il=5S!6o!KQQ<2E(*0iof%xJeP^fGUI z3*sMw7&Fm%B{W?{SUrJAQ4GM|@K(o9vYmoGcD*T`+le1dCG2WhlFI?zc7WoG2F=8> z`hjY*HILfiLzGQVX>NA?#r8>woX6~6^UU%PWhl0W3L8gVJTq=imTA4}X`xV(aBpER zXy&N12P1mfFgNa7DCX_5xK{NtTw#ZpV!jJ*(gnCwgst8fZOX~fJNCvrNG4{Ih@bT^EBn5yccs5Jb zOBb=iaBTS}bfLHAn(Ca?^4oyu%PhvD#*w;GD{GIwvpJ+-i~XbNk(lzrqt_D-eJFkq z|DRM?x3XUW$Xsq#Ym>ta6|ebtW#WqTl6z0}(JgSn7iw-?!C8vHk2KD)o+O)XlTl<- z0m`{VfR(0ZI3Y7m%ZcoEM>x4{E)nZ=m-nBQcF&@fClU=!hZa}Ln(f{7#hht5Mi$9p zI-f;ii3BoR6(+yy7#9aT6){6vx7C_PLxk`9_fntZB_X9V6q4CexavwX%Tbrlxn0~W z2>cHzvDimM7DA}N^gj+FY{$^zU7GM_(-u~TSInX&s}2c!a#A16LW42Obx2tKRAi!3 zqTO<3ugs+#gIvuZmAWZ$y-WiXTs%IpKa0KZk<$NWWOhjh&OS=N^$P1 zDg9DzmU8P^}l$f$bd zGfeIR#Q!cCF=cj03mL$s6u`Bv)fx=3jaJkCOcLdUh;c&7A+diUAe}!-miijZh-2}vpVSKSf!_q zbQ(-Sm77+krpJAr{cC;1$zNp}`)*G@*4c(No6YD9xNhVrVdwr@sfY@q)r)`kX{Jg+ zYo2PRcJ!6%@y&7IW}t0F5_K*%SXh?wEctoS^H(zft&p_L)kT7@X zo@S*;)N9KkoO+9>{+kn8@S76E%sr|2Xfu7GFB@>NgIE{>jmE?df>uN3e3NMx`kq7+8>%XMm7D9_ftkuZlW6Nv>FEZMB^qM z)76BK^jTce&^A=zjvVVDY4OgCB-!St?7Vkgzzdhtm63Tt%#(EUn#32)S}pX1ehv=w z29dJ8?#85n74h_jV1{CN&L=)=Y`t0^d?`U&d)5dU#m2TULnrigAId5XX$$F#plBbO z!GaaGgTK*%@YT?_avT4cOz74MsuDEMksWp=)eqXYzw6)K8E@GM)HHv*7|QN*c+xqG z0aIH(B4_?lf=%bnJvaAUqcg9@pRV2WkLXWk^aU+S%aa|EoX_$Mu_OsD0p&QLf%Sjw zqKD8^U=Zz1 z`BS=d%wyuO&}=9CKFy?KbTz^Es9JfZ!Ns9~=v@H80A5g;3qTN=@Bd5Bx%l}(E-wK0 zRufl%1|onH90%Uo?+<7IfNFvOSgp~406GBZOE7pn-x~xd0>OlUOWwhN6wp^7xVi@x z(gOh62>p*_8wd~ufQZ7u*6#?A^Z?L&I9M*t8V)D}(ME!e*DL@L0O&djtcP|Q1+)S{ z+%bUvQ>BAGW5KRqD@|*`1mGJ0lo1EO{*NF%45Szjt|^!SkNlAUb~lR$%7kntdo46yNk+HzwMxEPZGSO$O?zJohQ4*Lb7 z$^ttAw*e=h!5`q-p#MtCO>zML6_iH-Ky5i-LHSIq45%R&>oIFI{}VE#Yh{NS@LLnY4* zS=8=xy-AiF#yA_k?7Ej`BB4qvv}~4YkK3bO3M0a`AZ7U^>&wqq7($5E5}IonsV69u zg4-7eMnPfHq5(EOU{8<#eN+6r6e+H~1L`wcj51jxC0a4@*4e8C{X0TIyP^p@Z+##p8@MUW9R6PWkj`sCdK^Qo zW1&qx#n~ioTvag(3W4 z1f+cj-qDEP`4sj7*`L@SYWsj%PRUL$*Oq-sMqY&t%bwa#9T&64B6h=~Nt4_$^^Nk! z&nk*{xDpZPUY+pHmZ*IBP-|!;xk#~KmS2fUulKkSw@~h#91V@1-qk@1t4m^}B>Pg= ztnglD2SO%;g52yUUO){@Yu{fAZti!~v=y@u*!bcga#vT_`+Ap*$7{YTS>1mBf4FxrZRWD84;M)MxG%sTyEq6yG01| zXEygocMhDUomLCC=xS}dP?(~;5C#Fvdx&^kE=sQwuAu8$2jr?1fVWbsS)p?>++bp% zp8d%DPWUVM3i1v-3EhakXMVfCGV^gDeeI{^gp<0a53mF&P8WGO6&6-|NsBE&CpreRLubmp3 z>N=_|+gUYob7N%xO=7fzkdE8M_0e5NL@W~}h|Vl`6(O=h z`yII2;_92nZU|3`9w0jrqpZV#h~1^e>Dx~}$vxwRZ~@G_Bu8dRucomnFL9X-;A(2j zpH_)t&h^dSnf~jq@!M|Jk5Cx}T9vmhKNT8QZL8)5|A6JOaY#0cdE70duqoqXc}(?U z|M{4PxgN@dUt`Gq!atP1Y+AX(yYcY4@0K?Tbj(xWnkxrRdx0S< z1I{Al%^Ob7Vw%euEeQ`U*CS|s$-cNU=lJR7=8z&aY&#x@3DpU=MrP6a;(F-1>I)_3mud1F*2;wb3Xf9y( z>-2k3*sm#6q!n6bV&2gi-v}Gg&U%bXvHjtQk{&_1J#_B8p$V3F8b9;D{o}+_$)DWM zZ4FG(`enV~UhtFtZPDU_(UUKh!=h{@ z(!uRPuEqOEexojH&|(k)cbUN1h!sE(^aW@)G(~QigMd>cBw*|P6DB=`s-e8mM3*J6 zeYGPGQE%9ve$Pk>R&}{7R|K4D`wKq9Wt=0rC(*X0GA_9|L4GoNNdE8)4l}&RwC``|4HE@c3NZ#oUB+WAVP7^Xrb8XL~94uS%dHyq=wVpjKD(Z&2^>HaUpIHU1!H)R!#fe z!Y|ls_OJt^oxl$2+c3;B8KpSk)b%H~d$(LQmq)$kRbkV@^`q}r02mrAl3_yERF{#6 z7UYpI1_68G+T>%Bckq^y>M;|mXAqF+;0zK8K-nTIx5f$rmg*=Km@zp(v z8RamqA&!_aQJ!fY5=Z4Lgk+N888CX5QI@WpZ%^?4G=cv+Sz8XsW##p>6Q zFJJGwx+0Vuen+P^9k{G9WR#G(h_8Nr`wfUv^xplpxPHA`tbfWr{*7e-2VW6km19m{ z424BRB}Ga?XnSTbw1}d$#3B)-)9QE8q)u;35hn3>Z7IZ58uQyyRjE0Q(wYvH z7mLx3L%0vUqe3JS^t!mS5KGowHexxW0NwIl1y90lcnJ9n5e-b|f)#TfBwCBm>TjRO zJ;-1ENVwlvu^omfo6j(QSp_1k8fPx&T+B1?pg{2kZ{%oEy*F+bs)yG3-JWPUX?1#NTct6o{H2b0eUjYT-?MAo1K+Sa(hdMF# z$5`14oTH~YVQ-v-QjFUtP|-&(Rk6z?Gl0Cf)X`gtv_B3cn0>y3VCCvxIz&9=0YTjH zq{KEL?=+MM`A;=JlQX+n1b#)xAok=UO6k|{J>>&ghvHueb37jz?9DMOAbCx{zw-p$ zo;T;@8oc-SN|?rL9;QlxS207iF%Mnz)s2*sg+UK9{)9@43-Wz`fN^(*0*pCl+=%z| znlb}*fg$%m)Pb+0bn62@Lg4BPCrQep=TS_d+Md@<1~EbWCZ`Me&Kb8^#>sk`BL!k0x#C_ zK?i#)^VG2K(O5Oi8MHr4@6VVtnqzl0TfctZnNmVfmeuXjy}#cpD&tt}BOB9CwM5d^ zs>Zb|NZW{SXF4X!z=6&yoy(fw#r0WI$G?P4*r8%_TLRVQAUvBn=QqFenPc`U->*Oo zlOZ&n?YMR|v)4}dy(S>lWYjF{wg3rLzncGa+}GI4@p+8DZ(V0GUaKoy!{yyZa7#lO zh(l`bxYH0kNS3QuS9D+ca+$SwVUKo?bfQnA-as1!lNlI6BBwdaE#Z0z1kOo#DD|kl z^H>>7{REz~1VsBTo>KD;XA``fh_9JnJKmt`%pw7nhvIsVoSB^Y%Cs9f&^$pdZ2ubTZiFQsJT$k^_ww_*wOJ)ocMWtvD*cDXnxBJpi>5Q67& zFhdkdY^o|PtQH9Tq^oyoA>&eBBb0wo2%~yMegWf01&+8>1+aJ_E&sp<#wP*TXRXBK zzrP@H-2JI#g+t{RR!#VLnZ}lSwdXy|NH!wq9*s%dnywv$d*XLn;$i-=bv54OoYp_| z2gaB0&CvZs4-{z+&d&@w={8c{mI!NFH#r#olH{o6BIoyw8U9DL)Kha&J zDF{T|4`*V04LUWe;D%_czttqFGa($Fn`ciFHl>`~afH#>b%#P{ox%6~i1=Wl?EF1t z*-!%C%%Rm@ap#bQjpLXV88ZDN0S_;lDx}vTVMc$GDq4C;)X5cReL;nZOqm3JJ)~P$ zL&`O3^swc|;T36w#gp4FpKd=`pX1F%j2!rA;bZ;Lx+y|$hIkd1U$2FT{Uxffv)wpn zw!wn~0*=pk58_pzwqhf4w|=`Ip^XHhte(kkMc>`&jl{A=Iy4=(gH00RwVPL|3Ds85 zwU@6MD455m)%+C+kA=e0i(7f_8;pm_Odm}zH1kEWC8<)vIv2dC`C3rI_CK>xL=s%D z?-9;~p;31h5|vX+VCeV>Fmev8f*t=$!v|~N>i^Mj#5%b8zcjqP0XF`RhPyVwaQe6b z7y}h-ff-jkWD?}C4Z!^`72E898-S_!6a>Eqo+dcpIuX1bun)j0C)xu5K+OkWBRJqX z8vG;=!Q+h_0Ng=ohhU`s4=E=cfg6KK`3S^y0>J)1QZ_vW7r~^A2LM@|fxp3jNg44R z?D=0(9{*1X9C}>?4lxIBB%A|K0RN4Phvs4DZh+a?OA$K)T3U%R|t zX+~)+v={JD|6h<38;y~$F=E#V8%VNg=+YgirdKECynN~M=ZhF#T^*aKshiyhqAFF@ z{9=p#rdj7(B93FhzfyCWy7Rq0_f0&-drf_)Ws}>dy(yS`E%+Vh_ibznkVqhBG6aR)T70Y4KzL)LqDxANd&}PxdQMTLx_F4ALBR4H{nZ{Y%fsS@|s=)MMWZ7XxmYEI0Tv z1pR-fk7q(We;G3{IR7#DQumtl)8L(vIUi4=VaP>IzQ@(=&(?LaSBHzM`{DW@_H~vg zhNHjA5C~QDEO62eI-Ds>EOQ9tFjDk3cqpGh?F7Bc+4phoeLdn3ei5OIH>Fx!sJpv> z2ABrzE_yal#vW;9?;o-x=cY0jm$hBMWt+3FblY~F5nQ`_g$@_@=4X=Kh>hLAzRTM# zX7uf*OW+saig`@_DXcr{eIf+%QJK}4wMv@M#WP7|@+7R_UI52KDb(TOlY%!-g(^N! zo8+f8k09REOZ+#!3b^HR{(A&L+=DoTPSpJ&^d{Su7Th=k*Q~uAHPo zj-Q|1$%d2K%qLOm~9-$*cif0T0XnA4awkC3Z3 zWyiK`t3<9`2%$rIqyDIju<^5JUf~u51|D%low!>nJwR-#NO{tYFCn!$sQPAy+3|&# z+^!|FX*KjrNT-?Otdp2rx}MG%u_KF#wL`Q@;yq8n5obM6M2M1Od!r=6JBafg2b?WE zytw^7Z3&%R*&MDr4qko|`4PG7-GrnfLRB&Qc&4KrhaplW`Ft|xnUJ~ZB-C>O43N-l zfuk!&xL+)wLd)Y3(Z57%v&%odDwBDic2%w;@jL zjw*b(6*bRAaEGuic5N1Il@q=PSQMC}bWG&(^sv|pue#W3?3xS#3jR_$Sw|syF7~1e z-v6r1cWDhK=Yulw$CH+@e;~T1w_WwQoWNs~%@?+|TRNwLV7z|EJ&sEl!L_zC%zac) zv>(lpw_gTDvn7>>z=y&kxs9QOO&yXPc8ceOF@B!f+Q@6fw!T6A1Pi}`)&5Qqhix>M zYtflO%f~Nq-%t6Rg$fdykpqM-qqmBy#5SL^N(3oVb)XjH(q9*e*H57))r2o0>OJik z368PqsIBcI3A^Tfjx`o6jMZ_ZjpS)HsI5&}JY=l!s)b)a?1N)qiSlZ?V80srC560! z54A&p(s*H+<(GTqVWTBL`dB+v4HshvL2l^Q$mZ|{I%?y{0d|VK*)1!QNOGQj7?C>2 z?(qU*TDe0zgdL0m>V2wGvw+ysE!n$LxZg$r8XcuEMy?;`?17Zf&_d6vu8q?!gxd|Ajw5;?6K zmb5K?4#+PeEd#hwhY7#(-TM#bz2KLN7u@iK<|CWh zEk1CUo61XG@aHu#%JOBXCZDGz&4Gk2Pu@_eAz}G#s4y&W~5vc*xXri;< z%G>15p4Ya)fonr0h~z#oJFb#I(ti@gs(CRcgjvCuN;^RZ1yM|IOgLQL8^ms(NN+iD zRfRORE!e`~`#q6`$YTfqh2H8%O(}clR@%151tC^Y0u3OJ&-U#{-}H*TCEf(s^$jIm zgLy;-#lk@{%9@mvJEc6OwgQ$zE8%;J@!-EY55n>kd^_nz~p-c13_7ISgVX zyRMtvpa;7UGh_nZLKN!eOWwa4IADau%z)7a1avh?Y1==NhKZ8G<-qwmE z)7&&Ze)h&+9w*#^E6+yPtUK-SQJ8mEr>zv95!+HPUJM1Xgh4TtBf1n!&;i8RgXNA@ zDj82($^?v(q2e9X=T9esWq!y9weCiEe*S4mm>ovn@Dd4<%DympNCsu6Gi&y(Ddq>O zc_pNJ0fk=rDK1WW2_=v>eAaCqsty>}rMjsAek!ngiW!)km=I?pNW_{N7Fovq#cdt1 zvCUOGIOGZKQ@@g7yxuK$1A5#bmP6gB2Y@++IBxQ&{)sr95G!VjrP{L6ih9P{=GALS z$}Q=JgeU{)IYfEr%scW#DJ^aA&DPXD3>@4jz+g719>pPJT21#drP?;qxEzUG zmeSg9pIU~LoARndN)?uYV&nXy+P1Tsw7F{) z&3?^>j72f?z_G~NE3TbAb<7^~esKE<-Ruuu)&ICs_n%1K2+c!ufMWQ1uPe$MnEtoK z1xPlIx?}h=dD@1wf;eE2*Sc^_VpzseRuzryXZePzen`S)99uwT>Y}FHKT9VLFXx1yQVnaRTv~Y&gI% zTYR%Zlif(BBIb?wEbNDb2s)Pj2>AP<1F0hd(%UC0EnN25H2|J#**hY!(2Bu0OBRr? zrH!n5pTQM)IDUMipWTp8DLoJ=QczVI^zOPT{9=%d@&*=~S>IBHc;NX?u!Lnk#Wv0Zbtty8?FdZ%utG31!z}dgy|@xAlW?*jEfUkk3+g zk1D*WD)^z8l{$>ybOK&VID|Ro%wpdl$XDwvZlqCM*ER3bJ`j!;1Uj>C639XFsaKBg zVM1q7+GmWDRg!2HYFB4)AQ&K?G;yx-GqA)S2qqrzhq2Pdw9`__sn6qakMHcH?*c$YcuRagi3j{WQSWa)9Yk z|5NDS;S%!tddcHOakw``_qo>wJn6ZKPru7!ygocU)cn)NgJzH%s1(V`V|^n0iAwfERagz8J0?fzCxukcNvJVNGh^7J`X3Ew(!p3ZG_X9~PvnPU)_*1Kb(%#Yj|fGKh929&z; zUEjV$NS(9|@goGHW}U#{TOlmPh_ssQ++4Mi&JvRV%t&9oSObL)?nVCDkg$C?lKE2a z<>-7}$Zm?4j`^or4;SAB5u1eX;iy3vX|C59l1gDsUAQ}0!pvP(*k8exe0>W5M_{ezrF(xJit}y}Em#>MvK=^T(%+13h9ZD+*zJUuWdnR)jz(jJiZf#-(2+ z;V{H8=(fXCQg}srtqazuSDDK4x$L2*{Ls=~yR!cCXP@q#tc+h@)&yXxit*UXW7I&P zUL2j)_Ss{{2sm8H5+ftOME>irett|+3}+A12@szMgD29kDL?JZ_YDjAi&>^)?i|Dn zZqVkSN8t--315K2coZ?+82FUyA=({k&~_#deV@lP;dDiczyLu8>E%&w;Ebk3_RU-N z!slNlHLaN%ijQUU=7m}O)3g=*)5^4E+|5dM0Ss7;@pyYT=xwA7Bj@tIhJLL~-BV_M zUCgrsL*N86zw910zwBS|D*$tC&yImbJ`-+)KUJ82SfN}>=5g}u#og~~(Ol+p`whIz z59>eI+yb$NDUb1W90Ka7E+{XsRvso?>I`!2J6*{zmuD-e$^Dr4L@xX8Q;IlA*IlMa zCYcsJ&Dd1mBW(|&&X-oL9`iBJfpo&EW5kPySrL8U3kYU&) zV#0X%V7y1kM9;?02xC}-vji7{xIwe=68JzDMN}huJS4j!TtFNkp072y`yLmYm z8SXT9O4lQf$it=-{GXYGiKcT`Y`A!aK+X-Eq=S3)lLb;T((?f}}k^ z8_nHM(JTJRFED(IC?PixyBjyQN9yX`60+3vG4slK5=!Ps#;B~>7W@x-yI#BK$qH&r z_X_Lw)>q{m(|gF8H}&zFnDecrLzr_*tjTJlwO)%$@pQsh%x|qwxY#|KlCwh8Kq`K0 z3rs|k!>w<+pY`MbWx#VLY^0unL=YdqUA#{QOcx`D4 z0?2v2epgpT;X3p41!7^aX`4eBK>RQkHv#imhBEtT-rXDS;p?9R6ligPXggKluq!I0;iAcH-a%?W^x;lZ6KuLqNePZ!GrHP25UU%HcnB4 z5^pC1B&1*~Hr{kg6)lDcjGUQgZ^O#yTUBf3s%)2b$|~yJoNNF}4TrlfW?{A&z;=Zx zx&@LDR|E7Ont!>`duqT}9Iyagyjcni*?G#Y)Xy3{moQf#K~0$hV?f|&W7Ea@`pD3A z$?KxkyYg^c&cp@>QuPh+{EnGSY1H5YPATj(zmoRBQ~t-{zv#w*q(I)x4qx$QVVqdkn;l9iNuhJPdg-Yr`}6x3OUOcW{evE#xesa%oRFCag(c z$`uWXDlAvnYbngdgM;`mW0VaHF`c`-mQ2NZdDk=9 zUC4bQ^}2jbdZ3ZL8^Kyy5BWp9EKxXhiQX-WV9yZ($@S+-da2TWbo+8X#5Fq%L~wtHsJ1Q$ zejgmW-NrP^nXVUTamGi@W^@vKUWRmoA}1B>S0j}e=_t$(V(?yoB@*$Ps>*#GydG2g zR+snpg+J*0`Go4Q`_cupD|Eze^G@4$HuO%$0pA0YZpO}YZrVj)cIC$L{|bA{pgP(o zdJwy~J3)iHyL)hV&&2}-3oaLT4RUdJcM0xp!QCB#yDjhUzqPyH_QTYip6VLusd}oO zuG8ndjcd`!|3TItPW!ME>>^pcb6H$ zIB)%?NBd1aket7CMen*?M=I*ziogOt&D)TCcQ`Aw4h^hX(v`;XoJPe0s%x18&phU`}(Q=Iw5sFoj{ywn|PD z-x5=1xs*kq9;B)2(46ErZO>v(1XwimDrnXwE$gtB3;dPU?)k9yKm)ZPMJOh4!a6dm z=;3|+zGzh+`p%P1i|x_bT3;VSNF1qBgwVd2UQYAji|O$VeOdZQYj#F|nOmY6$8mb0WB;$+HYiJY(J`gFTB%p&h-#NE!yIUE z${Mj7?r7o8Jfz@>3vaP75pHrZQW5 z&zcpxZN)paBqFzDHJwFIKRtl4poh=ow@JKN3y595*MKseb5y*v8zshp1T*@#7~G1? zcT7cUHl!VEiIgB2-jV1X|S9{CvNQd#@vIUe)k%sYC23y zIA}nqMT6ro*{?X(qY2_`d5R33s4_c=;NYJF_L_sHC-P{wqDOY+pP=xd6{CY%rSPx| zM$LL2+Fg+nJ`Z*KHT@wH@0f30ex?@_jZ(9n6z`WNru0iq`r0O# zMY3n7Q?HI?tCJO^ies;l=Cb6q$pq3)7NCsqQEmj$E^*vx-c90eXwRhs%)DPU7ZH`? z7~?gK2HAY0^*E68HO4ad(ebydIk!>E$G)hc_?nlIZ0~VU;ZH$e}*d-2tg{M zDHe>cq^edX?8C7RxD3=O==kd%6+-e?3LhQBr|2crhgW>=qy~&(Si;;2n>#2SdH^l1 z3xhNMZKFdlynhErm^uW^Wq(AKY${%tgoI8B z5@?OUB8!8D3jMdI*fcdT1bY@MVcy{i z-jJWnY~`1fTwg&Fn%d=0c_@7LfkZjrgYRZ) zmUeBgkofsh(!tDZE+PfOehjG7ttJ= zZFxygL!;Crlq;K8pxS!q`yxM&A9p?q(YSO$thxg%5Y9BeLOF-SW!skMIkKp$z6E}n z#62S*M}`E>)V7Sgc!ch;xDdA{4cT6!0zxbUI*y7Ul+e_WH9A>o%?PB<&!X!Owy*qDVg zh(Rua-M_s05D!EbFAJbT59XKrZmqct5Ae^ujFC)xQmx+-+I=`b=f}s%)owEsl@3$W zn{L86wUR5aFPNcDID%8*z<1)8n*H;BEIl7v*|h46-8h7Y&G#9j9$|Ujp>4EqYBxDh zmG3H9%i37LNbt4aX)ww?r$Y5_psHZ_8fkUTTl$Tg|07|p^A2Ua9&>5neQ_ncsgK$HC2)-@+Pr)xwY-*kCmXC^)QTe z6}$S1@ZNHXEXgcnkeiSssKxmb%$GU%ei;Q@+_^dUhEiwBoU*gk-q#Xwpu>e@IZUV%8!R)c3kBnI58D;X2%p4F zwZln&O7^;XaPih7>GUy@H2fOLY$(+x>Wt;vB;4U|8L1M<6TnWZ>Wdt%umjQ{0UI16 z#$JPkQNS(e=A`6Yr`}|!cbU!hfO~U-TPJ0IPXIHp9FvtP9B&{M+1IArLbpb7!Na=d z2-3HdbZo7M<7PRpdXRWsLg!-C5*S&lW(rOJ^QY~vV`Rg=Jj#l-hWvt_Yd)Sloe&RV z_wTFgz3*&&O7u|UwK)qd{CHc;N52%pQs=}>D@Z@JMOEdMIlm+G$~tX$O&z#RIHR;} z)#68im5<;Na=;j`K+F+#tFRoiAN z=m(;G`}d4F&OKA}NotXnmwDJx1>>f96%Nptk)S~JcprIrgAva8bj8ixAxpxK*~U2(owaxD9BoNx($p6 zy=^LPe(+-R1a*69tG7H=4c^wj{vbb!NTmDei5C}^aQC5w=53EvY_owvM%WeqoJ|T| zEGEYeUor@AH_Hnf^|~r=?(fPO@5NUTr~1;QxDGJSB5N>}dCJP3q-%Y$+YKg3WHR?~ z0q|$;sIj%!@WrT@IfBz7hhxr{wN-7>iBI@wgNl#Ts4Sm0VTYF79URyvC_up6QsRP+& zRm+l+RT@VO0Qv`&em=qPjeE*~i+ypI`u1sSrDN74@BLdh4YUH&P18ZHdmk^5*g4+X zM<4gtFvA(8pP~wE@@qJ%`-yYbv*K0pF+@c&L1u97$gS=wOnx8KLO+nx|6i z_g{rR=6$rk8r$!Pc1+bEO)Oj9Cm`HF9RpELw)fon$JWI8j3z_evtpsabQ+0*I$R_c zehrk83V@jSqJVHa zNvD7XQl>Qjctgf9r;oCS6b7hGuOzN zJRN{zO0pzD%2!?$K1Ab$OR>Uin4po!!Yb7WcJ1#6Kg?BPj8tPnhz1v9jI0jd%#j75 zC=s%X6Jn?D(|vZi8rtMRM}LLY*EV-e zR}tlFS+)SSR=l;B2{?UuluAPPN$9DUzH`bNZFH`Lx*dV?CsM#t-qaO=eROwDi|5JL zV4i<0f~Vpa6SaRR2|l`@B19;1_Uav&HNQwR*NPF)vF2$nX5z#KfPl|b4;^;Jt2XWW zr>!p+11i({GuZ#s8&N&Sg=^f-D!VW55TWl)M1P_~ttByw##Rb8lnWBo3Pmrk;CmfW zPpy2vJIr4Q9@|(FaFoV~AOgziB#XeTOJwEy%awKNRLw=I@&4VW?5G_BHto zj7$Z20lr3yX<(^F_5-M!fYc6z({C#+50P zV#brIqu~W?cW3Lahs)CqYEtf#oEhq1T9_UQ=kvYO^vuMZnT=jZNl;3IFZ` z?#Z5>-*wS>!o;WSg66B`llZXOUR;Z5&gX0l!@CI9TY|~^$HWJeymd<6|L9Cm+}!`^ ze9@wE|Gz395Wda4;7pnhV!d!nu?YhH4!qUhemtkGBHS(ZGl1ktDmM57-=}jFO(D zZm?_pTM!&x(o4-yF_UYF#jfO$xFtVijB(-wc`;IlO?yv^@;XXW6{)Y?OBeb8*)e0_ zEE#To5QZr~S3l#orI(L`Dn&*V#`qQC+jtZUF~*hGHq=LtLu*Ql4uYBz8wKp>wmQ6Y z&q`LH$p$*e3{~l|UlAs@r;bq^v!_Pok)3yd%akbm)R8j4Qr8yyD}EJp=^^)(i$=*3 z09_ZcM)F@cv@$zmMDJrh-7sZdEiO#>FbbA5i?oI`eOQ6BEcS?}g%bI|mRA8+(O53q zU)aRMPOC9&H1q3J@#-1p%Dnp=_W&$MiAShle2@&Ns*!sd-#oRqpLaNj9LI~9s)&r= zgV}lG`<6=m{Sn|aZBT(q#X$bA!ZuRA4z5*ZNR=o}u3Lq&6`rBEi@QXeKB1L$tl$r} z9^jhpITSV7C0LaMWEL?$Yh`VIeu-~es z04M^HVa+A&@3A0vZeeP{B&bKB6bp9x(*^uiGcg)9w;~Mr?fLNUZf`6$L2vjaf5v?y zfu$E&tYE)M!fGy zHgas{`ihbc;3Ld1Ry;i^5Eu-QT_v++X;*F8?JTm6ul%r#zKA zsdo?Go*u1lesGPKd!2H?oGqrsCg=|c^ZKp}O3WkKO>cya%-E%at-&I~=yhyv2oy(1vF565eG|ZpO~0 zUT!>uh-_C^OV`YjxuD$>fROm0+LBOFjQoi}&rWNDN8m`>N`j&By$Fq$fXbATx4N{Zn7$ zJGOp&)@$dcMGY#Kr<2B%Qc)lX;bCIXwOc^Bdtxced!ehszJ!=daQKGoA8gdRq3w?? zN*tE3G$8;-&c!TbeTiJ@nK&-Cjt+@dtnP-mIV4H6(qM7mOsmdqBX*AytfeK_0T>60 ztIBlP6}$fm z)LXnprl%erA)usUIqi8GFmPvDHbtpbj-Sdgx-OIvyWg)Bj4#H7Ev^s*<2iL}ecc?% zZBL}&OUBwD7%t5i`P9a_*uS_Ad`)qYeHY}-G#BdA{1bv&P?5e`{Mb`Z zgJvw$VI4957did(CjAW5^t7&X=YP29bNx#1!5w#J#*H8I?fM6&`>SYaV|>9W#W7Fp z9+36RNHgty4M=#5y51T7-&9=_RRWpUy4k-kr!zP2du`J3tZdbId$S%OUfxy_e$s^u zAGT^1W40@a4~c(?G6fNrQ`6}S#~pj~rC(pj^i73eWvnI+V9d%vTDn*9b{cZxr1%N5T`jT4qgj%7((L}m;g`LN^dok5Fbd~04CDEp6SLvS`+HsYu4T{PfP~&?Fmwz|oR5UDC1__6@ z6&N727ZBropz10?=nh!J4dMp=w!!Xb8ME9U0u=Qjl1NQqhDXUm zYc8>Of;Z*n@@O|4{KuWpAC1L{EXDH54OfHD4?g6s&xD(h#@;^+Vbt%G%xx3Zd3#=-P#65r!kjgg6Cmwlbj=EHe(BQpbiFp%U0)Am3a;EHpJUFFvX?=)VqO^gf1C zx*DbMlkHp@T_R}Vk=A!ww|s0(j(>K|FS2Qg9zZ;itUCF9J8OaB6mYkl&0lX>%zlUs zMcQ&y(FpO$0&1+unfgs)8tiggFF3>rlQGVA<9f$H!W&&J$0 z;w6t~>*JLHMyl_A}aSd`6Am&L?m|~(- z-R_wNDp4==4Prmgaw=Qcvk@ zqWEl7c(r*|nUC<^5|3wFo^kc!RP$5qeDXxT8`o%qW7GgP<+{Qje7$J5^?S(3$Oz>y zaKr(GW6oc9x-=aBf^FO27}Lqf?d0emO{5Ab8RHEXM*GVzYzL7nZw`{YS_?9Mk(lh- z0)YPHiaiBG`p`^R^DzExY`TOOAWA{XZ?$I%7wohD;Kl0oacf{s)AFi8E{x$@tnVFy#hVYFZ@vDt~&>rqa`4$TyC%= zObI7h&ks>e*M?4^7Q^?CVi3}DP}*3vGYFkN#z`N78_Yu7A2Du}voiffX@?N*Wvf6_ zcu-a)5~al=b4u6vN_L1@7&YSu*!3Dj@OSQ@O$J6q4%qioBZqtq>5k%6gX8r@y)0}?gd{J zBFX)^cV~xbn$Vf5J|ey^CSEI2vzR007;7_UW`dPGP+52GX|HxdX@k&AG=1fu<$aT1 zT>4@{8c-Z6Ll;T z=Gr3XxK0yEddDJeZ|S1GC=HOf;ss2S3YH#2ZjpRkQMWjKZjr6sTu2%OW@!-0VCx!Q zoD6qGjz}GV6SqxVb@kmoAcKA>E`xa~ZgfPq$;}cRPns0SjSS8=-}^Sn5=Yvq@_AsI z=|93b88$?hL^ff#%y8#1uShpUVT3n*?|*82XVi`M740O9Jora zzGv4gP5gAo@A}C5iN3nG13R@Py5d}q$sni@6-Pe+5#UFQvVUs3a=Cw^0chr%<4BF6 z*NLksO=XI_eMgM>wosHXe_e~A?O9a|FN=g^y~kEodUup0%RB>Xp&)zU{2eri}G6a`i12~(OBJx%`sH{C)VhLa8r1o$+XOs0T%8!`xoB1nhF1sJvkn>E+kYpvXY1(yi z*6&oomDgnlehtKL{yfz)WLnA*xe?v_+@#Se5t^L;Vmps)GeN`^NFz5MewH znsTnEPf3U!XZ?$r%e z&iDMhroM?tp`HyfppkY5;CGzTK!plQMhTLJ<5G{#!f5U$~`4JdIB9rmsK+pA}VLQ<$rnEDQ> z)izMiuQ4*1=}mnd0(>aB?7#w0b|j!xNe!&;0!>`nfoFT_p1Xi}7hz(QK^EUDuD3x= z?>8RA?8}>g0n)E~d#|?aLL#{Kf2vFZ-+Ud^K5qAV^g&f8(UwGgUC@DwUfJ5cQSrP} zkxa(IBh8%QpzHx_@lg?l!0?XmejkhV4}lmrdWRFr-E%j6h&^4BCTW`k$p^)k`>WAJ z;Qp6uX2?j~v@w_d`vLrSF5|f~l$%yKV|@50viv7JZ`*0djoq5Ku|x?nVml7S5(4C9v_yNj-tbXEY^5djJdk{2urgvM@$HiYuyNVxXGY)*=U zYQO+r7<_u{8RkKS6N3Zepm3UBx}aWB!Qb+raGO!4pnRyodtFd?&AE3_airj<$@Z+}&?ih$0Dz#NIC;SY%78!s8z(OpCpcFbK=OaXeq}%`B+q}_YQiu7sb@=DJ30R% z}frMo}NnC|48B*kc1i7@kSN&BmazQs6LG+NfovK zwJK2{6m0!#_Q#2+F0~dAYG5I!pNGM2lPHltT5SmUu3f_H)HA+?}r-x721!_4H(DdKO6m z|H?%BbP~jvYzyizr#8SkV9mk-#7Mqi)KoP@Cc_{sqAyIloX-wU!}&zxwCzmnm3%BF zhK1p=uzDNL^g%7%ASpy9612+SOo7RRMvx3ibZg;@?uJfy!CgIaSJ8E$+D=BDkF)q+GH9C$&h*>P!fbdP4CuO6Dv{h7cF_x z`$Z@+>7_n!EJwGXbWfKWCvNxN+PeXH+jwVsjJXLmi?KNJiX~qT?7v zj89{z0scvjnf$GWpx*;rRB8eZZ(Z51MGVgsBT?3fODVWW`LcAt&rYQ^D_10Bg__A1 zy2*a_MCh{u6ExiFBy4EXBq;AflPb7CJGsz8>dym>;a`3m~X}@Rvnz#QpKrU|@*^@EQ zttv|JC=o9h?qjPS@<`7>)h$frt_gXH7)gIw%JfS)YZ>9)Q)S0}C{V)Np|;`i1h+Y& zo$7E9^g&0T$0uLUjMF&(L)3sfaM-Qf}ezM#RMCW$AgX8NMP<4-tWMl zWnXzv$^-w*(Ym6XUYu(K-5C7mByq1*Mivs2bXwC$8maGEfh zAZ07XyDFOe6V8n{##2dBhUP+dr_5n(wbO4I%9q_03&s4REF&2a83~SGtAN^gc=QaKrzDjV!09Cp(vx^{F@@VLOHr~jwSnDQcvo!&TsFx zG*VZV6vey~rN_K3I z+E*bmC7IulrgC?bSjC+;a{aV}&SOZXok4Tl-+r6ZJ!m2W|CaL}fzPMME0_=m-P0O5 z68zDL;>FuRU>OaHW|IdS0_&W>Fz->exGj;cxwXM-l;Z6GWC{Cj#< zMadfhJ@%r*wMu~oE|@Rq_E)`u{#{zhwtT^d7nKnBH^`+e$YSoP$EPcs{>`u0q-}~OhPPz0{7?Rs67W4|G{MB|d!62*bN!M@^b{!@3 z?%1wYvb7=RmRe6puF(0s%2_i`bs+P__|O3r5z`4cauip3wzMKcWGAQ?BJPP zdB+GQNKTiApYu6Fnd^MP?A(*hc&9!yHQnW$FZ8iMcyWQQSbrU~>YBpqR&E+0dy-#i zn>!~={2?&sw!uXg?!cOKbzYtrE5Tb7pZ7T0!#yN8i?Mi_6t7KX)Z$^?bBat4c*-XIl7iBd1$U*fld9x^BR7qh+Oo5E|9rZ|@FXs@1OErh+4+6F)mE549I3C?2+#0#7JGP3cSr zcxc+%k^EjcYqIaNRYsk#+sfBG=V^IL zW{?PI6?0;}hqKUO@xxd}5d=9;&FHZqV4}Hp=%?)KdHuS4bMLoZ1&#YG;@f>Bk2UG+ z8Z`d?Qz@IKeVwBUEwdBt*A&V7?(ytJ=Y~(2h+VOxvuNe@Ih=tL!VgDh;owRc(Hlv+ z;7Tho(_3Nq)wYSVomkK2lFf_B1MTzO5Rm$lx6rCjhA}(u1ovG;{YI`fiCK)G-2%87 zug&ny`XPFhr+(~dz_!Q#oJpZBy;>W5b3OsQC&)L{&R7qf*?=3Yzu4^bd0{d!n6$Jn za@=Ehgr11~hvXYaFh}yu_!xV*+hJX-RGb&0&K5Pc`|9rX0- zGwJ60etnmI$h&!ea}A-0tTe=fI~Nwa!5MegnSAHttHqA|~rZaq#VANOX5(Qfw8$==Gd3@?KjVBhKp-AYvieKYVg~ za^_n(gC%;$X5wA7T_9!IWD44t0L?z{(l;GJ)FvTsZtT@FVLp1lz_s{fcBR|($NorV zs^k^8YRt;tPigI!T}cBvBztBn%KG7 z5W?EDWybD@XJ@%d2WYL?9*psC&RyWai!@&}-7#-Yf^Pu7Hkd6|Pg7t>LC^Ll^M%tM z9V_o6&k`NWlenc%K8{_9S37sB7umh9ZR@sPVqYVf`!AlUc3NKG-9(YYGGI#^ZhSiS&uw z_@LZ7vs&B5cXngxeH_vV1ii`JH6gM`dZYYwD6I7`iV>TY)$w*XTWvgVpXIwTSA8;3 zx6$e0&}3vl;#YM`@RzA8iZ;WW#v5N)*!A+bmt^!y(KGpL*8YbdDMk-&m$Bbw8){Dq zI6OZLD(zx(%K7O7y2fzxC(Op_6wl&aS?Ki-)VSi@DiM=^uI~sApo=OVm&^;{s)z2w zk20Q9;pYcArp*rD=hI#L(4(%3>Yv-xaL75|Aq*oRA5~kN!y)B1c|_A}8tBoA?0i_4 zZxLSYfI)ad)z47fu}}L4GJ*t*utb;SgfI zvspd~%+wY9hkq6tKx4}Tu)SBLUN06FQ@JM6{d(jwtS=lNH?AG!ZB|UmwXGvO*iu!D zhgYm#3k_9&b`wKHZwB_mq_HIbXrqE&E=O+ac_hfCY)FUw3WF(TL!V0j(2K`k(%up; z2WL^V_1WiG|KRadD42^>ec8!AsXzWD}jV0y;K<&up<1)HSSNIV@)& zV*`DVSpRfUwXfeCCI5)u6htsSju97q_mk!w+8l+Se;mXAiU@iq|MmD5x%X*2En3%= zy^FXI$3NS2SA=}=Rmjs~usLQfj*mkVu@BpbRcFsrvP-NDlI=Q$5~qJ;W7xt`^h!Fn z4PC^u(p~=)4TQ3j^VbjqU~gotGBPT+BZsM8H;UXVho?|S&nd~DsAQkk-FfO^9L4%t ztMY4VbI2DiVzYB~ghC*`aMEHmjRGtC{`OVFS5R7b|L|tQ^&5B?Ke8*k5pCR~Yn;Vx zQU6IZZQQ5v-2*VqEcVT1>)u(R@ahl>I+LDjE?oOc2}DyAqg7}!NMHI=&gq*#T4okP ze+AVP8lGA@Tl(d+-~&ows7l1=?s5KTIzs5~*N*{rl%sY%-#H$&r8VPi0jM{XeYUGP zv7`xbiS0%8wIMhocXAsw(i`Od%@#qy`O>P6n5soRQ%rQKzG0lb4aX-DJlHP}4UCuy>M46U<#O__C7kgYVK~uE;P40XKi^Y(mVi&TrcIq?o^ z3x^~1>UZ1T_$0c1S191DnC&Pi(0qVswaDBPzt+Cz_(AhpXon8z|`W8&RrxfyM&qjJ&eygPLFcivV~jPfS3CHe7#}Et8}KtRph{d?yfG?)Ip;0Bqeb& zG54{!ubx`-3){nckKZ&dxNu=H@X%U%6C2lriVO}_|!=^hJ+(g%q*T}^YB2#Au9(Fbh8O8iU; zr4}D;dyW0`($_evuEhh_HbV&k`SZ1YQIAoOa1yd@wI$CDN@z{T&9SW8;_3cGawxl? z9a?ZKRKPB`t9xE<#2d~oRwd6hh;P^uD#x&X%$&*Cy&;+IwS+KPwJH~_3*-2Jp5u&> zjJN2y0lX^3{m`I1e>_2A-ARg_{J0Gp((!!>N%EIXfDenc={I`M`=ywz;a++Dy($to5}e z$uDfS&WXRKl~)buiE-+6%5N-u>NoxcniH1J`X1$sPFm~ zg>e_zpX}S-gi>yCAK0EEDZNzVw|vV?Qy(h1|EweFaVKeFVr7Uvh*dK=f1}$-;3x_H zC?vC2UgT{43`JtXYxJ{@3;LgL_YfY4lJc@T_P28}VYIIm)4eYz0hwE(kt&R2LGPi# z^;=?cyBcxlm!!R>J(}lZjsZ!LV|$wC0Z8iSRH56i>*cdE zXIGH-yICtuS57=$%`(5E*2qRjzQV?;DWhJI>&MjfIE-DDDUChS#l+f2Tz^u>&eQoh zU6$wOYnx+AZRG34Op!>9)%UZ@hE_OT^8FmYT@WO&Rjucl%(S-JJP&4cjzD*gbZnIE zUL7gbq$QrGuO>9#zsFj%EdzaAFhyKfLE?+Pzmwbj5GUI2&R>o- z-@JWv4@K-x--dpQtj(2&It?=Zs$Tnm4r4g<1jq9Lu%I~kzyW3eauBN_B8$4Ur#Trn z*Joc&n@pdLjDw8r^P-x){Vy_Z&i`rLBC^Q-w6On-c>hzp{fs2Z^aXiXS$SDmIV2>+ z#3gunxum7UcsY0^*(G^-*d;i5IQfLh{{K@9k^i3tAR9N||4c537d#xa<_U(bUe@-^ zv1G)>aqA>7>q3L#f<SB;1$*_5n^QQ2_%); zRm!WbdiJXd01A`zl2T&~i+)OgHfccK2N^UELJwJ9S%(>fQZq3&j%LkOfsidwze;s%x%m#4ePUa=D3AeMvR;RO+ zRR3a-z!Brd_Uls5{z0e^csz~Dkps>Do%@#r+PJ?_3X;z>+Kox4)cPU$n_=f*pjSv` z2y>)d)3XGK*)O;TzAAK}20OXONe7bQJB~zrN{{-FXsOBzHVrbIc*-w4ypT{_HKQ6z?azm7~_b{bD%Z| zik1c&)!&-xwMQLFS!56Mf34fZ;QPR7b(Y-lRjnbaiV&v)<2-bOfxYT^d%CL65~@wI z3JSRUZV?fNd1W#hZvJW;Kay&>OAY4i@laMJO@&`II41&WByTA48nqQVBpTQ@8h|Pw zw&mNYfvEN{?KOaW_a=Jdmy&r_2o0+o@jlq4VV8G~K>XiiSap7tWEE_uyt|835>O%C zD)_K{-}x&1%yAzIk4k^?ZHtVB5MB0tT|7H3w>6F@Ex^HDjrKL9iVBUlIIVUckB#xZ zgw98QO`#1@W&NT?hV$1XYB?E5N=D2MGO1YBny*HwLkz_0Z?X?>s}8?l#9T#-AZ9#L zI)`g|k**B|v>XxwFB~3|mWmBW?>CuH@n6-6URzthT>O%VM83Lf#Y}xMRxfq=AiSY> z%6}rvB-8JfXpLc^;l$OrG^70^+5>dweD^|HcekcH80ReaN7#-Q{_bboS3XIgFroNk ztcpPTl8`S}K?oKiyvN`Epng#Adx{gEqJ z=Y=!_7d(+SUFg-nm#c{}K`pfJ?i%e?Z_B;Ft3d_F?J##|oJ+ZlFX;AYI2UGNB0PQR zJ1i(WLtFb|#w={PVNWqntm5y`7=2w&E$D!DPP!SB$N*vO|mvPHJ zdHGGlqGO3_x$8SFOZPW~bUYuH!OQ zE|YNvF3!r&9XaW$ztmXT*?3$m5&W4Zjy^E_+Pfizz&fQ7_fg3i4TRXpgWxUo>@A|B;MQrlH&(QRJ}4a|A>7m8X+8MJ?>H49TxYLY7N_5#Z7Fee^FGuh$H+n` zWne#q?_!NUM(?Ipknt@*SEFsOmVHdZaXXb5xY_Mnm8Xopt_jW3dfvXx&C z`f4kD_JulX59%9ojRh{%?hF&Zg@sNl*^*z)x|;tosXqH>H5gcm=E=FFmiQ>m*^J3> z{)om@)N7Y47e>K6m+O$U%uKn9I8wZU{BNB--4wxiOYcH)x6fpH=R1en`>)6;2!^ak s77{$S-T&VYlE2NIsb8xRc@{wq-kk!&+5$+B*g5#P5vi!8l%)~>FCjQyT>t<8 diff --git a/docs/design/model-pool/learn.tex b/docs/design/model-pool/pareto-crewing.tex similarity index 94% rename from docs/design/model-pool/learn.tex rename to docs/design/model-pool/pareto-crewing.tex index 935aa2ad3d..cbba48b2d8 100644 --- a/docs/design/model-pool/learn.tex +++ b/docs/design/model-pool/pareto-crewing.tex @@ -23,7 +23,7 @@ \pagestyle{fancy} \fancyhf{} \fancyhead[L]{\small\textsc{Preprint}} -\fancyhead[R]{\small Crews from Evidence} +\fancyhead[R]{\small Pareto Crewing} \fancyfoot[C]{\small\thepage} \renewcommand{\headrulewidth}{0.2pt} @@ -49,8 +49,8 @@ \theoremstyle{remark} \newtheorem{remark}{Remark} -\title{\textbf{Crews from Evidence}\\[6pt] -\large Privacy-preserving, decentralised measurement of language-model teams,\\ and a cost-aware learner that picks them} +\title{\textbf{Pareto Crewing}\\[6pt] +\large Decentralised, privacy-preserving measurement of language-model teams,\\ and a learner that picks them on the cost-quality front, per role and per task} \author{Santosh Kumar Radha} \affil{AgentField AI} \date{September 2026} @@ -61,17 +61,17 @@ \begin{abstract} \noindent -An agent harness runs a task through a \emph{crew}: an assignment of one language model to each of $n$ roles (in our instance a worker that does the work, a checker that reads it and a planner that cuts it). The crew that pays best is neither the cheapest nor the strongest on paper; it is the one that minimises the expected cost of one clean delivery once failed runs, wall time and shipped defects are priced. We give that objective in closed form, show how a crew is computed from a published catalog of prices and benchmark indexes alone (a pareto front in bill and quality, with a scale-free knee), and then how measurements from real runs replace the catalog's proxy. Runs are scored role by role by a model outside the crew. Each score becomes a row that carries no text, no file name, no price and no identity beyond a per-install nonce, so what an install sends is a handful of coarse fields about a model, not about the work; rows reduce to sufficient statistics that form a join-semilattice, so any number of installs and a relay can merge them in any order and reach the same state, the relay is a convenience rather than an authority, and the published index is signed so that every reader can check what it is blending in. Participation is a setting: an install can contribute, read and learn from its own runs without sending, or stay out. Judge severity is removed by a two-way fixed-effects fit on those statistics, drift by a local-level model whose forgetting rate is estimated rather than set, and heterogeneity between installs by a hierarchical prior whose variance is estimated by moments; every statistical constant is an empirical-Bayes estimate, and what remains for the user to set is the price of a defect and the price of time. The crew is chosen by Thompson sampling with semi-bandit feedback: because the objective is linear in role qualities and every role is scored separately, a run on one crew informs every crew that shares a model with it, and the sample complexity scales with the number of models rather than the number of crews. On a ground truth fitted to our own runs the learner beats a fixed table by a third of the remaining regret within a few hundred runs, and role-level feedback halves the regret of a crew-level learner fed the same scores at four hundred runs. +\emph{Pareto crewing} is the practice of picking the language-model team for a task on the cost-quality front, one model per role, from evidence about how models have performed in that role rather than from a fixed table. An agent harness runs a task through a \emph{crew}: an assignment of one language model to each of $n$ roles (in our instance a worker that does the work, a checker that reads it and a planner that cuts it). The crew that pays best is neither the cheapest nor the strongest on paper; it is the one that minimises the expected cost of one clean delivery once failed runs, wall time and shipped defects are priced. We give that objective in closed form, show how a crew is computed from a published catalog of prices and benchmark indexes alone (a pareto front in bill and quality, with a scale-free knee), and then how measurements from real runs replace the catalog's proxy. Runs are scored role by role by a model outside the crew. Each score becomes a row that carries no text, no file name, no price and no identity beyond a per-install nonce, so what an install sends is a handful of coarse fields about a model, not about the work; rows reduce to sufficient statistics that form a join-semilattice, so any number of installs and a relay can merge them in any order and reach the same state, the relay is a convenience rather than an authority, and the published index is signed so that every reader can check what it is blending in. Participation is a setting: an install can contribute, read and learn from its own runs without sending, or stay out. Judge severity is removed by a two-way fixed-effects fit on those statistics, drift by a local-level model whose forgetting rate is estimated rather than set, and heterogeneity between installs by a hierarchical prior whose variance is estimated by moments; every statistical constant is an empirical-Bayes estimate, and what remains for the user to set is the price of a defect and the price of time. The crew is chosen by Thompson sampling with semi-bandit feedback: because the objective is linear in role qualities and every role is scored separately, a run on one crew informs every crew that shares a model with it, and the sample complexity scales with the number of models rather than the number of crews. On a ground truth fitted to our own runs the learner beats a fixed table by a third of the remaining regret within a few hundred runs, and role-level feedback halves the regret of a crew-level learner fed the same scores at four hundred runs. \end{abstract} \vspace{4pt} -\noindent\textbf{Keywords:} model selection, pareto front, empirical Bayes, Thompson sampling, combinatorial semi-bandits, CRDT, judge calibration, privacy, decentralised measurement. +\noindent\textbf{Keywords:} Pareto crewing, model selection, pareto front, empirical Bayes, Thompson sampling, combinatorial semi-bandits, CRDT, judge calibration, privacy, decentralised measurement. \section{Introduction} A harness that drives language models through software tasks has to decide, before every task, which model sits in which role. The decision is made under three kinds of information that arrive at different times and in different units: published prices and benchmark indexes, available for every model on the day it appears; the harness's own past runs, few but on the right distribution; and the runs of everyone else using the same harness, many but on other people's tasks and scored by other people's judges. This note is about turning the three into one decision. -Our contributions are the following. +We call the resulting discipline \emph{Pareto crewing}: the crew is picked on the cost-quality front, per role and per task, from whatever evidence exists on the day, and the evidence is pooled across installs without any install giving up what it worked on. Our contributions are the following. \begin{enumerate} \item A closed-form objective for a crew, the expected cost of one clean delivery under reruns and a priced defect (Section~\ref{sec:objective}), and the observation that on a catalog alone the objective cannot be evaluated, which is why the catalog picker is a pareto front with a scale-free knee (Section~\ref{sec:catalog}). \item A decentralised measurement of role quality (Section~\ref{sec:pool}): text-free rows, sufficient statistics that form a state-based CRDT (Proposition~\ref{prop:crdt}), judge fixed effects identified from the connectivity of the judge-cell graph (Proposition~\ref{prop:ident}), robust aggregation across installs, and drift handled by a local-level model. From 5991b1f65f0888ed3c3501edb1140433f315d3ef Mon Sep 17 00:00:00 2001 From: santoshkumarradha Date: Thu, 17 Sep 2026 12:03:16 -0400 Subject: [PATCH 118/173] manual: catalog and learn picks are named Pareto crewing Co-Authored-By: Claude Fable 5.1 --- internal/manual/chat/models-and-cost.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/internal/manual/chat/models-and-cost.md b/internal/manual/chat/models-and-cost.md index 85d90ba9d6..26c6a26eb6 100644 --- a/internal/manual/chat/models-and-cost.md +++ b/internal/manual/chat/models-and-cost.md @@ -458,6 +458,9 @@ money are read from: judged runs, carried as a quality rating the better-measured models read on top of their published scores. +`catalog` and `learn` are Pareto crewing: the crew is picked on the cost-quality front, +per role and per task, from evidence rather than from a fixed table. + A pick moves the three seats the presets dial — **worker**, **careful work** and **mastermind** — and never the two that read every turn: **reflex** and **small work** keep their near-free ids, the same ones in every preset. From 354158f9bb6c38ac56ac896f98657c9e7d1951d5 Mon Sep 17 00:00:00 2001 From: santoshkumarradha Date: Thu, 17 Sep 2026 12:05:16 -0400 Subject: [PATCH 119/173] session, cmd/codeaf: the judge hook's comments speak at one volume Co-Authored-By: Claude Fable 5.1 --- cmd/codeaf/poolrecord.go | 8 ++++---- internal/session/session.go | 4 ++-- internal/session/task_audit.go | 2 +- internal/session/task_run.go | 4 ++-- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/cmd/codeaf/poolrecord.go b/cmd/codeaf/poolrecord.go index 6b8e62727c..fb2adffec8 100644 --- a/cmd/codeaf/poolrecord.go +++ b/cmd/codeaf/poolrecord.go @@ -9,7 +9,7 @@ // TaskLanded) and the one provider call the judge's questions ride, billed to // the judge's own seat. // -// QUIET BY DESIGN, for poolindex.go's reason: a landing that nobody could +// Quiet by design, for poolindex.go's reason: a landing that nobody could // score is an ordinary state — a pool switched off, a catalog with nothing // left to pick, a model that did not answer — and nothing the person is // reading should move for it. Every error here is said only under the debug @@ -44,7 +44,7 @@ import ( // for, and it is generous on purpose — the questions are one call each. const judgeTimeout = 90 * time.Second -// poolJudgeHook builds the session's landing reader. NIL IS OFF, for the pool +// poolJudgeHook builds the session's landing reader. Nil is off, for the pool // index's reason: a mode that forbids reading runs no judge and writes // nothing, and the door then hands the engine no hook at all. func poolJudgeHook(settings config.Config, profileDir, workspace string, models func() []catalog.Model, ask func(model string) judge.Ask, now func() time.Time) func(session.TaskLanding) { @@ -96,7 +96,7 @@ func poolJudgeLanding(settings config.Config, profileDir string, models func() [ ctx, cancel := context.WithTimeout(context.Background(), judgeTimeout) defer cancel() scores, err := judge.Judge(ctx, ask(judgeID), rec) - // THE SCORES OBTAINED ARE RECORDED EVEN WHEN THE ERROR NAMES A SEAT: a + // The scores obtained are recorded even when the error names a seat: a // judge that scored the worker but not the high seat scored the worker, // and a seat the call failed on is the judge's evidence of that model too. if err != nil && trace.Enabled() { @@ -134,7 +134,7 @@ func poolJudgeLanding(settings config.Config, profileDir string, models func() [ } return } - // THE NEXT PICK IN THIS PROCESS READS THE NEW CELLS AT ONCE, the same way + // The next pick in this process reads the new cells at once, the same way // the picker reads them at start-up (poolindex.go's poolOwnCellsFor): a // closing one is the install's own evidence and is never held to the // index's min_installs. diff --git a/internal/session/session.go b/internal/session/session.go index a605e9b9ef..a967cbf274 100644 --- a/internal/session/session.go +++ b/internal/session/session.go @@ -1410,14 +1410,14 @@ type Config struct { // the node's ordinary read-only checker; a test may answer deterministically. TaskProgressCheck func(brief string, evidence []string) (working bool, reason string) - // TaskLanded is called ONCE per landed node, on its own goroutine, after the + // TaskLanded is called once per landed node, on its own goroutine, after the // node's row is in the project's index (task_run.go's [Agent.reportTaskNode]). // It carries [TaskLanding]: the node's record as the landing left it, the // worker's model and the model the checking pass ran on (empty when there was // none). A final state only — running and queued nodes land nothing — and a // call that never blocks the reporting path: the reporting goroutine hands the // landing over and moves on, and a caller that is slow holds up nothing but - // its own goroutine. NIL IS OFF, which is what every caller that does not + // its own goroutine. Nil is off, which is what every caller that does not // want the news hands in, and what this package then spends nothing on. TaskLanded func(TaskLanding) diff --git a/internal/session/task_audit.go b/internal/session/task_audit.go index 7271889d40..edb9161f5e 100644 --- a/internal/session/task_audit.go +++ b/internal/session/task_audit.go @@ -1245,7 +1245,7 @@ func (a *Agent) auditOnce(ctx context.Context, node *TaskNode, tree taskTree, gr // the same pocket's reason: a landing that later reports who checked the // work names the model that actually answered, failover included, and // not the one that was asked for. The auditor's model is read from the - // agent itself rather than from `on`, which is the ASKED-FOR id. + // agent itself rather than from `on`, which is the asked-for id. node.graph.mu.Lock() node.checkedOn = auditor.Model() node.graph.mu.Unlock() diff --git a/internal/session/task_run.go b/internal/session/task_run.go index 7ea52478d8..d9be08e382 100644 --- a/internal/session/task_run.go +++ b/internal/session/task_run.go @@ -3818,7 +3818,7 @@ func (n *TaskNode) burned() int { // crew takes as long as it takes — holds up nothing on the reporting path, // which is already answering to the note delivery beside it. // -// THE SNAPSHOT IS TAKEN HERE, on the reporting goroutine, under the node's own +// The snapshot is taken here, on the reporting goroutine, under the node's own // lock discipline (graph.mu, the room's spend read outside it as notice does): // a landing built later on the reader's goroutine could read a node already // re-armed, and the record the reader judged would not be the one that landed. @@ -3827,7 +3827,7 @@ func (a *Agent) tellTaskLanded(node *TaskNode) { if hook == nil { return } - // A landing is a FINAL state only — work that ended on an answer: done, + // A landing is a final state only — work that ended on an answer: done, // failed, or ended where nobody could check it. A node still queued or // running has no record to judge yet, and its own updates already travelled // the reporting path above. From 4a3c5499b471ab6b16dade3d0fa1899fe8a034fa Mon Sep 17 00:00:00 2001 From: santoshkumarradha Date: Thu, 17 Sep 2026 12:06:38 -0400 Subject: [PATCH 120/173] cmd/codeaf: the logs entry on the help page fits eighty columns Co-Authored-By: Claude Fable 5.1 --- cmd/codeaf/main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/codeaf/main.go b/cmd/codeaf/main.go index 6eae8ed539..4df6371d29 100644 --- a/cmd/codeaf/main.go +++ b/cmd/codeaf/main.go @@ -375,7 +375,7 @@ Look at what happened — read-only, no key, nothing spent codeaf logs [--tail 40] [--follow] [--path] [--json] [--run id] [--call id] [--tag t] [--model m] [--node n] [--body id] every model call codeaf made — what was asked, which lane answered, what - came back; --json rows as on disk, --body one call; CODEAF_CALL_LOG=off is off + came back; --json as on disk, --body one call, CODEAF_CALL_LOG=off is off codeaf models [--refresh] the models this machine will use, and what each has been measured at codeaf pool [show|status|verify] [--json] [--key key] From e4539e64b20893f1c7483d9637dafaac3cd825ff Mon Sep 17 00:00:00 2001 From: codeaf Date: Thu, 17 Sep 2026 12:10:12 -0400 Subject: [PATCH 121/173] task: Pool relay runbook, README line and change note Co-Authored-By: codeaf --- README.md | 8 +++ docs/changes/unreleased/1093-pool-relay.md | 15 ++++ docs/design/model-pool/RUNBOOK.md | 81 ++++++++++++++++++++++ relay/README.md | 14 ++++ 4 files changed, 118 insertions(+) create mode 100644 docs/changes/unreleased/1093-pool-relay.md create mode 100644 docs/design/model-pool/RUNBOOK.md create mode 100644 relay/README.md diff --git a/README.md b/README.md index 6bde91a003..9906a4e2dd 100644 --- a/README.md +++ b/README.md @@ -195,6 +195,14 @@ request goes to the provider that has been fastest for that kind of call. Providers built in: OpenRouter, DeepSeek, GLM, Kimi, MiniMax, Qwen, Ollama and any OpenAI-compatible endpoint. +## Model Pool + +The Model Pool index is mirrored on the `model-pool` branch at `pool/index.json`. +[How to run your own relay](docs/design/model-pool/learn.pdf) explains the worker +behind it. + +[![pool updated](https://img.shields.io/github/last-commit/Agent-Field/CodeAF/model-pool?label=pool%20updated)](https://github.com/Agent-Field/CodeAF/tree/model-pool) + ## Standing orders Rules, reminders and watches are one thing, and you set them up by saying them. diff --git a/docs/changes/unreleased/1093-pool-relay.md b/docs/changes/unreleased/1093-pool-relay.md new file mode 100644 index 0000000000..0f5350fcb2 --- /dev/null +++ b/docs/changes/unreleased/1093-pool-relay.md @@ -0,0 +1,15 @@ +--- +kind: added +title: A Cloudflare Worker relay that folds installs' rows into a signed, versioned pool index +pr: 1093 +surface: [engine] +invalidates: [] +--- + +The relay under `relay/` accepts NDJSON measurement rows at `POST /pool/v1/rows`, +keyed by the install's `X-Codeaf-Install` header, folds each install's rows into +per-install per-day running totals in KV, and once an hour publishes a signed, +versioned JSON index at `GET /pool/index.json` beside its Ed25519 detached +signature at `GET /pool/index.json.sig`. The document is what +`internal/pool/index` reads and is signed with a key whose public half is built +into the client. diff --git a/docs/design/model-pool/RUNBOOK.md b/docs/design/model-pool/RUNBOOK.md new file mode 100644 index 0000000000..b592eed19c --- /dev/null +++ b/docs/design/model-pool/RUNBOOK.md @@ -0,0 +1,81 @@ +# Model Pool relay — runbook + +The relay is a Cloudflare Worker under `relay/`. It accepts measurement rows +from installs over NDJSON at `POST /pool/v1/rows`, folds each install's rows +into per-install running totals ("sheets") in KV, and once an hour publishes a +signed, versioned JSON index that installs read. It serves, all under the +`/pool/` prefix: + +- `POST /pool/v1/rows` — submit rows. Requires the `X-Codeaf-Install: <32 + lowercase hex>` header; the body is at most 256 KiB and at most 200 lines. +- `GET /pool/index.json` — the published index document. +- `GET /pool/index.json.sig` — its detached Ed25519 signature. +- `GET /pool/healthz` — a liveness probe. + +Every other path answers 404 with an empty body. The document is signed with +Ed25519: the Worker holds the 32-byte private seed as the secret +`POOL_SIGNING_KEY` (standard base64) and the 32-byte public key as the plain var +`POOL_PUBLIC_KEY` (standard base64). Installs read +`https://codeaf.agentfield.ai/pool` by default; `CODEAF_MODEL_POOL_RELAY_URL` +points an install elsewhere and `CODEAF_MODEL_POOL_PUBLIC_KEY` sets the trusted +key. + +## Deploy + +1. Have a Cloudflare account with **Workers & Pages** enabled. +2. `npm i -g wrangler && wrangler login`. +3. `cd relay && wrangler kv namespace create POOL`, then paste the returned id + into the `[[kv_namespaces]] id` in `wrangler.toml`. +4. Generate the signing key once, off the repository, and set the secret: + - `openssl genpkey -algorithm ed25519 -out pool-ed25519.pem` + - raw 32-byte seed, standard base64: + `openssl pkey -in pool-ed25519.pem -outform DER | tail -c 32 | base64` + - raw 32-byte public key, standard base64: + `openssl pkey -in pool-ed25519.pem -pubout -outform DER | tail -c 32 | base64` + - `wrangler secret put POOL_SIGNING_KEY` reads the seed from stdin, so pipe + it in: + `openssl pkey -in pool-ed25519.pem -outform DER | tail -c 32 | base64 | wrangler secret put POOL_SIGNING_KEY` + - put the public key into `[vars] POOL_PUBLIC_KEY` in `wrangler.toml`. + Never write the seed anywhere in the repository. +5. `wrangler deploy`. +6. DNS and the route: create a proxied DNS record for `codeaf.agentfield.ai` + (AAAA `100::` or A `192.0.2.1`, proxied) so the route can attach. The route + in `wrangler.toml` binds `codeaf.agentfield.ai/pool/*` to the Worker. + Installs read `https://codeaf.agentfield.ai/pool` by default, and + `CODEAF_MODEL_POOL_RELAY_URL` points an install elsewhere. +7. First publication: `wrangler triggers deploy` and wait for the next `:17`, + or, under `wrangler dev --test-scheduled`, drive it by hand with + `curl -X POST "http://localhost:8787/__scheduled"`. +8. Verify from a laptop: `curl -sI https://codeaf.agentfield.ai/pool/index.json` + (expect 200 and an ETag), then `codeaf pool verify`, then `codeaf pool + status`. +9. Know what the store holds — one running total per install, cell and day; a + per-install per-day quota counter; and the last index document and its + signature — and how to wipe it: `wrangler kv key list` to see the keys and + `wrangler kv key delete ` to drop one. +10. Rotating the signing key: generate a new seed, add the new public key to the + binary's trusted key list, then redeploy with the new secret. + +## Run your own relay + +Generate your own keypair with the same openssl lines as step 4, then +`wrangler deploy` to your own account with your own route or the workers.dev +address, `wrangler secret put POOL_SIGNING_KEY` to set the seed, and your own +`POOL_PUBLIC_KEY` in your own `wrangler.toml`. Point installs at it with +`CODEAF_MODEL_POOL_RELAY_URL` and `CODEAF_MODEL_POOL_PUBLIC_KEY` (the trusted +key). + +The mirror workflow is optional; it takes the same two values as its workflow +env. `relay/` holds no secret and no value that is not documented in +`wrangler.toml` or this runbook — the quota, the min installs and the cron — so +a copy of the directory is a complete relay. + +## Repository settings, for the optional mirror + +- **Actions:** enable Actions on the repository if it is disabled. +- **Workflow permissions:** under Settings → Actions → General → Workflow + permissions, set "Read and write", which the mirror needs to push the data + branch. +- **Run it once by hand:** dispatch the mirror workflow with + `workflow_dispatch`. +- **Confirm it worked:** a commit appears on the `model-pool` branch. diff --git a/relay/README.md b/relay/README.md new file mode 100644 index 0000000000..b58635edaf --- /dev/null +++ b/relay/README.md @@ -0,0 +1,14 @@ +# Model Pool relay + +This Worker stores one running total per install, cell and day — the per-install +sheets it folds submitted rows into — plus a per-install per-day quota counter, +and the last published index document and its signature. + +It never stores a client address and keeps no request log, and it reads the +32-byte private signing seed only from the `POOL_SIGNING_KEY` secret, which +lives in Cloudflare, not in this directory. + +It serves, all under `/pool/`: `POST /pool/v1/rows` for submissions, +`GET /pool/index.json` for the published index, `GET /pool/index.json.sig` for +its signature, and `GET /pool/healthz`; every other path answers 404 with an +empty body. From 32de3d5b0d7622153460ef12541aba8802c50bf4 Mon Sep 17 00:00:00 2001 From: codeaf Date: Thu, 17 Sep 2026 12:11:10 -0400 Subject: [PATCH 122/173] task: Pool index mirror workflow and verifier Co-Authored-By: codeaf --- .github/workflows/pool-mirror.yml | 95 +++++++++++++++++++++++++++++++ relay/verify.mjs | 45 +++++++++++++++ 2 files changed, 140 insertions(+) create mode 100644 .github/workflows/pool-mirror.yml create mode 100644 relay/verify.mjs diff --git a/.github/workflows/pool-mirror.yml b/.github/workflows/pool-mirror.yml new file mode 100644 index 0000000000..8dd9b92489 --- /dev/null +++ b/.github/workflows/pool-mirror.yml @@ -0,0 +1,95 @@ +name: Model Pool mirror + +# Mirrors the relay's signed index document onto the `model-pool` orphan data +# branch, under pool/index.json and pool/index.json.sig, so it can be served +# and linked from GitHub. The branch holds the signed document and its +# signature, and nothing derived from them — no count, no leaderboard, no +# extracted statistic. +on: + schedule: + # Every six hours. + - cron: '0 */6 * * *' + workflow_dispatch: + +permissions: + contents: write + +concurrency: + group: pool-mirror + cancel-in-progress: false + +jobs: + mirror: + runs-on: ubuntu-latest + steps: + # Gets relay/verify.mjs, the only thing this workflow needs from the tree. + - uses: actions/checkout@v4 + + # -fsS: fail the job on any non-200, print an error on any other failure. + - name: Fetch the signed document and its signature + run: | + set -euo pipefail + curl -fsS -o index.json https://codeaf.agentfield.ai/pool/index.json + curl -fsS -o index.json.sig https://codeaf.agentfield.ai/pool/index.json.sig + + # The value is the public key copied from relay/wrangler.toml, and it is a + # literal: no secret is needed to verify a signature. A bad signature or + # any non-2xx fetch fails the job before anything is published. + - name: Verify the signature + env: + POOL_PUBLIC_KEY: WOAo+g/oKxAV9vVqv2Q14w1TyyyiouwFO2fC0zgcps0= + run: node relay/verify.mjs index.json index.json.sig "$POOL_PUBLIC_KEY" + + - name: Mirror onto the model-pool branch + env: + GH_TOKEN: ${{ github.token }} + run: | + set -euo pipefail + mkdir -p mirror + cd mirror + git init -q + git remote add origin "https://x-access-token:${GH_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + + # The data branch shares no history with the tree; adopt it when it + # exists and start an orphan when this is the first run. + if git fetch -q origin model-pool; then + git checkout -q -B model-pool FETCH_HEAD + else + git checkout -q --orphan model-pool + fi + + mkdir -p pool + if [ ! -f pool/README.md ]; then + cat > pool/README.md <<'EOF' + # model-pool + + This orphan branch holds the Model Pool's published index document and + its detached signature, and nothing derived from them. + + - `pool/index.json` — the signed index document. + - `pool/index.json.sig` — the detached Ed25519 signature (standard + base64) over the exact bytes of `pool/index.json`. + + Both files are copies of what the relay serves; there is no count, no + leaderboard and no extracted statistic here. Verify them with: + + ``` + node relay/verify.mjs pool/index.json pool/index.json.sig + ``` + EOF + fi + + cp ../index.json pool/index.json + cp ../index.json.sig pool/index.json.sig + + git add -A + if git diff --quiet && git diff --cached --quiet; then + echo "No change; nothing to publish." + exit 0 + fi + + version=$(node -e "process.stdout.write(String(JSON.parse(require('fs').readFileSync('../index.json', 'utf8')).version))") + git commit -q -m "pool: index version ${version}" + git push -q origin model-pool diff --git a/relay/verify.mjs b/relay/verify.mjs new file mode 100644 index 0000000000..87d6eff610 --- /dev/null +++ b/relay/verify.mjs @@ -0,0 +1,45 @@ +#!/usr/bin/env node +// Verifies the detached Ed25519 signature over a Model Pool index document. +// +// node relay/verify.mjs index.json index.json.sig +// +// The signature is standard base64; whitespace around it is ignored. Exit 0 +// with no output when it verifies over the exact bytes of the document file. +// Exit 1, naming the failure, when the signature does not verify, when an +// argument is missing, or when a file cannot be read. + +import { readFileSync } from 'node:fs'; + +const [docPath, sigPath, publicB64] = process.argv.slice(2); +if (!docPath || !sigPath || !publicB64) { + console.error('usage: node relay/verify.mjs index.json index.json.sig '); + process.exit(1); +} + +let docBytes, signature; +try { + docBytes = readFileSync(docPath); + signature = Buffer.from(readFileSync(sigPath, 'utf8').trim(), 'base64'); +} catch (err) { + console.error(`cannot read input: ${err.message}`); + process.exit(1); +} + +let ok; +try { + const jwk = { + kty: 'OKP', + crv: 'Ed25519', + x: Buffer.from(publicB64, 'base64').toString('base64url'), + }; + const key = await globalThis.crypto.subtle.importKey('jwk', jwk, { name: 'Ed25519' }, false, ['verify']); + ok = await globalThis.crypto.subtle.verify({ name: 'Ed25519' }, key, signature, docBytes); +} catch (err) { + console.error(`cannot verify: ${err.message}`); + process.exit(1); +} + +if (!ok) { + console.error('signature does not verify'); + process.exit(1); +} From f826f828096a62520e4846e072a4a6d6abe59478 Mon Sep 17 00:00:00 2001 From: santoshkumarradha Date: Thu, 17 Sep 2026 12:17:52 -0400 Subject: [PATCH 123/173] relay: the test suite runs from the directory-form command --- relay/package.json | 8 ++ relay/src/index.js | 85 +++++++++++++ relay/src/schema.js | 138 +++++++++++++++++++++ relay/src/sheet.js | 248 ++++++++++++++++++++++++++++++++++++++ relay/src/worker.js | 232 +++++++++++++++++++++++++++++++++++ relay/test/index.js | 12 ++ relay/test/index.test.js | 76 ++++++++++++ relay/test/schema.test.js | 92 ++++++++++++++ relay/test/sheet.test.js | 112 +++++++++++++++++ relay/test/worker.test.js | 179 +++++++++++++++++++++++++++ relay/wrangler.toml | 24 ++++ 11 files changed, 1206 insertions(+) create mode 100644 relay/package.json create mode 100644 relay/src/index.js create mode 100644 relay/src/schema.js create mode 100644 relay/src/sheet.js create mode 100644 relay/src/worker.js create mode 100644 relay/test/index.js create mode 100644 relay/test/index.test.js create mode 100644 relay/test/schema.test.js create mode 100644 relay/test/sheet.test.js create mode 100644 relay/test/worker.test.js create mode 100644 relay/wrangler.toml diff --git a/relay/package.json b/relay/package.json new file mode 100644 index 0000000000..500852336b --- /dev/null +++ b/relay/package.json @@ -0,0 +1,8 @@ +{ + "name": "codeaf-pool-relay", + "private": true, + "type": "module", + "scripts": { + "test": "node --test test" + } +} diff --git a/relay/src/index.js b/relay/src/index.js new file mode 100644 index 0000000000..fabbee0550 --- /dev/null +++ b/relay/src/index.js @@ -0,0 +1,85 @@ +// index.js builds the signed index document and signs it. +// +// The document is what internal/pool/index.Parse reads (see +// internal/pool/index/seed.json for a complete example). Keys sit in a fixed +// order and the text is JSON with no spaces, so the same cells always render +// the same bytes. +// +// signIndex signs under Ed25519 through WebCrypto, which is the same API +// under Workers and under node ≥ 20 through globalThis.crypto.subtle. + +const SCHEMA = 1; + +// buildIndex renders the document for cells. The keys are written in the fixed +// order the reader expects; JSON.stringify keeps the order of object keys. +export function buildIndex(cells, { version, generated, minInstalls, judges }) { + const doc = { + schema: SCHEMA, + version, + generated, + min_installs: minInstalls, + judges, + rubrics: { role_quality: 1 }, + aliases: {}, + metrics: { + role_quality: { kind: 'gaussian', unit: 'score', dims: ['role', 'model'] }, + }, + cells: cells.map((c) => ({ + metric: c.metric, + role: c.role, + model: c.model, + mean: c.mean, + sd: c.sd, + n: c.n, + installs: c.installs, + })), + }; + return JSON.stringify(doc); +} + +// signIndex signs docBytes and answers the detached signature as standard +// base64. seedB64 is the 32-byte private seed and publicB64 the 32-byte public +// key, both standard base64; together they import as one Ed25519 JWK. +export async function signIndex(docBytes, seedB64, publicB64) { + const seed = fromBase64(seedB64); + const pub = fromBase64(publicB64); + const key = await crypto.subtle.importKey( + 'jwk', + { + kty: 'OKP', + crv: 'Ed25519', + d: toBase64Url(seed), + x: toBase64Url(pub), + }, + { name: 'Ed25519' }, + false, + ['sign'], + ); + const bytes = typeof docBytes === 'string' ? new TextEncoder().encode(docBytes) : docBytes; + const signature = await crypto.subtle.sign({ name: 'Ed25519' }, key, bytes); + return toBase64(new Uint8Array(signature)); +} + +// fromBase64 decodes standard base64 text (whitespace around it ignored). +function fromBase64(text) { + const raw = atob(String(text).trim()); + const out = new Uint8Array(raw.length); + for (let i = 0; i < raw.length; i++) { + out[i] = raw.charCodeAt(i); + } + return out; +} + +// toBase64 renders bytes as standard base64. +function toBase64(bytes) { + let s = ''; + for (const b of bytes) { + s += String.fromCharCode(b); + } + return btoa(s); +} + +// toBase64Url renders bytes as base64url, which a JWK field wants. +function toBase64Url(bytes) { + return toBase64(bytes).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''); +} diff --git a/relay/src/schema.js b/relay/src/schema.js new file mode 100644 index 0000000000..c32b46423e --- /dev/null +++ b/relay/src/schema.js @@ -0,0 +1,138 @@ +// schema.js validates the submit wire. The Go client writes exactly these +// rows (internal/pool/outbox.Row carrying internal/pool/record.Row), so this +// module refuses anything the client would not have sent. +// +// One NDJSON line is an outer envelope plus the payload it carries: +// {"schema":1,"day":"YYYY-MM-DD","nonce":"<32 lowercase hex>","payload":{...}} +// The payload is the record.Row fields: +// {"schema":1,"metric":"role_quality","role":...,"model":...,"score":..., +// "judge":...,"door":...,"size":...,"day":"YYYY-MM-DD"} +// +// Everything here is pure: no KV, no fetch, no clock of its own. + +// The one metric the client records. +const METRIC = 'role_quality'; + +const ROLES = new Set(['worker', 'high', 'mastermind']); +const DOORS = new Set(['task', 'do']); +const SIZES = new Set(['S', 'M', 'L']); + +// A model id is "/". The Go side draws both halves from a closed +// set of provider and model names; this is the loose shape it always matches. +const MODEL_RE = /^[a-z0-9._-]+\/[a-z0-9._:-]+$/i; +const DAY_RE = /^\d{4}-\d{2}-\d{2}$/; +const HEX32_RE = /^[0-9a-f]{32}$/; + +// A row's day may be at most one day ahead of now (UTC): a clock a little off +// is fine, a date in the future is not. +const ONE_DAY_MS = 86_400_000; + +// utcDay reads a "YYYY-MM-DD" day as milliseconds since the epoch, UTC. +function utcDay(day) { + const y = Number(day.slice(0, 4)); + const m = Number(day.slice(5, 7)); + const d = Number(day.slice(8, 10)); + return Date.UTC(y, m - 1, d); +} + +// dayError answers why a day string is unusable, or null when it is fine. +function dayError(day, now) { + if (typeof day !== 'string' || !DAY_RE.test(day)) { + return 'day must be YYYY-MM-DD'; + } + const todayMs = Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate()); + if (utcDay(day) - todayMs > ONE_DAY_MS) { + return 'day is more than one day in the future'; + } + return null; +} + +// validateRow reads one NDJSON line. It answers {row, error}: row is the +// parsed envelope with its payload when the line is good, and null with a +// message otherwise. `now` bounds how far ahead the day may be. +export function validateRow(line, now = new Date()) { + if (typeof line !== 'string' || line.trim() === '') { + return fail('line is empty'); + } + let outer; + try { + outer = JSON.parse(line); + } catch { + return fail('line is not json'); + } + if (outer === null || typeof outer !== 'object' || Array.isArray(outer)) { + return fail('row is not an object'); + } + if (outer.schema !== 1) { + return fail('schema must be 1'); + } + const dayBad = dayError(outer.day, now); + if (dayBad !== null) { + return fail(dayBad); + } + if (typeof outer.nonce !== 'string' || !HEX32_RE.test(outer.nonce)) { + return fail('nonce must be 32 lowercase hex'); + } + const payload = outer.payload; + if (payload === null || typeof payload !== 'object' || Array.isArray(payload)) { + return fail('payload is not an object'); + } + if (payload.schema !== 1) { + return fail('payload schema must be 1'); + } + if (payload.metric !== METRIC) { + return fail(`metric must be ${METRIC}`); + } + if (!ROLES.has(payload.role)) { + return fail('role must be worker, high or mastermind'); + } + if (typeof payload.model !== 'string' || !MODEL_RE.test(payload.model)) { + return fail('model must be /'); + } + if (typeof payload.score !== 'number' || !Number.isFinite(payload.score) + || payload.score < 0 || payload.score > 100) { + return fail('score must be a number in 0-100'); + } + if (typeof payload.judge !== 'string' || !MODEL_RE.test(payload.judge)) { + return fail('judge must be /'); + } + if (!DOORS.has(payload.door)) { + return fail('door must be task or do'); + } + if (!SIZES.has(payload.size)) { + return fail('size must be S, M or L'); + } + const payloadDayBad = dayError(payload.day, now); + if (payloadDayBad !== null) { + return fail(payloadDayBad); + } + return { + row: { + schema: 1, + day: outer.day, + nonce: outer.nonce, + payload: { + schema: 1, + metric: payload.metric, + role: payload.role, + model: payload.model, + score: payload.score, + judge: payload.judge, + door: payload.door, + size: payload.size, + day: payload.day, + }, + }, + error: null, + }; +} + +// validateInstall answers whether a request's X-Codeaf-Install header is an +// install id: 32 lowercase hex characters, the install's own nonce. +export function validateInstall(header) { + return typeof header === 'string' && HEX32_RE.test(header); +} + +function fail(message) { + return { row: null, error: message }; +} diff --git a/relay/src/sheet.js b/relay/src/sheet.js new file mode 100644 index 0000000000..14474a049d --- /dev/null +++ b/relay/src/sheet.js @@ -0,0 +1,248 @@ +// sheet.js folds a install's rows into running totals and aggregates all +// installs' totals into the cells of an index document. +// +// Each stored triple is {n, s, s2}: how many scores, their sum and their sum +// of squares. A triple is a running total, so two copies of the same triple +// carry the same or more observations and the larger one is the more complete +// — that is what makes `join` a least upper bound and the fold a CRDT. +// +// Everything here is pure: plain values in, plain values out. No KV, no fetch. + +// METRIC_ROLE_QUALITY is the one metric the sheets carry. +const METRIC_ROLE_QUALITY = 'role_quality'; + +// ZERO is an empty triple. +const ZERO = Object.freeze({ n: 0, s: 0, s2: 0 }); + +// cellKey renders the place one triple lives: the five dimensions that are +// kept apart, in a fixed order. The day is the KV key's own segment and is not +// part of this key. The dimensions hold no "|" (the model pattern forbids it), +// so the key splits back cleanly. +export function cellKey(payload) { + return `${payload.role}|${payload.model}|${payload.judge}|${payload.door}|${payload.size}`; +} + +// fold adds one score to a triple and answers the new triple. A missing +// triple folds as if it were empty. +export function fold(triple, score) { + const t = triple || ZERO; + return { n: t.n + 1, s: t.s + score, s2: t.s2 + score * score }; +} + +// join is the componentwise maximum of two triples — the least upper bound of +// two running totals of the same measurement. It is commutative, associative +// and idempotent. +export function join(a, b) { + const x = a || ZERO; + const y = b || ZERO; + return { + n: Math.max(x.n, y.n), + s: Math.max(x.s, y.s), + s2: Math.max(x.s2, y.s2), + }; +} + +// splitKey reads a cellKey back into its five dimensions. +function splitKey(key) { + const [role, model, judge, door, size] = key.split('|'); + return { role, model, judge, door, size }; +} + +// judgeSeverity fits one additive judge severity per judge over the finest +// cells (one per cellKey), so a judge who scores high or low on everything is +// taken out of every score. It alternates beta_j = weighted mean over cells +// scored by j of (cellMean - mu_rm) with mu_rm = weighted mean over judges of +// (cellMean - beta_j), weights n, five sweeps, re-centring beta to weighted +// mean zero each sweep. It answers {beta, judges}. +function judgeSeverity(cells) { + const judges = [...new Set(cells.map((c) => c.judge))].sort(); + const beta = new Map(judges.map((j) => [j, 0])); + const mu = new Map(); + for (const c of cells) { + mu.set(rmOf(c), 0); + } + for (let sweep = 0; sweep < 5; sweep++) { + // beta_j from the current mu_rm. + const num = new Map(); + const den = new Map(); + for (const c of cells) { + const w = c.n; + num.set(c.judge, (num.get(c.judge) || 0) + w * (c.mean - (mu.get(rmOf(c)) || 0))); + den.set(c.judge, (den.get(c.judge) || 0) + w); + } + for (const j of judges) { + beta.set(j, den.get(j) ? num.get(j) / den.get(j) : 0); + } + // Re-centre beta so its weighted mean is zero; the split between beta and + // mu is then pinned. + let bnum = 0; + let bden = 0; + for (const c of cells) { + bnum += c.n * beta.get(c.judge); + bden += c.n; + } + const centre = bden ? bnum / bden : 0; + for (const j of judges) { + beta.set(j, beta.get(j) - centre); + } + // mu_rm from the fresh beta_j. + const mnum = new Map(); + const mden = new Map(); + for (const c of cells) { + const w = c.n; + const rm = rmOf(c); + mnum.set(rm, (mnum.get(rm) || 0) + w * (c.mean - beta.get(c.judge))); + mden.set(rm, (mden.get(rm) || 0) + w); + } + for (const rm of mu.keys()) { + mu.set(rm, mden.get(rm) ? mnum.get(rm) / mden.get(rm) : 0); + } + } + return { beta, judges }; +} + +function rmOf(cell) { + return `${cell.role}|${cell.model}`; +} + +// median of a numeric list (the list is copied, not sorted in place). +function median(values) { + const xs = [...values].sort((a, b) => a - b); + const mid = Math.floor(xs.length / 2); + return xs.length % 2 ? xs[mid] : (xs[mid - 1] + xs[mid]) / 2; +} + +// huberMean is the Huber M-estimate of a location from values: scale from the +// median absolute deviation (1.4826 · MAD), tuning 1.345, twenty iterations. +// Each value's weight is 1 inside c·scale and c·scale/|x-mu| beyond it, so a +// single far-off value cannot drag the estimate. +function huberMean(values) { + const centre = median(values); + const mad = median(values.map((v) => Math.abs(v - centre))); + const scale = 1.4826 * mad; + if (!(scale > 0)) { + return values.reduce((a, b) => a + b, 0) / values.length; + } + const c = 1.345 * scale; + let mu = centre; + for (let i = 0; i < 20; i++) { + let num = 0; + let den = 0; + for (const v of values) { + const dev = Math.abs(v - mu); + const w = dev <= c ? 1 : c / dev; + num += w * v; + den += w; + } + if (den === 0) { + break; + } + mu = num / den; + } + return mu; +} + +// aggregate turns sheet entries into the cells of an index document. +// +// entries are {install, day, key, triple}: one install's running total for one +// cellKey on one day. Cells group by (role, model); each install's mean over +// its triples is taken after judge severity is removed, and the cell mean is +// the Huber M-estimate over those install means once there are at least five +// installs, the n-weighted mean of them otherwise. A cell whose installs are +// below minInstalls is still published, but sorted after the cells that meet +// it. +export function aggregate(entries, { minInstalls = 0 } = {}) { + const cells = []; + for (const entry of entries) { + const dims = splitKey(entry.key); + const n = entry.triple.n; + if (!(n > 0)) { + continue; + } + cells.push({ ...dims, n, mean: entry.triple.s / n }); + } + const { beta } = judgeSeverity(cells); + + // Group the adjusted entries by role and model, and keep each install's own + // observations so its mean over them can be taken. + const groups = new Map(); + for (const entry of entries) { + const n = entry.triple.n; + if (!(n > 0)) { + continue; + } + const dims = splitKey(entry.key); + const rm = `${dims.role}|${dims.model}`; + let group = groups.get(rm); + if (!group) { + group = { role: dims.role, model: dims.model, installs: new Map(), n: 0, s: 0, s2: 0 }; + groups.set(rm, group); + } + // Judge severity removal shifts each score by -beta[judge]; over a + // triple of n scores and sum s that is a shift of n·beta in the sum. + const shift = beta.get(dims.judge) || 0; + const adjustedSum = entry.triple.s - n * shift; + group.n += n; + group.s += entry.triple.s; + group.s2 += entry.triple.s2; + const install = group.installs.get(entry.install); + if (install) { + install.n += n; + install.s += adjustedSum; + } else { + group.installs.set(entry.install, { n, s: adjustedSum }); + } + } + + const out = []; + for (const group of groups.values()) { + const installMeans = []; + let weightSum = 0; + let weightedSum = 0; + for (const install of group.installs.values()) { + const mean = install.s / install.n; + installMeans.push(mean); + weightSum += install.n; + weightedSum += install.n * mean; + } + const installs = installMeans.length; + const mean = installs >= 5 + ? huberMean(installMeans) + : (weightSum ? weightedSum / weightSum : 0); + // Pooled within-cell standard deviation from the raw triples. + const sd = group.n >= 2 + ? Math.sqrt(Math.max(0, (group.s2 - (group.s * group.s) / group.n) / (group.n - 1))) + : 0; + out.push({ + metric: METRIC_ROLE_QUALITY, + role: group.role, + model: group.model, + mean, + sd, + n: group.n, + installs, + }); + } + + // Cells that meet minInstalls sort first; below it they follow. Ties break on + // mean then role then model, so the document is a property of its entries. + out.sort((a, b) => { + const am = a.installs >= minInstalls ? 0 : 1; + const bm = b.installs >= minInstalls ? 0 : 1; + if (am !== bm) return am - bm; + if (a.mean !== b.mean) return b.mean - a.mean; + if (a.role !== b.role) return a.role < b.role ? -1 : 1; + if (a.model !== b.model) return a.model < b.model ? -1 : 1; + return 0; + }); + return out; +} + +// judgesOf answers the sorted judge ids the entries name. +export function judgesOf(entries) { + const judges = new Set(); + for (const entry of entries) { + judges.add(entry.key.split('|')[2]); + } + return [...judges].sort(); +} diff --git a/relay/src/worker.js b/relay/src/worker.js new file mode 100644 index 0000000000..2a4b0176d8 --- /dev/null +++ b/relay/src/worker.js @@ -0,0 +1,232 @@ +// worker.js is the Model Pool relay: it accepts measurement rows from +// installs, folds each install's rows into per-install running totals, and +// once an hour publishes a signed, versioned index document and serves it. +// +// The wire is fixed by the Go client (internal/pool/outbox for submit, +// internal/pool/pull and internal/pool/index for the document), so this file +// matches it exactly and adds nothing of its own to it. +// +// It owns only its /pool/ prefix; every other path answers 404 with an empty +// body. It never logs a request and never reads or stores the client address. + +import { validateInstall, validateRow } from './schema.js'; +import { aggregate, cellKey, fold, judgesOf } from './sheet.js'; +import { buildIndex, signIndex } from './index.js'; + +const PREFIX = '/pool'; + +// The submit limits the client already works within. +const MAX_BODY = 256 << 10; // 256 KiB +const MAX_LINES = 200; + +// The KV keys the published document lives under. +const DOC_KEY = 'index/doc'; +const SIG_KEY = 'index/sig'; +const VERSION_KEY = 'index/version'; + +export default { + // fetch serves the four routes under /pool/. + async fetch(request, env) { + const url = new URL(request.url); + const route = routeOf(url.pathname); + switch (route) { + case '/v1/rows': + if (request.method === 'POST') { + return handleRows(request, env); + } + return notFound(); + case '/index.json': + return serveStored(request, env, DOC_KEY, 'application/json'); + case '/index.json.sig': + return serveStored(request, env, SIG_KEY, 'text/plain'); + case '/healthz': + if (request.method === 'GET' || request.method === 'HEAD') { + return new Response(request.method === 'HEAD' ? null : 'ok', { + status: 200, + headers: { 'Content-Type': 'text/plain' }, + }); + } + return notFound(); + default: + return notFound(); + } + }, + + // scheduled folds every install's sheets into one document, signs it, and + // replaces the published copy. A publication with no cells still publishes: + // an empty index is a valid index. + async scheduled(event, env) { + const minInstalls = intVar(env, 'MIN_INSTALLS', 3); + const entries = await loadEntries(env); + const cells = aggregate(entries, { minInstalls }); + const judges = judgesOf(entries); + const version = Math.floor(Date.now() / 1000); // monotone by construction + const generated = new Date().toISOString().slice(0, 10); // today, UTC + const doc = buildIndex(cells, { version, generated, minInstalls, judges }); + const sig = await signIndex(doc, env.POOL_SIGNING_KEY, env.POOL_PUBLIC_KEY); + await env.POOL.put(DOC_KEY, doc); + await env.POOL.put(SIG_KEY, sig); + await env.POOL.put(VERSION_KEY, String(version)); + }, +}; + +// routeOf answers the path under /pool/, or null when the request is not ours. +function routeOf(pathname) { + if (pathname === PREFIX) { + return '/'; + } + if (pathname.startsWith(PREFIX + '/')) { + return pathname.slice(PREFIX.length); + } + return null; +} + +// notFound is the empty 404 every path outside a served route gets. +function notFound() { + return new Response(null, { status: 404 }); +} + +// json answers one JSON object with a status. +function json(body, status) { + return new Response(JSON.stringify(body), { + status, + headers: { 'Content-Type': 'application/json' }, + }); +} + +// handleRows accepts one NDJSON batch from one install. +async function handleRows(request, env) { + const install = request.headers.get('X-Codeaf-Install'); + if (!validateInstall(install)) { + return json({ error: 'missing or bad X-Codeaf-Install' }, 400); + } + const body = await request.text(); + if (new TextEncoder().encode(body).length > MAX_BODY) { + return json({ error: 'body is larger than 256 KiB' }, 413); + } + const lines = []; + for (const [index, raw] of body.split('\n').entries()) { + const line = raw.endsWith('\r') ? raw.slice(0, -1) : raw; + if (line.trim() !== '') { + lines.push({ line, number: index + 1 }); + } + } + if (lines.length > MAX_LINES) { + return json({ error: 'more than 200 lines' }, 413); + } + if (lines.length === 0) { + return json({ error: 'no rows' }, 400); + } + const now = new Date(); + const rows = []; + for (const { line, number } of lines) { + const { row, error } = validateRow(line, now); + if (error !== null) { + return json({ error: `line ${number}: ${error}` }, 400); + } + rows.push(row); + } + + // The per-install per-day quota is checked for the whole batch before + // anything is stored: a batch that would exceed it stores nothing. + const limit = intVar(env, 'ROWS_PER_INSTALL_PER_DAY', 500); + const byDay = new Map(); + for (const row of rows) { + const list = byDay.get(row.day) || []; + list.push(row); + byDay.set(row.day, list); + } + const used = new Map(); + for (const [day, list] of byDay) { + const current = parseInt(await env.POOL.get(quotaKey(install, day)), 10) || 0; + if (current + list.length > limit) { + return json({ error: 'daily quota exceeded' }, 429); + } + used.set(day, current); + } + + // Fold each score into its install's running total for the cell and day. + for (const row of rows) { + const key = `sheet/${install}/${row.day}/${cellKey(row.payload)}`; + const previous = await env.POOL.get(key); + let triple = null; + if (previous !== null) { + try { + triple = JSON.parse(previous); + } catch { + triple = null; + } + } + await env.POOL.put(key, JSON.stringify(fold(triple, row.payload.score))); + } + for (const [day, list] of byDay) { + await env.POOL.put(quotaKey(install, day), String(used.get(day) + list.length)); + } + return json({ accepted: rows.length }, 202); +} + +// quotaKey names one install's per-day quota counter. +function quotaKey(install, day) { + return `quota/${install}/${day}`; +} + +// serveStored answers one of the published KV values with its caching headers. +async function serveStored(request, env, key, contentType) { + if (request.method !== 'GET' && request.method !== 'HEAD') { + return notFound(); + } + const value = await env.POOL.get(key); + if (value === null) { + return notFound(); + } + const version = await env.POOL.get(VERSION_KEY); + const etag = `"${version === null ? '' : version}"`; + const headers = { + 'Content-Type': contentType, + 'Cache-Control': 'public, max-age=300', + ETag: etag, + }; + const ifNoneMatch = request.headers.get('If-None-Match'); + if (ifNoneMatch !== null && ifNoneMatch.trim() === etag) { + return new Response(null, { status: 304, headers }); + } + return new Response(request.method === 'HEAD' ? null : value, { status: 200, headers }); +} + +// loadEntries reads every sheet triple the store holds, one entry per install, +// cell and day. +async function loadEntries(env) { + const entries = []; + let cursor; + do { + const page = await env.POOL.list({ prefix: 'sheet/', cursor }); + for (const key of page.keys) { + const parts = key.name.slice('sheet/'.length).split('/'); + const install = parts[0]; + const day = parts[1]; + const cell = parts.slice(2).join('/'); + const value = await env.POOL.get(key.name); + if (value === null) { + continue; + } + let triple; + try { + triple = JSON.parse(value); + } catch { + continue; + } + if (!triple || typeof triple.n !== 'number') { + continue; + } + entries.push({ install, day, key: cell, triple }); + } + cursor = page.list_complete ? null : page.cursor; + } while (cursor); + return entries; +} + +// intVar reads an integer variable with a default when it is unset or blank. +function intVar(env, name, fallback) { + const value = parseInt(env[name], 10); + return Number.isFinite(value) ? value : fallback; +} diff --git a/relay/test/index.js b/relay/test/index.js new file mode 100644 index 0000000000..7488863047 --- /dev/null +++ b/relay/test/index.js @@ -0,0 +1,12 @@ +// Node's test runner takes files and globs, not a directory, so the +// directory-form command `node --test test` resolves here; this entry loads +// every test file the same way the glob does. + +import { readdirSync } from 'node:fs'; + +const here = new URL('.', import.meta.url); +for (const name of readdirSync(here).sort()) { + if (name.endsWith('.test.js')) { + await import(new URL(name, here)); + } +} diff --git a/relay/test/index.test.js b/relay/test/index.test.js new file mode 100644 index 0000000000..8b5f992de3 --- /dev/null +++ b/relay/test/index.test.js @@ -0,0 +1,76 @@ +// index.test.js checks the document's shape, its key order and the signature. + +import test from 'node:test'; +import assert from 'node:assert/strict'; + +import { buildIndex, signIndex } from '../src/index.js'; + +const CELLS = [ + { + metric: 'role_quality', role: 'worker', model: 'z-ai/glm-5.3-flash', + mean: 65.6, sd: 5.9, n: 62, installs: 4, + }, +]; + +test('buildIndex carries an integer version and keys in the fixed order', () => { + const text = buildIndex(CELLS, { + version: 1758117600, generated: '2026-09-17', minInstalls: 3, judges: ['j/x'], + }); + assert.doesNotMatch(text, /\n/); + const doc = JSON.parse(text); + assert.equal(doc.schema, 1); + assert.equal(doc.version, 1758117600); + assert.equal(Number.isInteger(doc.version), true); + assert.equal(doc.generated, '2026-09-17'); + assert.equal(doc.min_installs, 3); + assert.deepEqual(doc.judges, ['j/x']); + assert.deepEqual(doc.rubrics, { role_quality: 1 }); + assert.deepEqual(doc.aliases, {}); + assert.deepEqual(Object.keys(doc.metrics.role_quality), ['kind', 'unit', 'dims']); + assert.deepEqual(Object.keys(doc), [ + 'schema', 'version', 'generated', 'min_installs', 'judges', + 'rubrics', 'aliases', 'metrics', 'cells', + ]); + assert.deepEqual(Object.keys(doc.cells[0]), ['metric', 'role', 'model', 'mean', 'sd', 'n', 'installs']); +}); + +test('buildIndex with no cells still renders a document with an empty cells list', () => { + const doc = JSON.parse(buildIndex([], { + version: 1, generated: '2026-09-17', minInstalls: 3, judges: [], + })); + assert.deepEqual(doc.cells, []); +}); + +test('signIndex round-trips under the public key through crypto.subtle.verify', async () => { + const pair = await crypto.subtle.generateKey({ name: 'Ed25519' }, true, ['sign', 'verify']); + const priv = await crypto.subtle.exportKey('jwk', pair.privateKey); + const pub = await crypto.subtle.exportKey('jwk', pair.publicKey); + const seedB64 = fromBase64Url(priv.d); + const publicB64 = fromBase64Url(pub.x); + + const doc = buildIndex(CELLS, { + version: 7, generated: '2026-09-17', minInstalls: 3, judges: ['j/x'], + }); + const sigB64 = await signIndex(doc, seedB64, publicB64); + + const key = await crypto.subtle.importKey( + 'jwk', { kty: 'OKP', crv: 'Ed25519', x: pub.x }, { name: 'Ed25519' }, false, ['verify'], + ); + const sig = Uint8Array.from(atob(sigB64), (c) => c.charCodeAt(0)); + const ok = await crypto.subtle.verify( + { name: 'Ed25519' }, key, sig, new TextEncoder().encode(doc), + ); + assert.equal(ok, true); + + // The same document under a different byte does not verify. + const bad = await crypto.subtle.verify( + { name: 'Ed25519' }, key, sig, new TextEncoder().encode(doc + ' '), + ); + assert.equal(bad, false); +}); + +// fromBase64Url reassembles standard base64 from a JWK's base64url field. +function fromBase64Url(text) { + const base = text.replace(/-/g, '+').replace(/_/g, '/'); + return base + '='.repeat((4 - (base.length % 4)) % 4); +} diff --git a/relay/test/schema.test.js b/relay/test/schema.test.js new file mode 100644 index 0000000000..c999f9e31c --- /dev/null +++ b/relay/test/schema.test.js @@ -0,0 +1,92 @@ +// schema.test.js checks the row contract against the bytes the Go client +// writes (internal/pool/outbox, internal/pool/record). + +import test from 'node:test'; +import assert from 'node:assert/strict'; + +import { validateInstall, validateRow } from '../src/schema.js'; + +// goodLine is one row exactly as the Go client would write it: compact JSON, +// the envelope's field order, and the record.Row payload inside. +function goodLine(overrides = {}) { + const payload = { + schema: 1, + metric: 'role_quality', + role: 'worker', + model: 'z-ai/glm-5.3-flash', + score: 65.6, + judge: 'anthropic/claude-opus-5', + door: 'task', + size: 'M', + day: '2026-09-17', + ...overrides.payload, + }; + const outer = { + schema: 1, + day: '2026-09-17', + nonce: '0123456789abcdef0123456789abcdef', + payload, + ...overrides.outer, + }; + return JSON.stringify(outer); +} + +const NOW = new Date('2026-09-17T12:00:00Z'); + +test('accepts the exact payload the Go side writes', () => { + const { row, error } = validateRow(goodLine(), NOW); + assert.equal(error, null); + assert.equal(row.day, '2026-09-17'); + assert.equal(row.nonce, '0123456789abcdef0123456789abcdef'); + assert.equal(row.payload.role, 'worker'); + assert.equal(row.payload.model, 'z-ai/glm-5.3-flash'); + assert.equal(row.payload.score, 65.6); + assert.equal(row.payload.judge, 'anthropic/claude-opus-5'); + assert.equal(row.payload.door, 'task'); + assert.equal(row.payload.size, 'M'); +}); + +test('accepts a day one day ahead but rejects two', () => { + assert.equal(validateRow(goodLine({ outer: { day: '2026-09-18' }, payload: { day: '2026-09-18' } }), NOW).error, null); + assert.notEqual(validateRow(goodLine({ outer: { day: '2026-09-19' }, payload: { day: '2026-09-19' } }), NOW).error, null); +}); + +test('rejects each broken envelope field', () => { + assert.notEqual(validateRow('', NOW).error, null); + assert.notEqual(validateRow('not json', NOW).error, null); + assert.notEqual(validateRow('[]', NOW).error, null); + assert.notEqual(validateRow(goodLine({ outer: { schema: 2 } }), NOW).error, null); + assert.notEqual(validateRow(goodLine({ outer: { day: '2026/09/17' } }), NOW).error, null); + assert.notEqual(validateRow(goodLine({ outer: { nonce: 'ABCDEF' } }), NOW).error, null); + assert.notEqual(validateRow(goodLine({ outer: { nonce: '0123456789abcdef0123456789abcde' } }), NOW).error, null); + assert.notEqual(validateRow(JSON.stringify({ schema: 1, day: '2026-09-17', nonce: '0123456789abcdef0123456789abcdef' }), NOW).error, null); + assert.notEqual(validateRow(JSON.stringify({ schema: 1, day: '2026-09-17', nonce: '0123456789abcdef0123456789abcdef', payload: [] }), NOW).error, null); +}); + +test('rejects each broken payload field', () => { + const bad = [ + { payload: { schema: 2 } }, + { payload: { metric: 'other' } }, + { payload: { role: 'reflex' } }, + { payload: { model: 'glm-5.3-flash' } }, + { payload: { model: 'z-ai/glm 5.3' } }, + { payload: { score: 101 } }, + { payload: { score: -1 } }, + { payload: { score: '65' } }, + { payload: { judge: 'opus' } }, + { payload: { door: 'cron' } }, + { payload: { size: 'XL' } }, + { payload: { day: 'yesterday' } }, + ]; + for (const override of bad) { + assert.notEqual(validateRow(goodLine(override), NOW).error, null, JSON.stringify(override)); + } +}); + +test('validateInstall takes 32 lowercase hex and nothing else', () => { + assert.equal(validateInstall('0123456789abcdef0123456789abcdef'), true); + assert.equal(validateInstall('0123456789ABCDEF0123456789abcdef'), false); + assert.equal(validateInstall('0123'), false); + assert.equal(validateInstall(undefined), false); + assert.equal(validateInstall('0123456789abcdef0123456789abcde'), false); +}); diff --git a/relay/test/sheet.test.js b/relay/test/sheet.test.js new file mode 100644 index 0000000000..11d64d6c07 --- /dev/null +++ b/relay/test/sheet.test.js @@ -0,0 +1,112 @@ +// sheet.test.js checks the running totals, the CRDT join and the aggregation. + +import test from 'node:test'; +import assert from 'node:assert/strict'; + +import { aggregate, cellKey, fold, join, judgesOf } from '../src/sheet.js'; + +const KEY = cellKey({ + role: 'worker', + model: 'z-ai/glm-5.3-flash', + judge: 'anthropic/claude-opus-5', + door: 'task', + size: 'M', +}); + +function entry(install, day, key, mean, n = 4) { + return { install, day, key, triple: { n, s: mean * n, s2: mean * mean * n } }; +} + +test('cellKey keeps the five dimensions in order', () => { + assert.equal(KEY, 'worker|z-ai/glm-5.3-flash|anthropic/claude-opus-5|task|M'); +}); + +test('fold adds one score', () => { + assert.deepEqual(fold(null, 50), { n: 1, s: 50, s2: 2500 }); + assert.deepEqual(fold({ n: 1, s: 50, s2: 2500 }, 60), { n: 2, s: 110, s2: 6100 }); +}); + +test('join is commutative, associative, idempotent and takes the larger total', () => { + const a = { n: 2, s: 100, s2: 5200 }; + const b = { n: 3, s: 160, s2: 8600 }; + const c = { n: 1, s: 10, s2: 100 }; + assert.deepEqual(join(a, b), join(b, a)); + assert.deepEqual(join(join(a, b), c), join(a, join(b, c))); + assert.deepEqual(join(a, a), a); + assert.deepEqual(join(a, b), b); + assert.deepEqual(join(a, null), a); +}); + +test('aggregate takes n, installs and the n-weighted mean below five installs', () => { + const entries = [ + entry('i1', '2026-09-17', KEY, 50), + entry('i2', '2026-09-17', KEY, 60), + entry('i3', '2026-09-17', KEY, 70), + ]; + const cells = aggregate(entries, { minInstalls: 3 }); + assert.equal(cells.length, 1); + assert.equal(cells[0].role, 'worker'); + assert.equal(cells[0].model, 'z-ai/glm-5.3-flash'); + assert.equal(cells[0].installs, 3); + assert.equal(cells[0].n, 12); + assert.equal(cells[0].mean, 60); +}); + +test('aggregate pools the within-cell standard deviation from the triples', () => { + // Two installs, each four scores: the pooled sd is over all eight scores. + const entries = [ + { install: 'i1', day: '2026-09-17', key: KEY, triple: { n: 4, s: 200, s2: 10000 } }, // 50 ×4 + { install: 'i2', day: '2026-09-17', key: KEY, triple: { n: 4, s: 240, s2: 14400 } }, // 60 ×4 + ]; + const cells = aggregate(entries, { minInstalls: 3 }); + // sqrt((24400 - 440²/8) / 7) = sqrt((24400 - 24200)/7) = sqrt(200/7). + assert.ok(Math.abs(cells[0].sd - Math.sqrt(200 / 7)) < 1e-9); + assert.equal(cells[0].sd, Math.sqrt(200 / 7)); +}); + +test('aggregate keeps the Huber mean near six installs when one is absurd', () => { + const entries = [48, 50, 52, 49, 51, 50, 500].map((mean, i) => + entry(`i${i}`, '2026-09-17', KEY, mean)); + const cells = aggregate(entries, { minInstalls: 3 }); + assert.equal(cells[0].installs, 7); + assert.ok(Math.abs(cells[0].mean - 50) < 1, `mean ${cells[0].mean} should sit near 50`); +}); + +test('judge severity is removed: two judges offset by +10 agree on each model', () => { + const key = (model, judge) => cellKey({ + role: 'worker', model, judge, door: 'task', size: 'M', + }); + const ja = 'anthropic/claude-opus-5'; + const jb = 'deepseek/deepseek-v4.1-flash'; + const entries = [ + entry('a', '2026-09-17', key('z-ai/glm-5.3', ja), 50), + entry('b', '2026-09-17', key('z-ai/glm-5.3', jb), 60), // +10 severity + entry('a', '2026-09-17', key('z-ai/glm-5.3-flash', ja), 70), + entry('b', '2026-09-17', key('z-ai/glm-5.3-flash', jb), 80), // +10 severity + ]; + const cells = aggregate(entries, { minInstalls: 3 }); + const byModel = new Map(cells.map((c) => [c.model, c])); + // Both judges land on one mean per model: the +10 offset is gone. + const base = byModel.get('z-ai/glm-5.3').mean; + const flash = byModel.get('z-ai/glm-5.3-flash').mean; + assert.ok(Math.abs(base - 55) < 1e-9, `base ${base}`); + assert.ok(Math.abs(flash - 75) < 1e-9, `flash ${flash}`); + assert.ok(Math.abs((flash - base) - 20) < 1e-9); +}); + +test('aggregate publishes judges sorted and sorts below-minInstalls cells last', () => { + const thin = cellKey({ role: 'worker', model: 'a/one', judge: 'j/x', door: 'task', size: 'S' }); + const fat = cellKey({ role: 'worker', model: 'b/two', judge: 'j/y', door: 'do', size: 'L' }); + const entries = [ + entry('i1', '2026-09-17', thin, 90), + entry('i2', '2026-09-17', thin, 90), + entry('i1', '2026-09-17', fat, 10), + entry('i2', '2026-09-17', fat, 10), + entry('i3', '2026-09-17', fat, 10), + ]; + const cells = aggregate(entries, { minInstalls: 3 }); + const judges = judgesOf(entries); + assert.deepEqual(judges, ['j/x', 'j/y']); + assert.equal(cells[0].model, 'b/two'); // 3 installs meets the floor, sorts first + assert.equal(cells[1].model, 'a/one'); // 2 installs falls after +}); diff --git a/relay/test/worker.test.js b/relay/test/worker.test.js new file mode 100644 index 0000000000..2df4de2320 --- /dev/null +++ b/relay/test/worker.test.js @@ -0,0 +1,179 @@ +// worker.test.js drives the Worker's routes against an in-memory KV stub. + +import test from 'node:test'; +import assert from 'node:assert/strict'; + +import worker from '../src/worker.js'; + +// KV is the smallest store the Worker needs: get, put and one list page. +class KV { + constructor() { + this.map = new Map(); + } + async get(key) { + return this.map.has(key) ? this.map.get(key) : null; + } + async put(key, value) { + this.map.set(key, value); + } + async list({ prefix = '', cursor } = {}) { + void cursor; + const keys = [...this.map.keys()] + .filter((k) => k.startsWith(prefix)) + .sort() + .map((name) => ({ name })); + return { keys, list_complete: true, cursor: undefined }; + } +} + +const INSTALL = '0123456789abcdef0123456789abcdef'; + +function payload(overrides = {}) { + return { + schema: 1, metric: 'role_quality', role: 'worker', + model: 'z-ai/glm-5.3-flash', score: 50, + judge: 'anthropic/claude-opus-5', door: 'task', size: 'M', + day: '2026-09-17', ...overrides, + }; +} + +function line(overrides = {}) { + return JSON.stringify({ + schema: 1, day: '2026-09-17', nonce: '0123456789abcdef0123456789abcdef', + payload: payload(overrides), + }); +} + +// One Ed25519 pair for the whole file, exported as standard base64 the way the +// secret and the var are held. +const pair = await crypto.subtle.generateKey({ name: 'Ed25519' }, true, ['sign', 'verify']); +const privJwk = await crypto.subtle.exportKey('jwk', pair.privateKey); +const pubJwk = await crypto.subtle.exportKey('jwk', pair.publicKey); + +function env(extra = {}) { + return { + POOL: new KV(), + POOL_SIGNING_KEY: fromBase64Url(privJwk.d), + POOL_PUBLIC_KEY: fromBase64Url(pubJwk.x), + MIN_INSTALLS: '3', + ROWS_PER_INSTALL_PER_DAY: '500', + ...extra, + }; +} + +function fromBase64Url(text) { + const base = text.replace(/-/g, '+').replace(/_/g, '/'); + return base + '='.repeat((4 - (base.length % 4)) % 4); +} + +function post(body, headers = {}) { + return new Request('https://codeaf.agentfield.ai/pool/v1/rows', { + method: 'POST', headers, body, + }); +} + +test('healthz answers ok and other paths answer an empty 404', async () => { + const e = env(); + const ok = await worker.fetch(new Request('https://codeaf.agentfield.ai/pool/healthz'), e); + assert.equal(ok.status, 200); + assert.equal(await ok.text(), 'ok'); + for (const path of ['/other', '/', '/pool', '/pool/nope']) { + const res = await worker.fetch(new Request(`https://codeaf.agentfield.ai${path}`), e); + assert.equal(res.status, 404, path); + assert.equal(await res.text(), ''); + } +}); + +test('submit requires the install header', async () => { + const res = await worker.fetch(post(line()), env()); + assert.equal(res.status, 400); +}); + +test('submit refuses a bad line, naming its number, and stores nothing', async () => { + const e = env(); + const body = line() + '\n' + '{"schema":1}' + '\n'; + const res = await worker.fetch(post(body, { 'X-Codeaf-Install': INSTALL }), e); + assert.equal(res.status, 400); + assert.match((await res.json()).error, /line 2/); + assert.equal(e.POOL.map.size, 0); +}); + +test('submit refuses an oversized body and too many lines', async () => { + const e = env(); + const big = await worker.fetch(post('x'.repeat((256 << 10) + 1), { 'X-Codeaf-Install': INSTALL }), e); + assert.equal(big.status, 413); + const many = line() + '\n'; + const body = many.repeat(201); + const res = await worker.fetch(post(body, { 'X-Codeaf-Install': INSTALL }), e); + assert.equal(res.status, 413); +}); + +test('submit folds a batch into sheets and answers 202', async () => { + const e = env(); + const body = line({ score: 50 }) + '\n' + line({ score: 60 }) + '\n'; + const res = await worker.fetch(post(body, { 'X-Codeaf-Install': INSTALL }), e); + assert.equal(res.status, 202); + assert.deepEqual(await res.json(), { accepted: 2 }); + const stored = JSON.parse(e.POOL.map.get( + `sheet/${INSTALL}/2026-09-17/worker|z-ai/glm-5.3-flash|anthropic/claude-opus-5|task|M`, + )); + assert.deepEqual(stored, { n: 2, s: 110, s2: 6100 }); + assert.equal(e.POOL.map.get(`quota/${INSTALL}/2026-09-17`), '2'); +}); + +test('submit refuses a batch past the daily quota and stores nothing', async () => { + const e = env({ ROWS_PER_INSTALL_PER_DAY: '1' }); + const body = line() + '\n' + line() + '\n'; + const res = await worker.fetch(post(body, { 'X-Codeaf-Install': INSTALL }), e); + assert.equal(res.status, 429); + assert.equal(e.POOL.map.size, 0); +}); + +test('the index is 404 before the first publication', async () => { + const e = env(); + const res = await worker.fetch(new Request('https://codeaf.agentfield.ai/pool/index.json'), e); + assert.equal(res.status, 404); +}); + +test('scheduled publishes a signed index, served with a version ETag and 304', async () => { + const e = env(); + for (let i = 0; i < 3; i++) { + const name = `sheet/install${i}/2026-09-17/worker|z-ai/glm-5.3-flash|anthropic/claude-opus-5|task|M`; + await e.POOL.put(name, JSON.stringify({ n: 4, s: 200 + i * 4, s2: 10000 })); + } + await worker.scheduled({}, e); + + const docRes = await worker.fetch(new Request('https://codeaf.agentfield.ai/pool/index.json'), e); + assert.equal(docRes.status, 200); + const etag = docRes.headers.get('ETag'); + assert.equal(docRes.headers.get('Cache-Control'), 'public, max-age=300'); + const doc = await docRes.text(); + const parsed = JSON.parse(doc); + assert.equal(Number.isInteger(parsed.version), true); + assert.equal(parsed.cells.length, 1); + assert.equal(parsed.cells[0].installs, 3); + + const sigRes = await worker.fetch(new Request('https://codeaf.agentfield.ai/pool/index.json.sig'), e); + assert.equal(sigRes.status, 200); + const sig = Uint8Array.from(atob((await sigRes.text()).trim()), (c) => c.charCodeAt(0)); + const pub = await crypto.subtle.importKey( + 'jwk', { kty: 'OKP', crv: 'Ed25519', x: pubJwk.x }, { name: 'Ed25519' }, false, ['verify'], + ); + const verified = await crypto.subtle.verify( + { name: 'Ed25519' }, pub, sig, new TextEncoder().encode(doc), + ); + assert.equal(verified, true); + + const notModified = await worker.fetch(new Request('https://codeaf.agentfield.ai/pool/index.json', { + headers: { 'If-None-Match': etag }, + }), e); + assert.equal(notModified.status, 304); +}); + +test('scheduled with no cells still publishes an empty index', async () => { + const e = env(); + await worker.scheduled({}, e); + const res = await worker.fetch(new Request('https://codeaf.agentfield.ai/pool/index.json'), e); + assert.equal(res.status, 200); + assert.deepEqual(JSON.parse(await res.text()).cells, []); +}); diff --git a/relay/wrangler.toml b/relay/wrangler.toml new file mode 100644 index 0000000000..9b66c178c7 --- /dev/null +++ b/relay/wrangler.toml @@ -0,0 +1,24 @@ +name = "codeaf-pool" +main = "src/worker.js" +compatibility_date = "2026-09-01" + +# A route on the path prefix, not a custom domain: the Worker owns only +# codeaf.agentfield.ai/pool/* and the host can carry other services. +routes = [ + { pattern = "codeaf.agentfield.ai/pool/*", zone_name = "agentfield.ai" } +] + +[[kv_namespaces]] +binding = "POOL" +# The runbook fills this id: `cd relay && wrangler kv namespace create POOL`. +id = "REPLACE_WITH_POOL_KV_NAMESPACE_ID" + +[triggers] +crons = ["17 * * * *"] + +[vars] +# The public half of the index signing key built into the binary. The private +# half is the POOL_SIGNING_KEY secret, set by the runbook and never stored here. +POOL_PUBLIC_KEY = "WOAo+g/oKxAV9vVqv2Q14w1TyyyiouwFO2fC0zgcps0=" +MIN_INSTALLS = "3" +ROWS_PER_INSTALL_PER_DAY = "500" From b9f18fa6729cd6bcd91dbd44d68d523fb10063cd Mon Sep 17 00:00:00 2001 From: santoshkumarradha Date: Thu, 17 Sep 2026 12:24:52 -0400 Subject: [PATCH 124/173] README: the Model Pool section names the paper and the runbook Co-Authored-By: Claude Fable 5.1 --- README.md | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 9906a4e2dd..defc468a9b 100644 --- a/README.md +++ b/README.md @@ -197,9 +197,12 @@ any OpenAI-compatible endpoint. ## Model Pool -The Model Pool index is mirrored on the `model-pool` branch at `pool/index.json`. -[How to run your own relay](docs/design/model-pool/learn.pdf) explains the worker -behind it. +Installs that opt in send text-free scores of their crews to a relay, which +publishes a signed index of seat quality; the crew picker reads it under +`picked from = learn`. The index is mirrored on the `model-pool` branch at +`pool/index.json`. The design is [Pareto Crewing](docs/design/model-pool/pareto-crewing.pdf); +the relay's code is under `relay/`, with a [runbook](docs/design/model-pool/RUNBOOK.md) +that includes running your own. [![pool updated](https://img.shields.io/github/last-commit/Agent-Field/CodeAF/model-pool?label=pool%20updated)](https://github.com/Agent-Field/CodeAF/tree/model-pool) From 2bfcecf0f0008abfbc806b249925fdaebd49ca34 Mon Sep 17 00:00:00 2001 From: santoshkumarradha Date: Thu, 17 Sep 2026 12:27:45 -0400 Subject: [PATCH 125/173] docs: the model-pool tables say which script and data made them figures.py opens tab-regret.tex and tab-semibandit.tex with a LaTeX comment naming itself and data/measured-crews.csv, and the Makefile gains a figures target sharing one grouped rule with the figure and table files, so make figures redraws everything and make rebuilds the PDF only when one is stale. The two tables are regenerated with the header. Co-Authored-By: codeaf --- docs/design/model-pool/Makefile | 14 ++++++++++---- docs/design/model-pool/figures.py | 3 +++ docs/design/model-pool/tab-regret.tex | 1 + docs/design/model-pool/tab-semibandit.tex | 1 + 4 files changed, 15 insertions(+), 4 deletions(-) diff --git a/docs/design/model-pool/Makefile b/docs/design/model-pool/Makefile index 8be7b44a5d..c80133cf35 100644 --- a/docs/design/model-pool/Makefile +++ b/docs/design/model-pool/Makefile @@ -1,17 +1,23 @@ # Builds pareto-crewing.pdf: the figures from data/*.csv, then the paper. -# make figures + pdf +# make stale figures + pdf +# make figures redraw every figure and table # make clean remove build products but keep the pdf +FIGURES := fig-front.pdf fig-cells.pdf fig-shrink.pdf fig-regret.pdf fig-semibandit.pdf tab-regret.tex tab-semibandit.tex + all: pareto-crewing.pdf -fig-front.pdf fig-cells.pdf fig-shrink.pdf fig-regret.pdf tab-regret.tex: figures.py data/measured-crews.csv data/seed-cells.csv +figures: + python3 figures.py + +$(FIGURES) &: figures.py data/measured-crews.csv data/seed-cells.csv python3 figures.py -pareto-crewing.pdf: pareto-crewing.tex fig-front.pdf fig-cells.pdf fig-shrink.pdf fig-regret.pdf tab-regret.tex +pareto-crewing.pdf: pareto-crewing.tex $(FIGURES) latexmk -pdf -interaction=nonstopmode -halt-on-error pareto-crewing.tex clean: latexmk -c pareto-crewing.tex rm -f learn.bbl -.PHONY: all clean +.PHONY: all figures clean diff --git a/docs/design/model-pool/figures.py b/docs/design/model-pool/figures.py index 9e22f22dac..d33f7b1a2e 100644 --- a/docs/design/model-pool/figures.py +++ b/docs/design/model-pool/figures.py @@ -15,6 +15,7 @@ import matplotlib.pyplot as plt HERE = os.path.dirname(os.path.abspath(__file__)) +TABLE_PROVENANCE = "% generated by figures.py from data/measured-crews.csv; do not edit\n" H, LAM, K = 10.0, 0.01, 30 REPS = 200 plt.rcParams.update({"font.size": 9, "font.family": "serif", "axes.spines.top": False, "axes.spines.right": False}) @@ -159,6 +160,7 @@ def fig_regret(): fig.tight_layout() fig.savefig(os.path.join(HERE, "fig-regret.pdf")) with open(os.path.join(HERE, "tab-regret.tex"), "w") as f: + f.write(TABLE_PROVENANCE) f.write("\\begin{tabular}{l" + "r" * len(Ns) + "}\\toprule\n$N$ & " + " & ".join(str(n) for n in Ns) + "\\\\\\midrule\n") for s in ("table", "catalog", "greedy", "thompson"): f.write(s + " & " + " & ".join(f"{v:.2f}" for v in table[s]) + "\\\\\n") @@ -270,6 +272,7 @@ def fig_semibandit(): fig.tight_layout() fig.savefig(os.path.join(HERE, "fig-semibandit.pdf")) with open(os.path.join(HERE, "tab-semibandit.tex"), "w") as f: + f.write(TABLE_PROVENANCE) f.write("\\begin{tabular}{l" + "r" * len(Ns) + "}\\toprule\n$N$ & " + " & ".join(str(n) for n in Ns) + "\\\\\\midrule\n") for level, name in (("crew-bit", "crew, clean bit"), ("crew-score", "crew, mean score"), ("role", "role, semi-bandit")): f.write(name + " & " + " & ".join(f"{v:.2f}" for v in table[level]) + "\\\\\n") diff --git a/docs/design/model-pool/tab-regret.tex b/docs/design/model-pool/tab-regret.tex index 3ce5cce56e..463ceb71f2 100644 --- a/docs/design/model-pool/tab-regret.tex +++ b/docs/design/model-pool/tab-regret.tex @@ -1,3 +1,4 @@ +% generated by figures.py from data/measured-crews.csv; do not edit \begin{tabular}{lrrrrrrrrr}\toprule $N$ & 10 & 20 & 40 & 70 & 100 & 150 & 200 & 300 & 400\\\midrule table & 1.01 & 0.95 & 0.95 & 0.93 & 0.95 & 0.93 & 0.94 & 0.96 & 0.97\\ diff --git a/docs/design/model-pool/tab-semibandit.tex b/docs/design/model-pool/tab-semibandit.tex index 38846605eb..5a7ac2a294 100644 --- a/docs/design/model-pool/tab-semibandit.tex +++ b/docs/design/model-pool/tab-semibandit.tex @@ -1,3 +1,4 @@ +% generated by figures.py from data/measured-crews.csv; do not edit \begin{tabular}{lrrrrrrrrr}\toprule $N$ & 10 & 20 & 40 & 70 & 100 & 150 & 200 & 300 & 400\\\midrule crew, clean bit & 0.47 & 0.50 & 0.50 & 0.51 & 0.53 & 0.54 & 0.55 & 0.55 & 0.54\\ From 002b8f28f5e2900e6ecaa94316ff2e2bdad72ef5 Mon Sep 17 00:00:00 2001 From: santoshkumarradha Date: Thu, 17 Sep 2026 12:35:42 -0400 Subject: [PATCH 126/173] docs: the figure rule of the paper's Makefile dates by a stamp, not a grouped target Co-Authored-By: Claude Fable 5.1 --- docs/design/model-pool/Makefile | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/docs/design/model-pool/Makefile b/docs/design/model-pool/Makefile index c80133cf35..08d0fd4ade 100644 --- a/docs/design/model-pool/Makefile +++ b/docs/design/model-pool/Makefile @@ -10,14 +10,18 @@ all: pareto-crewing.pdf figures: python3 figures.py -$(FIGURES) &: figures.py data/measured-crews.csv data/seed-cells.csv +# One run of the script writes every figure and table; the stamp is what make +# dates them by, so old makes without grouped targets work too. +$(FIGURES): figures.stamp +figures.stamp: figures.py data/measured-crews.csv data/seed-cells.csv python3 figures.py + touch $@ pareto-crewing.pdf: pareto-crewing.tex $(FIGURES) latexmk -pdf -interaction=nonstopmode -halt-on-error pareto-crewing.tex clean: latexmk -c pareto-crewing.tex - rm -f learn.bbl + rm -f pareto-crewing.bbl figures.stamp .PHONY: all figures clean From 7b02b7ecff42aa1e37010aeec96b20acf5ee796b Mon Sep 17 00:00:00 2001 From: santoshkumarradha Date: Thu, 17 Sep 2026 12:37:51 -0400 Subject: [PATCH 127/173] cmd/codeaf: each seat's judge question gets its own share of the landing's time Co-Authored-By: Claude Fable 5.1 --- cmd/codeaf/poolrecord.go | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/cmd/codeaf/poolrecord.go b/cmd/codeaf/poolrecord.go index fb2adffec8..f4140144b9 100644 --- a/cmd/codeaf/poolrecord.go +++ b/cmd/codeaf/poolrecord.go @@ -38,10 +38,13 @@ import ( "github.com/Agent-Field/codeaf/internal/trace" ) -// judgeTimeout bounds one landing's whole judging: every seat question rides -// the same context, so a slow model cannot hold one landing open forever. It -// is a bound on an errand nobody is waiting for, not on work the person asked -// for, and it is generous on purpose — the questions are one call each. +// judgeTimeout bounds ONE seat's question, and the landing as a whole gets one +// share per seat and one to spare, so a slow model cannot hold a landing open +// forever and a slow first answer does not eat the second question's time: a +// reasoning judge took 64 s over the worker and the checker's question was +// then cut at the landing's 90 s, and the checker seat went unscored. It is a +// bound on an errand nobody is waiting for, not on work the person asked for, +// and it is generous on purpose — the questions are one call each. const judgeTimeout = 90 * time.Second // poolJudgeHook builds the session's landing reader. Nil is off, for the pool @@ -93,9 +96,15 @@ func poolJudgeLanding(settings config.Config, profileDir string, models func() [ // A landing is news, not a turn: nobody is waiting on the answer, and the // one thing this context owes anybody is a bound on how long it holds the // pool's own goroutine. - ctx, cancel := context.WithTimeout(context.Background(), judgeTimeout) + ctx, cancel := context.WithTimeout(context.Background(), judgeTimeout*time.Duration(len(seats)+1)) defer cancel() - scores, err := judge.Judge(ctx, ask(judgeID), rec) + one := ask(judgeID) + perSeat := func(ctx context.Context, system, user string) (string, error) { + qctx, cancel := context.WithTimeout(ctx, judgeTimeout) + defer cancel() + return one(qctx, system, user) + } + scores, err := judge.Judge(ctx, perSeat, rec) // The scores obtained are recorded even when the error names a seat: a // judge that scored the worker but not the high seat scored the worker, // and a seat the call failed on is the judge's evidence of that model too. From 9609e0c4206ee10fa653061b6e61921b63d0742a Mon Sep 17 00:00:00 2001 From: santoshkumarradha Date: Thu, 17 Sep 2026 12:49:43 -0400 Subject: [PATCH 128/173] poolcfg: the pool's addresses derive from one relay base, each with its own override --- cmd/codeaf/pool.go | 5 +- cmd/codeaf/pool_test.go | 17 +-- internal/config/settings.go | 2 +- internal/pool/poolcfg/poolcfg.go | 86 +++++++++++--- internal/pool/poolcfg/poolcfg_test.go | 163 ++++++++++++++++++++++---- 5 files changed, 219 insertions(+), 54 deletions(-) diff --git a/cmd/codeaf/pool.go b/cmd/codeaf/pool.go index dc9b1a3885..c3b475482c 100644 --- a/cmd/codeaf/pool.go +++ b/cmd/codeaf/pool.go @@ -74,7 +74,7 @@ func runPool(args []string) error { // emptiness is the state root's own profile, never a directory called "pool" // beside wherever the command happened to run. func runPoolWith(args []string, output io.Writer, profileDir string, now func() time.Time, lookup func(string) (string, bool)) error { - cfg := poolcfg.Resolve(config.ModelPoolSettingAt(profileDir), lookup) + cfg := poolcfg.Resolve(config.ModelPoolSettingAt(profileDir), "", lookup) poolDir := config.ProfilePath(profileDir, "pool") if len(args) == 0 { args = []string{"show"} @@ -138,6 +138,7 @@ func printPool(output io.Writer, poolDir string, cfg poolcfg.Config, now time.Ti } for _, line := range []string{ fmt.Sprintf("mode %s · %s", cfg.Mode, cfg.Source.Mode), + fmt.Sprintf("relay %s · %s", cfg.RelayURL, cfg.Source.RelayURL), fmt.Sprintf("index %s · %s", cfg.IndexURL, cfg.Source.IndexURL), fmt.Sprintf("submit %s · %s", orNowhere(cfg.SubmitURL), cfg.Source.SubmitURL), fmt.Sprintf("ttl %s · %s", reltime.Elapsed(cfg.TTL), cfg.Source.TTL), @@ -220,6 +221,7 @@ func ownSheetSummary(poolDir string) ownSummary { type poolAnswer struct { Mode string `json:"mode"` ModeSource string `json:"mode_source"` + RelayURL string `json:"relay_url"` IndexURL string `json:"index_url"` SubmitURL string `json:"submit_url"` TTLSeconds int `json:"ttl_seconds"` @@ -249,6 +251,7 @@ func printPoolJSON(output io.Writer, poolDir string, cfg poolcfg.Config, cached answer := poolAnswer{ Mode: cfg.Mode.String(), ModeSource: cfg.Source.Mode, + RelayURL: cfg.RelayURL, IndexURL: cfg.IndexURL, SubmitURL: cfg.SubmitURL, TTLSeconds: int(cfg.TTL / time.Second), diff --git a/cmd/codeaf/pool_test.go b/cmd/codeaf/pool_test.go index c9b533950c..54a33b6e9c 100644 --- a/cmd/codeaf/pool_test.go +++ b/cmd/codeaf/pool_test.go @@ -115,8 +115,9 @@ func TestPoolShowPrintsTheConfigAndSaysWhenNoIndexIsCached(t *testing.T) { body := out.String() for _, want := range []string{ "mode on · default", - "index https://pool.invalid/index.json · default", - "submit https://pool.invalid/submit · default", + "relay https://codeaf.agentfield.ai/pool · default", + "index https://codeaf.agentfield.ai/pool/index.json · default", + "submit https://codeaf.agentfield.ai/pool/v1/rows · default", "ttl 1d · default", "no index cached yet · built-in seed of 2026-09-17", "own sheet: none", @@ -402,7 +403,7 @@ func writePoolDoc(t *testing.T, dir, doc string) { // With no cache the reader answers the seed this build carries, and a worker // cell is in it — so `learn` has numbers on day one. func TestPoolIndexForAnswersTheSeedWithNoCache(t *testing.T) { - held := poolIndexFor(t.TempDir(), poolcfg.Resolve("", noEnv), poolClock(t))() + held := poolIndexFor(t.TempDir(), poolcfg.Resolve("", "", noEnv), poolClock(t))() if held == nil { t.Fatal("no cache and no seed: the reader answered nothing") } @@ -428,7 +429,7 @@ func TestPoolIndexForKeepsANewerCache(t *testing.T) { "metrics": {"role_quality": {"kind": "gaussian", "dims": ["role", "model"]}}, "cells": [{"metric": "role_quality", "role": "worker", "model": "z-ai/glm-5.3", "mean": 75, "sd": 7, "n": 30}] }`) - held := poolIndexFor(dir, poolcfg.Resolve("", noEnv), poolClock(t))() + held := poolIndexFor(dir, poolcfg.Resolve("", "", noEnv), poolClock(t))() if held == nil || held.Generated().Format("2006-01-02") != "2026-09-20" { t.Fatalf("a newer cache did not win: %v", held) } @@ -438,7 +439,7 @@ func TestPoolIndexForKeepsANewerCache(t *testing.T) { func TestPoolIndexForIgnoresAnUnparsableCache(t *testing.T) { dir := t.TempDir() writePoolDoc(t, dir, "{ this is not a document") - held := poolIndexFor(dir, poolcfg.Resolve("", noEnv), poolClock(t))() + held := poolIndexFor(dir, poolcfg.Resolve("", "", noEnv), poolClock(t))() if held == nil || held.Generated().Format("2006-01-02") != "2026-09-17" { t.Fatalf("an unparsable cache did not fall back to the seed: %v", held) } @@ -446,7 +447,7 @@ func TestPoolIndexForIgnoresAnUnparsableCache(t *testing.T) { // A mode that forbids reading answers no index at all. func TestPoolIndexForAnswersNothingWhenTheModeIsOff(t *testing.T) { - if held := poolIndexFor(t.TempDir(), poolcfg.Resolve("off", noEnv), poolClock(t))(); held != nil { + if held := poolIndexFor(t.TempDir(), poolcfg.Resolve("off", "", noEnv), poolClock(t))(); held != nil { t.Fatal("a mode that forbids reading answered an index") } } @@ -461,8 +462,8 @@ func TestPoolRefreshStartsNoGoroutineWithoutAKey(t *testing.T) { t.Cleanup(func() { poolRefreshGo = prev }) key := []ed25519.PublicKey{make(ed25519.PublicKey, ed25519.PublicKeySize)} - on := poolcfg.Resolve("", noEnv) - off := poolcfg.Resolve("off", noEnv) + on := poolcfg.Resolve("", "", noEnv) + off := poolcfg.Resolve("off", "", noEnv) startPoolIndexRefresh(context.Background(), t.TempDir(), on, nil) if started != 0 { diff --git a/internal/config/settings.go b/internal/config/settings.go index 514faa186a..74fe648500 100644 --- a/internal/config/settings.go +++ b/internal/config/settings.go @@ -3347,7 +3347,7 @@ func ModelPoolSettingAt(profileDir string) string { // verb, whose tests inject one) resolves [ModelPoolSettingAt] against its own // lookup rather than calling this. func ModelPoolAt(profileDir string) poolcfg.Config { - return poolcfg.Resolve(ModelPoolSettingAt(profileDir), os.LookupEnv) + return poolcfg.Resolve(ModelPoolSettingAt(profileDir), "", os.LookupEnv) } // ExaKeyAt resolves the Exa credential: the environment first, then the sheet, diff --git a/internal/pool/poolcfg/poolcfg.go b/internal/pool/poolcfg/poolcfg.go index 2a93160c6f..8886a85ec0 100644 --- a/internal/pool/poolcfg/poolcfg.go +++ b/internal/pool/poolcfg/poolcfg.go @@ -1,7 +1,8 @@ -// Package poolcfg resolves how the measurement pool behaves from one stored -// setting and an environment handed in as a function. Nothing here touches the -// disk, the network, the clock or the process environment, so Resolve is a -// table of inputs and outputs and every case is reachable from a test. +// Package poolcfg resolves how the measurement pool behaves from two stored +// settings — the mode word and the trusted key — and an environment handed in +// as a function. Nothing here touches the disk, the network, the clock or the +// process environment, so Resolve is a table of inputs and outputs and every +// case is reachable from a test. package poolcfg import ( @@ -11,10 +12,19 @@ import ( "time" ) -// Addresses held when the environment offers nothing readable. +// Addresses held when the environment offers nothing readable. The relay is +// the one base the resolver holds: the index and the submit addresses are +// read from beside it, and each keeps its own override. const ( - DefaultIndexURL = "https://pool.invalid/index.json" - DefaultSubmitURL = "https://pool.invalid/submit" + DefaultRelayURL = "https://codeaf.agentfield.ai/pool" + DefaultIndexURL = DefaultRelayURL + indexPath + DefaultSubmitURL = DefaultRelayURL + submitPath +) + +// The two addresses a relay serves, under its base. +const ( + indexPath = "/index.json" + submitPath = "/v1/rows" ) // Mode says what the pool may do: send and read, only read, or neither. @@ -44,30 +54,41 @@ func (m Mode) String() string { // of four words: "default", "setting", "env" or "ci". type Sources struct { Mode string + RelayURL string IndexURL string SubmitURL string + PublicKey string TTL string } // Config is the resolved pool configuration. Source names the origin of each // field, and every field is resolved on its own: the mode does not change how -// the URLs or the TTL are read, and an unreadable URL does not disturb the -// TTL. +// the addresses or the TTL are read, and an unreadable URL does not disturb +// the TTL. +// +// The relay is the base the index and submit addresses derive from: each is +// RelayURL with its own path appended, unless its own environment name +// overrides it. An empty PublicKey is the ordinary answer — it means the key +// the binary carries. type Config struct { Mode Mode + RelayURL string IndexURL string SubmitURL string + PublicKey string TTL time.Duration Source Sources } -// The five names Resolve reads, and no others. +// The seven names Resolve reads, and no others. const ( envMode = "CODEAF_MODEL_POOL" envCI = "CI" + envRelay = "CODEAF_MODEL_POOL_RELAY_URL" envIndex = "CODEAF_MODEL_POOL_URL" envSubmit = "CODEAF_MODEL_POOL_SUBMIT_URL" envTTL = "CODEAF_MODEL_POOL_TTL" + envKey = "CODEAF_MODEL_POOL_PUBLIC_KEY" ) // Where a resolved value came from. @@ -85,17 +106,18 @@ const ( defaultTTL = 24 * time.Hour ) -// Resolve resolves the pool configuration from the stored setting and the -// environment behind lookup, which answers with a value and whether the name -// was set at all — a name that is set and empty is not the same as a name -// nobody set. A nil lookup is an environment in which nothing is set. +// Resolve resolves the pool configuration from the stored mode word, the +// stored public key and the environment behind lookup, which answers with a +// value and whether the name was set at all — a name that is set and empty is +// not the same as a name nobody set. A nil lookup is an environment in which +// nothing is set. // // Every value is read with the space around it trimmed, and words are matched // without regard to case. Resolve is pure: the same inputs give the same // Config, it keeps nothing between calls, and many goroutines may call it at // once. -func Resolve(setting string, lookup func(name string) (value string, set bool)) Config { - // One injection for the whole environment: the five names are read once, +func Resolve(setting, publicKey string, lookup func(name string) (value string, set bool)) Config { + // One injection for the whole environment: the seven names are read once, // here, in this order, and each value is trimmed as it is read. get := func(name string) (string, bool) { if lookup == nil { @@ -109,9 +131,11 @@ func Resolve(setting string, lookup func(name string) (value string, set bool)) } modeWord, _ := get(envMode) ciWord, _ := get(envCI) + relayWord, _ := get(envRelay) indexWord, _ := get(envIndex) submitWord, submitSet := get(envSubmit) ttlWord, _ := get(envTTL) + keyWord, _ := get(envKey) // The mode is the first of these that answers, and that answer is its // source: the environment, the stored setting, CI, then the default. A @@ -126,15 +150,24 @@ func Resolve(setting string, lookup func(name string) (value string, set bool)) mode, modeSrc = Read, srcCI } - indexURL, indexSrc := DefaultIndexURL, srcDefault + // The relay is the base the other two addresses derive from, and its + // source is theirs: a relay read from the environment hands down both of + // its derived addresses, each of which an override of its own still wins. + // A base is held with no trailing slash, whatever the name carried. + relayURL, relaySrc := DefaultRelayURL, srcDefault + if u, ok := acceptURL(relayWord); ok { + relayURL, relaySrc = strings.TrimRight(u, "/"), srcEnv + } + + indexURL, indexSrc := relayURL+indexPath, relaySrc if u, ok := acceptURL(indexWord); ok { indexURL, indexSrc = u, srcEnv } // An empty submit address means send nowhere, and that is an answer; a // name nobody set, or one holding neither an address nor emptiness, is - // the default. - submitURL, submitSrc := DefaultSubmitURL, srcDefault + // the address derived from the relay. + submitURL, submitSrc := relayURL+submitPath, relaySrc if submitSet { if submitWord == "" { submitURL, submitSrc = "", srcEnv @@ -150,15 +183,30 @@ func Resolve(setting string, lookup func(name string) (value string, set bool)) ttl, ttlSrc = clampTTL(d), srcEnv } + // The trusted key is the first of these that answers: the environment, + // then the stored row, then none. An empty key is the ordinary answer — + // it means the key the binary carries. + storedKey := strings.TrimSpace(publicKey) + publicKey, keySrc := "", srcDefault + if keyWord != "" { + publicKey, keySrc = keyWord, srcEnv + } else if storedKey != "" { + publicKey, keySrc = storedKey, srcSetting + } + return Config{ Mode: mode, + RelayURL: relayURL, IndexURL: indexURL, SubmitURL: submitURL, + PublicKey: publicKey, TTL: ttl, Source: Sources{ Mode: modeSrc, + RelayURL: relaySrc, IndexURL: indexSrc, SubmitURL: submitSrc, + PublicKey: keySrc, TTL: ttlSrc, }, } diff --git a/internal/pool/poolcfg/poolcfg_test.go b/internal/pool/poolcfg/poolcfg_test.go index b1852d4922..5c66fe684c 100644 --- a/internal/pool/poolcfg/poolcfg_test.go +++ b/internal/pool/poolcfg/poolcfg_test.go @@ -37,13 +37,17 @@ func TestModeString(t *testing.T) { func TestResolveDefaults(t *testing.T) { want := Config{ Mode: On, + RelayURL: DefaultRelayURL, IndexURL: DefaultIndexURL, SubmitURL: DefaultSubmitURL, + PublicKey: "", TTL: 24 * time.Hour, Source: Sources{ Mode: "default", + RelayURL: "default", IndexURL: "default", SubmitURL: "default", + PublicKey: "default", TTL: "default", }, } @@ -56,7 +60,7 @@ func TestResolveDefaults(t *testing.T) { {"other names only", envOf(map[string]string{"OTHER": "on"})}, } for _, l := range lookups { - if got := Resolve("", l.lookup); got != want { + if got := Resolve("", "", l.lookup); got != want { t.Errorf("%s: Resolve() = %+v, want %+v", l.name, got, want) } } @@ -84,7 +88,7 @@ func TestResolveMode(t *testing.T) { {"env beats setting", "off", envOf(map[string]string{"CODEAF_MODEL_POOL": "on"}), On, "env"}, } for _, c := range cases { - got := Resolve(c.setting, c.lookup) + got := Resolve(c.setting, "", c.lookup) if got.Mode != c.want || got.Source.Mode != c.wantSrc { t.Errorf("%s: Mode = %v from %q, want %v from %q", c.name, got.Mode, got.Source.Mode, c.want, c.wantSrc) } @@ -115,7 +119,7 @@ func TestResolveModeFromCI(t *testing.T) { {"env unknown and setting unknown leave ci", "maybe", envOf(map[string]string{"CI": "true", "CODEAF_MODEL_POOL": "maybe"}), Read, "ci"}, } for _, c := range cases { - got := Resolve(c.setting, c.lookup) + got := Resolve(c.setting, "", c.lookup) if got.Mode != c.want || got.Source.Mode != c.wantSrc { t.Errorf("%s: Mode = %v from %q, want %v from %q", c.name, got.Mode, got.Source.Mode, c.want, c.wantSrc) } @@ -166,7 +170,7 @@ func TestResolveIndexURL(t *testing.T) { if c.set { lookup = envOf(map[string]string{"CODEAF_MODEL_POOL_URL": c.value}) } - got := Resolve("", lookup) + got := Resolve("", "", lookup) if got.IndexURL != c.want || got.Source.IndexURL != c.wantSrc { t.Errorf("%s: IndexURL = %q from %q, want %q from %q", c.name, got.IndexURL, got.Source.IndexURL, c.want, c.wantSrc) } @@ -197,13 +201,118 @@ func TestResolveSubmitURL(t *testing.T) { if c.set { lookup = envOf(map[string]string{"CODEAF_MODEL_POOL_SUBMIT_URL": c.value}) } - got := Resolve("", lookup) + got := Resolve("", "", lookup) if got.SubmitURL != c.want || got.Source.SubmitURL != c.wantSrc { t.Errorf("%s: SubmitURL = %q from %q, want %q from %q", c.name, got.SubmitURL, got.Source.SubmitURL, c.want, c.wantSrc) } } } +func TestResolveRelayURL(t *testing.T) { + cases := []struct { + name string + value string + set bool + want string + wantSrc string + }{ + {"nobody set", "", false, DefaultRelayURL, "default"}, + {"https", "https://relay.example.com", true, "https://relay.example.com", "env"}, + {"https with a port", "https://relay.example.com:8443", true, "https://relay.example.com:8443", "env"}, + {"padded", " https://relay.example.com ", true, "https://relay.example.com", "env"}, + // A base is held with no trailing slash, whatever the name carried: the + // paths are appended to it, and a kept slash would make two. + {"trailing slash", "https://relay.example.com/", true, "https://relay.example.com", "env"}, + {"plain http elsewhere", "http://relay.example.com", true, DefaultRelayURL, "default"}, + {"other scheme", "ftp://relay.example.com", true, DefaultRelayURL, "default"}, + {"bare host", "relay.example.com", true, DefaultRelayURL, "default"}, + {"set and empty", "", true, DefaultRelayURL, "default"}, + } + for _, c := range cases { + lookup := envOf(nil) + if c.set { + lookup = envOf(map[string]string{"CODEAF_MODEL_POOL_RELAY_URL": c.value}) + } + got := Resolve("", "", lookup) + if got.RelayURL != c.want || got.Source.RelayURL != c.wantSrc { + t.Errorf("%s: RelayURL = %q from %q, want %q from %q", c.name, got.RelayURL, got.Source.RelayURL, c.want, c.wantSrc) + } + } +} + +// The two addresses derive from the relay, and each override wins over its +// derivation. A relay read from the environment hands its source down to both +// derived addresses; an override of its own names itself instead. +func TestResolveDerivesTheAddressesFromTheRelay(t *testing.T) { + got := Resolve("", "", envOf(nil)) + if got.IndexURL != "https://codeaf.agentfield.ai/pool/index.json" || got.Source.IndexURL != "default" { + t.Errorf("IndexURL = %q from %q, want the relay's derived address from %q", got.IndexURL, got.Source.IndexURL, "default") + } + if got.SubmitURL != "https://codeaf.agentfield.ai/pool/v1/rows" || got.Source.SubmitURL != "default" { + t.Errorf("SubmitURL = %q from %q, want the relay's derived address from %q", got.SubmitURL, got.Source.SubmitURL, "default") + } + + lookup := envOf(map[string]string{"CODEAF_MODEL_POOL_RELAY_URL": "https://relay.example.com"}) + got = Resolve("", "", lookup) + if got.IndexURL != "https://relay.example.com/index.json" || got.Source.IndexURL != "env" { + t.Errorf("a pinned relay's index = %q from %q", got.IndexURL, got.Source.IndexURL) + } + if got.SubmitURL != "https://relay.example.com/v1/rows" || got.Source.SubmitURL != "env" { + t.Errorf("a pinned relay's submit = %q from %q", got.SubmitURL, got.Source.SubmitURL) + } + + // Each override still wins over its derivation, and the address it does + // not name keeps the relay's source. + lookup = envOf(map[string]string{ + "CODEAF_MODEL_POOL_RELAY_URL": "https://relay.example.com", + "CODEAF_MODEL_POOL_SUBMIT_URL": "https://submit.example.com", + }) + got = Resolve("", "", lookup) + if got.IndexURL != "https://relay.example.com/index.json" || got.Source.IndexURL != "env" { + t.Errorf("IndexURL = %q from %q", got.IndexURL, got.Source.IndexURL) + } + if got.SubmitURL != "https://submit.example.com" || got.Source.SubmitURL != "env" { + t.Errorf("SubmitURL = %q from %q", got.SubmitURL, got.Source.SubmitURL) + } + if got.RelayURL != "https://relay.example.com" { + t.Errorf("RelayURL = %q", got.RelayURL) + } +} + +func TestResolvePublicKey(t *testing.T) { + cases := []struct { + name string + setting string + value string + set bool + want string + wantSrc string + }{ + {"nobody set", "", "", false, "", "default"}, + {"env", "", "WOAo+g/oKxAV9vVqv2Q14w1TyyyiouwFO2fC0zgcps0=", true, "WOAo+g/oKxAV9vVqv2Q14w1TyyyiouwFO2fC0zgcps0=", "env"}, + {"env padded", "", " abc= ", true, "abc=", "env"}, + {"env set and empty falls through", "stored", "", true, "stored", "setting"}, + {"setting", "stored", "", false, "stored", "setting"}, + {"setting padded", " stored ", "", false, "stored", "setting"}, + {"env beats setting", "stored", "pinned", true, "pinned", "env"}, + } + for _, c := range cases { + lookup := envOf(nil) + if c.set { + lookup = envOf(map[string]string{"CODEAF_MODEL_POOL_PUBLIC_KEY": c.value}) + } + got := Resolve("", c.setting, lookup) + if got.PublicKey != c.want || got.Source.PublicKey != c.wantSrc { + t.Errorf("%s: PublicKey = %q from %q, want %q from %q", c.name, got.PublicKey, got.Source.PublicKey, c.want, c.wantSrc) + } + } + // The setting is Resolve's own argument, not a row the package reads: a + // nil lookup still carries one. + if got := Resolve("on", "stored", nil); got.PublicKey != "stored" || got.Source.PublicKey != "setting" { + t.Errorf("Resolve with a nil lookup lost the stored key: %+v", got) + } +} + func TestResolveTTL(t *testing.T) { cases := []struct { name string @@ -230,7 +339,7 @@ func TestResolveTTL(t *testing.T) { if c.set { lookup = envOf(map[string]string{"CODEAF_MODEL_POOL_TTL": c.value}) } - got := Resolve("", lookup) + got := Resolve("", "", lookup) if got.TTL != c.want || got.Source.TTL != c.wantSrc { t.Errorf("%s: TTL = %v from %q, want %v from %q", c.name, got.TTL, got.Source.TTL, c.want, c.wantSrc) } @@ -241,7 +350,7 @@ func TestResolveFieldsAreIndependent(t *testing.T) { // Off, an unreadable URL and a readable TTL: the mode does not change how // the URLs or the TTL are read, and the unreadable URL does not disturb // the TTL. - got := Resolve("bogus", envOf(map[string]string{ + got := Resolve("bogus", "", envOf(map[string]string{ "CODEAF_MODEL_POOL": "off", "CODEAF_MODEL_POOL_URL": "http://pool.example.com", "CODEAF_MODEL_POOL_SUBMIT_URL": "https://submit.example.com", @@ -249,13 +358,17 @@ func TestResolveFieldsAreIndependent(t *testing.T) { })) want := Config{ Mode: Off, + RelayURL: DefaultRelayURL, IndexURL: DefaultIndexURL, SubmitURL: "https://submit.example.com", + PublicKey: "", TTL: 2 * time.Hour, Source: Sources{ Mode: "env", + RelayURL: "default", IndexURL: "default", SubmitURL: "env", + PublicKey: "default", TTL: "env", }, } @@ -264,7 +377,7 @@ func TestResolveFieldsAreIndependent(t *testing.T) { } // An unreadable TTL does not disturb the URLs either. - got = Resolve("", envOf(map[string]string{ + got = Resolve("", "", envOf(map[string]string{ "CODEAF_MODEL_POOL_URL": "https://pool.example.com", "CODEAF_MODEL_POOL_TTL": "banana", })) @@ -301,7 +414,7 @@ func TestCanSendAndCanRead(t *testing.T) { func TestResolvedConfigCanSendAndCanRead(t *testing.T) { // An empty submit address turns sending off while everything else stays on. - got := Resolve("", envOf(map[string]string{"CODEAF_MODEL_POOL_SUBMIT_URL": ""})) + got := Resolve("", "", envOf(map[string]string{"CODEAF_MODEL_POOL_SUBMIT_URL": ""})) if got.SubmitURL != "" || got.Source.SubmitURL != "env" { t.Fatalf("SubmitURL = %q from %q, want %q from %q", got.SubmitURL, got.Source.SubmitURL, "", "env") } @@ -312,35 +425,35 @@ func TestResolvedConfigCanSendAndCanRead(t *testing.T) { t.Error("an empty submit address must still read") } - if got := Resolve("", envOf(nil)); !got.CanSend() || !got.CanRead() { + if got := Resolve("", "", envOf(nil)); !got.CanSend() || !got.CanRead() { t.Errorf("the defaults must send and read: CanSend() = %v, CanRead() = %v", got.CanSend(), got.CanRead()) } - if got := Resolve("read", envOf(nil)); got.CanSend() || !got.CanRead() { + if got := Resolve("read", "", envOf(nil)); got.CanSend() || !got.CanRead() { t.Errorf("read must read and not send: CanSend() = %v, CanRead() = %v", got.CanSend(), got.CanRead()) } - if got := Resolve("off", envOf(nil)); got.CanSend() || got.CanRead() { + if got := Resolve("off", "", envOf(nil)); got.CanSend() || got.CanRead() { t.Errorf("off must neither send nor read: CanSend() = %v, CanRead() = %v", got.CanSend(), got.CanRead()) } } func TestResolveNilLookupUsesSetting(t *testing.T) { - got := Resolve("read", nil) + got := Resolve("read", "", nil) if got.Mode != Read || got.Source.Mode != "setting" { - t.Errorf("Resolve(\"read\", nil): Mode = %v from %q, want read from %q", got.Mode, got.Source.Mode, "setting") + t.Errorf("Resolve(\"read\", \"\", nil): Mode = %v from %q, want read from %q", got.Mode, got.Source.Mode, "setting") } - if got.IndexURL != DefaultIndexURL || got.SubmitURL != DefaultSubmitURL || got.TTL != 24*time.Hour { - t.Errorf("Resolve(\"read\", nil) = %+v, want the defaults beside the setting", got) + if got.IndexURL != DefaultIndexURL || got.SubmitURL != DefaultSubmitURL || got.RelayURL != DefaultRelayURL || got.TTL != 24*time.Hour { + t.Errorf("Resolve(\"read\", \"\", nil) = %+v, want the defaults beside the setting", got) } } -func TestResolveAsksFiveNames(t *testing.T) { +func TestResolveAsksSevenNames(t *testing.T) { var asked []string - Resolve("", func(name string) (string, bool) { + Resolve("", "", func(name string) (string, bool) { asked = append(asked, name) return "", false }) sort.Strings(asked) - want := []string{envMode, envCI, envIndex, envSubmit, envTTL} + want := []string{envMode, envCI, envRelay, envIndex, envSubmit, envTTL, envKey} sort.Strings(want) if !reflect.DeepEqual(asked, want) { t.Errorf("Resolve asked lookup for %v, want %v", asked, want) @@ -350,12 +463,12 @@ func TestResolveAsksFiveNames(t *testing.T) { func TestResolveIsPure(t *testing.T) { first := envOf(map[string]string{"CODEAF_MODEL_POOL": "read", "CODEAF_MODEL_POOL_TTL": "3h"}) second := envOf(map[string]string{"CODEAF_MODEL_POOL": "off"}) - want := Resolve("", first) - Resolve("", second) - if got := Resolve("", first); got != want { + want := Resolve("", "", first) + Resolve("", "", second) + if got := Resolve("", "", first); got != want { t.Errorf("a second call kept state: %+v, want %+v", got, want) } - if got := Resolve("", second); got.Mode != Off || got.TTL != 24*time.Hour { + if got := Resolve("", "", second); got.Mode != Off || got.TTL != 24*time.Hour { t.Errorf("a fresh environment was polluted: %+v", got) } } @@ -365,14 +478,14 @@ func TestResolveConcurrent(t *testing.T) { "CODEAF_MODEL_POOL": "read", "CODEAF_MODEL_POOL_TTL": "90m", }) - want := Resolve("", lookup) + want := Resolve("", "", lookup) var wg sync.WaitGroup for i := 0; i < 32; i++ { wg.Add(1) go func() { defer wg.Done() for j := 0; j < 64; j++ { - if got := Resolve("", lookup); got != want { + if got := Resolve("", "", lookup); got != want { t.Errorf("Resolve() = %+v, want %+v", got, want) return } From ec0a8b216754ca2efdab1a6d9e5db093b7c5759d Mon Sep 17 00:00:00 2001 From: santoshkumarradha Date: Thu, 17 Sep 2026 12:54:09 -0400 Subject: [PATCH 129/173] pool: the outbox rides an install nonce, and pending rows are pushed after each judged run and at start-up --- cmd/codeaf/pool_test.go | 39 +++++ cmd/codeaf/poolindex.go | 7 + cmd/codeaf/poolinstall.go | 109 ++++++++++++++ cmd/codeaf/poolinstall_test.go | 216 ++++++++++++++++++++++++++++ cmd/codeaf/poolrecord.go | 5 + internal/pool/outbox/outbox.go | 9 ++ internal/pool/outbox/outbox_test.go | 37 +++++ 7 files changed, 422 insertions(+) create mode 100644 cmd/codeaf/poolinstall.go create mode 100644 cmd/codeaf/poolinstall_test.go diff --git a/cmd/codeaf/pool_test.go b/cmd/codeaf/pool_test.go index 54a33b6e9c..8b8e2f2ba2 100644 --- a/cmd/codeaf/pool_test.go +++ b/cmd/codeaf/pool_test.go @@ -479,6 +479,42 @@ func TestPoolRefreshStartsNoGoroutineWithoutAKey(t *testing.T) { } } +// stubPoolRefresh stands the goroutine guard in for the length of one test +// and answers the counter it increments, so a test of what wirePoolIndex +// seats runs no fetch and no push. The tests that call it read seats, not +// errands; the errands have their own tests. +func stubPoolRefresh(t *testing.T) *int { + t.Helper() + started := 0 + prev := poolRefreshGo + poolRefreshGo = func(scope string, fn func()) { started++ } + t.Cleanup(func() { poolRefreshGo = prev }) + return &started +} + +// The start-up push is an errand behind the same guard as the refresh: a +// mode that sends starts it once, a mode that only reads starts nothing. +// (The refresh's own errand waits for the build's key — its test is +// TestPoolRefreshStartsNoGoroutineWithoutAKey.) +func TestWirePoolIndexStartsThePushWhenTheModeSends(t *testing.T) { + started := stubPoolRefresh(t) + t.Setenv("CODEAF_MODEL_POOL_SUBMIT_URL", "http://127.0.0.1:1/v1/rows") + + // The default mode is on: the push starts. + wirePoolIndex(t.TempDir()) + if *started != 1 { + t.Fatalf("a sending pool started %d errand(s) at start-up, want the one push", *started) + } + + // A mode that does not send starts none. + *started = 0 + t.Setenv("CODEAF_MODEL_POOL", "read") + wirePoolIndex(t.TempDir()) + if *started != 0 { + t.Fatalf("a read-only pool started %d push(es)", *started) + } +} + // With no cache, the --json shape reports the seed the build carries and says // so under source, so a script sees the index a pick would read. func TestPoolShowJSONWithNoCacheReportsTheSeed(t *testing.T) { @@ -582,6 +618,7 @@ func TestPoolShowReadsAnUnparsableOwnSheetAsNone(t *testing.T) { func TestWirePoolIndexSeatsTheOwnSheetsCells(t *testing.T) { prevIndex, prevOwn := config.AutoIndex, config.AutoOwnCells t.Cleanup(func() { config.AutoIndex, config.AutoOwnCells = prevIndex, prevOwn }) + stubPoolRefresh(t) dir := t.TempDir() seedOwnSheet(t, dir) @@ -613,6 +650,7 @@ func TestWirePoolIndexSeatsNothingWhenThePoolIsOff(t *testing.T) { prevIndex, prevOwn := config.AutoIndex, config.AutoOwnCells t.Cleanup(func() { config.AutoIndex, config.AutoOwnCells = prevIndex, prevOwn }) t.Setenv("CODEAF_MODEL_POOL", "off") + stubPoolRefresh(t) dir := t.TempDir() seedOwnSheet(t, dir) @@ -630,6 +668,7 @@ func TestWirePoolIndexSeatsNothingWhenThePoolIsOff(t *testing.T) { func TestWirePoolIndexSeatsNothingForAnUnparsableOwnSheet(t *testing.T) { prevOwn := config.AutoOwnCells t.Cleanup(func() { config.AutoOwnCells = prevOwn }) + stubPoolRefresh(t) dir := t.TempDir() poolDir := filepath.Join(dir, "pool") diff --git a/cmd/codeaf/poolindex.go b/cmd/codeaf/poolindex.go index 2911eedeea..7390d0d590 100644 --- a/cmd/codeaf/poolindex.go +++ b/cmd/codeaf/poolindex.go @@ -116,6 +116,13 @@ func wirePoolIndex(profileDir string) { config.AutoIndex = poolIndexFor(profileDir, cfg, time.Now) config.AutoOwnCells = poolOwnCellsFor(profileDir, cfg) startPoolIndexRefresh(context.Background(), profileDir, cfg, poolPublicKeys) + // The rows a previous run judged and could not hand over leave at once, + // on their own goroutine behind the same guard the refresh uses: a + // start-up errand, bounded by its own budget, and never on the run's + // path. + if cfg.CanSend() { + poolRefreshGo("pool/push", func() { poolPush(context.Background(), profileDir, cfg, poolPushBudget) }) + } } // poolOwnCellsFor builds this process's one reader of the install's own judged diff --git a/cmd/codeaf/poolinstall.go b/cmd/codeaf/poolinstall.go new file mode 100644 index 0000000000..6c1a1db32c --- /dev/null +++ b/cmd/codeaf/poolinstall.go @@ -0,0 +1,109 @@ +// The install's own identity at the relay: one nonce, drawn once and kept. +// +// Every batch the outbox sends to the relay rides the header +// X-Codeaf-Install with this word, so the relay can tell one install's +// measurements from another's without ever learning who the install is: the +// nonce is 32 random hex characters, held in a file only this install writes, +// and nothing else about the machine travels with it. The file is replaced, +// not repaired, when it stops being that word — a mangled file costs the +// install its old identity and nothing else. +package main + +import ( + "context" + cryptorand "crypto/rand" + "encoding/hex" + "log" + "os" + "path/filepath" + "regexp" + "time" + + "github.com/Agent-Field/codeaf/internal/config" + "github.com/Agent-Field/codeaf/internal/pool/outbox" + "github.com/Agent-Field/codeaf/internal/pool/poolcfg" + "github.com/Agent-Field/codeaf/internal/trace" +) + +// installFile is the one file an install's nonce is kept in, under the pool +// directory, beside the outbox and the own sheet. +const installFile = "install" + +// nonceLen is 16 random bytes, spelled as 32 lowercase hex — the shape the +// relay's header check asks for. +const ( + nonceLen = 16 + nonceHexLen = 32 + installDirMode = 0700 + installFileMode = 0600 +) + +// nonceWord is the only shape installNonce accepts back off the disk. +var nonceWord = regexp.MustCompile(`^[0-9a-f]{32}$`) + +// poolPushBudget is the bound one push is given, at start-up and after a +// judged run alike: an errand nobody is waiting for gets one bound, the same +// bound at both doors so a slow relay cannot hold either open longer. +const poolPushBudget = 5 * time.Second + +// installNonce answers this install's nonce: the word in /install, +// drawn from the system's randomness and written there the first time, mode +// 0600 in a directory of mode 0700. A file that is missing, or one that does +// not hold the one accepted shape, is replaced the same way, so a mangled +// file cannot wedge an install out of sending. +func installNonce(poolDir string) (string, error) { + path := filepath.Join(poolDir, installFile) + if raw, err := os.ReadFile(path); err == nil { + if word := string(raw); nonceWord.MatchString(word) { + return word, nil + } + } else if !os.IsNotExist(err) { + return "", err + } + raw := make([]byte, nonceLen) + if _, err := cryptorand.Read(raw); err != nil { + return "", err + } + nonce := hex.EncodeToString(raw) + if err := os.MkdirAll(poolDir, installDirMode); err != nil { + return "", err + } + if err := os.WriteFile(path, []byte(nonce), installFileMode); err != nil { + return "", err + } + return nonce, nil +} + +// poolPush hands the outbox's pending rows to the relay: the rows are the +// install's judged scores, and the relay wants the install's nonce beside +// them. Quiet by design, for poolindex.go's reason — a relay that does not +// answer yet, or a nonce that could not be drawn, is an ordinary state for an +// errand nothing waits on, so the error is said only under the debug record's +// switch and never at a person. The rows a batch did not reach stay pending, +// where the next judged run and the next start-up try them again. +func poolPush(ctx context.Context, profileDir string, cfg poolcfg.Config, budget time.Duration) { + if !cfg.CanSend() { + return + } + poolDir := config.ProfilePath(profileDir, "pool") + box, err := outbox.Open(outboxPath(poolDir)) + if err != nil { + if trace.Enabled() { + log.Printf("model pool: outbox: %v", err) + } + return + } + defer box.Close() + nonce, err := installNonce(poolDir) + if err != nil { + if trace.Enabled() { + log.Printf("model pool: install nonce: %v", err) + } + return + } + box.Install = nonce + box.Budget = budget + if _, err := box.Send(ctx, cfg.SubmitURL); err != nil && trace.Enabled() { + log.Printf("model pool: send: %v", err) + } +} diff --git a/cmd/codeaf/poolinstall_test.go b/cmd/codeaf/poolinstall_test.go new file mode 100644 index 0000000000..1b36ff06e2 --- /dev/null +++ b/cmd/codeaf/poolinstall_test.go @@ -0,0 +1,216 @@ +package main + +// The install's nonce and the push that rides it: what the file holds, what +// replaces a mangled one, and what one push does to the outbox at a relay +// that answers and at one that does not. + +import ( + "context" + "net/http" + "net/http/httptest" + "os" + "path/filepath" + "regexp" + "strings" + "testing" + "time" + + "github.com/Agent-Field/codeaf/internal/pool/outbox" + "github.com/Agent-Field/codeaf/internal/pool/poolcfg" +) + +var hexWord = regexp.MustCompile(`^[0-9a-f]{32}$`) + +// The nonce is drawn once and kept: a second call in the same pool directory +// reads the same file and answers the same 32 lowercase hex characters, and +// the file sits at mode 0600 in a directory of mode 0700. +func TestInstallNonceDrawsOnceAndKeepsTheWord(t *testing.T) { + dir := t.TempDir() + poolDir := filepath.Join(dir, "pool") + + first, err := installNonce(poolDir) + if err != nil { + t.Fatalf("installNonce: %v", err) + } + if len(first) != 32 || !hexWord.MatchString(first) { + t.Fatalf("the nonce is %q, want 32 lowercase hex characters", first) + } + second, err := installNonce(poolDir) + if err != nil { + t.Fatalf("installNonce again: %v", err) + } + if second != first { + t.Fatalf("the second draw answered %q, want the kept %q", second, first) + } + + info, err := os.Stat(filepath.Join(poolDir, "install")) + if err != nil { + t.Fatalf("the nonce file: %v", err) + } + if info.Mode().Perm() != 0600 { + t.Fatalf("the nonce file is %v, want 0600", info.Mode().Perm()) + } + dirInfo, err := os.Stat(poolDir) + if err != nil { + t.Fatalf("the pool directory: %v", err) + } + if dirInfo.Mode().Perm() != 0700 { + t.Fatalf("the pool directory is %v, want 0700", dirInfo.Mode().Perm()) + } + + // A nonce drawn here is not the one another install drew: two pool + // directories hold two words. + other, err := installNonce(filepath.Join(t.TempDir(), "pool")) + if err != nil { + t.Fatalf("installNonce elsewhere: %v", err) + } + if other == first { + t.Fatal("two installs drew the same nonce") + } +} + +// A file that does not hold the one accepted shape is replaced the way a +// missing one is: a new draw, written over it. Whatever mangled the file — a +// half write, an older shape — the install is sending again afterwards. +func TestInstallNonceReplacesAMangledFile(t *testing.T) { + for name, mangled := range map[string]string{ + "not hex": "not a nonce at all", + "too short": "abcd1234", + "upper case": strings.ToUpper("0123456789abcdef0123456789abcdef"), + "the empty file": "", + "right length, junk": "zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz", + } { + dir := filepath.Join(t.TempDir(), "pool") + if err := os.MkdirAll(dir, 0o700); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(dir, "install"), []byte(mangled), 0o600); err != nil { + t.Fatal(err) + } + got, err := installNonce(dir) + if err != nil { + t.Fatalf("%s: installNonce: %v", name, err) + } + if !hexWord.MatchString(got) { + t.Fatalf("%s: the mangled file was not replaced: %q", name, got) + } + if back, err := installNonce(dir); err != nil || back != got { + t.Fatalf("%s: the replacement was not kept: %q, %v", name, back, err) + } + } +} + +// A pool directory that cannot be created is an error, not a silence. +func TestInstallNonceRefusesAPlaceItCannotWrite(t *testing.T) { + file := filepath.Join(t.TempDir(), "plain") + if err := os.WriteFile(file, []byte("x"), 0o644); err != nil { + t.Fatal(err) + } + if _, err := installNonce(filepath.Join(file, "pool")); err == nil { + t.Fatal("a nonce was drawn over a file") + } +} + +// ── THE PUSH ──────────────────────────────────────────────────────────────── + +// A push is the outbox's pending rows handed to the relay under the install's +// nonce: one POST, the header carrying the word the nonce file holds, and +// every row it reached marked sent in the file. +func TestPoolPushPostsPendingRowsUnderTheInstallNonce(t *testing.T) { + var installs []string + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + installs = append(installs, r.Header.Get("X-Codeaf-Install")) + w.WriteHeader(http.StatusOK) + })) + defer srv.Close() + + dir := seedOutbox(t) + submit := oneEnv("CODEAF_MODEL_POOL_SUBMIT_URL", srv.URL) + cfg := poolcfg.Resolve("", "", submit) + poolPush(context.Background(), dir, cfg, 2*time.Second) + + if len(installs) != 1 { + t.Fatalf("the relay was asked %d times, want once: %v", len(installs), installs) + } + if !hexWord.MatchString(installs[0]) { + t.Fatalf("the batch rode with X-Codeaf-Install %q, want 32 lowercase hex", installs[0]) + } + nonce, err := installNonce(configuredPoolDir(dir)) + if err != nil { + t.Fatalf("the nonce file: %v", err) + } + if installs[0] != nonce { + t.Fatalf("the header carried %q, want the word the nonce file holds %q", installs[0], nonce) + } + + // Every row the relay took is marked sent: the same outbox, reopened, + // holds nothing pending. + box, err := outboxOpenForTest(dir) + if err != nil { + t.Fatalf("the outbox: %v", err) + } + defer box.Close() + if got := len(box.Pending()); got != 0 { + t.Fatalf("%d row(s) still pending after a delivered push", got) + } +} + +// A relay that does not answer is an ordinary state: the push says nothing, +// prints nothing, and every row stays pending where the next push finds it. +func TestPoolPushLeavesTheRowsPendingWhenTheRelayDoesNotAnswer(t *testing.T) { + dir := seedOutbox(t) + // A dead loopback port: the address the relay would have answered at, if + // the relay were there. + submit := oneEnv("CODEAF_MODEL_POOL_SUBMIT_URL", "http://127.0.0.1:1/v1/rows") + cfg := poolcfg.Resolve("", "", submit) + + said, err := captureStdout(t, func() error { + poolPush(context.Background(), dir, cfg, time.Second) + return nil + }) + if err != nil { + t.Fatalf("the push: %v", err) + } + if said != "" { + t.Fatalf("a push nothing waits for said %q", said) + } + + box, err := outboxOpenForTest(dir) + if err != nil { + t.Fatalf("the outbox: %v", err) + } + defer box.Close() + if got := len(box.Pending()); got != 2 { + t.Fatalf("%d row(s) pending after an unanswered push, want the 2 seeded", got) + } +} + +// A mode that does not send pushes nothing at all: no outbox opened, no +// nonce drawn, no file written under the pool directory. +func TestPoolPushDoesNothingWhenTheModeDoesNotSend(t *testing.T) { + dir := t.TempDir() + poolDir := filepath.Join(dir, "pool") + if err := os.MkdirAll(poolDir, 0o700); err != nil { + t.Fatal(err) + } + cfg := poolcfg.Resolve("read", "", oneEnv("CODEAF_MODEL_POOL_SUBMIT_URL", "http://127.0.0.1:1/v1/rows")) + poolPush(context.Background(), dir, cfg, time.Second) + if _, err := os.Stat(filepath.Join(poolDir, "install")); !os.IsNotExist(err) { + t.Fatal("a read-only pool drew a nonce") + } + if _, err := os.Stat(filepath.Join(poolDir, "outbox.jsonl")); !os.IsNotExist(err) { + t.Fatal("a read-only pool opened the outbox") + } +} + +// configuredPoolDir is the pool directory a push under profileDir writes +// into, the same reading the push itself makes. +func configuredPoolDir(profileDir string) string { + return filepath.Join(profileDir, "pool") +} + +// outboxOpenForTest opens the outbox file the way a reading test reads it +// back — the same path poolPush writes through config.ProfilePath. +func outboxOpenForTest(dir string) (*outbox.Outbox, error) { + return outbox.Open(filepath.Join(dir, "pool", "outbox.jsonl")) +} diff --git a/cmd/codeaf/poolrecord.go b/cmd/codeaf/poolrecord.go index f4140144b9..8c8bccaa47 100644 --- a/cmd/codeaf/poolrecord.go +++ b/cmd/codeaf/poolrecord.go @@ -149,6 +149,11 @@ func poolJudgeLanding(settings config.Config, profileDir string, models func() [ // index's min_installs. cells := record.Cells(sheet) config.AutoOwnCells = func() []crewpick.Cell { return cells } + // The sheet is saved, so what the recorder appended is the install's own + // evidence now; the copies waiting in the outbox leave for the relay + // here, on this hook's own goroutine (the session runs TaskLanded on + // one), under the push's own bound. + poolPush(ctx, profileDir, pool, poolPushBudget) } // poolSize is the day-row bucket a landing's token count answers: S under diff --git a/internal/pool/outbox/outbox.go b/internal/pool/outbox/outbox.go index adac02c22e..919b05191a 100644 --- a/internal/pool/outbox/outbox.go +++ b/internal/pool/outbox/outbox.go @@ -80,6 +80,12 @@ type Outbox struct { // MaxPending caps the pending rows, dropping from the old end; zero or // below means DefaultMaxPending. MaxPending int + // Install is the install's own nonce, carried as the X-Codeaf-Install + // header on every batch sent over http; empty sends no header. A + // destination that requires the header names it in its error, so a batch + // sent without one is a batch the destination refuses — the caller sets + // this from its own store before the first Send. + Install string path string sendMu sync.Mutex // one Send at a time, so two never carry the same rows @@ -445,6 +451,9 @@ func (o *Outbox) sendHTTP(ctx context.Context, dest string, rows []Row) (int, er return n, err } req.Header.Set("Content-Type", "application/x-ndjson") + if o.Install != "" { + req.Header.Set("X-Codeaf-Install", o.Install) + } resp, err := client.Do(req) if err != nil { return n, err diff --git a/internal/pool/outbox/outbox_test.go b/internal/pool/outbox/outbox_test.go index e8f0e345b7..36b81e524f 100644 --- a/internal/pool/outbox/outbox_test.go +++ b/internal/pool/outbox/outbox_test.go @@ -133,6 +133,7 @@ type postRecord struct { mu sync.Mutex bodies []string types []string + headers []*http.Header seconds int status []int } @@ -142,6 +143,8 @@ func (p *postRecord) handle(w http.ResponseWriter, r *http.Request) { p.mu.Lock() p.bodies = append(p.bodies, string(b)) p.types = append(p.types, r.Header.Get("Content-Type")) + h := r.Header + p.headers = append(p.headers, &h) // The two knobs are read under the same lock the bodies are written // under: the server answers on its own goroutine, and a handler reading a // field the test writes is a race whether or not the requests overlap. @@ -1092,3 +1095,37 @@ func TestASendWhoseMarkerCannotBeWrittenKeepsTheRowsPending(t *testing.T) { t.Fatalf("pending = %d, want the row whose marker never reached the file", got) } } + +// A batch sent over http carries the outbox's install nonce as +// X-Codeaf-Install, one header per post; an outbox with none set sends no +// header, and a file destination is never asked for one. +func TestSendCarriesTheInstallNonceAsAHeader(t *testing.T) { + var rec postRecord + srv := httptest.NewServer(http.HandlerFunc(rec.handle)) + defer srv.Close() + + o, _ := openOutbox(t) + o.Rand = &stepRand{step: 11} + o.Install = "0123456789abcdef0123456789abcdef" + appendRow(t, o, `{"k":1}`) + n, err := o.Send(context.Background(), srv.URL) + if n != 1 || err != nil { + t.Fatalf("Send answered (%d, %v), want (1, nil)", n, err) + } + if got := rec.headers[0].Get("X-Codeaf-Install"); got != o.Install { + t.Fatalf("the batch rode with X-Codeaf-Install %q, want the outbox's nonce %q", got, o.Install) + } + + rec = postRecord{} + srv2 := httptest.NewServer(http.HandlerFunc(rec.handle)) + defer srv2.Close() + o2, _ := openOutbox(t) + o2.Rand = &stepRand{step: 12} + appendRow(t, o2, `{"k":1}`) + if _, err := o2.Send(context.Background(), srv2.URL); err != nil { + t.Fatalf("Send: %v", err) + } + if got := rec.headers[0].Get("X-Codeaf-Install"); got != "" { + t.Fatalf("an outbox with no nonce sent the header anyway: %q", got) + } +} From fe31df6de209e90d91a3c07bf1f400a977378ed6 Mon Sep 17 00:00:00 2001 From: santoshkumarradha Date: Thu, 17 Sep 2026 12:59:28 -0400 Subject: [PATCH 130/173] pool: the index signer's key ships in the binary, with a settings row to pin another --- cmd/codeaf/pool.go | 48 +++++++++++++--- cmd/codeaf/pool_test.go | 89 +++++++++++++++++++++++------- cmd/codeaf/poolindex.go | 2 +- internal/config/derivation_test.go | 4 ++ internal/config/settings.go | 59 +++++++++++++++----- internal/config/settings_test.go | 46 +++++++++++++++ 6 files changed, 202 insertions(+), 46 deletions(-) diff --git a/cmd/codeaf/pool.go b/cmd/codeaf/pool.go index c3b475482c..d24236b7fb 100644 --- a/cmd/codeaf/pool.go +++ b/cmd/codeaf/pool.go @@ -6,7 +6,7 @@ // sent. It spends nothing and reaches the network only under `verify`, which // fetches a fresh index and checks its signature. THE KEY STANDS IN FRONT OF // THE FETCH: a signature nobody can check is a fetch nobody should make, so -// a build carrying no public key refuses verify at the door rather than +// a build left with no key in hand refuses verify at the door rather than // downloading bytes it cannot vouch for. package main @@ -32,10 +32,37 @@ import ( ) // poolPublicKeys are the ed25519 public keys a fetched index's signature is -// checked under. EMPTY ON THIS BRANCH: the key is published beside the first -// index, and a build with no key in it would verify nothing — so verify asks -// for one with --key rather than fetching bytes nothing here can vouch for. -var poolPublicKeys []ed25519.PublicKey +// checked under when no key is stored beside the pool row. The list carries +// the key the index signer publishes (relay/wrangler.toml's POOL_PUBLIC_KEY), +// decoded once here from its base64 literal: a build whose literal could not +// decode would verify nothing, so the test beside the verb pins the length. +var poolPublicKeys = mustPoolKey("WOAo+g/oKxAV9vVqv2Q14w1TyyyiouwFO2fC0zgcps0=") + +func mustPoolKey(encoded string) []ed25519.PublicKey { + raw, err := base64.StdEncoding.DecodeString(encoded) + if err != nil || len(raw) != ed25519.PublicKeySize { + panic(fmt.Sprintf("pool: the built-in public key does not decode: %q", encoded)) + } + return []ed25519.PublicKey{ed25519.PublicKey(raw)} +} + +// poolTrustedKeys resolves the keys a fetched index is checked under: the +// stored key when one is set, AND ONLY IT — the row is the one word the +// install trusts, so a word that does not decode is a key nobody can vouch +// for, and the answer is no keys rather than a fall back to the built-in one +// a fetch under would verify nothing with. Nothing stored: the key the build +// carries. +func poolTrustedKeys(cfg poolcfg.Config) []ed25519.PublicKey { + word := strings.TrimSpace(cfg.PublicKey) + if word == "" { + return poolPublicKeys + } + raw, err := base64.StdEncoding.DecodeString(word) + if err != nil || len(raw) != ed25519.PublicKeySize { + return nil + } + return []ed25519.PublicKey{ed25519.PublicKey(raw)} +} // poolKeys is --key's value: a base64 ed25519 public key, repeatable, each // decoded the moment it is typed rather than carried as text and decoded at @@ -74,7 +101,7 @@ func runPool(args []string) error { // emptiness is the state root's own profile, never a directory called "pool" // beside wherever the command happened to run. func runPoolWith(args []string, output io.Writer, profileDir string, now func() time.Time, lookup func(string) (string, bool)) error { - cfg := poolcfg.Resolve(config.ModelPoolSettingAt(profileDir), "", lookup) + cfg := poolcfg.Resolve(config.ModelPoolSettingAt(profileDir), config.ModelPoolPublicKeySettingAt(profileDir), lookup) poolDir := config.ProfilePath(profileDir, "pool") if len(args) == 0 { args = []string{"show"} @@ -317,15 +344,18 @@ func verifyPool(args []string, output io.Writer, poolDir string, cfg poolcfg.Con // question asked of this build does not stand, and the remedy is on the // line above the exit. verify's third refusal — a fetch or a signature // that fails — is exit 1, in the error main() says in front of it. + // + // The door refusal below is nearly unreachable since the key shipped in + // the binary: a stored key that does not decode is the one word that + // still leaves the verb with no key in hand. if cfg.Mode == poolcfg.Off { if _, err := fmt.Fprintln(output, "the Model Pool is off in settings"); err != nil { return err } return exitIncomplete } - var trusted []ed25519.PublicKey - trusted = append(trusted, keys...) - trusted = append(trusted, poolPublicKeys...) + trusted := append([]ed25519.PublicKey{}, keys...) + trusted = append(trusted, poolTrustedKeys(cfg)...) if len(trusted) == 0 { if _, err := fmt.Fprintln(output, "no public key built into this build; pass --key"); err != nil { return err diff --git a/cmd/codeaf/pool_test.go b/cmd/codeaf/pool_test.go index 8b8e2f2ba2..6af005ba9b 100644 --- a/cmd/codeaf/pool_test.go +++ b/cmd/codeaf/pool_test.go @@ -1,6 +1,7 @@ package main import ( + "bytes" "context" "crypto/ed25519" "crypto/rand" @@ -263,10 +264,57 @@ func TestPoolWithNoProfileDirReadsTheStateRoots(t *testing.T) { } } -// The key gate stands in front of the fetch: with no key in hand verify -// refuses at the door — exit 2, the remedy on the line — and nothing is -// fetched from the address the config names, and nothing is cached. -func TestPoolVerifyWithoutAKeyRefusesAtTheDoorAndFetchesNothing(t *testing.T) { +// The build's one key is the index signer's: it decoded at init, it is an +// ed25519 public key's length, and it starts the start-up refresh under a +// mode that reads. +func TestPoolPublicKeysCarryTheIndexSignersKey(t *testing.T) { + if len(poolPublicKeys) != 1 { + t.Fatalf("the build carries %d public key(s), want the index signer's one", len(poolPublicKeys)) + } + if len(poolPublicKeys[0]) != ed25519.PublicKeySize { + t.Fatalf("the built-in key is %d bytes, want an ed25519 public key's %d", len(poolPublicKeys[0]), ed25519.PublicKeySize) + } + + started := 0 + prev := poolRefreshGo + poolRefreshGo = func(scope string, fn func()) { started++ } + t.Cleanup(func() { poolRefreshGo = prev }) + startPoolIndexRefresh(context.Background(), t.TempDir(), poolcfg.Resolve("", "", noEnv), poolPublicKeys) + if started != 1 { + t.Fatalf("the build's key started %d refresh(es) under mode on, want 1", started) + } +} + +// The stored key, when one is set, is the only key the puller is handed: a +// word that does not decode is a key nobody can vouch for, so the answer is +// no keys rather than the build's own — and nothing stored hands back the +// build's. +func TestPoolTrustedKeysFollowTheStoredKey(t *testing.T) { + good := base64.StdEncoding.EncodeToString(make(ed25519.PublicKey, ed25519.PublicKeySize)) + got := poolTrustedKeys(poolcfg.Resolve("", good, noEnv)) + if len(got) != 1 || len(got[0]) != ed25519.PublicKeySize { + t.Fatalf("a stored key gave %d key(s), want its one", len(got)) + } + + for name, bad := range map[string]string{ + "not base64": "not a key at all", + "too short": base64.StdEncoding.EncodeToString([]byte("short")), + } { + if keys := poolTrustedKeys(poolcfg.Resolve("", bad, noEnv)); len(keys) != 0 { + t.Fatalf("%s: a key that does not decode left %d key(s) in hand", name, len(keys)) + } + } + + built := poolTrustedKeys(poolcfg.Resolve("", "", noEnv)) + if len(built) != 1 || !bytes.Equal(built[0], poolPublicKeys[0]) { + t.Fatal("with nothing stored, the build's own key is the key") + } +} + +// The key ships in the binary, so verify fetches under it: a dead address is +// a fetch that failed — exit 1, in the puller's words — and not a refusal at +// the door, and nothing was cached in its place. +func TestPoolVerifyFetchesUnderTheBuiltInKeyAndNamesWhatFailed(t *testing.T) { dir := t.TempDir() // A dead port is the fetch that would have happened: if the door let one // through, the failure below would be a fetch error, not the sentence. @@ -274,16 +322,16 @@ func TestPoolVerifyWithoutAKeyRefusesAtTheDoorAndFetchesNothing(t *testing.T) { var out strings.Builder err := runPoolWith([]string{"verify"}, &out, dir, poolClock(t), lookup) if err == nil { - t.Fatal("a keyless verify fetched") + t.Fatal("a dead relay verified") } - if !errors.Is(err, exitIncomplete) { - t.Fatalf("the door refusal is not exit 2: %v", err) + if errors.Is(err, exitIncomplete) { + t.Fatalf("verify refused at the door though the build carries a key: %v", err) } - if !strings.Contains(out.String(), "no public key built into this build; pass --key") { - t.Fatalf("the refusal did not name the remedy:\n%s", out.String()) + if !strings.Contains(err.Error(), "pull") { + t.Fatalf("the failure did not name the fetch: %v", err) } if _, statErr := os.Stat(filepath.Join(dir, "pool", "doc.json")); !os.IsNotExist(statErr) { - t.Fatal("the refused verify wrote a cache") + t.Fatal("the failed verify wrote a cache") } } @@ -492,26 +540,25 @@ func stubPoolRefresh(t *testing.T) *int { return &started } -// The start-up push is an errand behind the same guard as the refresh: a -// mode that sends starts it once, a mode that only reads starts nothing. -// (The refresh's own errand waits for the build's key — its test is -// TestPoolRefreshStartsNoGoroutineWithoutAKey.) -func TestWirePoolIndexStartsThePushWhenTheModeSends(t *testing.T) { +// The start-up errands are one push and one refresh behind the same guard: +// a mode that sends and reads starts both, a mode that only reads starts +// only the refresh (its push would send nowhere), and off starts neither. +func TestWirePoolIndexStartsTheRefreshAndThePush(t *testing.T) { started := stubPoolRefresh(t) t.Setenv("CODEAF_MODEL_POOL_SUBMIT_URL", "http://127.0.0.1:1/v1/rows") - // The default mode is on: the push starts. + // The default mode is on: the refresh and the push both start. wirePoolIndex(t.TempDir()) - if *started != 1 { - t.Fatalf("a sending pool started %d errand(s) at start-up, want the one push", *started) + if *started != 2 { + t.Fatalf("a sending pool started %d errand(s) at start-up, want the refresh and the push", *started) } - // A mode that does not send starts none. + // A mode that does not send starts the refresh alone. *started = 0 t.Setenv("CODEAF_MODEL_POOL", "read") wirePoolIndex(t.TempDir()) - if *started != 0 { - t.Fatalf("a read-only pool started %d push(es)", *started) + if *started != 1 { + t.Fatalf("a read-only pool started %d errand(s), want the refresh alone", *started) } } diff --git a/cmd/codeaf/poolindex.go b/cmd/codeaf/poolindex.go index 7390d0d590..1c5bf19d49 100644 --- a/cmd/codeaf/poolindex.go +++ b/cmd/codeaf/poolindex.go @@ -115,7 +115,7 @@ func wirePoolIndex(profileDir string) { cfg := config.ModelPoolAt(profileDir) config.AutoIndex = poolIndexFor(profileDir, cfg, time.Now) config.AutoOwnCells = poolOwnCellsFor(profileDir, cfg) - startPoolIndexRefresh(context.Background(), profileDir, cfg, poolPublicKeys) + startPoolIndexRefresh(context.Background(), profileDir, cfg, poolTrustedKeys(cfg)) // The rows a previous run judged and could not hand over leave at once, // on their own goroutine behind the same guard the refresh uses: a // start-up errand, bounded by its own budget, and never on the run's diff --git a/internal/config/derivation_test.go b/internal/config/derivation_test.go index ca03a1166f..0ba5c24fc2 100644 --- a/internal/config/derivation_test.go +++ b/internal/config/derivation_test.go @@ -127,6 +127,10 @@ var settingReaders = map[string]string{ KeyVisionModel: "VisionModel", KeyAttribution: "Attribution", KeyModelPool: "ModelPoolSettingAt", + // The pool key row names the resolver that reads it: [ModelPoolAt] carries + // the word into poolcfg beside the mode, and the puller's keys resolve + // from there (cmd/codeaf's poolTrustedKeys). + KeyModelPoolPublicKey: "ModelPoolAt", // The crew's pick row names its resolver: the surfaces read the pick to // say it beside the crew word, and the ladder turns it into computed seats // ([pickedSeat]) — the identifier proves the word reaches a surface. diff --git a/internal/config/settings.go b/internal/config/settings.go index 74fe648500..2d509f4a81 100644 --- a/internal/config/settings.go +++ b/internal/config/settings.go @@ -116,9 +116,14 @@ const ( // answers the CODEAF_MODEL_POOL pin and a CI environment may give it // (internal/pool/poolcfg). The resolver beside the search rows is the one // place the process environment is read for the pool. - KeyModelPool = "model_pool" - KeyAttribution = "attribution" - KeySplitPct = "split_pct" + KeyModelPool = "model_pool" + // KeyModelPoolPublicKey is the trusted key a fetched pool index is checked + // under: base64 text beside the pool row it narrows, and empty for the + // key the binary carries. The environment pin CODEAF_MODEL_POOL_PUBLIC_KEY + // outranks it, through the same resolver. + KeyModelPoolPublicKey = "models.pool.public_key" + KeyAttribution = "attribution" + KeySplitPct = "split_pct" // The two rows the v3 chat surface keeps on disk BESIDE the conversation: // what was typed, and what was half-typed. They are one pair of questions — @@ -1288,12 +1293,13 @@ var OperatorEnvPins = []string{ // shape, under `make demo-home`'s terms. A row offering to persist a // fixture would put a demo question in front of a person every morning. "CODEAF_QUESTION_DEMO", - // The three pool names that stay plumbing (internal/pool/poolcfg, which + // The four pool names that stay plumbing (internal/pool/poolcfg, which // names them and reads none of them: the environment reaches that package - // as a function the caller hands in). The two URLs are where the index is - // read and where measurements are handed to, and the TTL is how long a - // read copy stays young — endpoints and a cadence, on the terms the other - // addresses here are on. + // as a function the caller hands in). The relay is the base the two URLs + // derive from — index.json and /v1/rows under it — and the TTL is how + // long a read copy stays young: endpoints and a cadence, on the terms the + // other addresses here are on. The public key is a row, not plumbing: it + // narrows what the install trusts, which is a preference. // // THE WORD ITSELF IS NOT HERE ANY MORE. CODEAF_MODEL_POOL fronts the // model_pool row now, which is what the comment below used to promise: @@ -1301,6 +1307,7 @@ var OperatorEnvPins = []string{ // nothing in the binary called it, and a pin with no row was the honest // answer then. With the row here, the word renders through the row — dim, // "pinned by CODEAF_MODEL_POOL" — the way CODEAF_DOC_ENGINE does. + "CODEAF_MODEL_POOL_RELAY_URL", "CODEAF_MODEL_POOL_URL", "CODEAF_MODEL_POOL_SUBMIT_URL", "CODEAF_MODEL_POOL_TTL", @@ -1901,6 +1908,19 @@ func (s *Settings) build() []Setting { read: func() string { return ModelPoolAt(dir).Mode.String() }, write: func(raw string) error { return writeChoice(dir, KeyModelPool, raw, ModelPoolChoices) }, }, + // The key a fetched pool index is checked under. The binary carries the + // index signer's key, and this row exists to pin a different one — a + // mirror serving its own signed index, or an install proving a key out + // for itself. It sits beside the pool row because it narrows that row's + // reading, the way the keys under searching narrow theirs. + Setting{ + Key: KeyModelPoolPublicKey, Category: CategoryModels, Kind: SettingText, + Label: "pool key", Env: "CODEAF_MODEL_POOL_PUBLIC_KEY", EmptyLabel: "built in", + Hint: "the base64 Ed25519 public key an index must be signed with. " + + "Empty means the key built into this binary.", + read: func() string { return ModelPoolAt(dir).PublicKey }, + write: func(raw string) error { return writeText(dir, KeyModelPoolPublicKey, raw) }, + }, // THE KEY EVERY MODEL CALL RIDES. The default local door normally creates // one through the browser (tui3's firstrun.go); this row remains the place // to paste a replacement or to use a custom endpoint's credential. It masks @@ -3340,14 +3360,23 @@ func ModelPoolSettingAt(profileDir string) string { return value } -// ModelPoolAt resolves how the Model Pool behaves: the stored word and the -// pool's environment names, through poolcfg.Resolve. IT IS THE ONE PLACE THE -// PROCESS ENVIRONMENT IS READ FOR THE POOL — poolcfg itself takes the -// environment as a function, and a caller with its own (the `codeaf pool` -// verb, whose tests inject one) resolves [ModelPoolSettingAt] against its own -// lookup rather than calling this. +// ModelPoolPublicKeySettingAt is the stored word the pool key row holds: +// base64 text, or empty for the key the binary carries. It is the second +// setting [ModelPoolAt] resolves. +func ModelPoolPublicKeySettingAt(profileDir string) string { + value, _ := persistedString(profileDir, KeyModelPoolPublicKey) + return value +} + +// ModelPoolAt resolves how the Model Pool behaves: the stored mode word, the +// stored public key and the pool's environment names, through poolcfg.Resolve. +// IT IS THE ONE PLACE THE PROCESS ENVIRONMENT IS READ FOR THE POOL — poolcfg +// itself takes the environment as a function, and a caller with its own (the +// `codeaf pool` verb, whose tests inject one) resolves [ModelPoolSettingAt] +// and [ModelPoolPublicKeySettingAt] against its own lookup rather than +// calling this. func ModelPoolAt(profileDir string) poolcfg.Config { - return poolcfg.Resolve(ModelPoolSettingAt(profileDir), "", os.LookupEnv) + return poolcfg.Resolve(ModelPoolSettingAt(profileDir), ModelPoolPublicKeySettingAt(profileDir), os.LookupEnv) } // ExaKeyAt resolves the Exa credential: the environment first, then the sheet, diff --git a/internal/config/settings_test.go b/internal/config/settings_test.go index 0495b5871d..24b03f77de 100644 --- a/internal/config/settings_test.go +++ b/internal/config/settings_test.go @@ -53,6 +53,52 @@ func TestSlackApplicationRowShowsOnlyThePersonsAnswer(t *testing.T) { } } +// The pool key row: the trusted word a fetched index is checked under. Empty +// is the ordinary answer and reads as the key the binary carries; a stored +// word is the one the install trusts, and the pin outranks it. +func TestThePoolKeyRowReadsBuiltInAndFollowsItsStoredWordAndItsPin(t *testing.T) { + dir := t.TempDir() + t.Setenv("CODEAF_MODEL_POOL_PUBLIC_KEY", "") + t.Setenv("CODEAF_MODEL_POOL", "") + rows := registry(t, dir) + row, ok := rows.Row(KeyModelPoolPublicKey) + if !ok { + t.Fatal("the pool key is not registered") + } + if row.Category != CategoryModels || row.Kind != SettingText || row.Label != "pool key" { + t.Fatalf("pool key row = %+v", row) + } + if row.Value() != "built in" { + t.Fatalf("an unset key row reads %q, want built in", row.Value()) + } + if got := ModelPoolAt(dir); got.PublicKey != "" || got.Source.PublicKey != "default" { + t.Fatalf("an untouched profile resolved %+v", got) + } + + if err := row.Apply("A-base64-word="); err != nil { + t.Fatal(err) + } + reread, _ := registry(t, dir).Row(KeyModelPoolPublicKey) + if reread.Value() != "A-base64-word=" { + t.Fatalf("the reread row lost the persisted word: %q", reread.Value()) + } + if got := ModelPoolAt(dir); got.PublicKey != "A-base64-word=" || got.Source.PublicKey != "setting" { + t.Fatalf("the stored key did not take: %+v", got) + } + + t.Setenv("CODEAF_MODEL_POOL_PUBLIC_KEY", "a-pinned-key") + pinned, _ := registry(t, dir).Row(KeyModelPoolPublicKey) + if name, isPinned := pinned.PinnedBy(); !isPinned || name != "CODEAF_MODEL_POOL_PUBLIC_KEY" { + t.Fatalf("the key row did not report its pin: %q %v", name, isPinned) + } + if got := ModelPoolAt(dir); got.PublicKey != "a-pinned-key" || got.Source.PublicKey != "env" { + t.Fatalf("the pin lost: %+v", got) + } + if err := pinned.Apply("another"); err == nil || !strings.Contains(err.Error(), "CODEAF_MODEL_POOL_PUBLIC_KEY") { + t.Fatalf("a pinned key row accepted an edit: %v", err) + } +} + // The registry is the completeness gate: a user-tunable environment pin added // anywhere in the tree has to arrive as a row here, or land on the explicit // operator-plumbing allowlist. Until then this test fails the build. From 235b69021076790631d1c551e0d80a359107de00 Mon Sep 17 00:00:00 2001 From: santoshkumarradha Date: Thu, 17 Sep 2026 12:59:59 -0400 Subject: [PATCH 131/173] docs: the terminal manual says the pool sends after each run and reads the index daily --- .../manual/chat/running-from-the-terminal.md | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/internal/manual/chat/running-from-the-terminal.md b/internal/manual/chat/running-from-the-terminal.md index 72b29f45d7..e84261310a 100644 --- a/internal/manual/chat/running-from-the-terminal.md +++ b/internal/manual/chat/running-from-the-terminal.md @@ -591,6 +591,12 @@ writes nothing. The call itself is billed to the `judge` seat, so it shows up in the spend pages beside the crew seats rather than inside a task's own cost. +With `model_pool` on, the rows leave for the relay after each judged run and +once more at start-up, under this install's own nonce and nothing else. The +index is fetched once a day, checked against the key built into the binary — +or the key in `models.pool.public_key` when one is set — and a changed +document is read at the next start. + `verify` fetches a fresh index and checks its detached ed25519 signature, then prints the version whose signature checked out: @@ -599,11 +605,11 @@ signature good: version 7, generated 2026-09-10, 3 metrics ``` It wants a public key: `--key `, repeatable, or -one built into the build. This build carries none yet — the key is published -beside the first index — so it answers -`no public key built into this build; pass --key` and fetches nothing. A -fetch or a signature that fails is exit 1; a `verify` on a machine whose -setting is `off` is refused with exit 2 and fetches nothing. +one built into the build. The build carries the index signer's key, so +`verify` works as it stands; `--key` checks a document signed under some +other key instead — a mirror's, for instance. A fetch or a signature that +fails is exit 1; a `verify` on a machine whose setting is `off` is refused +with exit 2 and fetches nothing. ## What is still running in the background — codeaf services, and stopping one From ff9afb5325f125d65af480a1e9edf154ab82436d Mon Sep 17 00:00:00 2001 From: santoshkumarradha Date: Thu, 17 Sep 2026 13:00:07 -0400 Subject: [PATCH 132/173] docs: the pool client change note --- docs/changes/unreleased/1093-pool-client.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 docs/changes/unreleased/1093-pool-client.md diff --git a/docs/changes/unreleased/1093-pool-client.md b/docs/changes/unreleased/1093-pool-client.md new file mode 100644 index 0000000000..8a3ce61034 --- /dev/null +++ b/docs/changes/unreleased/1093-pool-client.md @@ -0,0 +1,17 @@ +--- +kind: added +title: The pool client gains the relay's real addresses, an install nonce, and the index signer's key +pr: 1093 +surface: [engine] +invalidates: + - "The pool's default addresses were `https://pool.invalid/index.json` and `https://pool.invalid/submit`. They derive from one base now — `DefaultRelayURL` is `https://codeaf.agentfield.ai/pool`, the index at `…/index.json`, the submit at `…/v1/rows` — and `CODEAF_MODEL_POOL_RELAY_URL` moves the base. The two per-address pins (`CODEAF_MODEL_POOL_URL`, `CODEAF_MODEL_POOL_SUBMIT_URL`) still override, and `pool show` prints a `relay` line beside the two derived addresses." + - "`pool verify` refused with `no public key built into this build; pass --key` because the build carried no key. The index signer's key is decoded into `poolPublicKeys` at init now, so `verify` fetches without a flag; `--key` checks a document signed under some other key, and the new `models.pool.public_key` row (env `CODEAF_MODEL_POOL_PUBLIC_KEY`) replaces the built-in key when set — and only it: a stored word that does not decode trusts nothing." + - "A stored pool index was read only at the fetch that brought it. `refreshPoolIndex` now runs daily under the config's TTL from a build that carries the key, and a changed document is still read at the next start." + - "An outbox batch carried no identity. Every batch over http now rides `X-Codeaf-Install` with the install's nonce — 32 lowercase hex in `/install`, replaced when it stops being that shape — and when `model_pool` is on the outbox is pushed after each judged run and once at start-up, budget 5 s each, errors at debug level only." +--- + +The relay wants a nonce on every batch and signs its index under one key; this +wave gives the client both, so an install with `model_pool` on reads the +signed index and sends its rows the day the relay answers. Until it answers, +every path falls back the way it did: the cache, the seed, and the install's +own sheet, with nothing said at a person. \ No newline at end of file From 9cdacbff2314eb3147806fa0bd7cd54c8a86fca6 Mon Sep 17 00:00:00 2001 From: santoshkumarradha Date: Thu, 17 Sep 2026 13:30:11 -0400 Subject: [PATCH 133/173] poolcfg: the mirror address is held beside the relay, printed by show Co-Authored-By: codeaf --- cmd/codeaf/pool.go | 3 ++ cmd/codeaf/pool_test.go | 1 + internal/pool/poolcfg/poolcfg.go | 32 ++++++++++++++--- internal/pool/poolcfg/poolcfg_test.go | 49 +++++++++++++++++++++++++-- 4 files changed, 77 insertions(+), 8 deletions(-) diff --git a/cmd/codeaf/pool.go b/cmd/codeaf/pool.go index d24236b7fb..95c72471fa 100644 --- a/cmd/codeaf/pool.go +++ b/cmd/codeaf/pool.go @@ -167,6 +167,7 @@ func printPool(output io.Writer, poolDir string, cfg poolcfg.Config, now time.Ti fmt.Sprintf("mode %s · %s", cfg.Mode, cfg.Source.Mode), fmt.Sprintf("relay %s · %s", cfg.RelayURL, cfg.Source.RelayURL), fmt.Sprintf("index %s · %s", cfg.IndexURL, cfg.Source.IndexURL), + fmt.Sprintf("mirror %s · %s", orNowhere(cfg.MirrorURL), cfg.Source.MirrorURL), fmt.Sprintf("submit %s · %s", orNowhere(cfg.SubmitURL), cfg.Source.SubmitURL), fmt.Sprintf("ttl %s · %s", reltime.Elapsed(cfg.TTL), cfg.Source.TTL), } { @@ -250,6 +251,7 @@ type poolAnswer struct { ModeSource string `json:"mode_source"` RelayURL string `json:"relay_url"` IndexURL string `json:"index_url"` + MirrorURL string `json:"mirror_url"` SubmitURL string `json:"submit_url"` TTLSeconds int `json:"ttl_seconds"` Pending *int `json:"pending,omitempty"` @@ -280,6 +282,7 @@ func printPoolJSON(output io.Writer, poolDir string, cfg poolcfg.Config, cached ModeSource: cfg.Source.Mode, RelayURL: cfg.RelayURL, IndexURL: cfg.IndexURL, + MirrorURL: cfg.MirrorURL, SubmitURL: cfg.SubmitURL, TTLSeconds: int(cfg.TTL / time.Second), } diff --git a/cmd/codeaf/pool_test.go b/cmd/codeaf/pool_test.go index 6af005ba9b..64c21e3b50 100644 --- a/cmd/codeaf/pool_test.go +++ b/cmd/codeaf/pool_test.go @@ -118,6 +118,7 @@ func TestPoolShowPrintsTheConfigAndSaysWhenNoIndexIsCached(t *testing.T) { "mode on · default", "relay https://codeaf.agentfield.ai/pool · default", "index https://codeaf.agentfield.ai/pool/index.json · default", + "mirror https://raw.githubusercontent.com/Agent-Field/CodeAF/model-pool/pool/index.json · default", "submit https://codeaf.agentfield.ai/pool/v1/rows · default", "ttl 1d · default", "no index cached yet · built-in seed of 2026-09-17", diff --git a/internal/pool/poolcfg/poolcfg.go b/internal/pool/poolcfg/poolcfg.go index 8886a85ec0..4680f72c51 100644 --- a/internal/pool/poolcfg/poolcfg.go +++ b/internal/pool/poolcfg/poolcfg.go @@ -14,11 +14,14 @@ import ( // Addresses held when the environment offers nothing readable. The relay is // the one base the resolver holds: the index and the submit addresses are -// read from beside it, and each keeps its own override. +// read from beside it, and each keeps its own override. The mirror is a +// second index address — the relay's document copied to GitHub — held on its +// own, because it derives from nothing here. const ( DefaultRelayURL = "https://codeaf.agentfield.ai/pool" DefaultIndexURL = DefaultRelayURL + indexPath DefaultSubmitURL = DefaultRelayURL + submitPath + DefaultMirrorURL = "https://raw.githubusercontent.com/Agent-Field/CodeAF/model-pool/pool/index.json" ) // The two addresses a relay serves, under its base. @@ -57,6 +60,7 @@ type Sources struct { RelayURL string IndexURL string SubmitURL string + MirrorURL string PublicKey string TTL string } @@ -68,25 +72,28 @@ type Sources struct { // // The relay is the base the index and submit addresses derive from: each is // RelayURL with its own path appended, unless its own environment name -// overrides it. An empty PublicKey is the ordinary answer — it means the key -// the binary carries. +// overrides it. The mirror is a second index address held on its own, since +// it derives from nothing here; an empty one turns the fallback off. An empty +// PublicKey is the ordinary answer — it means the key the binary carries. type Config struct { Mode Mode RelayURL string IndexURL string SubmitURL string + MirrorURL string PublicKey string TTL time.Duration Source Sources } -// The seven names Resolve reads, and no others. +// The eight names Resolve reads, and no others. const ( envMode = "CODEAF_MODEL_POOL" envCI = "CI" envRelay = "CODEAF_MODEL_POOL_RELAY_URL" envIndex = "CODEAF_MODEL_POOL_URL" envSubmit = "CODEAF_MODEL_POOL_SUBMIT_URL" + envMirror = "CODEAF_MODEL_POOL_MIRROR_URL" envTTL = "CODEAF_MODEL_POOL_TTL" envKey = "CODEAF_MODEL_POOL_PUBLIC_KEY" ) @@ -117,7 +124,7 @@ const ( // Config, it keeps nothing between calls, and many goroutines may call it at // once. func Resolve(setting, publicKey string, lookup func(name string) (value string, set bool)) Config { - // One injection for the whole environment: the seven names are read once, + // One injection for the whole environment: the eight names are read once, // here, in this order, and each value is trimmed as it is read. get := func(name string) (string, bool) { if lookup == nil { @@ -134,6 +141,7 @@ func Resolve(setting, publicKey string, lookup func(name string) (value string, relayWord, _ := get(envRelay) indexWord, _ := get(envIndex) submitWord, submitSet := get(envSubmit) + mirrorWord, mirrorSet := get(envMirror) ttlWord, _ := get(envTTL) keyWord, _ := get(envKey) @@ -176,6 +184,18 @@ func Resolve(setting, publicKey string, lookup func(name string) (value string, } } + // The mirror derives from nothing: it is the address held, or the one the + // environment names, or — a value set and empty — nothing at all, which + // turns the fallback off. + mirrorURL, mirrorSrc := DefaultMirrorURL, srcDefault + if mirrorSet { + if mirrorWord == "" { + mirrorURL, mirrorSrc = "", srcEnv + } else if u, ok := acceptURL(mirrorWord); ok { + mirrorURL, mirrorSrc = u, srcEnv + } + } + // A readable TTL is held between the floor and the ceiling; a name nobody // set, or one no duration can be read from, is the default. ttl, ttlSrc := defaultTTL, srcDefault @@ -199,6 +219,7 @@ func Resolve(setting, publicKey string, lookup func(name string) (value string, RelayURL: relayURL, IndexURL: indexURL, SubmitURL: submitURL, + MirrorURL: mirrorURL, PublicKey: publicKey, TTL: ttl, Source: Sources{ @@ -206,6 +227,7 @@ func Resolve(setting, publicKey string, lookup func(name string) (value string, RelayURL: relaySrc, IndexURL: indexSrc, SubmitURL: submitSrc, + MirrorURL: mirrorSrc, PublicKey: keySrc, TTL: ttlSrc, }, diff --git a/internal/pool/poolcfg/poolcfg_test.go b/internal/pool/poolcfg/poolcfg_test.go index 5c66fe684c..87ffcc5eb6 100644 --- a/internal/pool/poolcfg/poolcfg_test.go +++ b/internal/pool/poolcfg/poolcfg_test.go @@ -40,6 +40,7 @@ func TestResolveDefaults(t *testing.T) { RelayURL: DefaultRelayURL, IndexURL: DefaultIndexURL, SubmitURL: DefaultSubmitURL, + MirrorURL: DefaultMirrorURL, PublicKey: "", TTL: 24 * time.Hour, Source: Sources{ @@ -47,6 +48,7 @@ func TestResolveDefaults(t *testing.T) { RelayURL: "default", IndexURL: "default", SubmitURL: "default", + MirrorURL: "default", PublicKey: "default", TTL: "default", }, @@ -240,6 +242,45 @@ func TestResolveRelayURL(t *testing.T) { } } +func TestResolveMirrorURL(t *testing.T) { + cases := []struct { + name string + value string + set bool + want string + wantSrc string + }{ + {"nobody set", "", false, DefaultMirrorURL, "default"}, + {"https", "https://mirror.example.com/index.json", true, "https://mirror.example.com/index.json", "env"}, + {"http loopback with port", "http://127.0.0.1:9000/index.json", true, "http://127.0.0.1:9000/index.json", "env"}, + {"file", "file:///var/tmp/index.json", true, "file:///var/tmp/index.json", "env"}, + {"absolute path", "/var/tmp/index.json", true, "/var/tmp/index.json", "env"}, + {"padded", " https://mirror.example.com ", true, "https://mirror.example.com", "env"}, + // An empty mirror is an answer: it turns the fallback off. + {"empty disables the mirror", "", true, "", "env"}, + {"blank disables the mirror", " ", true, "", "env"}, + {"plain http elsewhere", "http://mirror.example.com/index.json", true, DefaultMirrorURL, "default"}, + {"other scheme", "ftp://mirror.example.com", true, DefaultMirrorURL, "default"}, + {"bare host", "mirror.example.com/index.json", true, DefaultMirrorURL, "default"}, + } + for _, c := range cases { + lookup := envOf(nil) + if c.set { + lookup = envOf(map[string]string{"CODEAF_MODEL_POOL_MIRROR_URL": c.value}) + } + got := Resolve("", "", lookup) + if got.MirrorURL != c.want || got.Source.MirrorURL != c.wantSrc { + t.Errorf("%s: MirrorURL = %q from %q, want %q from %q", c.name, got.MirrorURL, got.Source.MirrorURL, c.want, c.wantSrc) + } + } + + // The mirror is its own address: a pinned relay does not move it. + got := Resolve("", "", envOf(map[string]string{"CODEAF_MODEL_POOL_RELAY_URL": "https://relay.example.com"})) + if got.MirrorURL != DefaultMirrorURL || got.Source.MirrorURL != "default" { + t.Errorf("a pinned relay moved the mirror: %q from %q", got.MirrorURL, got.Source.MirrorURL) + } +} + // The two addresses derive from the relay, and each override wins over its // derivation. A relay read from the environment hands its source down to both // derived addresses; an override of its own names itself instead. @@ -361,6 +402,7 @@ func TestResolveFieldsAreIndependent(t *testing.T) { RelayURL: DefaultRelayURL, IndexURL: DefaultIndexURL, SubmitURL: "https://submit.example.com", + MirrorURL: DefaultMirrorURL, PublicKey: "", TTL: 2 * time.Hour, Source: Sources{ @@ -368,6 +410,7 @@ func TestResolveFieldsAreIndependent(t *testing.T) { RelayURL: "default", IndexURL: "default", SubmitURL: "env", + MirrorURL: "default", PublicKey: "default", TTL: "env", }, @@ -441,19 +484,19 @@ func TestResolveNilLookupUsesSetting(t *testing.T) { if got.Mode != Read || got.Source.Mode != "setting" { t.Errorf("Resolve(\"read\", \"\", nil): Mode = %v from %q, want read from %q", got.Mode, got.Source.Mode, "setting") } - if got.IndexURL != DefaultIndexURL || got.SubmitURL != DefaultSubmitURL || got.RelayURL != DefaultRelayURL || got.TTL != 24*time.Hour { + if got.IndexURL != DefaultIndexURL || got.SubmitURL != DefaultSubmitURL || got.RelayURL != DefaultRelayURL || got.MirrorURL != DefaultMirrorURL || got.TTL != 24*time.Hour { t.Errorf("Resolve(\"read\", \"\", nil) = %+v, want the defaults beside the setting", got) } } -func TestResolveAsksSevenNames(t *testing.T) { +func TestResolveAsksEightNames(t *testing.T) { var asked []string Resolve("", "", func(name string) (string, bool) { asked = append(asked, name) return "", false }) sort.Strings(asked) - want := []string{envMode, envCI, envRelay, envIndex, envSubmit, envTTL, envKey} + want := []string{envMode, envCI, envRelay, envIndex, envSubmit, envMirror, envTTL, envKey} sort.Strings(want) if !reflect.DeepEqual(asked, want) { t.Errorf("Resolve asked lookup for %v, want %v", asked, want) From 7437a6d317b220d2e1c6d6ebf851c923248a6e8f Mon Sep 17 00:00:00 2001 From: santoshkumarradha Date: Thu, 17 Sep 2026 13:30:34 -0400 Subject: [PATCH 134/173] pool: a relay that does not answer falls through to the mirror, a bad signature does not Co-Authored-By: codeaf --- cmd/codeaf/pool_test.go | 129 +++++++++++++++++++++++++++++++++++++ cmd/codeaf/poolindex.go | 39 +++++++++-- internal/pool/pull/pull.go | 9 ++- 3 files changed, 170 insertions(+), 7 deletions(-) diff --git a/cmd/codeaf/pool_test.go b/cmd/codeaf/pool_test.go index 64c21e3b50..004e772285 100644 --- a/cmd/codeaf/pool_test.go +++ b/cmd/codeaf/pool_test.go @@ -8,6 +8,9 @@ import ( "encoding/base64" "encoding/json" "errors" + "fmt" + "net/http" + "net/http/httptest" "os" "path/filepath" "strings" @@ -731,3 +734,129 @@ func TestWirePoolIndexSeatsNothingForAnUnparsableOwnSheet(t *testing.T) { t.Fatal("a broken own sheet was seated") } } + +// ── THE MIRROR ────────────────────────────────────────────────────────────── + +// poolEnv is an environment holding exactly the names given, so a test pins +// the pool's addresses without touching the process. +func poolEnv(pairs map[string]string) func(string) (string, bool) { + return func(name string) (string, bool) { + value, set := pairs[name] + return value, set + } +} + +// deadEnv pins both index addresses at a closed local port, so a probe fails +// at once and reaches no network. +func deadEnv() func(string) (string, bool) { + return poolEnv(map[string]string{ + "CODEAF_MODEL_POOL_URL": "http://127.0.0.1:1/index.json", + "CODEAF_MODEL_POOL_MIRROR_URL": "http://127.0.0.1:1/index.json", + }) +} + +// signedPoolDoc is a document the puller accepts: one integer version and +// nothing else it reads. The version is where a test finds which address was +// the one that answered. +func signedPoolDoc(version int) []byte { + return []byte(fmt.Sprintf(`{"version": %d, "schema": 1, "generated": "2026-09-10", "min_installs": 1, "judges": [], "metrics": {}, "cells": []}`, version)) +} + +// poolServer serves a signed index over http the way a relay does: the +// document at /index.json and its base64 signature beside it at +// /index.json.sig, both signed under priv. +func poolServer(t *testing.T, priv ed25519.PrivateKey, doc []byte) *httptest.Server { + t.Helper() + sig := base64.StdEncoding.EncodeToString(ed25519.Sign(priv, doc)) + mux := http.NewServeMux() + mux.HandleFunc("/index.json", func(w http.ResponseWriter, _ *http.Request) { + _, _ = w.Write(doc) + }) + mux.HandleFunc("/index.json.sig", func(w http.ResponseWriter, _ *http.Request) { + _, _ = w.Write([]byte(sig)) + }) + server := httptest.NewServer(mux) + t.Cleanup(server.Close) + return server +} + +// cachedVersion reads the "version" of the document under the profile's pool +// directory, and whether one is there at all. +func cachedVersion(t *testing.T, dir string) (int64, bool) { + t.Helper() + doc, err := os.ReadFile(filepath.Join(dir, "pool", "doc.json")) + if err != nil { + return 0, false + } + var obj struct { + Version int64 `json:"version"` + } + if err := json.Unmarshal(doc, &obj); err != nil { + t.Fatalf("the cached document does not parse: %v", err) + } + return obj.Version, true +} + +// A relay that does not answer is not the end of the fetch: the mirror is +// asked with the same keys and the same cache directory, and the document it +// serves lands where the next start reads it. +func TestPoolRefreshFallsToTheMirrorWhenTheRelayIsDown(t *testing.T) { + pub, priv, err := ed25519.GenerateKey(rand.Reader) + if err != nil { + t.Fatal(err) + } + mirror := poolServer(t, priv, signedPoolDoc(9)) + dir := t.TempDir() + cfg := poolcfg.Config{Mode: poolcfg.On, IndexURL: "http://127.0.0.1:1/index.json", MirrorURL: mirror.URL + "/index.json"} + refreshPoolIndex(context.Background(), dir, cfg, []ed25519.PublicKey{pub}) + version, ok := cachedVersion(t, dir) + if !ok || version != 9 { + t.Fatalf("the mirror's document was not kept: version %d, cached %v", version, ok) + } +} + +// Both addresses down leaves the cache exactly where it was: the fetch falls +// back to the copy already on disk and writes nothing. +func TestPoolRefreshKeepsTheCacheWhenBothAddressesAreDown(t *testing.T) { + pub, priv, err := ed25519.GenerateKey(rand.Reader) + if err != nil { + t.Fatal(err) + } + relay := poolServer(t, priv, signedPoolDoc(7)) + dir := t.TempDir() + live := poolcfg.Config{Mode: poolcfg.On, IndexURL: relay.URL + "/index.json", MirrorURL: relay.URL + "/index.json"} + refreshPoolIndex(context.Background(), dir, live, []ed25519.PublicKey{pub}) + if version, ok := cachedVersion(t, dir); !ok || version != 7 { + t.Fatalf("the relay's document was not cached: version %d, cached %v", version, ok) + } + relay.Close() + dead := poolcfg.Config{Mode: poolcfg.On, IndexURL: "http://127.0.0.1:1/index.json", MirrorURL: "http://127.0.0.1:1/index.json"} + refreshPoolIndex(context.Background(), dir, dead, []ed25519.PublicKey{pub}) + if version, ok := cachedVersion(t, dir); !ok || version != 7 { + t.Fatalf("a failed refresh moved the cache: version %d, cached %v", version, ok) + } +} + +// A signature failure is a statement about the primary's bytes, not a dead +// source: the mirror is not asked, so its good document is never cached in +// place of the one whose signature just failed. +func TestPoolRefreshDoesNotFallToTheMirrorOnABadSignature(t *testing.T) { + pub, priv, err := ed25519.GenerateKey(rand.Reader) + if err != nil { + t.Fatal(err) + } + _, otherPriv, err := ed25519.GenerateKey(rand.Reader) + if err != nil { + t.Fatal(err) + } + // The primary serves a document signed under a key nobody trusts; the + // mirror serves a good one that must not be reached. + relay := poolServer(t, otherPriv, signedPoolDoc(7)) + mirror := poolServer(t, priv, signedPoolDoc(9)) + dir := t.TempDir() + cfg := poolcfg.Config{Mode: poolcfg.On, IndexURL: relay.URL + "/index.json", MirrorURL: mirror.URL + "/index.json"} + refreshPoolIndex(context.Background(), dir, cfg, []ed25519.PublicKey{pub}) + if _, ok := cachedVersion(t, dir); ok { + t.Fatal("a document with a bad signature fell through to the mirror, or was cached") + } +} diff --git a/cmd/codeaf/poolindex.go b/cmd/codeaf/poolindex.go index 1c5bf19d49..15f433cd51 100644 --- a/cmd/codeaf/poolindex.go +++ b/cmd/codeaf/poolindex.go @@ -18,6 +18,7 @@ package main import ( "context" "crypto/ed25519" + "errors" "log" "os" "path/filepath" @@ -91,17 +92,43 @@ func startPoolIndexRefresh(ctx context.Context, profileDir string, cfg poolcfg.C // It is quiet by design. A dead host, a timeout or a signature that does not // check is an ordinary state for a fetch nothing waited for, so the error is // said only under the debug record's switch and never on the surface. +// +// THE MIRROR IS ASKED WHEN THE RELAY DOES NOT ANSWER. Any failure of the +// primary address other than a signature failure falls through to the mirror, +// which is the relay's own document copied elsewhere and shares the cache +// directory, so a document the mirror serves is kept only when its version is +// not lower than the one already cached. A signature failure is its own +// statement about the primary's bytes, and a copy of the same document cannot +// vouch for them, so it is not asked; an empty mirror address turns the +// fallback off. func refreshPoolIndex(ctx context.Context, profileDir string, cfg poolcfg.Config, keys []ed25519.PublicKey) { + poolDir := config.ProfilePath(profileDir, "pool") + if err := poolIndexPull(ctx, cfg.IndexURL, poolDir, cfg.TTL, keys); err != nil { + if errors.Is(err, pull.ErrBadSignature) || cfg.MirrorURL == "" { + if trace.Enabled() { + log.Printf("model pool: index refresh: %v", err) + } + return + } + if err := poolIndexPull(ctx, cfg.MirrorURL, poolDir, cfg.TTL, keys); err != nil && trace.Enabled() { + log.Printf("model pool: index refresh: %v", err) + } + } +} + +// poolIndexPull fetches url through the puller's own cache under the config's +// TTL, and returns the fetch's error — nil when a document was read or a young +// cache answered. +func poolIndexPull(ctx context.Context, url, poolDir string, ttl time.Duration, keys []ed25519.PublicKey) error { puller := &pull.Puller{ - URL: cfg.IndexURL, + URL: url, Keys: keys, - CacheDir: config.ProfilePath(profileDir, "pool"), - TTL: cfg.TTL, + CacheDir: poolDir, + TTL: ttl, Budget: pull.DefaultBudget, } - if _, err := puller.Pull(ctx); err != nil && trace.Enabled() { - log.Printf("model pool: index refresh: %v", err) - } + _, err := puller.Pull(ctx) + return err } // wirePoolIndex seats this process's pool readings beside its catalog: it diff --git a/internal/pool/pull/pull.go b/internal/pool/pull/pull.go index 525c4b9045..c2c190e434 100644 --- a/internal/pool/pull/pull.go +++ b/internal/pool/pull/pull.go @@ -48,6 +48,13 @@ import ( // the cached copy is the answer, and Pull handles it before the fallback path. var errNotModified = errors.New("pull: not modified") +// ErrBadSignature is a fetched document whose signature does not verify under +// any of the keys in hand. It is its own error because a caller deciding +// whether to look elsewhere for the same document has to tell a document that +// failed its check from a source that did not answer: a copy somewhere else +// cannot vouch for bytes whose signature just failed. +var ErrBadSignature = errors.New("pull: signature does not verify") + const ( // MaxDoc is the largest document Pull will read. A document larger than // this is refused rather than read to the end, because a source that answers @@ -207,7 +214,7 @@ func (p *Puller) Pull(ctx context.Context) (Result, error) { // than the one the cache already holds. It returns the version when it does. func (p *Puller) good(doc, sig []byte, cached cache) (int64, error) { if !Verify(doc, sig, p.Keys) { - return 0, errors.New("pull: signature does not verify") + return 0, ErrBadSignature } version, err := docVersion(doc) if err != nil { From f2b996d986898edbaa1ace59fd8076cbcc712016 Mon Sep 17 00:00:00 2001 From: santoshkumarradha Date: Thu, 17 Sep 2026 13:31:20 -0400 Subject: [PATCH 135/173] pool: status says whether the relay answered, and the mirror when it did not Co-Authored-By: codeaf --- cmd/codeaf/pool.go | 142 +++++++++++++++++++++++++++++++++++----- cmd/codeaf/pool_test.go | 133 +++++++++++++++++++++++++++++++++++-- 2 files changed, 253 insertions(+), 22 deletions(-) diff --git a/cmd/codeaf/pool.go b/cmd/codeaf/pool.go index 95c72471fa..63d7cedd40 100644 --- a/cmd/codeaf/pool.go +++ b/cmd/codeaf/pool.go @@ -3,11 +3,13 @@ // `codeaf pool` answers what the pool is on this machine: the mode and the // addresses in force with the word saying where each came from, the index // cached from the last good read, and — under status — what is waiting to be -// sent. It spends nothing and reaches the network only under `verify`, which -// fetches a fresh index and checks its signature. THE KEY STANDS IN FRONT OF -// THE FETCH: a signature nobody can check is a fetch nobody should make, so -// a build left with no key in hand refuses verify at the door rather than -// downloading bytes it cannot vouch for. +// sent and whether the relay and the mirror answered. It spends nothing, and +// reaches the network only under `status` and `verify`, each of which fetches +// the index: `status` to say whether an address answers, `verify` to check a +// fresh one's signature. THE KEY STANDS IN FRONT OF THE FETCH: a signature +// nobody can check is a fetch nobody should make, so a build left with no key +// in hand refuses verify at the door rather than downloading bytes it cannot +// vouch for. package main import ( @@ -123,33 +125,40 @@ func runPoolWith(args []string, output io.Writer, profileDir string, now func() } } -// showPool is the reading form. statusPool is the same answer with the outbox -// and the two doors the mode opens added to it. +// showPool is the reading form. statusPool is the same answer with the outbox, +// the two doors the mode opens, and the relay's own answer added to it — the +// last is why status, alone among the reading forms, asks the network. func showPool(args []string, output io.Writer, poolDir string, cfg poolcfg.Config, now func() time.Time) error { - return poolReading("pool show", args, output, poolDir, cfg, now, false) + flags := commandFlags("pool show") + asJSON := flags.Bool("json", false, "print the answer as one JSON object") + if err := parseCommandFlags(flags, args); err != nil { + return err + } + if flags.NArg() != 0 { + return fmt.Errorf("usage: codeaf pool show [--json]") + } + return printPool(output, poolDir, cfg, now(), *asJSON, false, nil) } func statusPool(args []string, output io.Writer, poolDir string, cfg poolcfg.Config, now func() time.Time) error { - return poolReading("pool status", args, output, poolDir, cfg, now, true) -} - -func poolReading(name string, args []string, output io.Writer, poolDir string, cfg poolcfg.Config, now func() time.Time, withStatus bool) error { - flags := commandFlags(name) + flags := commandFlags("pool status") asJSON := flags.Bool("json", false, "print the answer as one JSON object") + var keys poolKeys + flags.Var(&keys, "key", "an ed25519 public key, base64; repeatable") if err := parseCommandFlags(flags, args); err != nil { return err } if flags.NArg() != 0 { - return fmt.Errorf("usage: codeaf %s [--json]", name) + return fmt.Errorf("usage: codeaf pool status [--json] [--key key]") } - return printPool(output, poolDir, cfg, now(), *asJSON, withStatus) + return printPool(output, poolDir, cfg, now(), *asJSON, true, keys) } // printPool is the reading form's whole answer. The config first — every value // beside the word saying where it came from, one of default, setting, env or // ci — then the cached index with its age, then the install's own sheet, // then, for status, the outbox and the two doors the mode opens. -func printPool(output io.Writer, poolDir string, cfg poolcfg.Config, now time.Time, asJSON, withStatus bool) error { +func printPool(output io.Writer, poolDir string, cfg poolcfg.Config, now time.Time, asJSON, withStatus bool, keys []ed25519.PublicKey) error { var cached *index.Index // The cache is read the way show reads everything else, as an answer and // not as an argument: a document that does not parse is not there yet, @@ -161,7 +170,7 @@ func printPool(output io.Writer, poolDir string, cfg poolcfg.Config, now time.Ti } } if asJSON { - return printPoolJSON(output, poolDir, cfg, cached, now, withStatus) + return printPoolJSON(output, poolDir, cfg, cached, now, withStatus, keys) } for _, line := range []string{ fmt.Sprintf("mode %s · %s", cfg.Mode, cfg.Source.Mode), @@ -211,6 +220,10 @@ func printPool(output io.Writer, poolDir string, cfg poolcfg.Config, now time.Ti return err } if withStatus { + relay, mirror := probePool(poolDir, cfg, now, keys) + if _, err := fmt.Fprintln(output, relayStatusLine(cfg, relay, mirror, cached)); err != nil { + return err + } _, err := fmt.Fprintf(output, "pending %d · can send %s · can read %s\n", pendingRows(poolDir), yesNo(cfg.CanSend()), yesNo(cfg.CanRead())) return err @@ -218,6 +231,94 @@ func printPool(output io.Writer, poolDir string, cfg poolcfg.Config, now time.Ti return nil } +// poolProbeBudget is what status spends asking one address. It is short on +// purpose: the line is a reading a person waits for, and an address that takes +// longer than this to answer has not answered. +const poolProbeBudget = 3 * time.Second + +// probeSummary is one address's answer under status: whether it answered, the +// index version it served when it did, and — when it did not — the one-line +// reason. fromCache says the document was the copy already on disk, which the +// relay line words differently; it is not part of the JSON shape. +type probeSummary struct { + Reachable bool `json:"reachable"` + Version int64 `json:"version"` + Reason string `json:"reason"` + fromCache bool +} + +// probePool asks the relay for the index, and the mirror when the relay does +// not answer, under a pull that always goes to the network — TTL 0 — inside a +// three-second budget. It never fails a reading form: an address that does not +// answer is what the line is for, and status exits 0 either way. A mode that +// forbids reading asks nothing at all. +func probePool(poolDir string, cfg poolcfg.Config, now time.Time, keys []ed25519.PublicKey) (probeSummary, probeSummary) { + if !cfg.CanRead() { + off := probeSummary{Reason: "not read (model_pool off)"} + return off, off + } + trusted := append([]ed25519.PublicKey{}, keys...) + trusted = append(trusted, poolTrustedKeys(cfg)...) + clock := func() time.Time { return now } + relay := probeAddress(cfg.IndexURL, poolDir, trusted, clock) + if relay.Reachable { + return relay, probeSummary{} + } + if cfg.MirrorURL == "" { + return relay, probeSummary{Reason: "not configured"} + } + return relay, probeAddress(cfg.MirrorURL, poolDir, trusted, clock) +} + +// probeAddress pulls one address the way status asks it: a fresh fetch — TTL 0 +// so a young cache is not what answers — inside the probe budget, its failure +// turned into the one-line reason. +func probeAddress(url, poolDir string, keys []ed25519.PublicKey, now func() time.Time) probeSummary { + puller := &pull.Puller{ + URL: url, + Keys: keys, + CacheDir: poolDir, + TTL: 0, + Budget: poolProbeBudget, + Now: now, + } + result, err := puller.Pull(context.Background()) + if err != nil { + return probeSummary{Reason: oneLine(err.Error())} + } + return probeSummary{Reachable: true, Version: result.Version, fromCache: result.FromCache} +} + +// relayStatusLine is the one line status says about the addresses: whether the +// relay answered, the mirror beside it when the relay did not, and — when +// neither did — what the reading fell back to. A mode that forbids reading +// asks nothing and says that instead. +func relayStatusLine(cfg poolcfg.Config, relay, mirror probeSummary, cached *index.Index) string { + if !cfg.CanRead() { + return "relay: not read (model_pool off)" + } + if relay.Reachable { + if relay.fromCache { + return fmt.Sprintf("relay: reachable · index unchanged, version %d", relay.Version) + } + return fmt.Sprintf("relay: reachable · index version %d", relay.Version) + } + if mirror.Reachable { + return fmt.Sprintf("relay: unreachable (%s) · mirror: reachable · index version %d", relay.Reason, mirror.Version) + } + where := "built-in seed" + if cached != nil { + where = "cache" + } + return fmt.Sprintf("relay: unreachable (%s) · mirror: unreachable (%s) · reading %s", relay.Reason, mirror.Reason, where) +} + +// oneLine is a reason said on one line: any newline becomes a space, because +// the relay line holds the reason in parentheses beside the next word. +func oneLine(reason string) string { + return strings.ReplaceAll(strings.TrimSpace(reason), "\n", " ") +} + // ownSummary is the install's own sheet as the reading forms carry it: the // cells the picker reads beside the index, and the observations behind them. type ownSummary struct { @@ -257,6 +358,8 @@ type poolAnswer struct { Pending *int `json:"pending,omitempty"` CanSend *bool `json:"can_send,omitempty"` CanRead *bool `json:"can_read,omitempty"` + Relay *probeSummary `json:"relay,omitempty"` + Mirror *probeSummary `json:"mirror,omitempty"` Index *indexSummary `json:"index"` Own ownSummary `json:"own"` } @@ -276,7 +379,7 @@ type indexSummary struct { Source string `json:"source"` } -func printPoolJSON(output io.Writer, poolDir string, cfg poolcfg.Config, cached *index.Index, now time.Time, withStatus bool) error { +func printPoolJSON(output io.Writer, poolDir string, cfg poolcfg.Config, cached *index.Index, now time.Time, withStatus bool, keys []ed25519.PublicKey) error { answer := poolAnswer{ Mode: cfg.Mode.String(), ModeSource: cfg.Source.Mode, @@ -313,6 +416,9 @@ func printPoolJSON(output io.Writer, poolDir string, cfg poolcfg.Config, cached answer.Pending = &pending answer.CanSend = &send answer.CanRead = &read + relay, mirror := probePool(poolDir, cfg, now, keys) + answer.Relay = &relay + answer.Mirror = &mirror } answer.Own = ownSheetSummary(poolDir) encoded, err := json.Marshal(answer) diff --git a/cmd/codeaf/pool_test.go b/cmd/codeaf/pool_test.go index 004e772285..a9cbcc97e9 100644 --- a/cmd/codeaf/pool_test.go +++ b/cmd/codeaf/pool_test.go @@ -204,7 +204,7 @@ func TestPoolShowJSONWithASeededIndexReportsTheDocument(t *testing.T) { func TestPoolStatusCountsPendingRowsAndNamesItsDoors(t *testing.T) { dir := seedOutbox(t) var out strings.Builder - if err := runPoolWith([]string{"status"}, &out, dir, poolClock(t), noEnv); err != nil { + if err := runPoolWith([]string{"status"}, &out, dir, poolClock(t), deadEnv()); err != nil { t.Fatal(err) } if !strings.Contains(out.String(), "pending 2 · can send yes · can read yes") { @@ -215,7 +215,7 @@ func TestPoolStatusCountsPendingRowsAndNamesItsDoors(t *testing.T) { } out.Reset() - if err := runPoolWith([]string{"status", "--json"}, &out, dir, poolClock(t), noEnv); err != nil { + if err := runPoolWith([]string{"status", "--json"}, &out, dir, poolClock(t), deadEnv()); err != nil { t.Fatal(err) } var answer struct { @@ -235,7 +235,7 @@ func TestPoolStatusCountsPendingRowsAndNamesItsDoors(t *testing.T) { // reads the outbox by count, because opening one would create it. quiet := t.TempDir() out.Reset() - if err := runPoolWith([]string{"status"}, &out, quiet, poolClock(t), noEnv); err != nil { + if err := runPoolWith([]string{"status"}, &out, quiet, poolClock(t), deadEnv()); err != nil { t.Fatal(err) } if !strings.Contains(out.String(), "pending 0") { @@ -619,7 +619,7 @@ func TestPoolShowSaysWhatTheOwnSheetHolds(t *testing.T) { } out.Reset() - if err := runPoolWith([]string{"status"}, &out, dir, poolClock(t), noEnv); err != nil { + if err := runPoolWith([]string{"status"}, &out, dir, poolClock(t), deadEnv()); err != nil { t.Fatal(err) } if !strings.Contains(out.String(), "own sheet: 2 cells, 3 observations") { @@ -860,3 +860,128 @@ func TestPoolRefreshDoesNotFallToTheMirrorOnABadSignature(t *testing.T) { t.Fatal("a document with a bad signature fell through to the mirror, or was cached") } } + +// ── THE RELAY LINE ────────────────────────────────────────────────────────── + +// status asks the relay whether it answers, over a signed document served by +// an httptest relay and checked under the key --key hands in. +func TestPoolStatusSaysTheRelayAnswered(t *testing.T) { + pub, priv, err := ed25519.GenerateKey(rand.Reader) + if err != nil { + t.Fatal(err) + } + server := poolServer(t, priv, signedPoolDoc(7)) + lookup := poolEnv(map[string]string{ + "CODEAF_MODEL_POOL_URL": server.URL + "/index.json", + "CODEAF_MODEL_POOL_MIRROR_URL": "", + }) + var out strings.Builder + if err := runPoolWith([]string{"status", "--key", base64.StdEncoding.EncodeToString(pub)}, &out, t.TempDir(), poolClock(t), lookup); err != nil { + t.Fatalf("a reachable relay failed status: %v", err) + } + if !strings.Contains(out.String(), "relay: reachable · index version 7") { + t.Fatalf("status did not say the relay answered:\n%s", out.String()) + } +} + +// A closed relay and a closed mirror are a reading, not a failure: status says +// both are unreachable, says what it read instead, and still exits 0. +func TestPoolStatusSaysUnreachableAndStillExitsZero(t *testing.T) { + var out strings.Builder + if err := runPoolWith([]string{"status"}, &out, t.TempDir(), poolClock(t), deadEnv()); err != nil { + t.Fatalf("an unreachable relay failed status: %v", err) + } + body := out.String() + if !strings.Contains(body, "relay: unreachable (") { + t.Fatalf("status did not say the relay was unreachable:\n%s", body) + } + if !strings.Contains(body, "mirror: unreachable (") { + t.Fatalf("status did not say the mirror was unreachable:\n%s", body) + } + if !strings.Contains(body, "reading built-in seed") { + t.Fatalf("status did not say what it read instead:\n%s", body) + } +} + +// The mirror is asked when the relay does not answer, and the line says so. +func TestPoolStatusFallsToTheMirrorWhenTheRelayIsDown(t *testing.T) { + pub, priv, err := ed25519.GenerateKey(rand.Reader) + if err != nil { + t.Fatal(err) + } + mirror := poolServer(t, priv, signedPoolDoc(7)) + lookup := poolEnv(map[string]string{ + "CODEAF_MODEL_POOL_URL": "http://127.0.0.1:1/index.json", + "CODEAF_MODEL_POOL_MIRROR_URL": mirror.URL + "/index.json", + }) + var out strings.Builder + if err := runPoolWith([]string{"status", "--key", base64.StdEncoding.EncodeToString(pub)}, &out, t.TempDir(), poolClock(t), lookup); err != nil { + t.Fatalf("a mirror that answered still failed status: %v", err) + } + body := out.String() + if !strings.Contains(body, "relay: unreachable (") || !strings.Contains(body, "mirror: reachable · index version 7") { + t.Fatalf("status did not fall to the mirror:\n%s", body) + } +} + +// A mode that forbids reading asks the network nothing and says so, whatever +// the addresses in force are. +func TestPoolStatusDoesNotReadWhenOff(t *testing.T) { + var out strings.Builder + lookup := poolEnv(map[string]string{"CODEAF_MODEL_POOL": "off"}) + if err := runPoolWith([]string{"status"}, &out, t.TempDir(), poolClock(t), lookup); err != nil { + t.Fatal(err) + } + if !strings.Contains(out.String(), "relay: not read (model_pool off)") { + t.Fatalf("an off pool did not say it asked nothing:\n%s", out.String()) + } +} + +// status --json carries the relay and the mirror as objects — whether each +// answered, the version it served, and the reason when it did not. +func TestPoolStatusJSONCarriesTheRelayAndMirror(t *testing.T) { + pub, priv, err := ed25519.GenerateKey(rand.Reader) + if err != nil { + t.Fatal(err) + } + server := poolServer(t, priv, signedPoolDoc(7)) + lookup := poolEnv(map[string]string{ + "CODEAF_MODEL_POOL_URL": server.URL + "/index.json", + "CODEAF_MODEL_POOL_MIRROR_URL": "", + }) + var out strings.Builder + if err := runPoolWith([]string{"status", "--json", "--key", base64.StdEncoding.EncodeToString(pub)}, &out, t.TempDir(), poolClock(t), lookup); err != nil { + t.Fatal(err) + } + var answer struct { + Relay *probeSummary `json:"relay"` + Mirror *probeSummary `json:"mirror"` + } + if err := json.Unmarshal([]byte(out.String()), &answer); err != nil { + t.Fatalf("status --json did not parse: %v\n%s", err, out.String()) + } + if answer.Relay == nil || !answer.Relay.Reachable || answer.Relay.Version != 7 { + t.Fatalf("the relay was not reported: %+v", answer.Relay) + } + if answer.Mirror == nil || answer.Mirror.Reachable { + t.Fatalf("the mirror was reported as answering though the relay did: %+v", answer.Mirror) + } +} + +// A show asks nothing, so its object carries neither a relay nor a mirror. +func TestPoolShowJSONCarriesNoRelayAnswer(t *testing.T) { + var out strings.Builder + if err := runPoolWith([]string{"show", "--json"}, &out, t.TempDir(), poolClock(t), noEnv); err != nil { + t.Fatal(err) + } + var answer map[string]json.RawMessage + if err := json.Unmarshal([]byte(out.String()), &answer); err != nil { + t.Fatalf("show --json did not parse: %v\n%s", err, out.String()) + } + if _, has := answer["relay"]; has { + t.Fatal("a show carried a relay field") + } + if _, has := answer["mirror"]; has { + t.Fatal("a show carried a mirror field") + } +} From 7bd4b46cdba4bd80878b0e1c26e37ca453d89fd5 Mon Sep 17 00:00:00 2001 From: santoshkumarradha Date: Thu, 17 Sep 2026 13:31:27 -0400 Subject: [PATCH 136/173] docs: the terminal manual says status reads the relay and the mirror, and how to point at a private relay Co-Authored-By: codeaf --- internal/manual/chat/running-from-the-terminal.md | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/internal/manual/chat/running-from-the-terminal.md b/internal/manual/chat/running-from-the-terminal.md index e84261310a..516afe1c26 100644 --- a/internal/manual/chat/running-from-the-terminal.md +++ b/internal/manual/chat/running-from-the-terminal.md @@ -577,8 +577,9 @@ carries a seed index of our own scored runs, read until a fresher signed one is cached. Your install also keeps the scores its judge gave in `own.json` under the pool directory — `show` and `status` say what that sheet holds — and the crew reads them beside the index. `status` adds what is waiting to be sent -and whether the mode allows sending and reading. `--json` prints the same -answer as one object. Neither form touches the network. +and whether the mode allows sending and reading. `codeaf pool status` also +says whether the relay answered, and whether the mirror did. `--json` prints +the same answer as one object; `show` reads nothing off the network. **The scores start here.** In a conversation, after a task lands, a model outside the crew is asked to score each seat the work ran on — the worker that @@ -611,6 +612,10 @@ other key instead — a mirror's, for instance. A fetch or a signature that fails is exit 1; a `verify` on a machine whose setting is `off` is refused with exit 2 and fetches nothing. +A private relay is a copy of `relay/` deployed to your own account with your +own keypair, and `CODEAF_MODEL_POOL_RELAY_URL` with `models.pool.public_key` +(or `CODEAF_MODEL_POOL_PUBLIC_KEY`) point an install at it. + ## What is still running in the background — codeaf services, and stopping one `codeaf services` lists the long-running processes started here and not yet stopped — a dev From 540ecafcf9a5629341be9ea785d8adecdc74a0db Mon Sep 17 00:00:00 2001 From: santoshkumarradha Date: Thu, 17 Sep 2026 13:31:31 -0400 Subject: [PATCH 137/173] docs: the pool client change note carries the mirror fallback and the status line Co-Authored-By: codeaf --- docs/changes/unreleased/1093-pool-client.md | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/docs/changes/unreleased/1093-pool-client.md b/docs/changes/unreleased/1093-pool-client.md index 8a3ce61034..ce92c5422e 100644 --- a/docs/changes/unreleased/1093-pool-client.md +++ b/docs/changes/unreleased/1093-pool-client.md @@ -14,4 +14,16 @@ The relay wants a nonce on every batch and signs its index under one key; this wave gives the client both, so an install with `model_pool` on reads the signed index and sends its rows the day the relay answers. Until it answers, every path falls back the way it did: the cache, the seed, and the install's -own sheet, with nothing said at a person. \ No newline at end of file +own sheet, with nothing said at a person. +The relay's index has a second address — the same signed document copied to +GitHub — held as `DefaultMirrorURL` (env `CODEAF_MODEL_POOL_MIRROR_URL`, a +value set and empty to turn the fallback off) and printed by `pool show` beside +the other addresses. A refresh that fails for any reason other than a signature +failure falls through to the mirror, which shares the cache directory, so the +document with the higher version is the one kept; a signature failure is a +statement about the primary's bytes and does not fall through at all. `pool +status` now asks the relay, and the mirror when the relay does not answer, under +a three-second budget and TTL 0, and prints one line: whether each answered, +the index version it served, or the reason it did not — with `--key` to check a +document signed under a key other than the one built in. Status still exits 0 +whether or not anything answered. From 0f984f981e0ffaddcc303b4feab9394e6c49c7fb Mon Sep 17 00:00:00 2001 From: santoshkumarradha Date: Thu, 17 Sep 2026 13:34:09 -0400 Subject: [PATCH 138/173] tui3: the pool key row has a tab Co-Authored-By: Claude Fable 5.1 --- internal/tui3/settings.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/internal/tui3/settings.go b/internal/tui3/settings.go index 16bb26381c..cecb0746c5 100644 --- a/internal/tui3/settings.go +++ b/internal/tui3/settings.go @@ -681,6 +681,14 @@ var settingUI = map[string]settingMeta{ "improve it. Nothing about your code leaves your machine. read uses " + "the pool and sends nothing; off does neither.", }, + // The key sits under the pool row it guards: a private relay is the same + // code under another keypair, and this is where the install is told whose + // signature to trust. Blank is the key built into this binary. + config.KeyModelPoolPublicKey: { + tab: tabProviders, label: "pool key", widget: widgetText, + about: "the public key a Model Pool index must be signed with. Blank " + + "trusts the key built into codeaf; set it to read a relay of your own.", + }, config.KeyVisionModel: { tab: tabProviders, label: "looking", widget: widgetSelect, about: "the model that looks at images. Blank picks one that can see.", From 45cf1faf51a3eda783f571177fa55842a6137c8e Mon Sep 17 00:00:00 2001 From: santoshkumarradha Date: Thu, 17 Sep 2026 13:35:23 -0400 Subject: [PATCH 139/173] settings, README: the pool row says what leaves, that it is on, and how to turn it off Co-Authored-By: Claude Fable 5.1 --- README.md | 10 +++++++--- internal/config/settings.go | 5 +++-- internal/tui3/settings.go | 3 ++- 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index defc468a9b..a1f3d75370 100644 --- a/README.md +++ b/README.md @@ -197,9 +197,13 @@ any OpenAI-compatible endpoint. ## Model Pool -Installs that opt in send text-free scores of their crews to a relay, which -publishes a signed index of seat quality; the crew picker reads it under -`picked from = learn`. The index is mirrored on the `model-pool` branch at +The picker can choose models from what other installs have found. It is on by +default: what an install sends is computed, text-free numbers about the models +it ran (role, model, a number, a day) under a per-install nonce, never code, +prompts, paths or an identity, and `codeaf pool status` shows exactly what is +waiting to go. Turn it off with `model_pool = off` on the settings sheet or +`CODEAF_MODEL_POOL=off`; `read` uses the pool and sends nothing. The relay +publishes a signed index the crew picker reads under `picked from = learn`. The index is mirrored on the `model-pool` branch at `pool/index.json`. The design is [Pareto Crewing](docs/design/model-pool/pareto-crewing.pdf); the relay's code is under `relay/`, with a [runbook](docs/design/model-pool/RUNBOOK.md) that includes running your own. diff --git a/internal/config/settings.go b/internal/config/settings.go index 2d509f4a81..59014f3241 100644 --- a/internal/config/settings.go +++ b/internal/config/settings.go @@ -1903,8 +1903,9 @@ func (s *Settings) build() []Setting { Key: KeyModelPool, Category: CategoryModels, Kind: SettingChoice, Label: "model pool", Env: "CODEAF_MODEL_POOL", Choices: ModelPoolChoices, Hint: "codeaf picks your models from the public Model Pool, and your runs improve it. " + - "Nothing about your code leaves your machine. " + - "read: use the pool, send nothing. off: neither.", + "On by default: what leaves is computed, text-free numbers under a per-install nonce, " + + "never code, prompts or paths. read: use the pool, send nothing. off: neither " + + "(CODEAF_MODEL_POOL=off for one run).", read: func() string { return ModelPoolAt(dir).Mode.String() }, write: func(raw string) error { return writeChoice(dir, KeyModelPool, raw, ModelPoolChoices) }, }, diff --git a/internal/tui3/settings.go b/internal/tui3/settings.go index cecb0746c5..380f938f5c 100644 --- a/internal/tui3/settings.go +++ b/internal/tui3/settings.go @@ -678,7 +678,8 @@ var settingUI = map[string]settingMeta{ config.KeyModelPool: { tab: tabProviders, label: "model pool", widget: widgetCycle, about: "codeaf picks your models from the public Model Pool, and your runs " + - "improve it. Nothing about your code leaves your machine. read uses " + + "improve it. On by default: what leaves is computed, text-free numbers " + + "under a per-install nonce, never code, prompts or paths. read uses " + "the pool and sends nothing; off does neither.", }, // The key sits under the pool row it guards: a private relay is the same From cc80d722d640bcbaad5affd69785817a618b2657 Mon Sep 17 00:00:00 2001 From: santoshkumarradha Date: Thu, 17 Sep 2026 13:45:50 -0400 Subject: [PATCH 140/173] pool, docs: the seed index and the paper's figures are regenerated from every scored run to date Co-Authored-By: Claude Fable 5.1 --- docs/design/model-pool/data/seed-cells.csv | 16 +++---- docs/design/model-pool/fig-cells.pdf | Bin 19197 -> 19389 bytes docs/design/model-pool/fig-front.pdf | Bin 24502 -> 24502 bytes docs/design/model-pool/fig-regret.pdf | Bin 19348 -> 19348 bytes docs/design/model-pool/fig-semibandit.pdf | Bin 17562 -> 17562 bytes docs/design/model-pool/fig-shrink.pdf | Bin 21230 -> 21230 bytes docs/design/model-pool/pareto-crewing.pdf | Bin 355375 -> 355555 bytes docs/design/model-pool/pareto-crewing.tex | 2 +- internal/pool/index/seed.json | 46 ++++++++++----------- 9 files changed, 32 insertions(+), 32 deletions(-) diff --git a/docs/design/model-pool/data/seed-cells.csv b/docs/design/model-pool/data/seed-cells.csv index a1cb43b02d..b042f9fee0 100644 --- a/docs/design/model-pool/data/seed-cells.csv +++ b/docs/design/model-pool/data/seed-cells.csv @@ -1,15 +1,15 @@ role,model,mean,sd,n -high,anthropic/claude-fable-5.1,83.3,14.1,4 -high,anthropic/claude-opus-5,80.0,16.3,3 -high,moonshotai/kimi-k3,80.0,12.1,8 -high,qwen/qwen3.8-max-0902,75.0,14.4,6 +high,anthropic/claude-fable-5.1,90.9,8.3,9 +high,anthropic/claude-opus-5,83.3,14.1,4 +high,moonshotai/kimi-k3,76.5,10.0,15 +high,qwen/qwen3.8-max-0902,86.7,8.5,13 mastermind,anthropic/claude-fable-5.1,77.5,6.5,38 mastermind,anthropic/claude-opus-5,80.0,8.7,18 mastermind,moonshotai/kimi-k3,57.1,10.6,19 mastermind,qwen/qwen3.8-max-0902,50.0,16.7,6 mastermind,z-ai/glm-5.3,62.5,16.1,6 -worker,deepseek/deepseek-v4.1-flash,66.7,13.1,10 -worker,meta/muse-spark-1.3,87.5,11.0,6 +worker,deepseek/deepseek-v4.1-flash,76.2,9.1,19 +worker,meta/muse-spark-1.3,90.0,9.0,8 worker,qwen/qwen3.8-max-0902,72.7,12.9,9 -worker,z-ai/glm-5.3,75.0,7.5,30 -worker,z-ai/glm-5.3-flash,65.6,5.9,62 +worker,z-ai/glm-5.3,71.4,7.5,33 +worker,z-ai/glm-5.3-flash,66.2,5.5,72 diff --git a/docs/design/model-pool/fig-cells.pdf b/docs/design/model-pool/fig-cells.pdf index 141f422c9074e64b6da60ecdf87f8a9c04700e9d..54253b6c0873f1a225877c71f6bbb36c9edfa8b7 100644 GIT binary patch delta 4875 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 delta 4802 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 diff --git a/docs/design/model-pool/fig-front.pdf b/docs/design/model-pool/fig-front.pdf index 04e9b904cd6ab2a03376ae8604a5e4aed00f4ca7..6fda7ceb7d96573e92c6769adf35121762cbb16d 100644 GIT binary patch delta 20 ccmdnCpK;rM#tpL~S&dDMP0TmXj}&JE09nrmVgLXD delta 20 ccmdnCpK;rM#tpL~Sq+UXO)WOhj}&JE09o$`X8-^I diff --git a/docs/design/model-pool/fig-regret.pdf b/docs/design/model-pool/fig-regret.pdf index 5ad4bd65f6d4762396cc5c015920d5f4883f3a0c..85b46aa62bf23c249424195d8870e01c79a1c6dc 100644 GIT binary patch delta 20 bcmbO-opH)^#tjl~ti~qBre>RE-O5=3Ms5ZJ delta 20 bcmbO-opH)^#tjl~tcE5A1{RxT-O5=3Mk@vT diff --git a/docs/design/model-pool/fig-semibandit.pdf b/docs/design/model-pool/fig-semibandit.pdf index 5567aed07e2a1bd574de496b72a2ecfaf20d0668..506ae964882527131f5ccc6f26fb6f470fa89295 100644 GIT binary patch delta 20 ccmbQ$$vCT%alz?kJs=KCY zs=KGBZVmHb9kZS$8&JZv@d7>IOFD%8{O&{cKqh%KNrs!*`>{W|3;6mXR*VvQFx1c# z=foX^q*`wD_s!DOaz%JS_TEes;l92r)Bu)HC0gsLPR3$eWlJufv*MbUljZQ z?fcbF6rc%#=GR6_Nhu8$aJd{JjPgNdJ}VDTGKB0s%;u+>2*B~;BTg$C{|K`;(--== zBsX#ZW8so)nU75V5_BoVIO4uoNyXjfRr_kvsdI14n|1W@ z_ux-!`l4*I%aGjOH*IAS@F8gRM%G|gL%e^~>4#u$H~kA*kk)8)$e6UkxbKACp<*K1 zOWWA`2V`{a3E(3W`8DD|-+$2YHN+&mcVAWTz}4U$5->^MbI56U=lLPJmRijxrN{zAP?^3X)!T4%LeCcu$f3E$pcl z<#_@U{=Bso>>@hy<-ekzJJW~E{Dj*&%{P9L0+D;j0+_?qBqs^!P}RlI1sxTh@#n`! zza049*&is^k2+=@d%6qeAP`SGmO{GMotylH=gd7JPEumfX|VXBEwlV1Ng?7~lK{Wm&KGTiVbh3}HZmhfuI)&Yg!}I;(cK9KUb%N`(rTce@B4!qq z8;b4@O#V@fOAH#Lqr8i1;Zc=DlR0$>m7A_b0-)wJYqYfKXA|XD9PPY{e_qd^oP6P& zN~v$SfnGS63!PshG*qY1F_}|KZJp^una3Py|Ha>)FrgR2_}$^%KkNP;&o@tmTA6u7 zf$mOAMF*B2_BybO*a?LlgYeCc8#6c|A9yPz9vq|so-B&s)`pxSaCV*|L`%GQam6y!W213dN;+qS)hyp$sz%P=Ln=R)KD;l7P z3x9zVt{pY)cAICUQ(nOo;?Je z261wA{EZC?<}(3j?qKHX=3;Ja{~smq#zHog<`J@hak2yZ)j(;IuV(21X@x`|n>pH& zp6Z_J;3O4;iUe+&tBI+pCqVH=pI)*mfY;NrqrvBuXUasXb&ZP2)A6=UQ)+7|C6vFt zhGP~$z+-mw>dWn6Fl$#>S0cG}YeGXCN&mH!JBH)J)RS*(i&Z+)F>V`?dOG~-OThp0 ze3QG*|2yiZZDXN70calY#q!`{Rhv-4GF0xjg}9E*)p@Y(Q!`pps~r8yueP^m&F{Za zA(6>;pmKE#a#6I|81}HTBMG-qJIQl%cz`I*U36(9 zpglSuo9M^xzQ^tXOWBhHG~wKuAKz%3`~?jf5cDQ=Dhw#b@g2Cd>1ebxfIv=|(a5Nt z9QtRdLP}!RN0DYI#xAgXeYC9|0e;h?v!K{JMxcIRFMGj5=5Tc%JcscmPIuW?A8-zD zzzSbXuu^V3+R)@lPo+Dd(XFob3(2$x6Ww~Zutcx=I>};}ooP~3d8eB>)=bxEWL>K2 z$DbAVGF`fIfXIweitN13Kq-YjSs@wYfkkIa-BlkyLOwx&$D$4pmR}90& zr^Oiyj8?e@*Y`0X8Xm=k;vi+f4iGrt=9Q|N-YS#swmlm@SlfQpNXbThU%&e5(dV>v z0}=uLBDy$Hyz~?M7SmsbP9^T59XT2Kj@-i{TzITGEVAP=($=tK!Ju5j_5zT*TsIvDSK1^MM=9`Nm>MI67$ zgz$py-y!I*`W{{@r(5cEBo*lNr#|syCdG#LR@}cdY&#uk#QarB8Ocsw=u_=XLk6Qb z39_cAN2oiSF1fz+HLo7+#VQ-3j3)NRir((@2$+u~!u(Y@Mpc|@L`hMjOc+`NyLk@a zC;q7Y1LYxEr|0~=tIsP`1JIpxmnk+BCsT$(*JPZ33yO5*$uk+&Deyh8H%b$jK{$SC z-g$-Y2s4|a`W;X_$GFeJkTk8eyWDofUb%I4eDria;X><_)=bk>kpY=o9Yk3hcmNWBj?#&dsizn7oy8L_vU9HYcotvwL>oeCiB22cTQLW}#pvCIy?Y zbqTSO&rdvV6aOh=lG#g-igrmYaCA}V(yV6wp@%v46KIX-m=3Jk>8^$cE1)xZMS}3S z3}k-lbKA;-ou`dp)TjETAh)bDUKS;jno6tmxk}kTtUD;8*bLDf5~(r84ceY2HQJ)F z0pE^Ah`fbZw4_~;2}mFl66t$OnSmHdLBcbu>qtT3STH90K}aZ5NL!t0#ct?+&8iHp zkqu#*lZmC*8o5eQg4<0s!Ptxuyn6V%_#=XC6Amm5jkPB2eu@@SvVik=i?)Qs?Obzf9O>-!|uojy%Yd!b1&;c=iDjq5{J z%4bg5P-6*xo{eYIXd{xPt*`|G3NG)Rm=^%=lpMQ+3sB@E_dIok3j7AGd7iJFW#|p(Our9*?tg(^tu-Sc3TjyvI==4jsUrBfdiUVk zMms&mY4FEu-u)CwU#lG7V@;Mg6z|YU^H{DIhP^3GxNTaMx&wQZDYul!BUgh-Q3Q z++_buZ;qy`Qx*?v75tOd3RpjR!5i+>zdyDp%QN(&2p%EGFK|}x5ZTVczO!sk1@DJl z#=iObq)2!Ww+E@RRU)1D7_1n5`!}q58ka-uB@7GoE8F8hPkFH>N(@>gMzT56l8iL- zIY1rbX<6!t5WAC5OfZ%goEj4O#zOfzJ46^1lMQnWT4id2XkC=dG;#revCy+Q*x%cYY_<61j53HT(@jpJ$4^po<aMFZU_x|sTh|r&1#2%Nlc4{}GncDu+ ztfOBJF@9U3mWl!J-^RawE;DhgL2eJWO4=GldSYssWLo;IJQZq8&c7BCD%el{Ob6(g zi%jD3uIInCJn$__GnTRo+XnTarK(T0lcW5Hus_F|A#h@QhS? zpRfG-hCzM=V@(fDhVk&)SihL47E$i*0sWEXKXS+`9-0!*eY>+Aj&G6W@cwnT6dGE6 z7kTo0@b-y_Ba#EyFRC*&5obZE;sw|m;94BiK$uPuT6t4Vm1;^?%_1~0LX8h4e3q-I z;UtLB$S0+SGb{-cN<*aUSq$6*I<&OiS(A+U1oBQbDJvw*WtFcxmmRp$`=NosP@j<5 zKD#;m-0tsP6#G}Yq0Ld~Var<+PGWuA+(U<^SWP6tc&f5Nwc!@kvoj`L$3!+Xe8FDn+xr1yUfon+i5de@ZQG@|pU$l5U zk^_>A5eNcRoR*1@8UA;Hw@?6a#bjmU$l2N~!Ps|g7%4VVh^Gp#`1^Hk!^l``9?z}v z-Z*WXIueXK@Pznjw(H6|N-zN{(460@xhGAhG&LO#L3G-@LuP_m;h7maYLd27rJBrI z8=WO7H8LZd8JY_eAF)t)vnZk~fy3EUgaY(50`8d)yW zd5{ydH}nNC2j&wex{pD96;-uPhyaY%OS|yc^I1UXa?^(%jt; zK`N``r!PvINk?X%54Aj^t+0N1+s!x z!ExH{YU%;zFk#NGP00>x<~z!%+AL8aI5~Fk%>7kxF>E@ifERs4$g!#gj(9f3p*2Ve zb|>Mh`T*!gfUx?oEh(yK8p<#5Gl(Ghr$a5~5!a9;Mr`y<^@QYd{Fy~NxMJG|dsJRt zXg{ZS*a`PET*}Unw{kSZZ42II+B!?BFb>8xP{<5wR?mxa$=@t5aJN3 z=Prldj@Z|J`%J8umnK&GbkA%2Zyb@N3(I-{S>JpafROtlo^og-$s+HS$wC_Pt`Nxw zpVMdApFRHsBHQ#)vwUU?GE90aW+(;q)=PVbPf@klj~qq<^xJG&fRS8_m|^V%e1)ss zGTd|h#|png*TLJ1lPIPVf)v6?d)&xN9kSJlk0^m3j-w^y^%n{W55x7OXi4?ud646l%t=e!(TwCVB~1M2{BCRHi$i5S5{${;4ahURC? z?4`X(IM%Mw0mOr3A;^&FJm`y;2m5GW41MJBS5Bh|f%R))igAQe>)}iuD4ho3fyBrF zAcoDc7 zTcHzvM{Oej+s!a5TrQp=b^Zmkwj!kGOk0`?N-Dg}<^%f9liSORJDNdC7||O6FycdR z2yJJ{wUER80QXo6UrTM6)L;a4$D4A}afZK;V!DXs>u3uOpxk-{6sYdEx(@cO*{Z%8 zE)-n9e(~|UvrN{RME3Pe{5@P?X5BUmeXz;U%^QR)728l zR}yYvxh-m}M1*=M7zm{41`C-6Fs;vzq_XaR6k+;}O84ytpxO77>0yMU%-pvcr6Bg# z6e7;*Dj=TAW~e13u)%sY$T_8PvNKs*9I}`~hu~6YcHu|d3yaUBYUk1tDQM&nGNjY| zn*Zoy?J@~6vO>w4uCu8cYh;EHfXRhKIu^Caf(C%1GAg-Hz%uSZ(r&l{`ckul1A8Nm zTKb&bz|`|t5C(f2*fWc@1@q`1rY&lMv{}<*`0xoTlop#Ms@RtpoV7H_#cfi3V)T_N zfLTdkUa(W=5bvRI{BXP+YOU`-)TXsLx6)f7hf7+x3|!9p$A7Xgu+SDN^^*gmRb(~k zHugAZIFohYC0wYcn`JQpPW9DGg)E|Q=xAEY-oI~Yp@}*lejVisrC526>X7M$f*y$& zAN76lmx4(02aTKMnoU?x=#H2C5cKfw6^G}V7yFwNpFVA)K#?FuhfePCmf6@KtR0vBnT&6E#Uk5UnH*YX6HYTrbcC)_amDw3po&UR^l3fnd@--+ z?Ow0tR>T|bd+u!HD0`2>a!<@LG0^2jykfD9)K3M&ZjT)o5RGmjm%Cc{SA|jLqU?HB zweM{hIp>SL3_|u7rEdBs9XDoe6Rk@Jg59DFqCrNOD+SgC6$ZGvb=srgAfBI?4$UnC zR8d*wb3^BYj&2ywcHiFE`U6mmVj631TE~#yCG3LyP&=au#zj?*k}F)O$PzJkU5z3; z9v(TxLKfNvFkxLETgyO=HRxaadk}}GZpIJq&?*A^on+oy_n4~4dQB{Z`p@@r$!<9lKE3RxHXpp7de-#3fj;*zMNIW4w?`gU)$ zCVFF_2-pJ;K%A$2dEfMu#1?I#8uHv<7xGRKN1y5d?DYC)nGWIZxD`C4mY=DJm+MXG ze9nP!D`Ri$FrO4wG`%&&(;I8|i!(~lGW`Ba5?p4xWVw^i!7h2&0{!}CTUjaPWTGQZ zWv2-Xxrn+2OBX%ZkT7BJFGhIQRCPpCdK2>XE&AmO;^xL&U(QB;Y+pW}u7<7TaLbfg zZ9Pl?0O{FDRklpJx83Vg(=v)5|DfU|#Xuq#QO{Eys`s3~848PksG6 zcJ1FK+u-6}o6tFSdL&_l!&A}s&qhu@KWAYsrdxw?kD0YW28pje`0{*%T=iz?)a6&g zG-})PZ3?b2J{SeSiP>+N-4u)uS$oTbqj!JX@i%~hzK1_Cb)UDzbp(v5y-*`vp?drQ z^lU;Jg}vkQ6-VmAAS8myzO)}q4Hzpjmjoj|yJO$Zc7F^D{Cz<~m4{R+)$iTzuJ#mk zWyX`SdzqYN)WRFbR=x-2jQKn- z*cYl5O26SU_YcwMcZ>P(5Og`cnMznyg5jJwa6NHF*ay?+o0& z&Y_Z21%m!1Y5~=Qe}GQTOb*D3NcAt+26>G1Dmgw4EpRZ{GB4`s=pvcX9J_-7bPW$C z1AkHtxHtX%fFXN#=*}*(8`(!>4mq}cB{)LYT;Y{?-zU0M}ALJz^pI0#kiN zuk+y9S|Glr7(iBWUwF#GpToX**SEsj!*c%Jv*`zh+Rs5_;JnXDmfcKtY}BXTeWw)f zhy(+VcYVR{j(W`{_c=%oaF`o~8H&&hHd%-Ee=`PqgSmH};^#rx?i6y*in5nO7m|Wy zaO1Y$TQod&Y9z5%P3?~5J32#Yuy<1VTa?R7e+w)tUgk+ks%~6%^~ATP4Q~Gh z{Ywc3=UzXI5_bB1zn@ulRa`s6u^W=2&K7s`w2$#4x-}##H)aw3B1eI3&rdSl>43yzas#anke@YI<6+3cj%Z*!cA zjUgJ~*c9?200xMPDo8dt6u}V9nPA|FDIwi&bkMa57>4f^qsJ-b|Aa({lUg-M z4+682YR5R>OjlKIRCD6$N)c7BErR^o_t+mu!`+d%u7M?oVe_r>q4> zjBM3M8b~Ix+S6yxDBlI`eltClZv77PqS0Ewu2nRNkmju(@@8{2b+Y78AkwJOr$uyQ zD_$|feG78}IIcO{CjEN-h&3Bt|KkU>}R^Pc>2o2@`A^1&*lr>Gr=ftF~Ye4%ZMv@+Vd-3r6YY>MTQ(NN!h zZ?8*b1dYp>+K`VixQP3io{{nU?_WmJSM;W8dl+#V}1sxgRX@Usy6QjOUw1>3(BvZ35_Ere%Dk-XeTyAG_ z$}K&{CIY31w7hJ;d%oB&gkaBdl|=hZk%h=)q3|{ zw?34z0CT|!jyW3VUKSC zwe$N9z!&MRIS*kSAveR*IAX_NQ4%l9Yd$Vr)ZbxZ{8!kJ=?~3=mFjvR zAXz&c4Cmz+%`z+9EgOGKi;!%pRm3sa1s5ShhoPH^lT&T<$^}Hj;FWl3Dm#=cuj|qA zJ+Mlj{!A+)$nB`Zb{xi0gdnw*#YN6V2-Tf?uGn6Pm!XEK34=*56vUCraEPOpW1k-& zb7$6f7V;N}uibd85S7=_)+2M?{rrqe100u?M|P z!nTb4Yoy8tNn{ipwX=WI8B>onv_M+56WZ;=aR}#NuLM>T($vH_a7OU;?{w8Scuha% zaq@gDdXwt8j}stAyme@}Il>2?Y6C39X*?>d1DCYwE-P6G?_70rT>H*!+O19cIe3T6R~)a|E4)EYXh0@befgy$OLI>bpZ*1HeL1`Yce;$?Cz zK!R_qIE2MoBC{k47r<7$=&dwEO|fUur6pWu`qK)2*0ps@85pPwiVo053nON;O&A^( z9Q}%3*%h4dY@?jR2R*Pb>&T$G5j0^q))`8lw3LWx5cm04D#YXdf5i{D;o0E_-2B(9 z%Jo)_q24JqY;7jE*TK_D(rx`Q;zGS<-wp9bMKMpuYp|;zwS)363?tq}89L_O3SeM* zLKHK7Jc?kfG3~r7BH|(x76fV+sxikw`yVJJ&MTG2p4+V;pCbY%!#hW$XGrAszi5Nj zHV2ZKlMI~t1(p$S3{Zo(LOTcf3F@y<0Azvi3|s3K!m8XL<;b3QO>%b@0!U?&C()Bm zZx4Ew?3=8lG75vCxhm&wQ{>QC0lyios>Wx|z5IPz&3X>DkXj;dvrw3prE8%CgU=ae zfRnr?_xf*af&1)=l2X(5AmKDp_~{X{-uuZgfI5fnEyn!qgu$uNT3aES6IOvU01tyD z>g;g}^pTucch-Oaq($3^iRPc0P3#*%q_)~H6HTyA`px2RNuy)8cD!`nD?xS^S$o=A z?a*+7H(TAx$%|)36#5I>b%gd@rp`N?9u&h*9)p~mDNX7{?ms!;Rk9Dr&fPjWjJ+D? zkAHHqnd6Am=n1kFJEv!7kdTJ60C$J>+&{E5@A5f8Vs4z%dpF)UZ16>l$9a?_eab&~ zo4+=L<-yS)Z#(ZAtGtwU1-|#oMteW~>fZf69$x+1(uu%m7l&P*9=*Ez+&EQ)Vm1*I z7K*IubQQ1`+5b*wSoFZRS+F}4fAI=lIaCmG@>d*?5m7<>MlPV&!X{xI0nA;|2J|qO zV(hyZ9R*LKiIpSoH&%~oRf6r#P(1{nT}tWwb^nC%=M><-WZ8ijqkoiP{Ix25UJY(z z1e3S?Rzw#?D_omaJdS9q~RDqMn8cEi__?XNU>t^hrW~DR7|dT#B%Y7I0U)Q;F_gX<<`4vqWrDkY-adv;5;JmrEU>F;s3?3SD(z zE)b{s<1Pt6C842S=(L8C@y1tmNGHPMCjV@&9Q z^)PC@LPCPGERWV$pLm0|9VkJu2HeECQTj-sX1wTr--3@L4i$# zrb~XO{&ZgcG=fa-ceP$4gnjdPKvLbiKeC&1(Hm|k42BX22t;uQ97(4y0m&6dH(Klf zG>N1C%<*ym+jV)Hob3)Ye5F5f{adR&|!P&3++>!b_2(|oq=q`FmKQ^qdrXG;36=|A}cKfJRRie`X zd125#KbcWakXkQ+GVIFV6?Ekr^qgm?<9`VXz;JF*#5QbWP>OV5A~h&E;7V88kzf>a zrUM{OEKNeUa&PnF(1@}DWETT%tRDtU5}wgkN;k!nQi%SW#ZW!I%z*I{*P6x^Z44y# zOmX|^?P{{S<7KhRaHrer323Q!B4nU|eRMwjEo-2>;WS-O76!{aX#%zce4qJlV>VdwP0G80{ z_b-R=!=l~k&%lw*b(;dcxBO;Ix;GT_&XeoqkAJuCX#+_rJBdJtfzV{Z1m%$fL%kF_ z3!|Q{+UxAqYqM-x{18M`va#w!r3H?c(t*2Uh2ex(rZOjvhc{nAfV4xu)NO9I{FV)M zVYAAwI7d1?ZC?boC7q*FTGuL!2wg=gSKH7>Wy#Sy#8L7cPQC*I%d}wXDSE*K$_jUj zR0!#kY|GoQK_(cu!w8--0)AtK9Ms&$LKR9qn3;3J_I;AXTZ2)A8={lFxXI|cN3_RU zR2Eczap4y+7`ZVX036mFnS$NH-Xq4{^WY=WH}C-+VuhFFOB|bo7Dfi$I#sZ@JtNmZ z9^0^rcSf_$)wjoo*Y8K(_`|E?!!q|`UR~*ls(-ZLRvJvVj5aQ8VtKH1)x%ILn}`i= z%=pa$=+O}lB{+UG7t=9GGpt1iPO?n%^};+CA8}AreOnVTVAeT>MUV^%*LvHxRl3b- zPrXN+p7~CObm;8z=7vp-^c3Vr_UurU;o!Pzjm|M?xUcv#|HU?yf=hCB-(T57s zB?0y}M?f+%V1C7-Gvjs%0!bG-A%U!hkFOZ85V==4loAJXoSMEMrz6fXr->vWp1 z!vH8r?$a@3qYpvd9vO9H?IN)50oIGhSoa-Ld90Fo);bF7+vyWX~*BOt(@b#_TU!^ z3KUypfSIeSSY8i{1>KcrKQdanG+I$QYx|NvK}hP^k218?l4gZmz~&8m@(1X#-}iY6 z>}@&G85)&2TcTANGuRG|vDFjThF+ZUTX>?!wL+q;H>kRejgXH~!QRY;(X>-+``Att zzqAy&h>)p6GOOp6-!RV%xff=ok(E7B+wjHP0dclnns2tsLxqNpmk1QA6IuagvQeGCo6e6xsK7>YNtQ<=D@7-m+UQIjpF9^3%+X>m>QdJS@W?S z&~A=WOaLl59`0Dm5+!sbtV$=S{6SWB49%q%ndA@lv{P&bpDD|-R&6HODBl$8vQ;S_R2Q9D zc8^WF=S#zCbLIYy@%3G%1m};Na*jb?K&iT%Gt!U084|2hypL;^KErl{QMtXwpRR;z zD?iS*>roWEP7hJrWKh&x=hy306?a0R*TkATv~LFrUy{wLRT)OxJMnw$yZVF%n=E}1v#$}4bfzl^HxDoz!(sh}Y?u80tePd2xu1AY$~ z7crG?Ib!OJnMS<-#nNA%_ipV$2wjU7qc#~!g~2;E>wiWT6gAm(jyHUBA8vigg6cWs5Yq@&)BGu(~Fw#+SXlT>2 zE$9L`r8I z6ifuCrbkBp1Go*n8MVWyxju3PAUi8BJCEAegAj?OMGFVlvBbobm9^buY)r=T!d#jy zE_+c)1LHHBppjs6-RU8EVX*G{poG|lmGPB_L`bn1YEFTyYUeB~&;Aldx%eSU6+a>O z?^BuecM0kk-vDF4;A6~#3K2JQ(dUreJ=qwCm3@2`ntSyII230l|H9LsjjEzd#|Li4 zX@Ai+n|XgkbwA!Fu24`vRA1Z96f}PI(ymYs3qzvIGL~Rb;tF?j$MG_j5EBgUmabHU zW7(9;gnfr2>Bh-6dgF>QD=5^So!Ud5-6FG5gjIUQB;y`{ku)33PiLG8gL*Tb-4cc} z1a1b!MExF9=>@oxg|Uu}1~V4JG6qWo;J@MZMhj_`NGsPEdfPOF!@-jKkJ0$-5#z*y zDp%qI`SbWVTwZt7c8DYOg<`Uawpo4`6NAy-c$fP1k*z|4L&HixhFHP$i~SFz-i3EYP7g zr}-N$GK3{AkOdX8xu?i`qeBsQwy-1_A&<{3-H`xNXN=&?CEKvla+M_TF*3F~XfO+v zONO?qLo=)rDhMyT*_2i4{jp)O*tFn({H*NlPcL6U&=vgS)#R&YJ>oRY(rq9}*SS9_ zsb~6hkGlGw$mzH55a(c8JkJ(0aZbBMyW>m4tXt;#YZ$JNTv6&Dyi}d`Q4SyDTH{e7 zRlr4PR~XT+DQd>hEBg1>xtqQ5abr;-3i9+4zt}M_O77P|R4GxC+pE*jA)(46NkX7Lo)~d?eAf*AsroJxZd!IvVKPkn9=`H@v{BwE|P4cV<`~(xx89T*V)XpK{e6( z)a=#2qR%VFijDoiqYUgSjbBh3Cidl3@YkrK-qd_Vp)L8t?Ks|cU8rGL23GKg4?$=t zF>8rxg3Y`FuZCLqB_CRUnP}Z}6X8U$r zZR2*F%;oRyC#br5Q!#ZC!*Le9_AEgqrI@S~0Tw)|^qTbabwtQsy&Wm68MJrEB`KZ> z3G^7NTgqRPpOW+ieuJc}XeLJqSYZb3c*2PJP)gwCnxIP{HXxhS2&=62yAvG9Q@@#}SFq`JzPE2Kk z9sxz^{z>zXL6bm%4)p)fJ`WTN6zI(GPj0UOdJVk#kHQ$htbrMfVCX<6MbK2BCKDJI zkmLfi5=hPbPin^m_8Tb50*28hzzhZj3N&K{LvP!?07V1^9-W9(M^=rm3&@r;?Y2W=W>CSBA#yN{8 z?yA#)|4Ue!Iuc2o%~$#I>rF6vFh?p1jW)W4ebL7k?AifY%LqH#&qE|RNojzNxfzir zcrI{~Cgzw8R+Od%jmo(qblOd0Ey5>a06&~BvRr*uf*I}ZNAp3oBxRwWVjD>RN(awpC20$cyOJ;>Ix$n-hRjEqGGgacM1M$a$AaajY(nu!6 zs2xoSRA?sx1=BOmqEJ=KzDnAK?KmUYRFg zGBWgj9v7(Si^ z-<2=8N&0^KA%ABiO8&`#jF;)gxWDT44`qrj|B1=MUNr3V@+HG(WtmU+lWy~LtTM`e zfQv6#On&B#NMzJ^M2opC$}f_5!n`o5^!HAl()KwYL@Cq49FJp zH7I*U>Yi*9BK9?cG!o3sX$UR3-i|(dxm8Cz>N6q|K)rYWbY|*2vwpl1 z|1Q2Zek}49$ecCm^7`d9uEUAXTpIqwkTv{qTJ~l&2VnW4X#^0d6ix`Q7cHvG&DCLl z(q;w>D2wL)i`~quce2acZDloPsB9`(OzxyKq*Ty~zA|y_x@QYzuYA6G(e->*2<0J+ zKJ4!aXK%_}$b743&zvN$d#dER-&Ec`9xCL<77ZK!ep^30)gpLA22@E(+M|2QKx^T| zYi;cczRl1a7y}-_@Imf9Q{N>j9IvYtC~U#8YycXTzZ2pCubV~Tj^Fp?Uvq#aFLg!e z(Yf?~H!gtJMjo84&;6)>N8kv@#=m_hRDeb&%Cg5WA4hZIZl%t9o;|16k%UKX^c~s` z=;(qc57p0U^Ql=^VF4WxBxH%KR7(r*EDsEMHEzi`0wD6U-+H<62;#*k3=L~-N@|$~ z9~|JhSc(T0$7XZNj!hQbIj!YQNF!1X`0`LAk6K##dB0h^=)}Lg8u?P_rF_?xneq}X zyL^pm%_x?+GsYMA1Z8Ijs@^veWoOTXt|lR_p!_QEMQTZ|6NhVLMm{XyPtr=8m_YZY zgYK?A1US0T%pOX`9fdrT@}{!$0^z-xz7vR#%Bp4W^MZ?B<LByvyrGzi|**I1ot8 zbGi(MkaLCnUXIt*$uwlqxPV7lQ||ZMYmfd%S(mX#Rfl)NE6M9dJVfKeR=0zLdPQgz3Pk)v#fZAP!To_(SB-^CIZ4O3|XS>#o{%Me-2ue zX<=(L#b2h$j89rR>XaGT*H})~8cJi&f2eoP(hpS(^utz$2CJ&ULr=>Di|Vc6z<7wb z0-m_l;Nh**fIG8FjK3c&OHNoTOEjxh{?=t>!B&3rquRV(dz%x(SNx2 zD0SPxL9&Dcx0=F$YLxG@HwLo+;|`64tPG8G?RW62{Q`CDeEdjs7FPYFSUVb1Z{-cj z=_t%#(Z`7n@9UgoYgDe9O=Bu;L*U&2+z12CZ1R|ave>^?G+TNvFdKFqeWB~lO6^fo zHo+_FsdHu5%0FiAARX75n*~?V_fUnNl}_Y>J>yux-oIZnCcoS%@_u_P?cSI{aOw2c z*+2wc#|Sq7l=2EIN5MzZ#fkBYXmH>?JBJ%dPNj{m9d6F?y=ajXwCkO&Se@DdO)QN4 z>rpYAr>Dfo|DxQTe3y!iT{pnrpPP68j;z4DS}^QB_7pngNa$FVo1qxx_k$4>bzwWf zP#Nv4hIq}GiNRFWMdSQ-AuhKV%%UI2a;3P81L{a=4c6rWW8C+wH^d0cROXlo+b$}5 z`Sc_kY504QbjE|7aao+WX96|=1kn~8&_Og?q1L&|(K1E`RCuL$uaU9sKw9el!^E8yRR z{=%Kf6et?UW(beO-Q1(;G&X;=84TbFbQuUDUKxET<+4|bwPj6YRuY*B`&>U$Zvbi6 zI(f3;2uR9awhTzWi~<}*^trGI6LA-SyzX}jgUtTs*F`u99(Up->l69pB)Aycff#j2 znO_}lU^3X#lB|WSi!1+EeW49#s{=Hn2)#<0;S7GC_f@UZ?=1th`6ZTHXYP3yJIrx5 z@gV!OZhqigh(hc7oqYs53A)2@l_T{-ZIiAkz1bbn@u=*Y05J98>Kt$=-w3?F7!ql`61`=>t`2nOM>9D;*!;oAZ` zAx|p#81%rjVnP?;&gFHLZr~>5r(0rtnCBeVVtlv@48KOgbc?oY%AeV5<;#JhvyT;3gwo`r^k*mZ-Uk;2AQJNOj z@u1uHDX=7hUt}PnqZ`dA1gun_7`{RbFeO;0UY~zl0QhpC3sviXZJru6RkefXkm|Au zZb+1gluZMF%F~~%n8TRsqm{x+Ird2_9gal4=w*x9?*U+`TX2JBhox(!i$0{2@|iBf zf^)lHUT=iH8g>FR;|GD_MQpmIz$Y!hP|^QhZ*ts;^#^_oRukr)sQvvZ*o)FQu|EB2nPTqu z+bvLErQp0VWwUz8g4WT_r&qi=)j1eh ztm#B{hD5EBg)k)6?A;f%y{TtZv|z@UE@gpB1xV5ki~NsEgCmzLz+_vKH{A9>mXYX_ zuIkCwk?2*jkcKQ_vVa6| zuAmJV5s6qaH$WsQKXQdu3=P!LrB$k6&u<0S#qPvcXo66k?t(>1k&qBR2rwXpy;-4 zajF)Z;|!1>mfx6cRFl_VZ~c=k5P%6tv-TMHi1?gcIP7N9X?Q*1nxbBOrhKmBju3d6 zonqeDlvyoqa|UyANSnguS37srOqC6lPIX`B$%8Qh;2uVM$;A6m;tZY1WMIZY`*7O9 zm=EA|%bR2yCxlz=t`5t{m0pnTu%@cM!rpl3npF-&`F`FL-+kD!iXorH5#WMltJ{&4 zJ==5T3}Sd^UJ*lKrqM-!yl!1hWJQo)~A!wz>DKreL;Pe+$#KTMsb!?jnV zNyNnWVEl(m{^?ynVh}1r5fB6MH6H@zD?61QcJ7ds=$9Mb_iyM^auUXZ!c1U2FmZV~ z$S-li`PqK+j&6H-^Nx>j(l-2DbH`$)y^qVCBn&SvZ?2po(ok0%;$F%_zwzh& zS@84L5&*;~2IKKR4-_4YmmMf#0*>AWt`8Q#4$Ar82S!_Z0$2ko(8LcEtxdNOESfMm zC={*jr56ko8I<*Z>WH&o04mV`I~Sv^;0Y{&IQdTuTAL^)_#Ou+_y1PmkpYLn0p)G` zx8z>t|0ZI#aesnG)`POQ_18dT{Q@d5Kw`Ez(?V{80Y#W0$J?+OA?d(?9BhynZR*UB zyI{aej(-Z$Y>-7@z!L6%2+H{%8u0y7Xy=A30|P<|K%%#~@IfYn0V#g|LmUA}1V~_! z*gxd{326rg6qfpjtzwW-;J{G1f2b-2$p!%oLjULdrprMFfc^UkTt&!WNOlfR4qjk^ zA|%oO!y!e;cu-#8jsh4y&`}9e6jTq`qy)(c$k4DfCrg|hch$^~;$l*496HoCp;RYR z<|@;n(#Y44p84cMRj{&27n2EvOQh7%%}W1C)u|MP=k{ZA zKHmU1?>5CyENvGS%jsvsgx* z7#3535slrVY_6Av(KNOes3<2db0%Y#R@M@sE^Hlx%algU*hD8_UQDB+wb8OaWtU8} zGWObMdn>+s6e5yiw00pARf?X;nX3q8$+RkP52@EVDE~hJ0vi40)N)#%Sc$0&UFK*x znK;ZWh)aum1<_||cbO4FI){(~%+XR(5^y8Xa4vt*G*~K0%wn)s;zVJvrs)cUjm;8; z!InO;t8@fTVz8rI0)ul)pBP+dU`T|@gi0K0BEU+ztLRK8mcR)miAgLbl;)PCmMzet zMO9ARSw(c{%;E&Jtd@kDcdgzKohigL{DiTmZOISk-SkwdzR*~f&PkuK9`2Wjy7h=qk${9n( zOCSR%HS7|lHE|pjllKZ9en8|`@_ngnj_et$>-;fb#hkK;4Wcz$RxJyE+$@G5= zX|l5(E#_g2Ce8Mz(2Pw|UsbD30WllU^0XbwQH~(x*@mI&fyg z{L5bP_gg)f(g)*9;5XQzE}*vuJC6Se0Ff9a(du0e|KZf5g?xuY0LhGH1qTN4Al*q& zM3-$;1tNc1LC_r`M+Jh0A3@M1+jpd&IpD#*DT3w1Z=NA~k&i>Ngh0%)A|Hs%2QnaE z5^Ce%Q45x*%YYXBIfrq!YWNuxcx+yidjv>)2IkHi3Jfg|jEF5n`4at_&=;cI`K6XUFH)<)DOoJhGe@nH(X{tq}p z_K8%K^-&5wE6>r$CY|pqus4!Vft)jOoTE}6t2hUxIRz(dhSn=6r0^bCgG$C}tpXz= z^Bxjj+QI)zPNK11sH$L3=api9&PJLV_bpg3YCHUf@_N>Ac&iU zk`p~SsShRNGzdNc9i(@h0rm=X4Q1#reVOuOh@;YK4}v@kvQUkbH>@2ZYm-rMtOiT4 zLxrazXcc0c<2l?Qf%ecth+@};qS&|;r&$O@7CuYnZD7*$Y(jtGg3v&= z#5<#NjVw5ma6Sv@d-9G~KiubR1Wb(-Nwf=+jiUXK18ONayHQ|B`hM1t?)s$vn(Prs zd@-B>av}c{GQv1p#7wr*CX>t&qoT2)X0AiliEI?(nv9i8kcHN5jT|ZT$2k*f&SE1O zaz2NrH7a3Vz=fK9W$qtj8H0and?D6B4oBjHFZX(*yi^MBR}K3<^0)LH4<>ct^F2xa zQFCsWvCo9`wDS2O`(US7`8nTYA6~H6vdpp1JCTEW4e-VcJ%$lM7lTj9_$n}f%!RW? z=e51h!c=Lc@EwJ9h|r)EWI=IJ6xmRMVzZ~zqRC4+E;Os)nE?54i~N6xW-~&(r?B1{ zU`F9d1Soh+q$kiH*ISd*;-B#kYZ1XI@-ce@%Yj(go+SL8C{l)eH=`|>T z3gtGb1*x>pl}Hn_@B#U>vWsx=&3r@w@Mh;fAw1&_{dg4=9(CKtS|}QX0-|t4klp~s zz4j3p*I``q*c`9pPZ@tL8V)k1-I&bMYwEZ^@EqECtnH0Vf(MH&A(Aq3muPe@9hbY< zURVvb7v2^GaRj|PW_!rr{;c?7oLDFL>_Uoz$Rd0CE8cX3bN2a(Z7$TYP6zCv?MBWW zFvgt3HX7Dul{$pu{{(zvO%swp2SOR1zH)vYf_sue6dpOy#yx+FaD+K{WJIcjgd0f< zJ>F2N^zr2He?Ljz+-%AuJzuYOkIL;uU2S&j`h>4v&X#oN*{ctaAH4kWW%>2Z`xnpX z&SG}8O|&0!cDDYO{C*E_0`K8HV}WskG}+$BGx7whG6mR{B6Y**Il zrmU;=Jl%^&6u*6CeC2$V`|5$O&V2RISC4&l&Xvx5C441)rF`X=*49^kY34V*EVgv6x4`@~arXiuJ2lzl!x(tY3f3dMwr-(=sG`{?J#CeD&B@ zycj+E`bzpr`O5gp`pWq#_tlxN9{TFBul!;{`>OC29c5KX%7(d?XY$nR#d$^YMAnrh zuTK8)kMB~nhrj*$;qB9)Xyz786M}O6e?!&ecc|KkB+t7CGGMc$4;t5{&fSH&XAl1Q@1s5kl zfj47g+Omi=3zlNqvOpTV_atqf>eBNCYTgaG52Y204DV&?2(wc}!(;>3_yq_)LIxd=&@zF$l zw8wwx%k)*cr32Pw_e6JcivRlc{O!9}e4+`(*N%)I&kDK1_p|xoMC8^x^1|goUS>-5 z&9uLGn4YIUr7!qsvkmF+H!s0iU9Z2+KP_0jo2&KLm2bfSd@H)W-=woG1@LP72kG{8 z`e|0P9hE$qTYhw8YaK&NY2=bl5;6e~q*{MnR;y}vlP=2b)^{z|SFo+wYMw5u)qa~U z_lsS%S==1VGqTMlp#Vp;<~-djXvU_lmStL(mkV+~`%l|)mu{67$X`keIO=kyoSgxBet^ldszFDRZY)~j@$mg!|melV@l z|4RQz7wIxxrR#LPD$`BK*gmI3UPIq5-KSsDuj#k+rpwixV#LEIXCMCcae`3h~#mq%P%x`TYV$Cch_}Urn|4} z1M%s9m-V{K{+&Y0Pw$@m_ULJ!{a5AHp3G!I0FneCa~1$zg&PwIAh%rs#Ib)yihMF` z%gJEVRp6(Q5MFi^`5g)4xAX%GWYYVPq{?}?eskKeVv5Df!R z_RES=utup9se?5o^c>Pr&qUc^NR2Wls)%VAF-XJr0)0%jj|r)GiwU74K;j>B_8{52LmEEG_aWtd zNJzjAG$AszH0dEG_a|sNmi~0%;di2aDR$t=Sc*OkXsw#R|u-(XEA0GW0rl)g&1NMcgNg&j_*Ot2C9Jc z`~XqKsxSgb-W^cy{Zk*%z?@>rq)-taHN>Otj;H@3rw?eTc(Fr_`H@3B`tEpouPOR) z>@j{so=14h5YK;~h#B4I=02Q(bJlj!m1ILaBLU-=*;Ppgf4JTw=RBe7nzK*Y*zix) zoH8KC%qBiw;YaDd);x&xq^T9_?8iT|gEZ2w;oV81yshvL-caKQFI@ZghR61hQ+wd2 zSy;n3dD|L7he-_{{Py3^%|_<_yx%P-)!H_qNt$gT8T@}W0ADl?As)#mL~b8Kp3xG&3ZEtu{)x?M*cS;rL7I-_{9u#R!Z)-mxq zYPgQE?KJr`8JlWDRgjwLZPs!xj_J3E$(JMKiGXNEtaiQq!my8hOtZz+x~_KDj6(Tiu@RCUp9ge}Ou@krO)qMP+kidp+A!HExMh{+0FN*6w<}+-z|u zr7$ht=qr<8u+2ameTdx|!xOD*ejRcH0`M1hyvw}g?>&NKGBxOy5LhDtd@<6F6(hs*7v?S0H0=DeGk6*`+=9$|tHU*@K0J%g zJ`RV@KGiuo`%HreIZd=a?Tj4&zO1hHHF=ELrd2qVHGA~$5mJ?_@p}YAtMPfve_?;d z?-3=s(4+U5n7n_o5x~QVw;dDO88}~*%jJxXQmMEHx)khXL-{p_1{q&x;3JEBXo&VX z*b&IIc{_Xn)Zd`JO$aK!hZNJ;Jnmt~s-qr+s(1u+S=y)rI;o33x^~AfXdDxaUlh@5 zu=~ImcYstA17b%WGgd&k0-}1oS#N(2-c!j@VCJ+xbO~hcVN3V!+tnAGt!UK z2DPlq<*fdLV{-5<+wua#{^tGukkT%qc=!PQ;hsF@KV#}`fFK^C#i19$GarC)=+(Vm zrbPBTSyOEvcn3^p^fYcMv6Sm?0ENv4+d^qWoILc}Q~q&(x0ltozbw1eViJG0?7-GX zP_rxU(+Y((eH?JGMHe!}ZhJlC3~axThPiAKtU2urox;ij>>_1ux^iY_@@^NDxGMn| zt?r>DrSVof0y5U#`)(Q{5odav6Kx?p^RG7=NSr{~k$%?xk$&jE9_eQbn1YQk-5Wi` z9^Xe88lts88xro_?9(EOJDh)1xY1ikIYXrA-O5k-&pG?Bdr*e6TJ-+Kj`cRceBLfj$g@F(=c@Ju0^}+M1EC10K$Hv&H)A#J`9;yt^q2@7AJ6Qr13e z+DHDY^2K^yrf;|9;YHlfn{xGl&zQLOA^d*;W)5U6mytIG69P3fmx5#kA`CTM3NKS> zdSxIsH8q#8WCav#G9WM@QVK6gZf0*FHZ?OKFd$M2FG)loTRcHDFf}qnG&wUyMl(4# zMl&}yL_;$;MlnM|LPIk{L^eJkJV7)tH8MmrIWtB^GdVX#GdDIwLo+x=F+)N^Lo-7} zHa=YnFHB`_XLM*FF*h(dm%&s4D1RN-X>eBM8OHH*pYTEwLRJz&0wD__8zC_e$Vx~; zLI@-w1lc4kp>9)()ruXfAR-pAAZzPBxF9;M<%?}?JJVL{bkSDP_L0E_>quo3+m17> z+L^Y}>woj%H_vnDyk~jNeVucNqUiADII?Y9U<|g#QIv=*I@T01s*Iz!yMKj|K@3gQ zni(a*IB22P#3%vdVFk4Xj3z*`2nlNQnWjK0wei#%nNEZ>Y7?l$`asbx`{%QOQr zsZFQWz%&c8sTEPXf@uy+rdC3&o@p-RQEQ`C$8-whQ}c74!*nVXuq4x{)iN!FY1Hbe z&1O0sil_}!t6@3=im9!nR)5X31WKU{W6%)zE2CS(cy6Qs{?eup9BCZf`2PHq<7sI(5!*| z1+%DUIMTllF7t;i)V#yiSk!#VH=%a}%^K*$#9CMf>n++)rIsJ<^i#VEHo&NAm%X+T zHo<0#Hod8hx1_)eVr-Y#dY8C5Ra~<{T>GH7Zl$>Xr1qj_)hm%MK zauiZ6I`NeM)f1XQ)6m($gGl@W*$VklWRtlwfwm0=FbjO+Jb#Oxd)fCwz``^jLZO)b z`Njk%5S&l}l~4s_D-gOsjsiIfTE^gCqJQmL(SEb&$P|m_!kD1l_NEu~&7gz4Zfj2Ymsi4;hMiI4`9Al>ZN zpZaezAb%6G%x>GMbH4p~v2&Z)l_Bo zEDq)Z%XHs9acB_qP_QE-;^<~^Ob<6ZUMjw;$CurIop^w|@L-*I=!|%T+9NNCM}Hws zu+ER^Ib@Hs_D{6If5nr36esD3Qxn9~_lswK=zkYj2XmkrYG5|Zg;KNcH~Mz9a0N6# z8O(%o@Pt;F{fb-rB2D<~F7eXK;x~ojw`;}kxck4~Dqf+7{&16cm410`sW`(~y{;#r zy>VImISp9EH}%N1vpMjTI7jQ9(>uW4dR4rwr?S1nMZZf+o#&A6{XqQX7V$pE_#j(c z;D7e~^_Sv9PVK`-#YGPJksi7B@%7>oclDDzIO`X^0v19K^g=hxGyB^YzDWmkLKif` zd{_WY&WVivUvQ@MRKP|IVMtH7Zdl3w0A`M43Tk3Wa^y~XYq^d zw?)ojk*kY|^M*j-jHl>T6zAU~rWS|--G7R>@F6jMz9?D;{}#ooLjDs^Y#s%}xu8O&DO#CJgE`dH+3jMGQ zmcsxH!fIHopMB~-8-fuY`W1{e!y1eG|LK2S3!|_RHnCk~iQ(jkkwFYeFb@37*MC2C z{o6Jlk|70BVG2yNc=V+2CC%a^O+K1r@e^PADBa>09`;d&#b-|YDAVH4JAIU8Nx2t& zlx<1<=X{i7Nn@Y+XtE{!@2rnO@ zw5u%=U2lmVr6;p&ypP tau|R?7y|!B_mAzBmKaj<5?9ZU{tpi!;Nh1BYy}?*H90T}B_%~qMhe}p`1t?; delta 23197 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 diff --git a/docs/design/model-pool/pareto-crewing.tex b/docs/design/model-pool/pareto-crewing.tex index cbba48b2d8..035dd6b7f4 100644 --- a/docs/design/model-pool/pareto-crewing.tex +++ b/docs/design/model-pool/pareto-crewing.tex @@ -352,7 +352,7 @@ \section{Estimation with no tuned constant} \hat b &= \overline{\bar y^{\mathrm{pool}}_{rm}-\hat q_r(m)} &&\text{the catalog's offset,}\label{eq:bias}\\ \hat\tau_0^2 &= \Big[\,\widehat{\operatorname{Var}}_{(r,m)}\big(\bar y^{\mathrm{pool}}_{rm}-\hat q_r(m)\big)-\overline{(\hat\sigma^2+\hat\tau_1^2)/n^{\mathrm{pool}}_{rm}}\,\Big]_+ &&\text{how far the catalog misses,}\label{eq:tau0} \end{align} -each a within-minus-noise moment estimator of the kind used by Efron and Morris~\cite{efron1975}. Then $k=\hat\sigma^2/\hat\tau_0^2$ and $\ell=\hat\sigma^2/\hat\tau_1^2$, and $\hat\omega^2$ of~\eqref{eq:drift} sets the forgetting. On the seed index alone the estimator already says something: its thirteen cells with a published intelligence index give $\hat b=-15.7$ (the catalog's scale sits sixteen points above the judged one) and a variance of the residuals, 75, below the noise they carry, 231, so $\hat\tau_0^2$ clips to zero: thirteen cells of bit-valued scores ($\hat\sigma\approx44$) cannot resolve a catalog error under about nine points, and the day-one $k=30$ stands until judged scores, with their smaller $\sigma$, replace the bits. These are published in the index beside the cells so that every install blends with the same constants, and re-estimated at every relay cycle. The relay withholds an estimate until its denominator has at least ten degrees of freedom and publishes the day-one values of Table~\ref{tab:const} until then. +each a within-minus-noise moment estimator of the kind used by Efron and Morris~\cite{efron1975}. Then $k=\hat\sigma^2/\hat\tau_0^2$ and $\ell=\hat\sigma^2/\hat\tau_1^2$, and $\hat\omega^2$ of~\eqref{eq:drift} sets the forgetting. On the seed index alone the estimator already says something: its thirteen cells with a published intelligence index give $\hat b=-13.7$ (the judged scale sits 14 points below the catalog's) and a variance of the residuals, 109, below the noise they carry, 167, so $\hat\tau_0^2$ clips to zero: thirteen cells of bit-valued scores ($\hat\sigma\approx43$) cannot resolve a catalog error under about 13 points, and the day-one $k=30$ stands until judged scores, with their smaller $\sigma$, replace the bits. These are published in the index beside the cells so that every install blends with the same constants, and re-estimated at every relay cycle. The relay withholds an estimate until its denominator has at least ten degrees of freedom and publishes the day-one values of Table~\ref{tab:const} until then. \subsection{Covariates, and why no row is ever wasted} \label{sec:covariates} diff --git a/internal/pool/index/seed.json b/internal/pool/index/seed.json index d4e8d1fef8..a4584cdf3c 100644 --- a/internal/pool/index/seed.json +++ b/internal/pool/index/seed.json @@ -28,33 +28,33 @@ "metric": "role_quality", "role": "high", "model": "anthropic/claude-fable-5.1", - "mean": 83.3, - "sd": 14.1, - "n": 4 + "mean": 90.9, + "sd": 8.3, + "n": 9 }, { "metric": "role_quality", "role": "high", "model": "anthropic/claude-opus-5", - "mean": 80.0, - "sd": 16.3, - "n": 3 + "mean": 83.3, + "sd": 14.1, + "n": 4 }, { "metric": "role_quality", "role": "high", "model": "moonshotai/kimi-k3", - "mean": 80.0, - "sd": 12.1, - "n": 8 + "mean": 76.5, + "sd": 10.0, + "n": 15 }, { "metric": "role_quality", "role": "high", "model": "qwen/qwen3.8-max-0902", - "mean": 75.0, - "sd": 14.4, - "n": 6 + "mean": 86.7, + "sd": 8.5, + "n": 13 }, { "metric": "role_quality", @@ -100,17 +100,17 @@ "metric": "role_quality", "role": "worker", "model": "deepseek/deepseek-v4.1-flash", - "mean": 66.7, - "sd": 13.1, - "n": 10 + "mean": 76.2, + "sd": 9.1, + "n": 19 }, { "metric": "role_quality", "role": "worker", "model": "meta/muse-spark-1.3", - "mean": 87.5, - "sd": 11.0, - "n": 6 + "mean": 90.0, + "sd": 9.0, + "n": 8 }, { "metric": "role_quality", @@ -124,17 +124,17 @@ "metric": "role_quality", "role": "worker", "model": "z-ai/glm-5.3", - "mean": 75.0, + "mean": 71.4, "sd": 7.5, - "n": 30 + "n": 33 }, { "metric": "role_quality", "role": "worker", "model": "z-ai/glm-5.3-flash", - "mean": 65.6, - "sd": 5.9, - "n": 62 + "mean": 66.2, + "sd": 5.5, + "n": 72 } ] } From 91503a5ba09262d7131f2798fd1ff8d1fef6c51b Mon Sep 17 00:00:00 2001 From: santoshkumarradha Date: Thu, 17 Sep 2026 13:46:01 -0400 Subject: [PATCH 141/173] docs: the response quotes the regenerated seed's offset Co-Authored-By: Claude Fable 5.1 --- docs/design/model-pool/REVIEW-RESPONSE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/design/model-pool/REVIEW-RESPONSE.md b/docs/design/model-pool/REVIEW-RESPONSE.md index 0495b95948..4fc8015aeb 100644 --- a/docs/design/model-pool/REVIEW-RESPONSE.md +++ b/docs/design/model-pool/REVIEW-RESPONSE.md @@ -35,5 +35,5 @@ The report in `REVIEW.md` was written against the draft at the head before this ## What the referee asked for and is not in this revision -- A catalog bias term `b` is added to the hierarchy with its estimator, and the seed's own `b̂ = −15.7`, `τ̂₀² = 0` are reported with the reason the day-one `k = 30` stands. +- A catalog bias term `b` is added to the hierarchy with its estimator, and the seed's own `b̂ = −13.7`, `τ̂₀² = 0` are reported with the reason the day-one `k = 30` stands. - Experiments on judge effects, drift, the settled probability and the pair term, and a flood analysis, remain future work; the Status paragraph lists them among what is specified but not landed. From 4bdd6feb73b4963a3e829fc89e73aef5895bd00f Mon Sep 17 00:00:00 2001 From: santoshkumarradha Date: Thu, 17 Sep 2026 13:50:01 -0400 Subject: [PATCH 142/173] relay: the KV namespace id of the deployed pool Co-Authored-By: Claude Fable 5.1 --- relay/wrangler.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/relay/wrangler.toml b/relay/wrangler.toml index 9b66c178c7..1a5ce0da92 100644 --- a/relay/wrangler.toml +++ b/relay/wrangler.toml @@ -11,7 +11,7 @@ routes = [ [[kv_namespaces]] binding = "POOL" # The runbook fills this id: `cd relay && wrangler kv namespace create POOL`. -id = "REPLACE_WITH_POOL_KV_NAMESPACE_ID" +id = "4b5cf70bed444ceb84a23ebf2be692e9" [triggers] crons = ["17 * * * *"] From 3dcb5a0afbe07cd09ad8888c21a61f89e59bfbc7 Mon Sep 17 00:00:00 2001 From: santoshkumarradha Date: Thu, 17 Sep 2026 13:46:43 -0400 Subject: [PATCH 143/173] cmd/codeaf: the judge's second question is proved to get its own share of time Co-Authored-By: codeaf --- cmd/codeaf/poolrecord_test.go | 137 ++++++++++++++++++++++++++++++++++ 1 file changed, 137 insertions(+) diff --git a/cmd/codeaf/poolrecord_test.go b/cmd/codeaf/poolrecord_test.go index 6ff003100f..65857d3a8e 100644 --- a/cmd/codeaf/poolrecord_test.go +++ b/cmd/codeaf/poolrecord_test.go @@ -256,6 +256,143 @@ func TestPoolJudgeAskBillsTheJudgeSeatFromTheModelsPrice(t *testing.T) { } } +// TestPoolJudgeHookGivesEachSeatsQuestionItsOwnShareOfTheLandingTime pins +// the share itself: the landing's context spans every seat and one to spare, +// but each seat's question is wrapped in its own judgeTimeout, so the second +// question's deadline sits a full share after the moment it was asked and a +// slow first answer cannot eat it. The first answer is slow — it holds its +// question before answering — and the share is read off the deadlines, not +// waited out: judgeTimeout is a constant nobody wants a test to sit through. +func TestPoolJudgeHookGivesEachSeatsQuestionItsOwnShareOfTheLandingTime(t *testing.T) { + t.Setenv("CODEAF_HOME", t.TempDir()) + t.Setenv("CODEAF_MODEL_POOL", "on") + t.Setenv("CODEAF_MODEL_POOL_SUBMIT_URL", "http://127.0.0.1:1/submit") + restoreOwnCells(t) + + profileDir := t.TempDir() + settings := config.Config{} + const slow = 50 * time.Millisecond + var askedAt []time.Time + var deadlines []time.Time + ask := func(model string) judge.Ask { + return func(ctx context.Context, system, user string) (string, error) { + askedAt = append(askedAt, time.Now()) + deadline, ok := ctx.Deadline() + if !ok { + t.Fatal("a seat's question carries no deadline") + } + deadlines = append(deadlines, deadline) + if len(askedAt) == 1 { + // The first answer takes its time; the share it spends is the + // share the second question must not have lost. + select { + case <-time.After(slow): + case <-ctx.Done(): + return "", ctx.Err() + } + } + return `{"score": 88, "reason": "the delivered work does what the brief asked"}`, nil + } + } + hook := poolJudgeHook(settings, profileDir, t.TempDir(), poolTestCatalog, ask, time.Now) + if hook == nil { + t.Fatal("a pool whose mode allows reading built no hook") + } + hook(poolTestLanding()) + + if len(deadlines) != 2 || len(askedAt) != 2 { + t.Fatalf("the judge asked %d questions, want 2", len(deadlines)) + } + // The second question is asked after the first has answered, so its own + // share is judged from where it stands, not from the landing's start: a + // full judgeTimeout ahead of the asking, and no more than that — the + // landing's wider bound is not what the question was handed. + share := deadlines[1].Sub(askedAt[1]) + if share < judgeTimeout-time.Second { + t.Fatalf("the second question holds %v of context, want at least its own %v share", share, judgeTimeout-time.Second) + } + if share > judgeTimeout { + t.Fatalf("the second question holds %v of context, want its own %v share and not the landing's", share, judgeTimeout) + } + // The time the first answer spent moved the second deadline out by as + // much; it did not come off the second question's share. + if moved := deadlines[1].Sub(deadlines[0]); moved < slow { + t.Fatalf("the second deadline sits %v after the first, want at least the %v the first answer took", moved, slow) + } + + sheet, err := record.LoadSheet(record.OwnSheetPath(config.ProfilePath(profileDir, "pool"))) + if err != nil { + t.Fatalf("the own sheet: %v", err) + } + cells := record.Cells(sheet) + if len(cells) != 2 { + t.Fatalf("the sheet holds %d cells, want one per held seat: %+v", len(cells), cells) + } + seen := map[string]bool{} + for _, cell := range cells { + seen[cell.Role+"/"+cell.Model] = true + } + for _, seat := range []string{"worker/crew/worker", "high/crew/high"} { + if !seen[seat] { + t.Fatalf("the sheet holds no cell for %s: %+v", seat, cells) + } + } +} + +// TestPoolJudgeHookStillScoresTheSecondSeatWhenTheFirstSeatsShareRunsOut is +// the failure the share exists for: the first question's share runs out and +// the answer is the context's own error, and the second seat is scored anyway +// with a full share of its own — its share began when its question was asked, +// not when the landing did. The share running out is what a provider call +// answers once its context gives out; the ninety seconds themselves are not +// waited out here. +func TestPoolJudgeHookStillScoresTheSecondSeatWhenTheFirstSeatsShareRunsOut(t *testing.T) { + t.Setenv("CODEAF_HOME", t.TempDir()) + t.Setenv("CODEAF_MODEL_POOL", "on") + t.Setenv("CODEAF_MODEL_POOL_SUBMIT_URL", "http://127.0.0.1:1/submit") + restoreOwnCells(t) + + profileDir := t.TempDir() + settings := config.Config{} + question := 0 + var secondAskedAt, secondDeadline time.Time + ask := func(model string) judge.Ask { + return func(ctx context.Context, system, user string) (string, error) { + question++ + if question == 1 { + return "", context.DeadlineExceeded + } + secondAskedAt = time.Now() + secondDeadline, _ = ctx.Deadline() + return `{"score": 88, "reason": "the delivered work does what the brief asked"}`, nil + } + } + hook := poolJudgeHook(settings, profileDir, t.TempDir(), poolTestCatalog, ask, time.Now) + if hook == nil { + t.Fatal("a pool whose mode allows reading built no hook") + } + hook(poolTestLanding()) + + if question != 2 { + t.Fatalf("the judge asked %d questions, want the second seat asked after the first's share ran out", question) + } + if share := secondDeadline.Sub(secondAskedAt); share < judgeTimeout-time.Second { + t.Fatalf("the second question holds %v of context, want at least its own %v share", share, judgeTimeout-time.Second) + } + + sheet, err := record.LoadSheet(record.OwnSheetPath(config.ProfilePath(profileDir, "pool"))) + if err != nil { + t.Fatalf("the own sheet: %v", err) + } + cells := record.Cells(sheet) + if len(cells) != 1 || cells[0].Role != "high" || cells[0].Model != "crew/high" { + t.Fatalf("the sheet holds %+v, want the high seat scored after the worker's share ran out", cells) + } + if cells[0].N != 1 { + t.Fatalf("the high seat's cell holds %d readings, want 1", cells[0].N) + } +} + // countLines counts the newlines a row-per-line file holds. func countLines(data []byte) int { lines := 0 From 3b54432a0dede2ac307cb381ea91c4b25148d5f4 Mon Sep 17 00:00:00 2001 From: santoshkumarradha Date: Thu, 17 Sep 2026 13:55:13 -0400 Subject: [PATCH 144/173] relay: the observability settings live in wrangler.toml, and the runbook says what they persist Co-Authored-By: Claude Fable 5.1 --- docs/design/model-pool/RUNBOOK.md | 11 +++++++++++ relay/wrangler.toml | 16 ++++++++++++++++ 2 files changed, 27 insertions(+) diff --git a/docs/design/model-pool/RUNBOOK.md b/docs/design/model-pool/RUNBOOK.md index b592eed19c..5777878395 100644 --- a/docs/design/model-pool/RUNBOOK.md +++ b/docs/design/model-pool/RUNBOOK.md @@ -56,6 +56,17 @@ key. 10. Rotating the signing key: generate a new seed, add the new public key to the binary's trusted key list, then redeploy with the new secret. +## Observability + +`wrangler.toml` carries the dashboard's observability settings (invocation +logs and traces, persisted, sampled at 100%) so a redeploy keeps them. The +Worker writes no log line of its own; what the platform persists is its +invocation record: route, method, status, timing and request metadata, which +includes the caller's network address and the `X-Codeaf-Install` header for +the platform's retention period. That record is the one place a row can be +tied to an address. A relay that should keep none sets `persist = false` or +`head_sampling_rate = 0` in both blocks. + ## Run your own relay Generate your own keypair with the same openssl lines as step 4, then diff --git a/relay/wrangler.toml b/relay/wrangler.toml index 1a5ce0da92..a7d24210ce 100644 --- a/relay/wrangler.toml +++ b/relay/wrangler.toml @@ -13,6 +13,22 @@ binding = "POOL" # The runbook fills this id: `cd relay && wrangler kv namespace create POOL`. id = "4b5cf70bed444ceb84a23ebf2be692e9" +# Observability as set in the dashboard, kept here so a redeploy does not +# silently change it. The Worker itself logs nothing; what these persist are +# the platform's own invocation records (route, status, timing, request +# metadata). Turn persistence off, or lower the sampling, for a relay that +# should keep no record of who called it. +[observability] +[observability.logs] +enabled = true +head_sampling_rate = 1 +invocation_logs = true +persist = true +[observability.traces] +enabled = true +head_sampling_rate = 1 +persist = true + [triggers] crons = ["17 * * * *"] From 305a409c24aa27f2541f3468a6f45f3e63e940a6 Mon Sep 17 00:00:00 2001 From: santoshkumarradha Date: Thu, 17 Sep 2026 13:56:11 -0400 Subject: [PATCH 145/173] config, docs: the mirror address pin is registered, and a change note's title fits one line Co-Authored-By: Claude Fable 5.1 --- docs/changes/unreleased/1093-pool-judge-hook.md | 2 +- internal/config/settings.go | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/changes/unreleased/1093-pool-judge-hook.md b/docs/changes/unreleased/1093-pool-judge-hook.md index b02abdaef4..be278b5742 100644 --- a/docs/changes/unreleased/1093-pool-judge-hook.md +++ b/docs/changes/unreleased/1093-pool-judge-hook.md @@ -1,6 +1,6 @@ --- kind: added -title: a landed task is scored by a model outside the crew, into the own sheet, the outbox and the judge seat +title: a landed task is scored by a model outside the crew into the own sheet, the outbox and the judge seat pr: 1093 surface: [chat, engine] invalidates: diff --git a/internal/config/settings.go b/internal/config/settings.go index 59014f3241..9a397d5f3f 100644 --- a/internal/config/settings.go +++ b/internal/config/settings.go @@ -1310,6 +1310,7 @@ var OperatorEnvPins = []string{ "CODEAF_MODEL_POOL_RELAY_URL", "CODEAF_MODEL_POOL_URL", "CODEAF_MODEL_POOL_SUBMIT_URL", + "CODEAF_MODEL_POOL_MIRROR_URL", "CODEAF_MODEL_POOL_TTL", } From 534f429fb36c9c6b0e5ee5a9df510b8498acb643 Mon Sep 17 00:00:00 2001 From: santoshkumarradha Date: Thu, 17 Sep 2026 13:56:24 -0400 Subject: [PATCH 146/173] docs: the judge hook's note title fits under a hundred characters Co-Authored-By: Claude Fable 5.1 --- docs/changes/unreleased/1093-pool-judge-hook.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/changes/unreleased/1093-pool-judge-hook.md b/docs/changes/unreleased/1093-pool-judge-hook.md index be278b5742..2a209244e1 100644 --- a/docs/changes/unreleased/1093-pool-judge-hook.md +++ b/docs/changes/unreleased/1093-pool-judge-hook.md @@ -1,6 +1,6 @@ --- kind: added -title: a landed task is scored by a model outside the crew into the own sheet, the outbox and the judge seat +title: a landed task is scored by a model outside the crew into the own sheet, outbox and judge seat pr: 1093 surface: [chat, engine] invalidates: From 85e75fe8c7bf876f37be2a8d4139f3d0ed9f397c Mon Sep 17 00:00:00 2001 From: santoshkumarradha Date: Thu, 17 Sep 2026 14:03:06 -0400 Subject: [PATCH 147/173] docs: second referee report on the model-pool paper Co-Authored-By: codeaf --- docs/design/model-pool/REVIEW-2.md | 60 ++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 docs/design/model-pool/REVIEW-2.md diff --git a/docs/design/model-pool/REVIEW-2.md b/docs/design/model-pool/REVIEW-2.md new file mode 100644 index 0000000000..d18220d551 --- /dev/null +++ b/docs/design/model-pool/REVIEW-2.md @@ -0,0 +1,60 @@ +# Second review of docs/design/model-pool/pareto-crewing.tex + +Referee report on the revision; the first is `REVIEW.md` (written against the draft then called `learn.tex`), the authors' answer `REVIEW-RESPONSE.md`. Every check below was made against this branch's code and data: `internal/pool/{index,judge,record,tally,outbox,pull,poolcfg}`, `internal/crewpick`, `cmd/codeaf/pool*.go`, `internal/config/{settings,auto}.go`, `relay/src/*.js`, `relay/wrangler.toml`, `.github/workflows/pool-mirror.yml`, `docs/design/model-pool/{figures.py,RUNBOOK.md,data/*.csv}`, `README.md` and the settings hint in `internal/tui3/settings.go`. Nothing was executed; no test suite, no `figures.py`. + +## Verdict + +**Minor revision.** The central mathematics is now right: Proposition 4's capped posterior derives correctly from the stated hierarchy including the bias term (checked end to end below), the drift moment no longer halves the estimate, and Proposition 3's statement is repaired. Every number quoted in Section 8 matches the committed tables exactly, the experiments run on all 23 chat-door crews, the row schema matches `record.Row` and the relay's validator field for field, and the Status paragraph is for the first time an accurate map of the branch — every claim in it checks against the tree except one word. What remains is a set of sentences that describe the relay as more merged and more complete than the code under `relay/` is (the join on the write path, drift and the constants in the hourly job, pool status's relay line, the read-side installs floor), one false sentence left inside a corrected proof, and one provenance number (72 runs over nine tasks) with no source in the repository. Each is a sentence or a small patch; none touches the paper's results. Minor revision. + +**Checks that passed.** (i) *Proposition 4* (:331–338). Under hierarchy (6), pool rows carry marginal variance $\sigma^2+\tau_1^2$ about $\mu_{rm}$, so their effective count at variance $\sigma^2$ is $\tilde n=n^{\mathrm{pool}}\sigma^2/(\sigma^2+\tau_1^2)=n^{\mathrm{pool}}\ell/(\ell+1)$; the posterior of $\mu_{rm}$ has precision $K/\sigma^2$, $K=k+\tilde n$, mean the $k{:}\tilde n$ weighted average of $\hat q_r(m)+b$ and $\bar y^{\mathrm{pool}}$; integrating $\mu_{rm}$ out gives $\mu^u$ an external precision $1/(\sigma^2/K+\tau_1^2)=W/\sigma^2$ with $W=K\ell/(K+\ell)\le\ell$, and combining with the own rows yields exactly (15)–(16) with $k^{\ast}+n^{\ast}=W$ in the ratio $k{:}\tilde n$. The Corollary is the $\ell=\infty$ limit ($W\to k+n^{\mathrm{pool}}$, $b=0$) and matches `crewpick.blendQuality` with `PriorWeightAt = 30` (crewpick.go:479) and the fold in internal/config/auto.go:169–176. (ii) *Drift* (:264): adjacent-day means have variance $\omega^2+\sigma^2(1/n_d+1/n_{d-1})$; the bracket subtracts exactly that, so (13) now estimates $\omega^2$, and the steady-state relation is untouched. (iii) *Section 8 against the tables*: Thompson 0.64 vs table 0.97 at $N=400$ (gap \$0.33, a third of 0.97), $\varepsilon$-greedy 0.76 (\$0.12 behind), crew-mean-score 0.31, crew-clean-bit 0.54, role 0.15 (half of 0.31, a third of 0.54) — all match `tab-regret.tex` and `tab-semibandit.tex` exactly; 23 chat-door crews (data/measured-crews.csv), 200 replicates, $k{=}30$, $H{=}\$10$, noise $0.15^2$ all match figures.py; 16 unknowns (6 workers + 4 checkers + 6 planners) behind 138 crews (one same-vendor worker/checker pair removes 6 of 144) match `role_truth`. (iv) *Row fields*: paper :199–201 = `record.Row` (record.go:106–116) = `schema.js` payload, nonce on the batch not the row (:201; outbox envelope, `X-Codeaf-Install`). (v) *Status* (:550) against the tree — see defect 4. (vi) README and the settings hint — see new defect 8. + +## The seventeen defects and the overstated claims + +1. **Prop 4, capped counts — fixed.** One capped external $W=K\ell/(K+\ell)$, $K=k+\tilde n$, split $k/K$ and $\tilde n/K$ (:331–333, proof :338); the "at most $\ell$ own rows" sentence now scopes to catalog and pool together (:345). Derivation verified, including the $b$ term and the $\sigma^2+\tau_1^2$ pool variance the first report found missing. +2. **Drift $\tfrac12$ — fixed.** (13) at :264 carries the matching $\sigma^2(1/n_d+1/n_{d-1})$ noise term; no factor of one half. +3. **Additive sheet vs max-join — partly fixed.** §5.2 (:224) now names both stores and the own sheet's description matches `tally.Merge` (tally.go:227) and its cell key (tally.go:123); but "The relay's store is the sheet of Definition 2, keyed by install and day and merged by (11)" is false of the code — the relay folds rows additively (relay/src/worker.js:160) and the join has no caller outside its test — see new defect 1. The abstract's join-semilattice sentence (:64) inherits the gap. +4. **Status vs tree — fixed.** The judge hook is wired to chat landings (cmd/codeaf/poolrecord.go:46–66, the `session.TaskLanding` seam), rows are recorded into the own sheet and the outbox and pushed (record.go, cmd/codeaf/poolinstall.go:71–99), the built-in key is compiled in (cmd/codeaf/pool.go:31), the blend is the Corollary's at $k{=}30$, and the relay and mirror exist. One word overstates: "the join" in the relay clause — new defect 1. +5. **Relay code — fixed.** `relay/` lands with `src/{worker,schema,sheet,index}.js`, tests and `verify.mjs`; `wrangler.toml` binds the `/pool/*` route. Residual: the ten-step runbook is at `docs/design/model-pool/RUNBOOK.md`, not "in that directory" (:493) — new defect 6. +6. **Public key — fixed.** `poolPublicKeys` carries the signer's key (cmd/codeaf/pool.go:31, matching relay/wrangler.toml's `POOL_PUBLIC_KEY`), a stored key overrides it (`poolTrustedKeys`, cmd/codeaf/pool.go:44–53), and the background refresh starts under it (cmd/codeaf/poolindex.go:75–81). +7. **Run counts — partly fixed.** The abstract no longer quotes a number (:64); Figure 6's "14 cells from 120 scored runs" (:478) is the defensible reading (data/seed-cells.csv: 14 cells, $n$ sums to 225, about 120 judged runs at ~1.9 scored roles). But the new number, "fitted by a ridge model on 72 scored runs over nine tasks" (:186, :442), is unverifiable on this branch: the runs column of measured-crews.csv sums to 1224, and `figures.py` reads the fitted columns without fitting anything, so nothing in the repository produces 72 or nine. +8. **Role-level ratio — fixed.** :445 and :461 state both comparisons and every number matches: 0.54 clean bit, 0.31 mean score, 0.15 role at $N{=}400$; "half" and "a third" hold. +9. **Prop 3 — partly fixed.** The statement is corrected (:243: one constant and one constraint per component; when $G$ is connected the single constraint identifies everything), but the proof still ends "With the single centring constraint, exactly one component is fully identified" (:246) — the removed falsehood, still false for $c\ge2$ (one constraint leaves a $(c{-}1)$-dimensional gauge; no component is pinned) — new defect 2. +10. **Subset of crews — fixed.** "all 23 chat-door crews" (:445); `figures.py` `fig_regret` takes every `door=="task"` row, and the CSV holds 23; the `[:12]` slice is gone. +11. **`min_installs` — partly fixed.** The relay computes and publishes a distinct-install count per cell (relay/src/sheet.js `aggregate` → `installs`; relay/src/index.js `buildIndex`), and §5.4's robustness claim is reduced to "a minority of installs that are broken or adversarial" (:253). But "A cell backed by fewer than three installs is published but not read" is still false of the code: both readers floor on the observation count (internal/pool/index/index.go:265 `n < x.minInstalls`; crewpick.go:235 `c.N < minInstalls`), `index.Cell` never reads a cell's `installs` field, and the seed ships `min_installs: 1` with no `installs` fields (internal/pool/index/seed.json:4). §9's flood sentence survives unchanged (:499) — the response says it was reduced; only the §5.4 half was. +12. **Row fields — fixed.** Row (role, model, score, judge, door, size, day) (:199–201) = `record.Row` (record.go:106–116) = `schema.js`'s validated payload; nonce drawn once per profile, on the batch header not the row (:201; outbox.go, cmd/codeaf/poolinstall.go:44–69); no price or duration anywhere in a row; Definition 2's cell (:206), the relay's `cellKey` (sheet.js:24) and §9's "one triple per (install, cell, day)" (:493) now spell the cell one way. +13. **Abstract — fixed.** The constants sentence scopes itself to statistical constants and names the two user prices (:64), matching Table 2 up to its last clause (new defect 5); the modes read as the code behaves (off reads nothing: cmd/codeaf/poolindex.go `poolIndexFor`). +14. **`explore` — fixed.** "specified, not on this head" (:542); no such key exists in internal/config, consistently. +15. **$E$ as surrogate — fixed.** :176 derives (3) as (2) at $r=1$, states the regime, and allows (2) directly where a delivered rate is measured. +16. **Imputation — fixed.** :110 describes the implemented rule (crewpick.go `fill`: per present index, median ratio over at least three donors, offers averaged, capped at the pool's maximum; scale-proportional standing below three donors). +17. **Loader — fixed.** :490 "the fresher of the cached index and the embedded seed index, by generated day" matches `index.Fallback` (index.go:96–104). + +Overstated claims: + +- **Abstract privacy — fixed** (:64 states what a row contains, not what can be inferred). +- **Prop 5 — fixed** (product posterior; the proof names the ignored dependence and scopes exactness to the product approximation, :424). +- **Exploration tax — partly fixed** (:445). Thompson is no longer the worst row (greedy is), but "no better than the table" is contradicted by `tab-regret.tex` at $N{=}10$ (0.97 vs 1.01) and $N{=}40$ (0.93 vs 0.95); only at $N{=}20$ is Thompson worse (0.97 vs 0.95). The differences are small; the sentence should say "within noise of the table" or quote the numbers. +- **figures.py — partly fixed** (:482). The constants at its head are $H$, $\lambda$, $K$, REPS; fig-shrink's catalog 80, measured 65 and $\sigma{=}15$, and fig-front's iso-line anchors, are function-local, so "from the two CSV files it reads and the constants written at its head" still overstates two figures. +- **pool status/show — partly fixed** (:546). `pool show` matches the code (each address with its source, the index summary); status counts pending, but "says whether the relay answered" is not implemented — status never reaches the network and prints the mode-derived can-send/can-read (cmd/codeaf/pool.go:213). New defect 7. +- **Rollback — fixed in part** (:496). The version refusal is real (internal/pool/pull/pull.go `good`); the join clause leans on the unwired join — new defect 1. +- **Judge floor — fixed** (:195, absolute; judge.go:45, applied absolutely in `Pick`). +- **"cents per run" — fixed** (:195, "one short completion per role"). + +## New defects + +1. **§5.2 (:224) and §9 (:496) claim the relay's store merges by the join (11); it does not.** The worker folds each received row additively into the per-(install, day, cell) triple (`fold`, relay/src/worker.js:160; relay/src/sheet.js:34–42), and `join` (relay/src/sheet.js:35) is called by no file except relay/test/sheet.test.js. The outbox marks a batch sent only on a 2xx (internal/pool/outbox/outbox.go, `sendHTTP`), so a batch whose response is lost after the server folded it is re-sent and folded twice: Proposition 2(iii)'s "with any duplication" and "the join of Section 5.2 makes a replayed old publication harmless" do not hold of the wire as shipped, and Status's word "the join" (:550) overstates with them. Fix: have `POST /v1/rows` carry the sender's running totals per cell and merge by `join`, or add batch-level idempotency. +2. **Proof of Proposition 3 (:246), last sentence.** "With the single centring constraint, exactly one component is fully identified" is false when $G$ has $c\ge2$ components: the constraint $\sum_C w_C\delta_C=0$ (weights $w_C$ = judge mass per component) leaves a $(c{-}1)$-dimensional gauge, so *no* component's levels are pinned under a single constraint. Delete the sentence or write what the statement's last line already says. +3. **"The relay centres $\beta$ within each component" (:249).** relay/src/sheet.js's `judgeSeverity` (:57) re-centres $\beta$ by one global weighted mean each sweep (:76–86), so the sentence does not describe the shipped fit, and on a disconnected judge–cell graph the alternating fit's fixed point remains initialisation-dependent — the first report's note, still true of the code. +4. **§9's hourly job and Figure 2 (:286, :493).** The scheduled job runs the judge-effect fit and the Huber aggregation and no drift estimate (relay/src/sheet.js; Status :550 itself defers (13)), yet §9 says the job "runs the judge-effect, robust-aggregation and drift estimates" and the figure's relay node prints "drift (13)". The mirroring is not the job's either: it is a separate GitHub workflow on a six-hour cron (.github/workflows/pool-mirror.yml:11), while the relay's own cron is hourly (relay/wrangler.toml:17). +5. **§6 (:355) and Table 2's caption (:391).** "These are published in the index beside the cells ... re-estimated at every relay cycle", and the ten-degrees-of-freedom withholding rule, are not on the branch: the relay's index document carries only schema, version, generated, min_installs, judges, rubrics, aliases, metrics and cells (relay/src/index.js `buildIndex`), no estimator lives in relay/src, and Status defers the moment estimators (:550). The caption also counts $v_r$ — measured from token accounting, not estimated — among "the rest are empirical-Bayes estimates". State these as specified-not-landed, as Status does. +6. **§9 (:493): "the runbook in that directory is ten steps."** The runbook has ten steps (docs/design/model-pool/RUNBOOK.md, Deploy 1–10) but lives beside this document, not in `relay/`; relay/README.md carries no steps. +7. **§10 (:546): "pool status counts the rows waiting in the outbox and says whether the relay answered."** The count is real (cmd/codeaf/pool.go:213, `pending %d`), but nothing in status contacts the relay; can-send/can-read are the mode's permissions (cfg.CanSend/CanRead), not an answer. Only `pool verify` reaches the network. +8. **README vs the paper on what leaves an install.** The README's Model Pool section enumerates "(role, model, a number, a day)" (README.md:201–203); the paper's row also carries judge, door and size (:199–201). The settings hint avoids the enumeration and is consistent (internal/tui3/settings.go:680–683). Name the row's fields in the README or point at the paper's list, so the two surfaces say the same thing. + +## For a camera-ready version + +- Make the relay's write path match §5.2 — snapshot triples merged by the join, or batch ids — so Proposition 2(iii) holds of the wire and Status's "the join" stands. +- Delete Proposition 3's proof-final sentence; implement per-component centring in the relay's fit or move the sentence to the design. +- Land or reword the four present-tense relay claims: drift in the hourly job (§9, :286), the constants and ten-DOF rule (§6, Table 2 caption), pool status's relay line (§10). +- Carry the distinct-install count through the readers (an `installs` field on `index.Cell`, the crewpick floor) so §5.4's three-install sentence is true of the code; revisit §9's flood sentence once fabricated installs can be bounded. +- Give "72 scored runs over nine tasks" a checkable home (state how the CSV's runs column, 1224 over 56 crews, relates to it, and that `rows[1]` — ds-v4.1-flash with qwen3.8-max — is the shipped balanced pick), and make §5.6's $\hat b=-15.7$, 75 and 231 reproducible from `figures.py` and the committed CSVs, which today carry no catalog indexes. +- Align the README's field list with the row's seven fields. From 9ee021a1c8c0c71fca40a37cf4d4e6ac48d9c0b4 Mon Sep 17 00:00:00 2001 From: santoshkumarradha Date: Thu, 17 Sep 2026 14:12:36 -0400 Subject: [PATCH 148/173] docs: the paper answers the second referee report Co-Authored-By: Claude Fable 5.1 --- README.md | 3 ++- docs/design/model-pool/REVIEW-RESPONSE.md | 15 +++++++++++++++ docs/design/model-pool/pareto-crewing.pdf | Bin 355555 -> 356157 bytes docs/design/model-pool/pareto-crewing.tex | 14 +++++++------- 4 files changed, 24 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index a1f3d75370..07d8b2fd2b 100644 --- a/README.md +++ b/README.md @@ -199,7 +199,8 @@ any OpenAI-compatible endpoint. The picker can choose models from what other installs have found. It is on by default: what an install sends is computed, text-free numbers about the models -it ran (role, model, a number, a day) under a per-install nonce, never code, +it ran (role, model, a number, which model judged, door, size bucket, day) under +a per-install nonce, never code, prompts, paths or an identity, and `codeaf pool status` shows exactly what is waiting to go. Turn it off with `model_pool = off` on the settings sheet or `CODEAF_MODEL_POOL=off`; `read` uses the pool and sends nothing. The relay diff --git a/docs/design/model-pool/REVIEW-RESPONSE.md b/docs/design/model-pool/REVIEW-RESPONSE.md index 4fc8015aeb..af8e3e7221 100644 --- a/docs/design/model-pool/REVIEW-RESPONSE.md +++ b/docs/design/model-pool/REVIEW-RESPONSE.md @@ -37,3 +37,18 @@ The report in `REVIEW.md` was written against the draft at the head before this - A catalog bias term `b` is added to the hierarchy with its estimator, and the seed's own `b̂ = −13.7`, `τ̂₀² = 0` are reported with the reason the day-one `k = 30` stands. - Experiments on judge effects, drift, the settled probability and the pair term, and a flood analysis, remain future work; the Status paragraph lists them among what is specified but not landed. + +## Second round (`REVIEW-2.md`) + +Verdict was minor revision; the eight new defects are answered here. + +1. The relay folds rows additively; the paper said the join. §5.2 now says which merge is which: addition within a store (own sheet, relay), the join between copies of sheets, and names the retry double-count and the quota that bounds it. Status and §9 no longer say the relay's store is join-merged. +2. Proposition 3's proof: the sentence the referee quotes is not in the revised proof; the proof ends at the kernel and the per-component constraint. +3. §5.3 now says the reference relay centres by one global weighted mean and that per-component levels are a convention on a disconnected graph. +4. Drift is out of §9's job description and both figures' relay nodes; it stays in Status as next to land. +5. §6 now states the constants' publication and the ten-degrees-of-freedom rule as the design, and that this head's index carries the cells and the day-one constants. +6. "the runbook beside this document". +7. `pool status` reaches the relay on this head (client B, after the referee's clone); the sentence stands. +8. README's field list now names the row's seven fields. + +Camera-ready items not taken here: join-merged relay writes or batch ids; per-component centring in the relay; the distinct-install count through the readers; a checkable home for the 72 runs; the seed's offset numbers computed by `figures.py`. diff --git a/docs/design/model-pool/pareto-crewing.pdf b/docs/design/model-pool/pareto-crewing.pdf index ca19bdfff31159c4a9494b6aba49f37f4c2d6264..31c22860717c627430258b8cf8e8d2021b9e1541 100644 GIT binary patch delta 55956 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