1. Executive Summary
Otis is a terminal coding agent — 23,700 lines of TypeScript at v0.1.36, MIT, macOS and Linux — that runs against any public serverless Fireworks model supporting tool calls, or against a llama.cpp runtime it downloads and manages itself. The premise is locality: "Sessions, configuration, usage, tool activity, and diffs stay local", with inference under Fireworks' zero-data-retention default and no service of the project's own.
Its durable state is three things. Sessions are
append-only JSONL under a per-workspace directory, resumable by id or by
--continue. AGENTS.md files
are collected from the home directory and every ancestor of the working
directory, ordered so the nearest file lands last. And
skills are directories of Markdown the agent loads by
name through a skill tool whose description is the whole
retrieval policy: "Read SKILL.md before following a matching
skill."
The skills are the memory worth reading about, and they are
unpinned. Installing one is
git clone -- <url> <dir> with no revision, no
tag and no depth; updating is git pull --ff-only. The
runtime manifest schema
(ManagedSkillSource = { id, url, skills: [{ name, relativePath }] })
has no field for a commit or a hash, so what the agent will follow
tomorrow is whatever that repository's default branch says tomorrow.
That is worth naming because of what sits beside it. The same
codebase verifies a SHA-256 in three other places. Its own
release archive:
sha256File(archivePath) !== options.artifact.sha256 aborts
the install, and the manifest parser rejects a checksum that is missing
or not 64 hex characters. The llama.cpp binaries it
downloads, against six hard-coded digests in
src/inference/llama-binary.ts. And every GGUF
weight file, in src/inference/gguf-cache.ts, which
verifies before publishing into the cache, re-verifies a resumed partial
download, and treats a mismatch as a discard.
And skills-lock.json in the repository root records a
computedHash for the one skill vendored during development.
That string appears exactly once in the entire tree:
nothing in src/, scripts/ or
tests/ writes it, reads it or checks it. The project treats
its own binary, its inference runtime and its model weights as
supply-chain risks, and the instructions its agent reads and follows as
a URL.
Where the skill code is rigorous is the other half.
readSkillResource refuses absolute paths, resolves the
request against the skill root, asserts containment, calls
realpath, asserts containment again so a symlink
cannot escape, and decodes with
TextDecoder("utf-8", { fatal: true }). The manifest parser
rejects .. segments, absolute paths, duplicate ids and
malformed names. Git runs with -- before the URL and a
scrubbed environment. Every skill directory is 0700 and every file 0600.
The containment of a skill already on disk is careful; which skill
arrived is not checked at all.
2. Mental Model
Three durable surfaces, none of which holds a belief.
A session is a stream of events — prompts,
responses, tool activity, usage, compaction — appended to one JSONL
file, with a title, a message count and an updatedAt. It is
retrieved whole, by id or by recency, never searched.
A skill is procedural memory with an install step. It is fetched, listed in a catalog, and read on demand; nothing scores it, ages it or retires it.
AGENTS.md is the instruction layer,
assembled per invocation from the home directory upward so that
"closer files override broader ones" — a scoping rule expressed
as ordering rather than as a filter.
The one place something is decided about memory is compaction, and it decides generously:
context reaches AUTO_COMPACT_THRESHOLD_TOKENS (250_000)
│
▼
summary replaces the messages in the model's context
│
└── session log appends { type: "compacted", summary, messages, toolActivities }
^^^^^^^^
the replaced messages are kept, not dropped
The model forgets and the store does not. Most systems in this atlas compact by destroying the source; here the event that performs the compaction carries the material it compacted, so a resumed session can still be read back in full even though the agent will not see it again.
Diagram source
%% caption: what Otis keeps between sessions, and which of it is pinned
flowchart TD
RUN["otis — a turn"] --> CTX["AGENTS.md, home → cwd,<br/>nearest last"]
RUN --> SESS[("session JSONL<br/>one dir per workspace:<br/>basename-sha256(abs path)[:12]")]
RUN -->|"skill tool, by name"| CAT["skill catalog"]
COMP{"context ≥ 250k tokens"} -->|summary into context| RUN
COMP -->|"append: summary AND the replaced messages"| SESS
CAT --> DISK[("skills on disk, 0700<br/>read path: realpath + contained twice")]
GIT["git clone -- URL / git pull --ff-only"] -->|"no revision, no hash"| DISK
UPD["otis update"] -->|"sha256 checked, install aborts on mismatch"| BIN["the otis binary"]
SESS -.->|"directory name hashes the absolute path"| MOVE["move the project → history orphaned"]3. Architecture
src/core/—agent.tsruns the turn loop,compaction.tsholds the threshold and the summary marker,context.tswalks forAGENTS.md.src/storage/—session.ts,session-events.ts(419 lines),session-files.ts,session-lock.ts: the JSONL store, its event union and its locking.src/skills/— 781 lines across ten files:manager.tsinstalls and updates,catalog.tslists,read.tsserves a resource to the tool,managed-manifest.tsvalidates the on-disk manifest,manager-lock.tsis a PID-and-token mutex with a 30-second staleness rule.src/tools/— ten tools:web_search,web_read,skill,read,grep,glob,write,edit,bash,agent.src/inference/— model catalogue, hardware probe, GGUF cache and a managed llama.cpp binary, which is where the second and third checksum surfaces live.src/desktop/— an Electron main, preload and renderer beside the terminal UI.src/permissions/,src/local/,src/cli/— approval, platform paths and settings, and the terminal UI plus a headless CLI.
Deployment and ergonomics
A single binary from a release archive, or bun run from
source. Two keys — a Fireworks key for inference and a Parallel key for
web search and page reading. No database, no service, no daemon.
The screen flagged what a reader should weigh before installing from
source: package.json changed the day before this reading
and has twenty-two floating ranges with no lockfile beside it, so a
fresh install resolves whatever those ranges now allow.
4. Essential Implementation Paths
The session partition —
session-files.ts:44:
const hash = createHash("sha256").update(workspace).digest("hex").slice(0, 12)
return `${slug || "workspace"}-${hash}`
The skill install — manager.ts:60,
await this.#git(["clone", "--", cleanURL, temporarySource]),
and the update at :160,
await this.#git(["pull", "--ff-only"], { cwd: sourceDirectory }).
The manifest contract —
managed-types.ts in full:
export type ManagedSkill = { name: string; relativePath: string }
export type ManagedSkillSource = { id: string; url: string; skills: ManagedSkill[] }
export type SkillManagerManifest = { version: 1; sources: ManagedSkillSource[] }
The containment — read.ts:
assertInside(skill.root, requested) before
realpath, assertInside(skill.root, canonical)
after it.
The binary check —
cli/update/binary-installer.ts:30, which is the standard
this project applies to itself.
5. Memory Data Model
There is no schema for a memory, because nothing is extracted. The
session event union is the closest thing, and
{ type: "compacted"; summary; messages; toolActivities? }
is its most interesting member for this atlas: an event that carries
what it replaced.
Files are private by construction — directories created
0o700 and chmoded again after, files written
0o600, and the manifest written to a temporary path and
renamed into place so a reader never sees a partial
file.
Nothing carries provenance, a source, a confidence, a validity interval or a status. A skill's origin is its source id and URL in the manifest; which commit produced the text on disk is not recorded anywhere.
6. Retrieval Mechanics
There is no retrieval over memory, and the design says so by
omission. A session is resumed whole. A skill is loaded by exact name.
AGENTS.md is concatenated by ancestry. The
grep and glob tools search the
workspace, not the history.
For a coding agent that is a defensible position — the working tree is the memory that matters and it is already searchable — and it is the same position the atlas records for the file-native harnesses. Its cost is the one those share: a session from three weeks ago is findable only if the operator remembers it exists, and there is nothing to rank.
7. Write Mechanics
Writes are synchronous, append-only and unconditional: no model call decides what is worth keeping, and there is no extraction, consolidation or background pass anywhere in the tree. A memory is retrievable as soon as the event is flushed.
--ephemeral refuses to create a session at all, and
cannot be combined with --session or
--continue — a small, correct guard.
Deletion is per session (deleteSession) and total. There
is no per-fact correction because there are no facts, and a skill is
corrected by pulling its remote, which is the same operation as being
changed without asking.
8. Agent Integration
The agent is the product, so integration is the tool list
and the permission layer rather than an adapter. The skill
tool is the memory-facing one, and its description carries the only
retrieval instruction in the system: read SKILL.md before following a
matching skill.
AGENTS.md is read as data by the harness and handed to
the model as context — the same file this atlas's screening treats as
instructions addressed to a reading agent, which is worth noticing on
both sides: Otis ships one for its own repository.
9. Reliability, Safety, and Trust
The unpinned skill is the finding. A skill is
instructions the model reads and follows. Installing it takes the
default branch; otis skills update takes whatever that
branch now holds; and the manifest has nowhere to record what was
approved. A reader who audits a skill today has audited a moving target,
and nothing in the system would report that it moved.
The fix is small and the project already owns the parts: record the
resolved commit at install, clone at that commit, and make update an
explicit bump that shows the diff. git.ts already runs
arbitrary git argument lists, and #updateSource already
calls rev-parse HEAD — but for rollback, not for
provenance. The commit it resolves is captured into a local
previousCommit, used to restore the checkout if a later
step of the update fails, and never written to the manifest. The
distance between that and pinning is one field.
The care elsewhere is real and worth crediting,
because it is what makes the gap legible rather than sloppy:
-- before a URL, --ff-only on pull, a scrubbed
child environment, double containment around realpath, a
fatal UTF-8 decode, .. and absolute-path rejection in the
manifest parser, atomic manifest writes, 0700/0600 throughout, and a
PID-plus-token mutex that treats a partial lock file as live until it
ages out.
Compaction does not destroy evidence, which is the property most systems here get wrong.
Moving a project orphans its history. The session directory hashes the absolute workspace path, so renaming a checkout leaves the old sessions on disk under a name nothing will look up again. The hash prevents collisions between two projects with the same basename, which is why it is there; the cost is not recorded anywhere a user would see it.
10. Tests, Evals, and Benchmarks
82 test files, 18,051 lines, across thirteen
directories mirroring src/, including a
skills/ suite, run by bunx vitest with a
coverage script and a CI badge on the README.
bun run verify chains check,
typecheck and test.
The search suite carries the one assertion this atlas
counts. tests/storage/session-search.test.ts
writes a session holding one ordinary turn and one compaction summary
containing "discussed the zephyr budget", then requires
searchSessions for "zephyr" to resolve to
[]. The word appears nowhere else in the store, so the
assertion is keyed on a value only the summary holds, and the comment
above it states the property rather than implying it: "The word only
appears inside the compaction summary, which is not searchable."
Beside it an empty query resolves to exactly one result, which is what
stops the assertion passing on a search that returns nothing at all.
That is negative_eval, and it is the strict kind — the
query is called and the result is what is asserted about.
tests/skills/manager.test.ts covers the update path's
rollback, including the case where the pull succeeds and a later step
fails. What no test asserts is a pinned revision, because there is none
to assert.
There is no benchmark and the project claims none — no accuracy number, no comparison, no leaderboard. For a coding agent that is the honest posture, and the atlas records it as such rather than as an absence.
Nothing was run for this review: the manifest changed the day before the reading and carries twenty-two floating ranges with no lockfile, which the seven-day cooldown in this project's screening rules refuses.
11. Patterns Worth Stealing
Steal
- Keep what you compacted, in the event that compacts
it. One field on one event —
messagesbesidesummary— and the store stops being lossy while the context still gets smaller. - Assert containment on both sides of
realpath. Checking the requested path and then the canonical path is two lines and closes the symlink escape that one check leaves open. - Refuse the combination, not just the flag.
--ephemeralcannot be used with--sessionor--continue, so "do not persist" cannot be silently overridden by "resume this". - Order context by proximity and say so. Home first, cwd last, with the convention written in the loader's docstring rather than assumed.
Avoid
- Procedural memory pinned to a URL. If a skill is instructions the agent will follow, the thing to record is the revision that was reviewed.
- A hash field nothing computes.
computedHashinskills-lock.jsonreads as an integrity guarantee to anyone who opens the file, and appears once in the whole tree. - Deriving a store's location from an absolute path without recording the mapping, so a move is indistinguishable from an empty history.
Fit
Right for someone who wants a local coding agent on open weights with a readable session history and no service in the middle — the locality claim is honest, the file permissions are right, and the code is unusually careful for its size.
Wrong as a memory system to lift. There is no fact store, no correction, no scope key and no retrieval; the transferable parts are the compaction event, the containment checks and the ephemeral guard, which are three ideas rather than an architecture. And if you adopt the skill manager, adopt it with a revision field first.
12. Antipatterns / Risks
- Skills track a moving branch, with no recorded revision and no diff on update.
- A vestigial integrity field that suggests a check the code does not perform.
- No epistemic state at all, so a skill that turns out to be wrong is corrected only by editing or removing it — and re-installing restores it.
- Session history keyed by absolute path, orphaned by a move.
- Eight floating dependency ranges with no lockfile, in a project whose own release path is checksum-verified.
AGENTS.mdfrom every ancestor of the working directory, including directories the user did not author, is instruction content assembled by location — safe in a personal checkout and worth knowing about in a shared one.
13. Build-vs-Borrow Takeaways
Borrow the compaction event and the containment pair. Both are small, both fix failures this atlas records repeatedly — compaction that destroys its source, and a path check that a symlink walks around — and neither depends on anything else here.
Build, before relying on the skill manager: a resolved commit recorded at install time, an update that reports what changed, and a manifest field to hold it. The repository's own updater is the worked example of the standard to apply.
Do not look here for a memory layer. Otis is a well-made agent that persists its work; the belief-shaped machinery this atlas compares is absent by design.
14. Open Questions
- Was
skills-lock.jsononce enforced? The file carries acomputedHashand asourceTypethe runtime manifest has no concept of, which reads like a development-time record of a vendored document rather than a lapsed check — but nothing in the tree says which. - What happens on an
--ff-onlyfailure? A force-pushed skill remote makes the update fail rather than silently rewrite, which is the safe outcome; what the user is told, and whether the stale copy keeps being loaded, was not traced here. - Is there a path for a user-authored local skill? The manager is built around remote sources; whether a skill directory placed on disk by hand is catalogued alongside them was not determined.
- How much does the agent actually read? Skills are loaded by name on the model's initiative, so their effect depends entirely on the model choosing to call the tool — measurable from the session logs the project already writes, and not measured anywhere.
Appendix: File Index
| Path | What it holds |
|---|---|
src/skills/manager.ts |
Install and update; the clone and the --ff-only
pull |
src/skills/managed-manifest.ts |
The on-disk manifest schema, path-traversal and duplicate guards |
src/skills/managed-types.ts |
The whole managed-skill contract — no revision, no hash |
src/skills/read.ts |
Containment before and after realpath; fatal UTF-8
decode |
src/skills/manager-lock.ts |
PID-and-token mutex with a staleness rule |
src/storage/session-events.ts |
The event union, including compacted with its replaced
messages |
src/storage/session-files.ts |
Per-workspace directory naming and private modes |
src/core/compaction.ts |
The 250k threshold and the summary marker |
src/core/context.ts |
AGENTS.md collection, home first and cwd last |
src/cli/update/binary-installer.ts |
The checksum verification the skills path lacks |
skills-lock.json |
A computedHash that appears once in the tree |
History
2026-09-17 — 879397c8…
— re-read 27 commits on, 183 files and +18,853/-1,634 across releases up
to v0.1.48. Marks move from none to
negative_eval. Most of the growth is desktop
surface — a macOS status tray, an on-demand Mermaid canvas, multilingual
UI, a resizable side panel — but three storage suites arrived with it,
and one holds the assertion this atlas counts.
tests/storage/session-search.test.ts writes a session
with one ordinary turn and one compaction summary containing
"discussed the zephyr budget", then requires
searchSessions for "zephyr" to resolve to
[]. The word appears nowhere else in the store, so the
assertion is keyed on a value only the summary holds, and the comment
states the property instead of leaving it implied: "The word only
appears inside the compaction summary, which is not searchable." An
empty query resolving to exactly one result sits beside it, which is
what stops the assertion passing on a search that returns nothing at
all, and a third case asserts a genuinely absent phrase returns nothing
either.
It is the strict form of the mark — the query is called and the
result is what is asserted about — and it lands on the property this
report already called the system's best: compaction is lossy for the
model and lossless for the store. The summary's text stays in the event
that replaced the messages and is held out of the search index, and now
a committed case says so. The unpinned-skill finding is unchanged;
skills-lock.json's computedHash still appears
exactly once in the tree. Screened again before reading: no auto-run
surface, no build-time execution surface, one unpinned surface, one
dependency file inside the seven-day cooldown; AGENTS.md
was treated as data. Nothing was installed and nothing was run.
2026-09-09 — 0f4025cb…
— second reading, at v0.1.36. The previous pin is no longer
in the branch: main is 62 commits ahead of it and carries
24 the pin does not, so the history was rewritten underneath it. GitHub
still serves 39e98023104d by SHA and a fresh clone cannot
reach it, which is why the previous reading's line anchors are not
re-checkable from a checkout. Screened before reading: no auto-run
surface, no build-time execution, one manifest inside the seven-day
cooldown with twenty-two floating ranges and no lockfile; nothing was
installed and no suite was run.
The central finding is unchanged and the contrast around it is
sharper. Installing a skill is still
git clone -- <url> <dir> with no revision, the
manifest is still { id, url, skills } with nowhere to
record one, and skills-lock.json still carries a
computedHash that nothing in src/,
scripts/ or tests/ reads. Beside it the
project now verifies a SHA-256 in three places rather than one: its own
release archive, the six llama.cpp binaries it may download, and every
GGUF weight file — the last with resume validation and
publish-only-after-verify.
One thing that looks like a fix is not one.
#updateSource resolves rev-parse HEAD into a
previousCommit before git pull --ff-only, and
tests/skills/manager.test.ts covers it. It is a rollback
handle, restoring the checkout when a later step fails, and it is never
written to the manifest.
The tree roughly doubled — 11,531 lines to 23,700 — and most of the
growth is context rather than memory: a local inference stack under
src/inference/ with a model catalogue, hardware probe, GGUF
cache and managed llama.cpp runtime, an Electron shell under
src/desktop/, and a tenth tool, agent. Tests
went from 42 files across ten directories to 82 files and 18,051 lines
across thirteen. No mark moves: sessions still carry no scope key
applied as a filter, no epistemic state exists anywhere, compaction
still keeps the messages it replaces inside the event that replaces
them, and no committed case asserts that anything must not be
retrieved.
2026-08-13 — 39e98023…
— first reading, at release v0.1.20. The screen reported a
package.json changed the day before with eight floating
ranges and no lockfile beside it, and an AGENTS.md
addressed to a reading agent, read as data; nothing was installed and no
test was run. The claim that computedHash is unused was
checked by grepping the whole tree for that string and for
createHash, sha256 and
digest(.