okami.mom/albedo

git clone https://git.nkp.pet/okami.mom/albedo

dawncompaction: split strategies into a read-only project and a separate compactdeef7f4

main
5.4 KiB167 linesraw
1//// Legacy cursor migration, chunked summaries, and image accounting are costly
2//// or impossible to drive reliably through the shared E2E daemon.
3
4import albedo/daemon/store
5import albedo/harness/compaction
6import albedo/harness/extensions
7import albedo/harness/extensions/rolling/extension as rolling
8import albedo/harness/runtime
9import albedo/openai_api/types
10import gleam/dynamic/decode
11import gleam/erlang/process
12import gleam/int
13import gleam/list
14import gleam/option.{None, Some}
15import gleam/result
16import gleam/string
17import gleeunit/should
18import harness/session_fixture
19import sqlight
20
21fn host(config: rolling.Config) -> #(runtime.Runtime, runtime.Session) {
22  let installed = [rolling.configured_extension(config)]
23  let assert Ok(host) =
24    runtime.start_with_config(
25      ":memory:",
26      extensions.Config(installed, ["rolling"]),
27    )
28  session_fixture.initialise(host)
29  session_fixture.create(host, "rolling-test", "/tmp")
30  let assert Ok(session) = runtime.open_session(host, "rolling-test", "/tmp")
31  #(host, session)
32}
33
34fn long_history() -> List(types.Input) {
35  [
36    types.User("u1 " <> string.repeat("a", 380)),
37    types.Assistant("a1 " <> string.repeat("b", 380)),
38    types.User("u2 " <> string.repeat("c", 380)),
39    types.Assistant("a2 " <> string.repeat("d", 380)),
40    types.User("u3 " <> string.repeat("e", 380)),
41    types.Assistant("a3 " <> string.repeat("f", 380)),
42  ]
43}
44
45/// A row saved before cuts counted user messages validates the old way, by
46/// item count under its source. A projection only reads it; the next
47/// compaction saves the portable form in its place.
48pub fn legacy_item_count_state_resumes_and_compaction_upgrades_it_test() -> Nil {
49  let #(host, session) = host(rolling.Config(Some(10_000), 90, 25))
50  let history = long_history()
51  let ledger = runtime.ledger(host)
52  let assert Ok(_) =
53    store.query(ledger, fn(db) {
54      sqlight.query(
55        "INSERT INTO rolling_compaction_state(session,summary,cutoff_count,source_hash) VALUES(?,?,?,?)",
56        db,
57        [
58          sqlight.text("rolling-test"),
59          sqlight.text("legacy facts"),
60          sqlight.int(4),
61          sqlight.text(
62            compaction.fingerprint(#("model-a", list.take(history, 4))),
63          ),
64        ],
65        decode.dynamic,
66      )
67    })
68  let assert Ok([types.User(summary), _, ..tail]) =
69    runtime.prepare_history_with(
70      host,
71      session,
72      "model-a",
73      "",
74      fn(_) { Error("must not summarize") },
75      history,
76    )
77  string.contains(summary, "legacy facts") |> should.be_true
78  tail |> should.equal(list.drop(history, 4))
79  let assert Ok([#(4, legacy)]) = saved_cut(ledger)
80  string.starts_with(legacy, "users:") |> should.be_false
81  let longer =
82    list.append(history, [
83      types.User("u4 " <> string.repeat("g", 380)),
84      types.Assistant("a4 " <> string.repeat("h", 380)),
85    ])
86  let assert Ok(_) =
87    runtime.compact_history_scoped(
88      host,
89      session,
90      "model-a",
91      "model-a",
92      None,
93      "",
94      fn(request: compaction.SummaryRequest) {
95        request.previous |> should.equal(Some("legacy facts"))
96        Ok("folded facts")
97      },
98      longer,
99    )
100  let assert Ok([#(3, cut_hash)]) = saved_cut(ledger)
101  string.starts_with(cut_hash, "users:") |> should.be_true
102  runtime.stop(host)
103}
104
105fn saved_cut(ledger: store.Store) -> Result(List(#(Int, String)), String) {
106  store.query(ledger, fn(db) {
107    sqlight.query(
108      "SELECT cutoff_count,source_hash FROM rolling_compaction_state",
109      db,
110      [],
111      {
112        use users <- decode.field(0, decode.int)
113        use hash <- decode.field(1, decode.string)
114        decode.success(#(users, hash))
115      },
116    )
117    |> result.map_error(string.inspect)
118  })
119}
120
121/// A long eviction folds into the summary chunk by chunk, each request under
122/// half the window, carrying the summary forward.
123pub fn long_eviction_summarizes_in_chunks_test() -> Nil {
124  let #(host, session) = host(rolling.Config(Some(2000), 90, 25))
125  let history =
126    list.flatten(list.repeat(long_history(), 4))
127    |> list.index_map(fn(input, index) {
128      case input {
129        types.User(text) -> types.User(int.to_string(index) <> text)
130        other -> other
131      }
132    })
133  let requests = process.new_subject()
134  let assert Ok(_) =
135    runtime.compact_history_scoped(
136      host,
137      session,
138      "model-a",
139      "model-a",
140      None,
141      "",
142      fn(request: compaction.SummaryRequest) {
143        process.send(requests, request)
144        Ok("after " <> int.to_string(list.length(request.evicted)))
145      },
146      history,
147    )
148  let assert Ok(compaction.SummaryRequest(_, None, first, _, _)) =
149    process.receive(requests, 0)
150  let assert Ok(compaction.SummaryRequest(_, Some(previous), _, _, _)) =
151    process.receive(requests, 0)
152  previous |> should.equal("after " <> int.to_string(list.length(first)))
153  should.be_true(compaction.estimate_inputs(first) <= 1000)
154  runtime.stop(host)
155}
156
157pub fn image_estimate_uses_dimensions_not_base64_transport_size_test() -> Nil {
158  let payload = string.repeat("A", 5 * 1024 * 1024)
159  let assert Ok(image) =
160    types.image("image/png", payload, 1024, 1024, 3 * 1024 * 1024)
161  let input = types.UserImage("inspect", [image])
162  let transport_includes_payload =
163    compaction.input_bytes(input) > 5 * 1024 * 1024
164  transport_includes_payload |> should.equal(True)
165  let estimate_excludes_payload = compaction.estimate_input(input) < 1000
166  estimate_excludes_payload |> should.equal(True)
167}