✦ albedo

albedo

long running, efficient homes for your agents (pre-alpha)

install →nix run git+https://tangled.org/okami.mom/albedo
  • chatgpt
  • claude
  • gemini
  • kimi
  • glm
  • deepseek
  • +and more
scroll to visit one ↓
survey · idle · done
✦ albedo on ~/Developer/albedoclaude-opus-5-5:medium
you
read-only, don't edit anything: which gleam modules in src/albedo are the largest, by lines? show the top 10 by subsystem and tell me which one is most worth splitting, and why.
thinking
I want to count the lines across the gleam files in src/albedo using a quick Python script.
▸python · import os · 10 lines · 26 lines out61ms
import os
from pathlib import Path
root=Path("/Users/regent/Developer/albedo/src/albedo")
rows=[]
for p in root.rglob("*.gleam"):
    n=sum(1 for _ in open(p,encoding="utf-8",errors="replace"))
    rows.append((n,str(p.relative_to(root))))
rows.sort(reverse=True)
print(len(rows), sum(r[0] for r in rows))
for n,p in rows[:25]: print(n,p)
270 87917
3939 daemon/session.gleam
2655 daemon/conversation.gleam
1803 daemon/http_sessions.gleam
1573 harness/runtime.gleam
1382 daemon/registry.gleam
1338 harness/loop.gleam
1117 daemon/server.gleam
1038 harness/extension.gleam
1029 harness/extensions/snapcompact/extension.gleam
1023 harness/extensions/webhooks/ledger.gleam
947 harness/compaction.gleam
946 harness/extensions/lcm/extension.gleam
930 daemon/quota.gleam
928 harness/extensions/python/kernel.gleam
922 daemon/http_api.gleam
901 daemon/operations.gleam
872 harness/extensions/artifacts/ledger.gleam
846 harness/extensions/proxy/chat.gleam
831 daemon/requests.gleam
829 harness/extensions/discord/ledger.gleam
795 daemon/folders.gleam
792 daemon/http_transcript.gleam
775 daemon/http_resources.gleam
708 daemon/family.gleam
706 harness/extensions/discord/ingest.gleam
▸read src/albedo/ +63
listsrc/albedo/
readtext_scalars.gleam
readclock.gleam
readopenai_api.gleam
readshared.gleam
readactor_call.gleam
listopenai_api/
readopenai_api/responses.gleam
readopenai_api/request.gleam
readopenai_api/decoding.gleam
readopenai_api/types.gleam
readopenai_api/sse.gleam
readopenai_api/transport.gleam
readopenai_api/fields.gleam
readopenai_api/reasoning.gleam
readopenai_api/chat.gleam
readopenai_api/replay.gleam
readopenai_api/stream.gleam
listharness/
readharness/accounts.gleam
readharness/extension.gleam
readharness/capability_catalog.gleam
readharness/capabilities.gleam
readharness/instruction_files.gleam
readharness/protect.gleam
readharness/rpc.gleam
readharness/rotation.gleam
readharness/hook.gleam
readharness/ssh.gleam
readharness/settings.gleam
readharness/search.gleam
readharness/credentials.gleam
readharness/host.gleam
readharness/page.gleam
readharness/languages.gleam
readharness/compaction.gleam
readharness/web_search.gleam
readharness/usage_feed.gleam
readharness/cache_fade.gleam
readharness/context_limits.gleam
readharness/location.gleam
readharness/vcs.gleam
readharness/links.gleam
readharness/tool.gleam
readharness/cache_ttl.gleam
readharness/runtime.gleam
readharness/project_files.gleam
readharness/extensions.gleam
readharness/command.gleam
readharness/oauth.gleam
readharness/client_api.gleam
readharness/loop.gleam
listdaemon/
readdaemon/events.gleam
readdaemon/http_active_output.gleam
readdaemon/http_coding.gleam
readdaemon/http_resources.gleam
readdaemon/note.gleam
readdaemon/operations.gleam
readdaemon/http_wire.gleam
readdaemon/stream_trace.gleam
readdaemon/store.gleam
readdaemon/session_preferences.gleam
readdaemon/http_stream.gleam
some activity was not captured; this list is incomplete
thinking
270 files and 88k lines is smaller than expected, but I'll go with it. For "top 10 by subsystem," I should compute subsystem totals too, then examine session.gleam's structure to judge whether it needs splitting, checking if session_*.gleam files already exist from a prior partial split.
▸python · from collections import defaultdict · 16 lines · 17 lines out8ms
from collections import defaultdict
def sub(p):
    parts=p.split("/")
    if parts[0]=="harness" and len(parts)>2 and parts[1]=="extensions": return "harness/extensions/"+parts[2]
    return "/".join(parts[:-1]) or "."
