Development #
Source files #
src/server/ and src/client/ use the same paths as upstream. They contain the
files the suite replaces and the files it adds. Edit them as ordinary Gleam,
Erlang, Python and Go source.
nix/modules/packages.nix uses the locked NUR package definitions. It checks the
source hash in upstream/pin.json and the original files in
upstream/*.sha256. It then copies the suite files before NUR's existing
post-patch hooks run. Upstream dependencies, patches and build checks remain
in use.
Updating upstream #
Follow the maintenance guide for source merges, pin/checksum rules and verification. The automated updater prepares a candidate; it does not replace compatibility review.
Packaged licenses and source #
Packages retain licenses, attribution and exact source snapshots under
share/doc/albedo-suite/. See corresponding source for
rebuild, distribution and network source-offer requirements.
checks.package-licenses verifies installed notices and source correspondence,
not an operator's network source offer.
Checks #
Run nix develop before using Python tooling. The shell exposes the pinned
upstream Python modules and E2E harness under .direnv/albedo-upstream, which
ty.toml uses to resolve dependencies absent from the source overlays.
nix build .#albedo-defaults .#albedo-patched
nix flake check
nix fmt
The checks exercise Home Manager options, source checksums, package contents,
Python formatter, LSP and MCP bindings, RTK routing and FFF search,
native MCP lifecycle, and the shell helpers.
The files under src/ keep upstream formatting and are excluded from the suite's
treefmt run.
Terminal UX #
Composer editing defines input-only NORMAL/INSERT modes. Lists, forms, agents and transcript navigation use the upstream keyboard controls.
The inline renderer overlays cli/third_party/bubbletea/cursed_renderer.go
with a port of bubbletea#1868.
Before a resize to a narrower width redraws the frame, it moves up past the rows
the terminal rewrapped. Without it, Ghostty's reflow of a full-width row, for
example when Hyprland narrows an unfocused window by one column, leaves one
stale header above the frame per narrowing. Go builds the replaced module from
vendor/, so packages.nix installs the overlay there after configure.
go test ./internal/terminal -run '^TestSuiteInline' covers it. Drop the
overlay when upstream's vendored Bubble Tea contains an equivalent fix.
Daemon tests #
python3 scripts/test-e2e.py --source /path/to/pinned/upstream --package /path/to/built/package
Use the selected package's server.src and actual built output, not an
installed package from another pin. The runner copies the source into a
writable temporary directory and uses the supplied binaries. SUITE_CASES and
UPSTREAM_CASES in scripts/test-e2e.py are the authoritative case lists.
Use --case <name> for focused work; omit it for the full upstream-update run.
Tests use fake providers, temporary homes and local servers. They do not activate your configuration, read your credentials or contact live providers. The runner waits for kernel reattachment before testing a forced upgrade. It also adapts native harness, kernel-detach, cache-miss, image-limit and repetition text extraction to the suite's typed Responses content parts, preserving their assertions. These six upstream fixtures are checksum-guarded and require review when changed. Skill discovery uses upstream's native catalog filter.
Additional supervised RTK/FFF tests run in the flake Python check.
test/fff_test.py exercises search through the production Erlang registry;
test/fff_stdio_test.py checks protocol bounds, root eviction, idle exit and
watchdog deadlines. See RTK and FFF for worker behavior.
Agent context #
Prompt context documents static reference reductions and append-only documentation retrieval. Cache regression tests assert unchanged instructions, schemas and prior request input after ordinary tool operations; never replace this with dynamic prompt rewriting or silent evidence eviction.
Kernel startup recovery #
Fresh local kernels validate the working directory before launching Python.
A missing directory, unavailable mount, or file in place of the directory
produces an error naming the path and suggesting restoration or /cd.
Albedo never creates an empty substitute or silently moves the session.
The check is in the queued-turn owner, before kernel creation, so a failed
preflight cannot leave an unverified kernel record that blocks /cd.
Kernel startup failures retain queued inputs and pause automatic retries. After fixing startup, send another message to retry; replaying the same input ID does not retry. Applying a workspace move resumes the retained queue. A daemon restart makes one new attempt. Other transient preparation failures keep the upstream 15-second retry.
The kernel-workspace flake check runs the real-daemon regression cases in
test/kernel_workspace_e2e_test.py, including the former retry deadline and
recovery after restoring or moving a workspace.
Zoxide folder suggestions #
With zoxide on the CLI's PATH, /cd includes matching directories from its
existing database after recent session folders, without duplicates. Bare words
use zoxide query --list with zoxide's ranking and multi-word matching; explicit
paths and SSH locations retain normal browsing. /cd words also resolves a
zoxide shortcut when no relative directory with that name exists. Queries are
debounced in the picker and bounded
by a one-second timeout. Successful local /cd moves run zoxide add; failed
moves and remote locations do not update the database. Missing zoxide or an empty
database leaves the existing picker usable. The integration runs only when the
CLI and daemon share a machine; it does not query remote zoxide databases.
Typed Responses input items #
src/server/src/albedo/openai_api/request.gleam replaces the upstream Responses
encoder, which writes messages it builds itself as {"role": ..., "content": ...} input items; a strict endpoint parses the input as a type-tagged union
and rejects the request before any model sees it (Zed's cloud answers
missing field 'type'). The replacement adds the message discriminator and
encodes message text as typed content parts (input_text, output_text,
input_image), which is what Zed's own client sends and what the Responses
schema documents. Locally built Chat Completions messages, and every other
encoder, are unchanged. Replayed items are already typed and pass through
verbatim.
The replacement also stops claiming OpenAI strict mode for a tool whose schema
cannot satisfy it: strict requires every object level to set
additionalProperties: false and to list every property as required, and the
built-in tools take optional parameters. "Every object level" means every schema
reachable through properties, patternProperties, additionalProperties,
items, prefixItems, anyOf, oneOf, allOf, not, if/then/else,
dependentSchemas, $defs and definitions; a schema that nests an object
behind one of those keywords and omits the rule is sent non-strict. The claim is
emitted only when the schema meets those rules, so a provider that enforces them
no longer refuses the whole request (Invalid schema for function 'python': ... 'required' is required to be supplied and to be an array including every key in properties). test/suite_zed_test.gleam pins both the rules and the traversal.
OpenCode Go's Responses route shares this encoder and gets the same typed items.
The server package runs test/suite_zed_test.gleam, which asserts every family's
provider_request field by field and fails when an input item or a message
content part loses its discriminator. Remove this source replacement, its
baseline checksum and its inventory entry when upstream emits typed items.
Temporary Codex error decoding #
src/server/src/albedo/openai_api/responses.gleam retains upstream support for
nested error.message and top-level message, and temporarily adds string
error and code-only error events. Messages take precedence over codes. Unknown
or non-string shapes retain typed malformed-error diagnostics without dumping raw
payloads. This exposes provider failures; it does not retry or suppress them.
The original failing payload was not captured, so its exact shape is unverified.
The server package runs test/suite_codex_error_test.gleam during its check phase.
Remove this source replacement, its baseline checksum and inventory entry, and
its package check when upstream handles these envelopes without dumping raw
payloads.
Remote bundle staging #
Concurrent SSH kernel starts publish their complete bundle with an atomic
directory rename. A connection that loses publication removes only its own
staging tree, including read-only directories shipped by Nix; it never moves a
second tree inside the winning bundle. A conflicting nonempty bundle is refused.
test/remote_staging_test.py covers writable/read-only duplicate staging,
concurrent first publication and incomplete-target rejection.
Job readiness #
The suite preserves upstream Job.text() and Job.wait_for(). The readiness
reader yields when an incomplete UTF-8 character prevents byte progress, so
later output can complete the character and the timeout remains reachable.
Regression tests exercise both paths through real supervised jobs.
Temporary compatibility layers #
Formatter and LSP support are local stopgaps for the pinned upstream version.
Do not add unrelated editor features or make other extensions depend on them.
Remove each layer when upstream covers its useful behavior and the native path
passes the relevant lifecycle tests. files.check() runs an explicit ordered
formatting/diagnostics batch. It is not a final-reply veto or a native Stop hook.
Disabling either feature must leave ordinary files, Python, MCP and streaming
behavior intact.
LSP servers shut down after 60 seconds without an LSP operation, including
batch diagnostics. Active requests are never interrupted by idle cleanup.
Healthy servers restart lazily on the next operation with fresh document state;
failed servers still require explicit lsp.close() before retrying. Idle cleanup
does not discard pending edit checks or run as a session background job.
LSP owns albedo_lsp.py, albedo_plugins/lsp.py,
harness/extensions/lsp/extension.gleam and nix/home-manager/lsp.nix.
Retirement also removes its file-write tracking in albedo_plugins/files.py,
its import/registration in extensions.gleam, its
settings/enable wiring in runtime.nix, its Home Manager import, and its tests
and fixture. Replace Infra's albedo/lsp.nix declaration with native settings
before removing its suite options.
Formatter owns albedo_formatter.py, albedo_plugins/formatter.py,
harness/extensions/formatter/extension.gleam and
nix/home-manager/formatter.nix. Retirement also removes file-write tracking in
albedo_plugins/files.py, its optional stage in files.check(), and formatter
registration/settings. Final-acceptance hook support has been retired; native
streaming and Stop behavior are unchanged. Compare replacements before deletion;
other suite changes may share a file. Keep lazy MCP and diff-display changes.
Remove retired cases from the flake checks and E2E runner, update documentation and source inventories, and rerun disabled-feature, ordinary streaming and MCP regressions against the native implementation. No runtime data migration is required by either layer; their settings are additive public configuration.
Provider and web integration #
The pristine upstream provider handles OpenCode Go OAuth, account rotation and
protocol selection. Suite wire/catalog additions preserve SDK-metadata routing;
package checks exercise rotation, OAuth organization headers and all four wire
routes. The native Responses bridge replaces the old local adapter; its narrow
reasoning-detail extension preserves encrypted items in both existing histories
and new turns, without changing native sparse tool-call indices.
test/suite_bridge_test.gleam checks both replay paths and index correlation.
See provider configuration.
upstream/pin.json records the current Albedo revision; flake.lock records
NUR. Do not maintain a second pin history here. Lazy MCP uses upstream's native
client, and Zed uses native transport method selection.
The suite packages ship the CLI and daemon without building or bundling the
web UI. There is no albedo-web output or packaged ALBEDO_WEB_DIST default.
Upstream's web-serving commands remain, but browser assets are not supplied.
checks.package-layout verifies that neither package's runtime closure includes
an albedo-web derivation. No upstream web source is replaced.
Automated sparse upstream updates #
python3 scripts/update-upstream.py selects the current NUR revision once in an
isolated temporary checkout. To reproduce a specific selection, use
python3 scripts/update-upstream.py --nur-rev <40-character-NUR-commit>.
The NUR commit is not the Albedo commit. The updater evaluates the unmodified
nur.repos.adam0.albedo-git.src from each locked NUR input, realizes both pristine
sources with Nix, verifies their NAR hashes, and records both identities in its
JSON report. Only the NUR lock node may change; unrelated lock updates fail closed.
Requires Nix with flakes enabled, Git and Python 3.11 or newer, and a clean
working copy. Jujutsu checkouts use jj diff --summary for the cleanliness
check; Git checkouts use git status. Neither path creates commits or pushes.
For each baseline replacement, git merge-file uses old pristine upstream as
base, suite as ours, and candidate pristine upstream as theirs. An unchanged
upstream file is a trivial merge retaining ours. Suite-only additions remain
unchanged. Conflicts, missing baselines, upstream deletions/possible renames,
file/directory collisions, symlinks, executable mode changes and changed binary
files require manual review. Renames are deliberately reported as deletions or
type changes, not guessed by similarity. No conflict markers are written.
All plans must resolve before any source, checksum or pin is written; write
errors roll back. No files outside the existing overlay are copied into src.
The manifests are generated from pristine candidate bytes. The existing
sourceFiles inventory continues to list the server overlay (including its
suite-only additions); client replacements are tracked by
upstream/client.sha256 alone and never appear in it. The whole-source NAR hash
pins the pristine candidate tree without a second file inventory. Generated
__pycache__ directories and .pyc files are excluded from overlay discovery
and inventories. The package's
source hash assertion, checksum guards, original
NUR post-patch hooks and both package variants remain intact. Equal Albedo
revision/hash is a no-op, even if NUR moved: packaging-only NUR updates require
separate review. The updater does not retire compatibility features or rewrite
NUR hook integrations; package and daemon regressions must detect incompatibility.
Changes to the web dependency hash, E2E fixture guard or build API may therefore
require manual changes rather than an automatically successful update.
The default report is /tmp/albedo-upstream-update.json; choose another location
with --report /absolute/path/report.json outside the checkout. Exit status is
0 for updated/no-change, 2 for unresolved merges, and 1 for operational errors.
The report lists old/candidate NUR and Albedo identities, automatically merged
paths and unresolved reasons. Reproduce a conflict with its exact candidate NUR
SHA, obtain both source paths with the source_expr expression in the script,
and compare the reported path in the old source, suite and candidate. Resolve in
a separate human branch, update pristine manifests and run the full checks.
After a successful local update, run bash scripts/validate-upstream.sh. This
runs updater fixtures, formatting verification, both builds, all flake checks,
and the complete isolated E2E case set using the candidate package's actual
server.src and built output. It sets Erlang's explicit CA bundle as required by
the existing checks. No live providers or personal credentials are used.
Publication boundary #
The updater is a local preparation tool, not an unattended update service. GitHub is a mirror: no scheduled Actions updater, publishing token, bot branch or pull request publisher is installed. Review candidate changes and run the validation script before publishing through the canonical repository. A clean text merge does not establish API compatibility or retire suite features.