| Age | Commit message (Collapse) | Author |
|
barrel
Wave 5c (final wiring) of transparent SSH support.
- host-bin: register exec-backend + ssh in CORE_EXTENSIONS (exec-backend before
the tool extensions that dependsOn it; ssh after, provides the remote-backend
factory + ComputerService at boot). +@dispatch/exec-backend/@dispatch/ssh deps +
tsconfig refs.
- transport-http: CR-5 — re-export computerServiceHandle + ComputerService type
from the package barrel (src/index.ts), mirroring lsp/mcp handles, so ssh imports
the typed symbol cleanly (no more dist/seam.js subpath workaround).
- orchestrator: added the @dispatch/exec-backend dep the host-bin agent missed +
bun install.
LIVE-VERIFIED: bun packages/host-bin/src/main.ts boots clean ('Dispatch booted',
no disabled extensions) — exec-backend + ssh + all tool extensions load together.
Verified: tsc -b EXIT 0, biome clean, 1690 vitest pass (+6 sshd-integration skipped).
DEFERRED (CR-6): listComputers usageCount stays 0 until a conversation-store
count-by-alias helper is added (non-blocking).
Refs: notes/ssh-support-plan.md. No merge or push.
|
|
New `mcp` standard extension (`packages/mcp/`) that makes Dispatch an MCP
host: spawns configured MCP servers (stdio child processes), performs the MCP
handshake (initialize → notifications/initialized), discovers tools via
tools/list, and registers each as a first-class Dispatch ToolContract via
host.defineTool. When the model calls an MCP tool, the extension proxies the
call to tools/call on the MCP server and returns the flattened result.
Architecture (sibling of `lsp` extension):
- Config: .dispatch/mcp.json (servers key) → opencode.json mcp key fallback,
resolved per-cwd (mirrors LSP config resolution)
- Transport: StdioTransport (spawn child, Content-Length framing + JSON-RPC 2.0)
- Client: initialize → tools/list → tools/call; handles list_changed
notifications for dynamic tool updates
- Registry: tool name namespacing (<serverId>__<toolName>), ToolContract
adapter that proxies execute → callTool, content flattening (text/image/
resource → string)
- Manager: one client per server, lazy-spawn, status(), shutdownAll()
- Extension: manifest (dependsOn session-orchestrator, capabilities spawn),
registers tools + a toolsFilter (drops disconnected server's tools),
mcpServiceHandle, deactivate kills all child processes
Phase 1 scope: stdio only, Tools only (no Resources/Prompts/HTTP/sampling).
Hand-rolled JSON-RPC + framing (zero external deps, adapts LSP patterns).
Wave 1 (agent): 12 source + 8 test files, 69 new tests.
Wave 2 (orchestrator): root tsconfig ref, host-bin CORE_EXTENSIONS
registration + package.json dep, bun install.
Verified: tsc -b EXIT 0, biome clean, 1537 vitest pass (was 1468, +69).
|
|
Register @dispatch/system-prompt in CORE_EXTENSIONS (after skills, before
cache-warming). Add dep + tsconfig ref. 1396 vitest pass, typecheck + biome clean.
|
|
New standard tool extension backed by a self-hosted transcriber service
(http://100.102.55.49:41090, Tailscale, no API key). One tool
youtube_transcript — fetches transcripts for YouTube videos. Returns
completed (full text + timestamped segments), queued/processing (position
+ ETA + .youtube_subtitles_pending retry convention), or failed (error).
Pure core: validateUrl + format* functions + truncateOutput. Injected
edge: TranscriptClient (injectable fetchFn, AbortSignal.any for
cancellation). concurrencySafe true, capabilities network. 30 tests.
Verified: tsc EXIT 0, 1152 vitest, biome clean (327 files). Boot smoke
clean.
|
|
New standard tool extension with a single todo_write tool (opencode
todowrite pattern: full-list replace, returns JSON, no business-rule
enforcement — the description guides the model). Per-conversation in-memory
state + per-conversation surface (rendererId: todo, scope: conversation)
via subscriber-notify (message-queue pattern).
Wave 0 (kernel contract): added conversationId?: string to ToolExecuteContext
(additive, backward-compatible). Wired in dispatch.ts — the kernel already
had it but wasn't passing it through to tools.
Wave 1 (todo extension): pure core (validateTodos — shape only; getTodos/
setTodos/clearTodos; buildTodoSpec; formatTodoResult). Shell:
createTodoWriteTool + surface provider. Tool description matches opencode's
todowrite.txt depth (when-to-use, examples, task states). Priority field
removed (bloats the tool with little value). 25 tests.
Wave 2 (host-bin): registered todo in CORE_EXTENSIONS + dep + root tsconfig ref.
Verified: tsc EXIT 0, 1123 vitest, biome clean (314 files). Boot smoke clean.
FE handoff: frontend-todo-handoff.md.
|
|
New standard tool extension with one tool web_search supporting 4 modes
(search, scrape, crawl, map) against a self-hosted Firecrawl instance.
Pure core: validateArgs (discriminated union by mode) + format* functions
+ truncateOutput. Injected edge: FirecrawlClient (injectable fetchFn/sleep/now,
AbortSignal.any for per-request timeout + caller cancellation). concurrencySafe
true, capabilities network. 38 tests, zero vi.mock.
Live-verified: umans-glm-5.2 called web_search → real Firecrawl results (also
the first live Umans API call).
|
|
Extract a generic @dispatch/openai-stream library from provider-openai-compat
(convert-messages, convert-tools, parse-sse, listModels, stream, provider),
parameterizing createOpenAICompatProvider with uid=1000(tradam) gid=1000(tradam) groups=1000(tradam),966(docker),968(ollama),998(wheel) + hook.
Refactor provider-openai-compat to import from the lib (byte-identical behavior).
New @dispatch/provider-umans extension wraps the Umans OpenAI-compatible backend
(https://api.code.umans.ai/v1). Self-contained: reads UMANS_API_KEY from env
directly (no auth-apikey dep). transformBody maps reasoningEffort →
reasoning_effort (capping xhigh/max → high). Dynamic listModels via GET /v1/models.
host-bin: registered provider-umans in CORE_EXTENSIONS + umans credential
(gated on UMANS_API_KEY — the credential is the model-catalog index).
Verified: tsc EXIT 0, 1059 vitest, biome clean (293 files). Boot smoke:
umans models appear in GET /models (7 models live).
|
|
A per-conversation message queue (new message-queue extension) holds user
messages enqueued while a turn generates; delivered mid-turn as steering at the
tool-result boundary (or carried to a new turn if no tool call fires).
- kernel: RunTurnInput.drainSteering callback (generic; kernel stays pure)
- wire 0.7.0->0.8.0: QueuedMessage, QueuePayload, TurnSteeringEvent (additive)
- transport-contract 0.11.0->0.12.0: POST /conversations/:id/queue + chat.queue WS op
- message-queue ext: queue state + per-conversation custom surface (rendererId message-queue)
- session-orchestrator: enqueue facade + drainSteering wiring + post-seal carry
- transport-http/ws: queue endpoint + chat.queue op (fixes WsClientMessage exhaustive switch)
- host-bin: register message-queue
1043 vitest + 199 transport bun pass; tsc/biome clean; boot smoke clean.
FE courier: frontend-message-queue-handoff.md.
|
|
cache bust
LSP + per-conversation CWD feature:
- new bundled `lsp` extension: hand-rolled JSON-RPC codec (framing/rpc), lazy
one-server-per-(serverID,root), per-cwd config resolution, on-demand `lsp` tool
- `conversation-store`: getCwd/setCwd (cwdKey); `session-orchestrator` defaults a
turn's cwd from the store
- `transport-http`: cwd + lsp status endpoints; wire types in transport-contract
- host-bin: register lsp; config wiring
Cache-warming fix (the warm read 0% on the first reheat after a message):
- warm assembled tools under a different cwd than the real turn (a reheat sends no
cwd, and the warm service had no store fallback). The skills filter rewrites the
cwd-sensitive `load_skill` description, so the tools block — the first bytes of
the prompt-cache prefix — diverged and the cache missed entirely. Warm now
resolves cwd as opts.cwd ?? conversationStore.getCwd(), mirroring handleMessage.
- capture warm sends as `provider.request` spans flagged `warm:true` (thread a
child logger into providerOpts) so warm vs real bodies are diffable (obs §3.1).
- kernel logger: span-close now merges child-bound attrs like span-open, so a
`warm:true` query finds the closed span (with usage/status), not just the open.
Tests: warm forwards a warm-flagged logger; warm falls back to stored cwd; logger
open/close attr consistency. Full suite green (873).
|
|
Backend-driven warming targeting whatever provider a conversation uses (incl. the
external Claude provider-anthropic). Core engine + on/off + last-cache-% done;
interval-as-view-control pending a ui-contract NumberField (surface-system gap).
Mechanism:
- kernel: expose HostAPI.emit (typed bus event emit; counterpart of on)
- session-orchestrator: turnStarted/turnSettled event hooks (conversationId/cwd/model);
warm() service (cacheWarmHandle) reusing the real-turn assembly (byte-identical prefix,
provider-agnostic), refuses mid-turn, never persists/emits, returns Usage
- cache-warming (new ext): per-conversation timers (arm on settle, cancel on start,
in-flight invalidation), calls warm(), pct=round(clamp(cacheRead/input,0,1)*100),
persists {enabled,intervalMs} (default on/240s), registers a controls surface
- host-bin: register cache-warming; transport-http: HostAPI stub +emit (fan-out)
Honors old-code invariants. 760 vitest + 109 bun = 869 tests; tsc -b EXIT 0; biome clean.
|
|
Skills are markdown in .skills/ dirs (~/.skills + <cwd>/.skills, cwd shadows home;
name = filename). Format: line1 summary, line2 ---, body line3+; load strips the
first two lines; malformed = no summary but still loadable.
Mechanism (first use of the context-assembly filter chain, §3.2):
- kernel: expose HostAPI.applyFilters (delegates to bus.applyFilters)
- session-orchestrator: define/export toolsFilter + ToolAssembly; apply once per turn
before runTurn (cache-stable across steps), threading cwd + conversationId
- skills (new ext): pure parse/merge/render + load_skill tool (live read, path-contained)
+ a toolsFilter filter rewriting load_skill's description + name enum per cwd
- host-bin: register skills in CORE_EXTENSIONS
- transport-http: fix HostAPI test stub for the new applyFilters method (fan-out)
734 vitest + 109 bun = 843 tests; tsc -b EXIT 0; biome clean; clean live boot.
|
|
Four standard-tier tool extensions (one tool per extension, zero ABI change):
- tool-read-file: read_file now lists directory contents (sorted, /-suffixed subdirs)
- tool-shell: run_shell (foreground, streamed, cancellable, cwd, timeout + output cap)
- tool-edit-file: edit_file (oldString/newString/replaceAll; errors on absent/non-unique)
- tool-write-file: write_file (explicit overwrite flag)
Registered in host-bin CORE_EXTENSIONS. Live boot clean (shell capability accepted).
686 vitest + 89 bun = 775 tests; tsc -b EXIT 0; biome clean.
|
|
New throughput-store extension records one token-weighted sample per turn
(model, output tokens, pure generation time = Σ step genTotalMs) into a
day-bucketed KV store, and aggregates per-model tok/s = Σtokens / Σgen-seconds
over a day/week/month (server-local boundaries; week = ISO Mon–Sun).
transport-http records a sample per turn (logged) and serves
GET /metrics/throughput?period=day|week|month&date=<...>. The response is typed
as transport-contract's ThroughputResponse, so store/wire drift is a compile
error. Pure period + aggregate logic fully unit-tested.
|
|
Add loadExternalExtensions(): fault-isolated dynamic import of out-of-repo
extensions declared via DISPATCH_EXTERNAL_EXTENSIONS. main.ts assembles the
credential-store in boot() so a 'claude' credential is registered when an
external anthropic provider is loaded; config.ts surfaces the anthropic model /
credential-key settings those extensions read.
|
|
log scope)
Make transport-http a full-fidelity extension that runs its own Bun.serve
inside activate(host) — symmetric with transport-ws. The Hono app is now built
with the extension-scoped host, so all HTTP edge logs are correctly attributed
extensionId=transport-http instead of the host-bin __host__ scope (verified
live in the journal).
- transport-http: createTransportHttpExtension() factory; activate builds the
app + Bun.serve, reads host.config httpPort (?? 24203); deactivate stops it.
- host-bin: drops the HTTP Bun.serve + createServer call; config.ts maps
BACKEND_PORT/PORT -> httpPort. host-bin now serves no transport (both
transports self-serve); boot log -> 'Dispatch booted'.
- +5 bun lifecycle tests wired into test:bun.
No contract change (composition wiring). Verified live: HTTP serves on :24203;
journal edge logs now scoped transport-http. typecheck clean, 498 vitest + 89
bun, biome clean.
|
|
split (B2)
FE slice 1 — backend-declared, frontend-agnostic surface system (verified live): new types-only @dispatch/ui-contract (SurfaceSpec / field kinds / region / ActionRef / catalog), surface-registry (typed service handle), transport-ws (Bun WS :24205, path-agnostic upgrade), surface-loaded-extensions (first real surface); kernel HostAPI.getExtensions; host-bin wiring; bin/up. Harness: retire AGENTS 'backend only', ORCHESTRATOR §3/§7/§8, frontend-design.md locked.
B2 — wire-types split (chat-slice prerequisite): new types-only @dispatch/wire single-sources the wire ABI (AgentEvent + 11 variants; conversation model Chunk/ChatMessage/Role/TurnId/StepId + 6 chunk variants; Usage) with zero @dispatch/* deps. @dispatch/kernel re-exports via shims so its public surface is byte-identical (zero consumer blast radius). transport-contract re-exports AgentEvent from @dispatch/wire and drops its @dispatch/kernel dependency, so HTTP clients (the web frontend) consume the wire without the kernel runtime.
tsc -b + biome clean; 460 vitest + 77 bun pass.
|
|
per-turn cwd through orchestrator/transport/host-bin
|
|
drain-last / restart) — 288 tests
host-bin spawns the out-of-process collector before serving (real Bun.spawn adapted to a ChildHandle), restarts on unexpected exit (backoff + restart-guard cap), drains on SIGINT/SIGTERM (collector final-drain, SIGKILL fallback on timeout). createCollectorSupervisor takes an injected spawn so the lifecycle is unit-tested with a fake (no real subprocess). Collector failures never crash the app (D3 subordinate/fail-safe). New env DISPATCH_TRACE_DB (default ./.dispatch-data/traces.db).
Verified: tsc -b clean, 288 tests (279 + 9 supervisor), biome 0/0. Live (clean single run): 1 collector during, trace DB auto-populated (nested easy-view), 0 collectors after shutdown.
|
|
sink (250 tests)
Structured, agent-first logging captured durably to an append-only journal file.
Kernel (contracts/logging.ts): leveled/attributed Logger + Span, auto-scoped per extension (host stamps manifest.id, unspoofable), incremental span records (open/close) for crash-reconstructable traces, injected LogSink (pure record-builder). ctx.log on ToolContract; runTurn opens turn/step/tool-call spans and captures the verbatim pre-mutation prompt (the 'before') on the step span.
journal-sink (new package, bootstrap dep — not an extension): LogSink appending NDJSON to a rotating journal; pure serialize + thin fs edge; fail-safe drop, never blocks a turn. host-bin injects it via HostDeps; session-orchestrator threads host.logger (childed per turn) into runTurn.
Redaction is per-extension self-redaction (no shared helper — isolation over DRY). The out-of-process collector + SQLite store + the verbatim 'after' provider.request capture are Phase B / next (notes/observability-design.md §10/§11).
Verified: tsc -b clean, 250 tests (218→+32), biome clean. Live boot: a turn's journal holds host logs + turn/step spans (open+close) + the prompt:before record with the verbatim messages array.
Harness: ORCHESTRATOR §3 rule-scoping map; .dispatch/rules/isolation-over-dry.md; notes/observability-design.md (design D1–D10 + Phase A/B plan).
|
|
storage-sqlite manifest honesty
host CR-1: createHost.getHostAPI() returns the canonical post-activation HostAPI
(registration closed) via a single builder — host-bin deletes its
buildPostActivationHostAPI duplicate and calls host.getHostAPI().
storage-sqlite CR-2: remove false contributes.services:["storage"] (backend is a
kernel bootstrap dep injected as HostDeps.storageFactory, not a bus service);
document the intentional no-op activate.
typecheck clean, 218 tests pass, biome clean; live boot + curl verified.
|
|
First TOOL extension (standard tier, fs capability). Pure-core/shell split with
workdir containment (realpath symlink guard). host-bin registers it in
CORE_EXTENSIONS; flows into runTurn via session-orchestrator's resolveTools.
Verified: typecheck clean, 214 tests pass (was 185), biome clean. Live curl
against flash produced a real tool-call + tool-result round-trip with correct
final answer. Proves the kernel tool-dispatch loop end-to-end (plan §3.3).
|
|
- kernel HostAPI: add getAuthProviders()/getAuthProvider(id) read-views (mirrors getProviders)
- provider-openai-compat: activate() resolves creds via host.getAuthProvider("apikey").resolve(); dependsOn auth-apikey; model stays config-driven
- host-bin: mirror the new getters in post-activation HostAPI stub
- auth-apikey is no longer vestigial; auth seam exercised end-to-end
- 185 tests pass; typecheck + biome clean; verified live (curl returns real response)
|
|
ORCHESTRATOR↔plan §5/§3.6, add HANDOFF.md, host-bin reads BACKEND_PORT (24203)
|
|
Bun.serve; full-fidelity wiring (178 tests)
|