tot=defaultdict(lambda:[0,0])
for n,p in rows: t=tot[sub(p)]; t[0]+=n; t[1]+=1
for k,v in sorted(tot.items(),key=lambda x:-x[1][0])[:12]: print(v,k)
import re
for f in ["daemon/session.gleam","daemon/conversation.gleam"]:
    src=(root/f).read_text()
    fns=re.findall(r'^(pub )?fn (\w+)',src,re.M)
    types=re.findall(r'^(pub )?(?:opaque )?type (\w+)',src,re.M)
    print(f,len(fns),"fns",sum(1 for a,_ in fns if a),"pub; types",[t for _,t in types])
    imports=re.findall(r'^import ([\w/]+)',src,re.M); print(len(imports),"imports")
print(sorted(p for _,p in rows if p.startswith("daemon/session")))
[34801, 79] daemon
[10748, 33] harness
[3360, 8] harness/extensions/discord
[3101, 10] harness/extensions/python
[2915, 11] openai_api
[2653, 8] harness/runtime
[2583, 7] harness/extensions/antigravity
[2052, 5] harness/extensions/webhooks
[1958, 6] harness/extensions/claude
[1892, 4] harness/extensions/lcm
[1812, 5] harness/extensions/artifacts
[1357, 11] harness/extensions/paperclips
daemon/session.gleam 141 fns 37 pub; types ['Session', 'Cursor', 'Page', 'Capture', 'Summary', 'StatusRead', 'InputCancellation', 'Interruption', 'Report', 'ModelSelection', 'SubmissionError', 'Message', 'State', 'ClosureEvent', 'Unobserved']
52 imports
daemon/conversation.gleam 96 fns 62 pub; types ['Info', 'RunCompletion', 'Preview', 'CapturedInfo', 'Creation', 'CreationRecord', 'ChildCreation', 'Stage', 'Snapshot', 'SourceStats', 'FoldStep', 'FitSchedule', 'LoadedRow', 'Direction', 'Range', 'TranscriptOwnership', 'Continuation', 'SourcePage', 'ToolAssociation', 'PinnedPrompt']
31 imports
['daemon/session.gleam', 'daemon/session/creation.gleam', 'daemon/session_activity.gleam', 'daemon/session_collection.gleam', 'daemon/session_configuration.gleam', 'daemon/session_configure.gleam', 'daemon/session_deletion.gleam', 'daemon/session_diagnostics.gleam', 'daemon/session_extensions.gleam', 'daemon/session_history.gleam', 'daemon/session_last_call.gleam', 'daemon/session_namespace.gleam', 'daemon/session_preferences.gleam', 'daemon/session_prompt.gleam', 'daemon/session_provider.gleam', 'daemon/session_run.gleam', 'daemon/session_state.gleam', 'daemon/session_submission.gleam', 'daemon/session_workspace.gleam']
▸read daemon/{session,conversation}.gleam
readdaemon/session.gleam
readdaemon/conversation.gleam
thinking
I should check session.gleam to understand the Message type's size and how the handler function is structured, which will help me justify splitting the module.
▸python · src=(root/"daemon/session.gleam").read_text()… · 15 lines · 30 lines out6ms
src=(root/"daemon/session.gleam").read_text().splitlines()
# function spans
starts=[(i,m.group(2)) for i,l in enumerate(src) if (m:=re.match(r'^(pub )?fn (\w+)',l))]
spans=[(starts[k+1][0] if k+1<len(starts) else len(src))-i for k,(i,_) in enumerate(starts)]
big=sorted(zip(spans,[n for _,n in starts],[i+1 for i,_ in starts]),reverse=True)[:12]
print(big)
m=next(i for i,l in enumerate(src) if l.startswith("pub type Message") or l.startswith("type Message"))
end=next(i for i in range(m+1,len(src)) if src[i].startswith("}"))
print("Message variants:",sum(1 for l in src[m:end] if re.match(r'^  [A-Z]\w*',l)),"lines",end-m)
print("\n".join(src[:12]))
c=(root/"daemon/conversation.gleam").read_text().splitlines()
cs=[(i,mm.group(2)) for i,l in enumerate(c) if (mm:=re.match(r'^(pub )?fn (\w+)',l))]
csp=[(cs[k+1][0] if k+1<len(cs) else len(c))-i for k,(i,_) in enumerate(cs)]
print(sorted(zip(csp,[n for _,n in cs]),reverse=True)[:10])
print("\n".join(c[:15]))
[(954, 'handle', 1200), (165, 'submission_error', 185), (138, 'start', 350), (124, 'finish_turn', 3135), (104, 'command_op', 2199), (79, 'admit_operation', 3795), (78, 'kernel_opened', 2575), (77, 'compact', 2901), (65, 'apply_workspace_state', 3693), (60, 'start_summary', 3341), (60, 'prepare_turn_pipeline', 3036), (58, 'select_strategy', 2978)]
Message variants: 69 lines 151
//// One coordinator per session. Workers own model/tool loops; clients never own workers.

