| Age | Commit message (Collapse) | Author |
|
Kimi was trying to use Python tools to analyze images rather than just
describing them. Updated both vision system prompts (consult_vision and
image compaction) to explicitly instruct: do not use any tools unless
specifically asked to — just use your vision to see the image and describe
it directly.
|
|
reconcile drops thinking-only messages
Root cause of tool-calls-in-thinking bug: append() assigns msgIdx as a LOCAL
index (reset to 0 per call), but load() grouped chunks by msgIdx alone. Since
the orchestrator persists messages one at a time (append([user]) at turn start,
then append([assistant, ...toolResults]) per step), all single-message appends
share msgIdx=0 and collapse into one giant user-role message. The model loses
its prior assistant responses and tool-call history, falls back to text-based
tool-call syntax inside reasoning_content, and the turn ends with finish_reason
stop (no structured tool_calls detected).
Fix 1 (store.ts load()): split message boundaries on role change too, not just
msgIdx. Handles the alternating user/assistant/tool pattern correctly.
Fix 2 (reconcile.ts hasContent): include thinking chunks as valid content so
thinking-only assistant messages are not silently dropped on load. The buggy
seq-14 output (assistant, thinking-only) was being deleted by reconcile,
destroying evidence of the bug.
Verified: load() on the affected conversation now produces 9 correct messages
(was 3 merged). All 1999 tests pass. See notes/tool-call-in-thinking-bug.md.
|
|
workspace-star
|
|
# Conflicts:
# packages/provider-concurrency/src/concurrency-manager.ts
# packages/provider-concurrency/src/extension.ts
|
|
safety + persist auto-reduce notice
Bug 1 (overshoot): acquire() fast-path now queues while a recycle-poll is in
flight (gatePolling), and the recycle defers its inFlight decrement until the
usage poll resolves — holding inFlight inflated through the poll window so a
concurrent caller cannot sneak through before upstream confirms room. The gated
path admits exactly one waiter per fresh poll (grantOne), robust against stale
upstream counts. Common-case throughput preserved: the fast-path still grants
immediately when no poll is in flight.
Bug 2 (unhandled rejection): fetchUsage is wrapped in safeFetchUsage — a
throwing getUsage() is caught, treated as undefined (cooldown-only fallback),
and reported via the new onUsagePollError opt (warn-level). No unhandled
promise rejection can crash the process.
Bug 3 (lost notice): loadLimits now calls restoreLimit (a new lower-level
state-seeding method that does NOT clear autoReduced) instead of setLimit
(which is a manual user action that clears the notice). The auto-reduce marker
(autoReducedFrom) is persisted under auto-reduce:<providerId> and restored on
activate via loadAutoReduce, so the frontend banner survives a restart.
+6 tests covering each bug.
|
|
concurrency service absent
Bug 1 (MEDIUM): In-memory starred cache leaks IDs for deleted workspaces.
The starredWorkspaces Set in the concurrency manager never cleaned up when a
workspace was deleted. FIX: the DELETE /workspaces/:id route now calls
concurrencyService.notifyWorkspaceStarred(id, false) after deleting, so the
deleted workspace ID is removed from the in-memory cache (preventing stale
IDs and preventing a re-created workspace with the same slug from inheriting
the old starred state).
Bug 2 (MEDIUM): Stale in-memory priority when concurrency extension is absent.
If provider-concurrency is not loaded, the star toggle persisted but the
in-memory priority cache was never updated (the optional chaining ?. silently
skipped the call). Already-queued agents kept their old priority until
restart. FIX: the star/unstar routes now check if concurrencyService is
defined and log a warning when it is absent, making the degraded behavior
visible. The starred state still persists correctly — it just does not
affect in-memory scheduling until the extension is loaded.
Tests: +5 (star round-trip with concurrency notification, invalid slug 400,
delete cleans up cache, absent-service warning log). All 1970 tests pass.
|
|
|
|
Merge dev to restore production-critical crash fixes that were committed
after the branch was cut: SSH pool permanent error listener, uncaughtException/
unhandledRejection guards in host-bin, MemoryMax=24G circuit breaker,
memory telemetry logging, LSP disable precaution, and crash investigation notes.
The provider-wrapper.test.ts:102 test passes — it was already fixed by the
acquire() signature change (adding workspaceId parameter) that realigned
the test arguments. No additional test fix needed.
Verification: typecheck 0 errors, 1965 tests pass, biome clean.
|
|
|
|
|
|
|
|
uncaughtException/unhandledRejection guards
Root cause of the live production crash (exit-1 'Timed out while waiting for
handshake'): the pooled ssh2.Client had no permanent 'error' listener after
connect, so a post-connect ssh2 error escaped as an uncaught EventEmitter
'error' with no process-level guard. See notes/crash-investigation-findings.md §1.
- packages/ssh/src/pool.ts: attach a permanent 'error' listener to the pooled
client in buildConnection that sets state=error, logs (alias, message, level),
and does not throw; cleanup() no longer removes it.
- packages/host-bin/src/main.ts: add process.on('uncaughtException') (graceful
shutdown after logging) and process.on('unhandledRejection') (log + continue),
both logging message/stack, memory snapshot, activeConversations count, and
timestamp so the failure site is observable.
|
|
- Disable LSP extension (import + CORE_EXTENSIONS) due to crashes
- Make transport-http tolerate LSP being absent (optional getService)
- Remove lsp from transport-http dependsOn manifest
- Change memory telemetry sample interval from 60s to 15s
|
|
|
|
background files
|
|
(document lifecycle) + leaked init promises
|
|
# Conflicts:
# packages/session-orchestrator/src/extension.ts
# packages/session-orchestrator/src/orchestrator.ts
|
|
When a non-vision model (e.g. GLM) calls consult_vision, the new Kimi
consultation tab now shows 'IMAGE - <question>' instead of the bare
question-derived title, making image-consultation tabs visually
distinguishable from normal conversation tabs.
- Add formatConsultationTitle(question) pure helper (pure.ts): prefixes
'IMAGE - ' and truncates the question to 80 chars (matching the
conversation store's TITLE_MAX) with an ellipsis.
- Add setConversationTitle dep to VisionHandoffDeps, wired in the
extension to the conversation store's setConversationTitle.
- Call it in consultVision BEFORE the turn starts so the title is
correct from the first moment (the store keeps a non-'Untitled'
title on first message append). Best-effort: a title-write failure
logs a warning but does not break the consultation.
- Tests: 4 pure + 2 service (title set + optional-dep graceful).
|
|
purge on compaction/close
|
|
host storage
|
|
blocks on acquire, re-emit "active" when slot granted
|
|
settings
|
|
|
|
auto-transcription
|
|
rejects temperature: 0)
The vision handoff hardcoded temperature: 0 for the transcription sub-call,
but the Moonshot/Kimi vision model only allows temperature: 1 (or omitted),
causing an HTTP 400 "invalid temperature: only 1 is allowed for this model"
that blocked the entire image analysis for non-vision models like GLM 5.2.
Fix: omit temperature entirely so each vision provider uses its own default —
the truly universal, provider-agnostic approach (different providers have
different temperature constraints).
|
|
overshoot from provider accounting lag
|
|
vision handoff
|
|
limits with oldest-agent-first scheduling
|
|
|
|
# Conflicts:
# packages/host-bin/package.json
# packages/host-bin/src/main.ts
# packages/session-orchestrator/src/orchestrator.ts
# packages/system-prompt/src/service.test.ts
# packages/system-prompt/src/service.ts
# packages/system-prompt/src/types.ts
# packages/transport-contract/package.json
# packages/transport-http/package.json
# packages/transport-http/src/app.test.ts
# packages/transport-http/src/app.ts
# packages/transport-http/src/extension.ts
# packages/transport-http/tsconfig.json
# tsconfig.json
|
|
|
|
|
|
|
|
|
|
|
|
Agents were being cut off mid-task at 50 steps. The MAX_STEPS=50
hardcoded limit was silently terminating turns while the model was
actively making tool calls, leaving conversations idle with a
dangling tool-result as the last chunk.
Setting MAX_STEPS to 0 disables the limit — the loop runs until the
model stops making tool calls naturally or the abort signal fires.
The max-steps code path is preserved for when MAX_STEPS > 0.
|
|
|
|
The test endpoint's runProbe() waited for the ssh2 stream's 'close' event,
which some SSH servers never emit for short-lived exec channels (the command
'true' exits instantly). This caused the promise to hang forever — the HTTP
response never returned, and the FE's Test spinner spun indefinitely.
Three fixes:
1. runProbe now resolves on the 'exit' event (not 'close') — the command has
finished and the exit code is available. 'close' is kept as a fallback.
Stream data/stderr are drained to prevent buffer deadlocks.
2. runProbe has a 15s timeout safety net — if the exec callback or 'exit'
event never fires (e.g. server requires a pty for exec), the probe
resolves false instead of hanging forever.
3. The entire test() method is wrapped in a 30s Promise.race timeout —
even if pool.acquire() or pool.drop() hangs, the endpoint ALWAYS
responds with { ok, error? }.
The probe is fully non-interactive (no blocking prompts). tsc EXIT 0,
biome clean, 1756 tests pass.
|
|
Two improvements to the SSH support feature:
1. KNOWN_HOSTS DISCOVERY (packages/ssh):
Computers are now auto-discovered from ~/.ssh/known_hosts (every hostname
you've ever connected to) in ADDITION to ~/.ssh/config (explicit Host
aliases). Config entries take precedence (full params); known_hosts entries
get defaulted params (User=defaultUser, IdentityFile=null→pool probes
default keys, Port from [host]:port or 22, knownHost=true). Zero-config —
no ~/.ssh/config file needed; hosts just appear.
Reject list: dispatch.toml [ssh].reject = [...] (glob patterns like
github.com, *.ts.net) filters noise from the catalog. Read from both
the global ~/.config/dispatch/dispatch.toml and the project dispatch.toml.
Parsed with Bun.TOML.parse (zero deps). Only filters discovery (catalog);
specific lookups (getComputer/getStatus/test/connect) ignore the reject
list (it's a visibility filter, not access control).
New pure functions: parseKnownHosts(), isRejected(), globMatch().
+26 tests. tsc EXIT 0, biome clean, 1756 tests pass.
2. REMOTE SYSTEM-PROMPT AWARENESS (packages/system-prompt):
When a conversation has a computerId set (remote turn), the system prompt
now resolves system:os, system:hostname, git:branch/git:status, and
file: reads against the REMOTE machine — not the local host. Previously
the prompt always said 'Arch Linux (WSL)' + local hostname even when the
agent was connected to a remote Artix Linux machine.
The ResolverAdapters' hostname()/platform() are now async (so a remote
adapter can run 'hostname'/'uname -s' over SSH). The system-prompt
extension builds remote adapters from the ExecBackend (readFile→SFTP,
spawn→SSH exec). Cache invalidation now checks computerId (switching
computers rebuilds the prompt). The compaction path also threads
computerId. @dispatch/system-prompt now depends on @dispatch/exec-backend.
|
|
Brings dev's retry-with-backoff (the transient `provider-retry` AgentEvent the
web frontend consumes) + the LSP-dead-server per-edit-hang fix into the SSH
feature branch, alongside the SSH waves 0-5c.
All code files auto-merged cleanly (run-turn.ts, orchestrator.ts, runtime.ts,
wire/index.ts, tool-edit-file/extension.ts, run-turn.test.ts — both computerId
threading and retry-with-backoff coexist). Only tasks.md conflicted (status
section — orchestrator-resolved; both feature sections kept).
Verified post-merge: tsc -b EXIT 0, biome clean (391 files), 1730 vitest pass
+6 sshd-integration skipped (was 1690; +40 from dev's retry/LSP tests).
Wire dist rebuilt so the FE can re-sync the pinned @dispatch/wire dep and pick
up BOTH provider-retry AND the SSH Computer/defaultComputerId types.
No merge or push (into dev or otherwise).
|
|
|
|
The LSP diagnostics path hung up to 60s per edit whenever a configured Ruby
language server was dead or slow (the reported Steep langserver case): a
killed/crashed server was never detected (stayed "connected" forever), servers
were queried sequentially with a 60s budget each, and a corrupted-but-alive
server (Steep's ~3h phantom-SyntaxError drift) had no recovery.
Four fixes, all in packages/lsp/ (the tool-edit-file call site lowered to 10s):
1. Dead-process detection: SpawnedProcess.onExit (Bun proc.exited) + stdout-end
defence flip the client to error, dispose the rpc, kill the proc. The manager
re-spawns a fresh server after the 30s backoff. Dead servers are now skipped
(0s) instead of polled for 60s.
2. Concurrent fan-out + 10s hard cap: new aggregateDiagnostics queries all
matching servers at once, each capped at 10s. A non-responder is skipped
with "LSP took too long (>10s), skipped — raise this to the user" instead of
blocking the fast server's results. Replaces the vague "unusually long"
warning (now structurally impossible: slow is always false).
3. Corruption self-heal: a detector flags a server re-emitting identical
non-empty diagnostics despite the file changing; after 5 repeats the client
is marked broken and re-spawned. Clean files never trip it. (Acknowledged
false-positive risk on persistent unfixed errors; CLI type-check gate stays
authoritative.)
4. sendRequest timeout: hover/definition/references cap at 10s so they can't
hang the turn against a dead server; the initialize handshake keeps its 45s
race.
Verification: typecheck clean; 1573 tests pass (96 files), +15 new LSP tests
(86 in packages/lsp); biome clean. No kernel/contract changes; onExit is
internal to packages/lsp.
|
|
When the upstream LLM API returns a retryable error (HTTP 429 / 5xx
"overloaded"), the kernel now retries provider.stream() with a stepped
backoff, visibly, until the 8h cumulative-sleep budget is exhausted — then
emits the final error and seals the turn. Retries fire only when no content
was emitted yet this step (safety invariant: never duplicate partial output).
- wire: new transient TurnProviderRetryEvent AgentEvent variant (emitted
before each sleep; not persisted to model history).
- kernel contracts: RetryStrategy (pure delayFor + injected sleep) + optional
retry? on RunTurnInput (omit = no retry, backward-compatible).
- kernel run-turn: retry loop in executeStep; providerRetryEvent constructor.
Kernel imports no timer (sleep injected).
- session-orchestrator: concrete schedule (5s..30m, repeat 30m, 8h budget) +
abortable setTimeout sleep, wired into RunTurnInput.retry.
tsc -b EXIT 0; biome clean; 1574 vitest pass (+16 new: 11 kernel retry tests
with injected fake sleep + pure delayFor, zero @dispatch/* mocks; 5 schedule
tests). Transports unchanged (transport-ws forwards AgentEvent verbatim in
chat.delta; transport-http is generic JSON.stringify).
Plan: notes/retry-with-backoff-plan.md. tasks.md updated with milestone +
optional CLI-renderer roadmap follow-up.
|
|
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.
|
|
Wave 5b of transparent SSH support. NEW standard extension @dispatch/ssh makes
remote execution actually work over SSH, transparently. ssh2 verified to run under
Bun (load-bearing decision #1 confirmed: connects to local sshd :22 + execs).
- config.ts: ~/.ssh/config reader via ssh-config -> Computer[]/ComputerEntry[]
(read-only discovery; resolves hostName/port/user/identityFile/knownHost).
- hostkey.ts: known_hosts auto-trust-and-pin (present->verify/reject-on-mismatch,
absent->accept+append; the accept-new analog).
- errors.ts: pure ssh2/SFTP -> node:fs-style .code error mapping (so tools'
existing ENOENT branches work unchanged).
- pool.ts: SshConnectionPool (per-alias ssh2.Client, lazy connect, keep-alive,
idle reap ~15m); key-only auth from ~/.ssh (config IdentityFile or default
id_ed25519/id_rsa); no agent-forwarding, no PTY.
- backend.ts: SshExecBackend implements ExecBackend (spawn via client.exec with
shell-quoted cwd; fs via SFTP).
- service.ts + extension.ts: activate provides BOTH handles the other units
consume — remoteExecBackendFactoryHandle (exec-backend: computerId->SshExecBackend)
AND computerServiceHandle (transport-http: listComputers/getComputer/getStatus/test).
- orchestrator: added packages/ssh to root tsconfig.json refs + bun install.
Tests: 45 pass + 6 sshd-integration skipped (it.skipIf(!process.env.SSH_TEST_HOST)).
Verified: tsc -b EXIT 0, biome clean, 1690 vitest pass (was 1641, +49).
CRs for wave 5c: host-bin registration; CR-5 transport-http barrel re-export;
CR-6 usageCount wiring (deferred-ok, defaults to 0).
Refs: notes/ssh-support-plan.md (decisions §0.5/§13). No merge or push.
|
|
exec-backend declares remoteExecBackendFactoryHandle (a consumer-defined
ServiceHandle<(computerId) => ExecBackend>) that the ssh package will provide
(standard→core layering). The resolver's computerId-set branch now lazy-looks-up
this factory (at tool-execute time, runtime) and calls it; if ssh isn't loaded,
getService throws → a clear 'SSH remote execution is not configured' error. The
computerId-undefined (local) branch is byte-identical to before.
This is the seam wave 5b (the ssh package) plugs into. +tests for both branches.
Verified: tsc -b EXIT 0, biome clean. No merge or push.
|
|
Wave 4 of transparent SSH support (3 parallel owner-agents on disjoint packages).
- transport-http: computer routes — GET /computers, GET /computers/:alias,
GET /computers/:alias/status, POST /computers/:alias/test (all delegate to a
new ComputerService seam, graceful []/disconnected when ssh not loaded);
GET/PUT/DELETE /conversations/:id/computer; PUT /workspaces/:id/default-computer
(mirror the cwd/default-cwd routes); /chat threads computerId into the
orchestrator. Defines ComputerService interface + computerServiceHandle
(defineService<ComputerService>('ssh')) in seam.ts — the seam the ssh package
provides via host.provideService in wave 5.
- transport-ws: chat.send + chat.queue thread computerId onto the route result
(mirrors cwd/workspaceId), forwarded to the orchestrator input.
- mcp: CR-1 fix — filterMcpTools now preserves computerId on the returned
ToolAssembly (mirrors cwd preservation), so the filter chain stays consistent.
- orchestrator: added @dispatch/wire dep to transport-http (build/config, my lane)
so its seam.ts Computer/ComputerEntry import resolves.
Verified: tsc -b EXIT 0, biome clean, 1641 vitest pass (was 1620, +21).
Refs: notes/ssh-support-plan.md (decisions §0.5/§13). No merge or push.
|
|
transport-contract API types
Wave 3 of transparent SSH support (2 parallel owner-agents on disjoint packages).
- session-orchestrator: thread computerId end-to-end through the turn, mirroring
cwd exactly — StartTurnInput/EnqueueInput/handleMessage/TurnLifecyclePayload
gain computerId; runTurnDetached resolves effectiveComputerId via
conversationStore.getEffectiveComputer(convId, override), persists the override,
threads into RunTurnInput + ToolAssembly. Register a remote-degradation
tools-filter (filterRemoteIncompatibleTools) that, when assembly.computerId is
set (REMOTE), drops the 'lsp' tool + any '__'-namespaced MCP tool (local
processes that can't see remote files); LOCAL (computerId undefined) is a
passthrough — byte-identical to today. +21 tests.
- transport-contract: + computerId on ChatRequest (flows to ChatSendMessage) +
computer endpoint API types (ComputerListResponse, ComputerResponse,
ComputerStatusResponse, SetConversationComputerRequest,
ConversationComputerResponse, SetWorkspaceDefaultComputerRequest,
TestComputerResponse) — mirrors the cwd/workspace endpoint types.
- CR-1 (non-blocking, folded into wave 4): MCP filter doesn't preserve computerId
on the returned ToolAssembly.
- cache-warming computerId threading intentionally DEFERRED (user request) —
noted as a known performance-only limitation in tasks.md.
Verified: tsc -b EXIT 0, biome clean, 1620 vitest pass (was 1599, +21).
Refs: notes/ssh-support-plan.md (decisions §0.5/§13). No merge or push.
|
|
Wave 2 of transparent SSH support (4 parallel owner-agents on disjoint
tool packages). The tools now resolve an ExecBackend per-call from
ctx.computerId and call backend.spawn / backend.readFile / etc. instead of
node:fs and node:child_process directly — so they are transport-agnostic
(local now; remote over SSH later, transparent to the agent). Still LOCAL-ONLY
this wave (computerId always undefined -> LocalExecBackend, behavior-identical).
- tool-shell: factory takes resolveBackend; execute calls backend.spawn.
spawn.ts DELETED (realSpawn was a verbatim duplicate of exec-backend's
LocalExecBackend.spawn — logic moved to the sanctioned shared package).
manifest dependsOn:[exec-backend]; host.getService at activation.
- tool-read-file: readFile/stat/readdir -> backend.* (pure logic untouched;
ENOENT .code branches kept).
- tool-write-file: exists/stat/writeFile -> backend.* (pure logic untouched).
- tool-edit-file: readFile/writeFile -> backend.* + forward-compatible REMOTE
diagnostics skip (ctx.computerId set -> skip LSP, return empty — plan §6.1;
local path byte-identical to today). LSP lookup stays lazy.
- orchestrator: pre-wired @dispatch/exec-backend dep into the 4 tool
package.jsons + bun install (build/config, my lane) so isolated verify
resolved cleanly; agents added the ../exec-backend tsconfig ref.
Verified: tsc -b EXIT 0, biome clean, 1599 vitest pass (was 1592).
Refs: notes/ssh-support-plan.md (decisions §0.5/§13). No merge or push.
|
|
Wave 1 of transparent SSH support (parallel owner-agents on disjoint packages,
plus the orchestrator-authored kernel contract seam from wave 0):
- packages/wire: + Computer/ComputerEntry (read-only view over ~/.ssh/config
Host aliases) + Workspace.defaultComputerId (string|null, null=local). Types
only; 3 conformance tests.
- packages/exec-backend (NEW core extension): the ExecBackend abstraction
(spawn + minimal fs surface) the bundled tools will program against instead
of node:fs/child_process. LocalExecBackend wraps today's node calls
(behavior-identical; node:fs-style .code errors). execBackendHandle +
ExecBackendResolver (sync; computerId undefined -> local; set -> throws until
the ssh package wires remote resolution in wave 5). 20 tests.
- packages/kernel (runtime only): thread computerId through dispatch.ts +
run-turn.ts exactly as cwd is threaded (opaque, forwarded to
ToolExecuteContext; absent = local = byte-identical to today). +2 tests.
- packages/conversation-store: computer (SSH alias) assignment + resolution
mirroring cwd — WorkspaceRow.defaultComputerId + setWorkspaceDefaultComputerId
+ getComputerId/setComputerId/clearComputerId + getEffectiveComputer
(override -> per-conv -> workspace default -> null/local). Fixes the 3
Workspace literal sites the new required wire field broke. +18 tests.
- orchestrator: root tsconfig.json ref for exec-backend + bun install.
Verified: tsc -b EXIT 0, biome clean, 1592 vitest pass (was 1549, +43).
Refs: notes/ssh-support-plan.md (decisions §0.5/§13). No merge or push.
|