Observations collected while working on streaming, sessions, and documentation (June 2026). Each item is small enough to land as its own PR; none blocks current functionality. Check items off as they land, and add new ones at the bottom of the matching section.
- Collapse the triple redraw per message.
refresh_uicallsset_status,set_activity, andset_queued_inputs, each of which queues its own full-frame redraw — three repaints per message-loop iteration. During delta bursts this triples render work. Add a single "set view state + one redraw" command onUi. (Done:Ui::set_live_viewreplaces the three setters; one command, one redraw per loop turn.) - Keep reasoning visible after the turn. Thinking-mode reasoning only
ever exists as the transient
Thinking …tail and is discarded once content starts. Surface it as a dimmed/collapsible transcript item so a user can review why the model did something. (Done: the engine emitsreasoning_messageafter streaming; the TUI commits a✻thought aside above the answer. Resume does not replay thoughts — sessions do not store reasoning for no-tool turns; see the auto-compaction/session items.) - Measure the activity label instead of reserving 13 columns.
ActivityPreviewReservedColumnshardcodes indent + widest label + slack; computing it from the actual label keeps the preview honest if labels change. (Done:streaming_activity_linemeasures the label's display width; only the renderer indent + slack remain a constant.) -
--continueresumes the most recent session. (Done:SessionStore::latestpicks the most recently active session by event-log mtime;openseek-tui --continueresumes it, errors when combined with--session, and starts fresh on an empty store.) - Session switching inside the TUI. A way to list and switch sessions
without restarting. Generated ids (
tui-YYYYMMDD-HHMMSS-mmm) are only discoverable via the startup banner oropenseek --session-listtoday. - Steering a running task.
Steerwhile running is rejected ("press Tab to queue") because the engine cannot accept mid-turn input. Needs an engine-side protocol (e.g. a control channel on stdin) before the TUI can offer it. (Done: Enter while running steers — the text rides the serve protocol's lossless channel, lands at the turn's next step boundary, wins over model-initiated completion, and is echoed into the transcript at the position the model actually saw it.) - Persistent engine per session. The TUI spawns a fresh engine per
prompt, so
moon_check --watchrestarts (and re-warms) every turn and its watcher state is lost between prompts. A long-lived engine process per session — with prompts delivered over a channel — would keep watchers warm and is also the prerequisite for steering. (Done: one--serveengine per TUI session — JSONL commands on stdin, shared runtime and tool registry across turns, Ctrl-C cancel → kill escalation, respawn on death from the durable session.)
Findings from a real 156-step session log (seek-toml, 2026-06-11: "Write a toml parser in MoonBit with high quality tests" — succeeded, but the log shows where steps and tokens went to waste):
-
One watcher per directory, not per option set. The
moon_checkregistry keys watchers by the full option set, so togglingdeny_warnstacked three concurrent watchers on one cwd. They competed for moon's build lock with each other and with the model's ownmoon testruns, produced stale results, and the model ended up runningpkill -9 -f "moon check"mid-task to recover. Same cwd + new options should replace (stop) the old watcher. (Done: the registry keys by cwd; a running watcher is reused when the incoming command matches and stopped+replaced (watcher=replaced) when the options changed, with the tool description telling the model so.) -
Stop resending historical
reasoning_content. The request encoder sends stored reasoning back on every historical assistant message — 11% of a 141K-token final context was old reasoning the model never needs again (DeepSeek's own docs say not to pass it back). Drop it when projecting session history to API messages. (Done: the wire encoder never emitsreasoning_content— the right chokepoint, since the 11% accrued within a single 156-step turn via the agent's incremental pushes, not only across turns. Sessions still store reasoning for the TUI transcript and the visualizer.) -
Remove stale watcher notices. 61 moon_check runtime notices (11% of final context) were appended, mostly identical re-reports from the duplicate watchers. The first mitigation coalesced notices per watcher; the follow-up removed the unused runtime-notice producer/API entirely.
moon_checknow returns watcher snapshots only from direct tool calls.)* -
First-class API-lookup tool (or document the probe recipe). A/B result (2026-06-12, 5 flash trials per arm): a prompt addendum teaching query forms, the no-results fallback ladder, multi-query batching, and the probe recipe did NOT help — pass rate 3/5 → 2/5, failed doc queries 11 → 18, zero multi-query adoption. Prompt-tail instruction alone is insufficient for flash; next candidates are a dedicated lookup tool with query rewriting, or folding examples into the base prompt's tool docs rather than an addendum. The model spent ~25 steps probing stdlib APIs: repeated
moon ide docqueries that returned "No results found" (7KB of failed lookups in one result), 8 consecutive failed one-liner compile experiments against@strconv, and onemoon ideOCaml assertion crash. Better query ergonomics (or a documented snippet-eval recipe) converts that tail of failures into one or two steps. Second A/B (2026-06-12, 5 pro trials per arm, TOML task): verified glob/multi-query/miss-recovery guidance folded into the prompts' tool docs (the placement the first result pointed at) — successes 1/5 → 5/5, no 160-step cap overruns (was 2), doc calls 25 → 37 while misses fell 23 → 17 (miss rate 0.92 → 0.46), package-listing exploration up 17 → 25. Promoted: every statement is CLI-verified and the trace improved across the board. Attribution caveats recorded: n=5; the taught glob (1 use) and multi-query (0 uses) forms themselves were barely adopted — the absorbed lesson looks like "trust doc lookups, recover by listing the package"; baseline's failures were step-cap finish discipline and project misplacement, not lookup tails. A dedicated lookup tool with query rewriting stays open as the zero-adoption-risk endgame; schema-level cues beat prose (see the multi-path read tool). -
Ground the model in its environment. The system prompt never states the working directory, so models guess on step 1: in a 5-run batch, one agent passed
cwd="/workspace"(rejected: does not exist), then explored the parent directory, found a concurrent sibling run's files, and adopted that workspace wholesale — two agents silently co-editing one project. Another opened withread /home/user. Inject the engine's cwd (and a "stay inside the workspace; siblings/parents are other projects" line) into the prompt at run start. (Done: an## Environmentsection names the realpath cwd on every prompt source; judged by a concurrency-5 experiment — hallucinated step-1 paths 2/5 → 0/5, sibling-workspace hijack eliminated, 5/5 artifacts in their own directories. Eval harnesses now run trial engines inside their staged workspaces so grounding and isolation agree.) -
Remove watcher notice payload path. The trimming idea is obsolete:
moon_checkno longer emits runtime notices, and diagnostics are returned from direct tool calls instead. -
Encourage batching independent tool calls. 154 of 156 steps made exactly one tool call; the model proved it can batch (it opened with two parallel reads). A system-prompt nudge for batching independent reads/checks would cut round trips — at ~7s a step, real minutes.
-
Timestamps on session events.
events.jsonllines carry no timestamps, so per-step latency and where wall-clock went cannot be reconstructed from the log (this analysis had to infer duration from file mtimes). Onetsfield per event line fixes it and feeds the viz. (Done: the store stamps every appended event withts(unix ms, required field — no external users, so old logs are simply re-created; in-memory appends without a clock carry the 0 sentinel).) -
Expose thinking controls.
run_with_runtimehardcoded max-effort thinking; make it an engine flag (--thinking no|high|max) and a TUI pass-through. (Done:OPENSEEK_THINKINGis env-backed on both binaries and threaded through@agent.run*as an optional param with max as the default.) -
Auto-compaction — deferred, reframed as a context-ceiling guard. Compaction exists only as the manual
--session-compact-*flow. Deliberately deferred (owner decision, 2026-06-12): compaction rewrites the model-facing history prefix, so every request after it is a DeepSeek prefix-cache miss until the new prefix re-warms — at cache-hit pricing a long append-only history is cheaper than periodically re-paying full price for a rewritten one, plus summary-generation cost and information-loss risk. When implemented, trigger only as the projected prompt approaches the model's context window (where the alternative is failure), not as a cost optimization. (Done as exactly that guard: after every response the loop compares reported total_tokens against 80% ofModel::context_window; over it, a checkpoint summary (thinking=No, the manual flow's prompt) is appended through the turn's append seam — prior turns first so the active task stays verbatim, then at most one full-range checkpoint per turn, disarming on failure (fail-open) — and the request buffer re-seeds from the compacted projection. Eventsauto_compaction_started/finished/failedare distinct from the manualcompaction_*names, which the TUI treats as run-terminal; surfacing auto-compaction in the TUI status line is a possible follow-up.) -
Richer
--session-list. Bare ids are hard to recognize; include last-activity time and the first user prompt as a label. (Done:SessionStore::listingsreturns id + last-activity + first prompt, newest first with unreadable sessions kept visible for cleanup; the CLI prints tab-separated rows, so| cut -f1recovers the old bare-id output.) -
Stale
session.lockrecovery. Verify what happens when an engine crashes while holding the lock, and document (or implement) the recovery path. -
Flush the JSONL queue on hard crash. The stdout drain trades crash-tail durability for purity (no C stub): lines queued but unwritten when the process aborts are lost. A panic hook that drains synchronously would close the gap.
-
Cheapen the engine probe.
engine_launchablespawns<engine> --helpon every TUI launch; cache the result per engine path (mtime-keyed) or accept the first spawn failing fast instead. -
Skills: advertise markdown playbooks, load on demand. (Done: workspace
.openseek/skillsplus a user-level library — default$HOME/.openseek/skills, override--global-skills-dir/OPENSEEK_GLOBAL_SKILLS_DIR, workspace shadows global by name — are listed name/description/path in a## Skillsprompt section; bodies load through the existingreadtool. Effectiveness A/B (2026-06-11, 5 flash trials per arm, marketplaceanthropics/skillsbrand-guidelines skill, "branded landing page" task): skill arm read the skill unprompted 5/5 and shipped the exact brand palette 5/5 (7–36 hex occurrences); no-skill control hit the palette 0/5; negative control — skill advertised, haiku task — read it 0/5, so the listing does not pollute unrelated tasks. Listing cost ~1 line/skill; the matched read added one step. Eval harnesses pinOPENSEEK_GLOBAL_SKILLS_DIRinto the trial workspace so a developer's personal library never leaks into trials.) Follow-ups: TUI surfacing of available skills, multi-skill libraries under noisier tasks.
- Unify
chatandchat_stream. They share request encoding but duplicate response and error handling. Implementingchatas streaming-with-no-callbacks leaves one wire path to maintain. - Retry transient HTTP failures. Both entry points are single-shot; a bounded retry with backoff on connect/5xx errors would remove the most common spurious turn failures.
- More CI platforms. CI is a single
check (nightly)job; the TUI's pty handling and the engine's stdio behavior are platform-sensitive (macOS pty quirks surfaced during verification). Add macOS, then Windows. - Make the live cram lifecycle test extension-proof. It asserts an
exact event-name set and broke when
*_deltaevents were added (now filtered). Assert the stable lifecycle subset is present instead of asserting the full set. (Done: the example whitelists the lifecycle events, so new payload event kinds cannot break it.) - Checked-in TUI integration harness. The pty driver used to verify
streaming and session memory lives outside the repo. Pair a small pty
driver with a recorded-stream replay engine (the TUI already accepts
--engine <replayer>) for deterministic offline TUI tests. - Run
moon fmt --checkbefore push. Two consecutive PRs hit CI-only formatting failures; a pre-push hook or documentedmoonalias would catch them locally. - CI does not exercise the JS packages.
ci.ymlrunsmoon check/moon teston the default (native) target only, so the visualizer's js-only packages (viz,cmd/viz_app) are silently skipped — they compile and pass locally but a regression there would not fail CI today. Add a--target jscheck/test step (or a target matrix). Their logic is currently covered only by local unit tests + a headless-browser run.
- Doc-exemplary prompt examples.
prompt/moon.pkgopts the system prompt out ofmissing_docbecause its example code blocks are model-facing text. Consider documenting those examples as a prompt change (with an eval run) so the model also learns the doc convention. - Document the TUI session default. The root README does not yet
mention that every TUI launch converses in a durable session and how to
resume one (
--session <id>from the startup banner). (Done: the README gains a Terminal UI section covering default sessions,--continue,--session, and the enriched--session-list, plus acmd/tuirow in the packages table.)
Follow-ups for the events.jsonl web viewer (viz, inspect,
cmd/viz_app, web/).
- Smarter
--session-rootdefault discovery. The default is the relative.openseek, resolved against the server's cwd;moon run inspectuses the module root as cwd (no.openseekthere), so the no-arg case shows nothing. Walk up from cwd to the nearest.openseek(like git finds.git) so the default "just works" from a subdirectory. - Optional self-contained binary for distribution. Today the server
serves
web/index.htmlfrom disk and auto-locates the builtviz_app.jsfrom_build/. If we ever want to ship the viewer as one binary, embed both via themd_to_mbt_string-styleruleand accept the js→embed→native build order. Not worth it for a local dev tool; embedding only the HTML is a half-measure (the binary still needs the JS), so it is all-or-nothing. - UI polish. Sidebar: show the last-active timestamp (the server
already returns it; the frontend ignores it). Turn headers: show a per-turn
summary (tool count, terminal status) on the collapsed
<details>line. Tool cards: collapse long output by default with an expand toggle. Navigation: jump-to-errors, text search/filter, expand/collapse all. - Persist the theme choice. The Light/Dark/System toggle resets to Light on reload; persist it (localStorage) so the choice sticks.
Observations from building the visualizer (native server + JS frontend in
one module). These are toolchain/language ergonomics — several are likely
upstream moon/MoonBit items rather than openseek changes — but they cost
real time and are worth tracking.
-
moon infodoes not generate.mbtifor js-only packages. In a module whose canonical backend is native,moon infowrites nopkg.generated.mbtiforsupported_targets = "js"packages — it only prints a perpetual "requested interfaces different from canonical backend Native" diff. I had to hand-copy the generated interface out of_build/. A per-package canonical backend (ormoon info --target jswriting the files) would fix it. - No cross-target build dependency.
moon buildis per-target, so there is no way to express "build the JS bundle, then embed it into the native binary" in one invocation — it forces a manual two-step. This is the root reason the viz bundle is served from disk rather than embedded. - Library aborts are invisible and uncontainable. A second
send_responseon a connection hit aguard … else-less panic (abort) in@async/httpthat tore downrun_forever; neithercatch(handlesError, not panics) norallow_failurecontained it. Checked errors are tracked in signatures, but panics/aborts are not — so you cannot tell which library calls can kill the process. Surfacing "can abort", or a task boundary that contains aborts, would make robust server code tractable. (Worked around in viz by computing each reply before a single send.) - Default trait methods are not callable through a trait object.
@fs.read_filereturns&@io.Data; the requiredbinary()/text()dispatch fine, but the defaultto_bytes()/to_bytesview()fail with[4039] Cannot use method … of abstract trait. The required-vs-default rule through&Traitis non-obvious and cost compile cycles. - A
moon fixautofixer for deprecations. A lot of small edit cycles came from deprecation warnings with mechanical rewrites:not(x)→!x,StringView::to_string→to_owned,String::substring→view slicing,@strconv.parse_int→@string.parse_int,derive(Show)→derive(Debug), plus+unnecessary_annotationcascading. An autofix (à lamoon fmt) would erase most of these. - Lint a
..cascade that discards a non-unit return.session..append(x)on an immutable builder (whereappendreturns a newSession) compiles and silently no-ops — it caused a real test bug. Warn when..drops a non-Unitresult. - Surface dependency examples/APIs in
moon ide doc. Learning third-party APIs (rabbita's callableEmit,@async/http.Server::Server,@utf8.decode_lossy,&Data::binary) meant grepping.mooncakes/.repos. Examples likehttp_file_serverwere invaluable once found; surfacing dep docs/examples throughmoon ide docwould shortcut discovery.