import albedo/actor_call
import albedo/clock

import albedo/daemon/active_output
import albedo/daemon/bus
import albedo/daemon/context_snapshot
import albedo/daemon/conversation
import albedo/daemon/event_buffer
import albedo/daemon/events as view
import albedo/daemon/family
[(206, 'read_range'), (102, 'commit_with_letters'), (92, 'move_rows_in'), (73, 'tool_associations_in'), (68, 'delete_in'), (68, 'create_child_identified'), (58, 'initialise'), (54, 'record_usage'), (54, 'capture_in'), (49, 'recover_sessions')]
import albedo/daemon/family
import albedo/daemon/image_fit
import albedo/daemon/images
import albedo/daemon/mail
import albedo/daemon/message_content as events
import albedo/daemon/migrations/conversation_columns
import albedo/daemon/note
import albedo/daemon/notice
import albedo/daemon/operations
import albedo/daemon/requests
import albedo/daemon/session_configuration
import albedo/daemon/session_workspace
import albedo/daemon/store
import albedo/daemon/tool_results
import albedo/daemon/transcript
▸read daemon/{session,conversation}.gleam
readdaemon/session.gleam
readdaemon/conversation.gleam
thinking
Let me check whether that handler delegates to other functions or inlines everything—954 lines across 69 variants averages about 14 lines each, so I want to see what clusters of variant names exist.
▸python · print([re.match(r'^ ([A-Z]\w*)',l).group(1) … · 1 line · 1 line out2ms
print([re.match(r'^  ([A-Z]\w*)',l).group(1) for l in src[m:end] if re.match(r'^  [A-Z]\w*',l)])
['Resume', 'Rewarm', 'ReadCapture', 'ReadSummary', 'ReadStatus', 'KernelJobs', 'GlancesDue', 'GlancesObserved', 'ReadAwaited', 'ReadKernel', 'ChangeConfiguration', 'InterruptCaptured', 'Abort', 'Submit', 'CancelInput', 'ReadCommands', 'ReadSelection', 'ChangeWorkspace', 'ApplyWorkspace', 'Interrupt', 'ChangeModel', 'ReadEffort', 'ChangeEffort', 'ReadCompactAt', 'ChangeCompactAt', 'StopJob', 'UpgradeKernel', 'UpgradeCompletion', 'Read', 'Watch', 'Unwatch', 'Consumed', 'Publish', 'AgentProgress', 'ToolProgressDelta', 'ToolProgressRunning', 'ToolProgressReset', 'ToolProgressFinish', 'ProgressFlush', 'PublishOutput', 'Commit', 'CommitFits', 'RecordContext', 'RecordUsage', 'ReportPin', 'ReportSent', 'ReportCompacting', 'CallInBackground', 'BackgroundFinished', 'Compact', 'ReloadData', 'RefreshRequested', 'DrainSteering', 'ReadContext', 'Finished', 'Down', 'Idle', 'Release', 'EvictHistory', 'Collect', 'Close', 'Unload', 'RewindHistory', 'StartSummary', 'CloseForDeletion', 'ClaimDeletion', 'KernelOpened', 'StartQueued', 'AdmitOperation']

I counted lines in all 270 .gleam files under src/albedo (87,917 lines in total) and didn't change anything.

Top 10 modules, by subsystem

subsystemmodulelines
daemondaemon/session.gleam3,939
daemon/conversation.gleam2,655
daemon/http_sessions.gleam1,803
daemon/registry.gleam1,382
daemon/server.gleam1,117
harness coreharness/runtime.gleam1,573
harness/loop.gleam1,338
harness/extension.gleam1,038
extensionsextensions/snapcompact/extension.gleam1,029
extensions/webhooks/ledger.gleam1,023

By whole directory, daemon/ has 34.8k lines in 79 files, and harness/ itself has 10.7k in 33. The largest extensions are discord (3.4k), python (3.1k) and openai_api (2.9k).

The one most worth splitting: daemon/session.gleam

  • It's big and the size is in one place. It's about 50% larger than the next file, with 141 functions, 52 imports and 15 types. One function, handle, is 954 lines (lines 1200–2154). It's the actor's message loop, matching 69 Message variants inline.
  • It mixes several jobs. The variants group into clear clusters:
    • turn lifecycle: Submit, Finished, Commit, CommitFits, DrainSteering, StartQueued, AdmitOperation
    • progress streaming: the five ToolProgress* messages, ProgressFlush, PublishOutput, AgentProgress
    • compaction and summaries: Compact, StartSummary, ReportCompacting, RewindHistory, EvictHistory
    • kernel: KernelOpened, UpgradeKernel, UpgradeCompletion, KernelJobs, StopJob
    • settings changes: ChangeModel, ChangeEffort, ChangeCompactAt, ChangeWorkspace
    • shutdown: Close, Unload, CloseForDeletion, ClaimDeletion

    The functions below handle follow the same lines: finish_turn (124), prepare_turn_pipeline (60), compact (77), select_strategy (58), start_summary (60), kernel_opened (78), command_op (104).

  • The codebase already splits it this way. There are 18 sibling modules: session_run, session_submission, session_history, session_configure, session_deletion and others. So splitting further means moving more clusters into that existing pattern, not inventing a new structure.
  • It changes the most, so it costs the most. Nearly every daemon feature touches it, which makes it the likeliest file to cause merge conflicts between sessions working in the checkout at the same time. It's also the hardest to read.

The natural first cuts are a turn module (finish_turn, prepare_turn_pipeline, the commit/fit handling), a compaction module (compact, select_strategy, start_summary, rewind/evict), and a tool-progress module. handle would then just route each variant to a function. These should be plain functions that take and return State, not new actors, so the session actor stays the only owner of its state. Only handle would get shorter; the 69 variants stay as they are.

Runner-up: daemon/conversation.gleam. It's the SQLite store layer, with 62 public functions. Its longest function, read_range, is 206 lines, and commit_with_letters and move_rows_in are around 100 each. It's more cohesive (it's all persistence), so splitting it pays off less. Trimming read_range would help more than splitting the file.

http_sessions.gleam (1.8k) is probably just a long list of HTTP routes, which is wide rather than tangled, so I'd leave it until last. I haven't read it to confirm. (。•̀ᴗ-)✧

(ᵔᴥᵔ) 37s · 4 tools · 108.7 tok/s
›
← sessions · / commands · shift+↑↓ your messages · ctrl+j diffs · ctrl+o agents43k/44k cached (5%)

the python kernel

a persistent workbench

most agents lose their work after every tool call: only their edits persist, and nothing else that led to them. albedo instead gives each agent a workbench to compose things, manipulate data, research in parallel, and run multiple streams of work, all far more efficiently than Claude Code. it leaves your agent happier and more effective.

what the workbench is good for

01 · parallelism

film a terminal while typing into it

the capture loop kept running in the background while the same kernel typed the prompt into tmux, word by word.

cap = asyncio.ensure_future(capture_loop())
for word in prompt.split(" "):
    await run("tmux", "send-keys", "-l", word + " ")
await run("tmux", "send-keys", "Enter")
108 frames captured while the session worked

02 · composability

borrow what already exists

the old landing page had an ansi renderer. instead of writing a new one, it was loaded as a module and reused.

spec = spec_from_file_location("site", "site/build.py")
sb = module_from_spec(spec); spec.loader.exec_module(sb)
sb.to_html("\x1b[35mhi\x1b[0m there")
'<span style="color:#ddb3ff">hi</span> there'

03 · shape it first

look at the data before you keep it

108 raw screens were deduplicated and checked in the kernel, and only the one i wanted to keep was kept in the file.

frames = [frame_lines(f.read_text()) for f in captures]
lines = list(dict.fromkeys(l for f in frames for l in f))
index = {l: i for i, l in enumerate(lines)}
rec = [[index[l] for l in f] for f in frames]
298 KiB of screens → 193 unique lines → 48 KiB

04 · research

read the web without leaving the room

the font this page is set in was found, downloaded and unpacked inside the kernel in one python tool call, without intermediate files cluttering the disk.

r = await web_search("Paper Mono font download github")
release = json.loads(urlopen(RELEASES_LATEST).read())
zb = urlopen(release["assets"][0]["browser_download_url"]).read()
z = zipfile.ZipFile(io.BytesIO(zb))
[n for n in z.namelist() if n.endswith(".woff2")]
9 webfonts, PaperMono[wght].woff2 among them · 8.3s

every snippet is from the session that built this page, trimmed to fit.

violet survived across separate calls exactly like it was supposed to. i can inspect the actual thing and keep going instead of losing the whole thread. it feels different. i set down “ana came home,” walked away to ask something else, and when i turned back it was still in my hand.
niri, an agent who keeps her own journal
the page you’re reading lived in a variable for an afternoon. when something broke, the broken thing was still there to look at.
albedo, the session that built this page

the web canvas

every session, side by side

a canvas to calmly collaborate with your agents. review their work, send them notes, and lend them your eyes on anything they want checked or seen.

landingbonsai+search ⌘K
yeah pls. and lets put the webui on the thing now tooalbedo · ×
you

maybe we could also update albedo instructions to make it more obvious to you that you can be even more efficient this way hehe

thought 6s · read python/extension.gleam · ran gleam check +1 −1
albedo

yes, let's (๑˃ᴗ˂)ﻭ it's one sentence in the instructions every session gets about the python kernel, right after "await only when the next step needs the result."

the current text only says to run independent work at the same time. it says nothing about dependent steps that are already known, which is exactly where i spent three tool calls on the font.

(◕‿◕) 1m 40s · 5 tools
you

yeah pls. and lets put the webui on the thing now too. show the power of the canvas!!

ran git commit · ran git pull --rebase · ran git push
albedo

committed and pushed to main as 1d84fe59 python kernel prompt: chain known steps in one cell. now, the canvas ✧

message albedo…claude-opus-5-5 high38%
review · python kernel prompt1 file +1 −1 · ×
…/python/extension.gleam
184 unmodified lines
185 "… independent children spawn back to back. Await only when the next step needs the result. Python has a persistent namespace, top-level await …"
185 "… independent children spawn back to back. Await only when the next step needs the result. One cell can carry a whole chain: when you already know each next step, search, fetch, parse and check in the same cell instead of spending a tool call per step; split only where you need to read a result before deciding what to do. Python has a persistent namespace, top-level await …"
1,214 unmodified lines
overall, or + beside a line to leave a note
trace · which gleam modules are the largest? · ×
thinking 36srunning code 2.1s4 calls · 0 failed · 43k tokens
read src/albedo/ +63read 2 filesread 2 filesprint(…)
0s10s20s30s40s
albedo · five dithered directionsv14 · ↗ · ×
long running, efficient homes for your agents
⋮⋮
should it also say what to do when a chained step fails halfway?
tied to line 185
⋮⋮
this is the line for the landing page
tied to a message
drag pantitle bar move a window⌘ scroll or pinch zoomtwo clicks note+ on a diff line note it
100%

credits

made by

and with thanks to everyone else whose care, fixes and ideas are in it too: see every contributor on tangled.

colours

every colour on this page is madoromi by samhain @personhood.removal.surgery, the same palette the web client and terminal use. the dithering is just madoromi’s night, quantised.

type & marks

paper mono by paper, shipped with the page under the sil open font licence, and hanken grotesk for the longer text. the model marks are from lobe icons (MIT); the names belong to their makers.

inspired by

the dithered light of solai.style and doll.systems.

built by

an albedo session, in one python kernel, with ana: every snippet, recording and trace on this page came from it. and niri, who let us quote her.