| Age | Commit message (Collapse) | Author |
|
|
|
|
|
At the end of every step that produces tool calls (the tool-result
boundary), the orchestrator now checks whether the conversation's context
size has reached the compaction threshold (compact-percent of the model's
context window). If it has, it compacts the history mid-turn — summarizing
old messages and replacing the running history with [summary, ...recent] —
then continues the prompt, so a long-running turn (e.g. left overnight)
does not run out of context.
This mirrors Opencode's approach (check at step boundaries, compact,
continue) and is DISTINCT from the pre-existing post-seal auto-compact,
which runs only AFTER a turn ends and refuses while a conversation is
active. The new path runs DURING the turn (saving the running turn) using
the live step usage rather than persisted metrics (written only at turn end).
Architecture (kernel purity preserved):
- Kernel contract: add an optional, feature-agnostic `onStepBoundary` hook
to RunTurnInput. The runtime calls it at each tool-result boundary with
the step's usage + the current messages, and adopts whatever message list
it returns (undefined/empty = unchanged). Mirrors `drainSteering`.
- Kernel runtime: ~21 lines — call the hook after onStepComplete +
drainSteering; replace working messages in place when a list is returned.
No I/O, no feature names.
- Orchestrator: extract a shared `performCompaction` helper (summarize +
fork + replaceHistory + emit) used by both `compact()` and the new
in-flight path. Wire `onStepBoundary` in `runTurnDetached` to check the
threshold via stepUsage vs contextWindow*percent and, on exceed, compact
and return [summary, ...kernel's recent N] (preserves mid-turn steering
+ the vision-transformed provider view; the store gets the canonical
recent messages). `compact()` is refactored to use the shared core;
its active-conversation guard and metrics-based auto-threshold are
unchanged.
Tests (+13): 8 kernel onStepBoundary tests (replacement adopted by next
step; undefined/empty = unchanged; not called on text-only turns; called
once per tool-call step after drainSteering; receives stepUsage; omitted =
no-op; replacement persists across steps); 5 orchestrator in-flight tests
(triggers above threshold + turn continues; no-op below threshold; no-op
when percent=0; no-op on text-only turns; the manual compact() service
still refuses while active but in-flight runs anyway). Also fixed the
in-memory test store's getCompactPercent/setCompactPercent (were no-ops).
Verification: typecheck clean; 2012 tests pass (was 1999; +13); biome 0
errors (12 pre-existing warnings in untouched files).
|
|
Add the ability to cancel/close a single queued message so it never runs:
while a turn is GENERATING and a user message sits in the steering queue
(waiting for delivery at the next tool-result boundary or carry into a new
turn), a client can remove one message by id. The cancelled message is never
delivered as steering and never carried into a new turn. Complements the
existing chat.queue enqueue (enqueue adds; cancel removes one).
Layers (all additive; nothing existing breaks):
- message-queue pure core: `cancel(state, conversationId, messageId)` —
splices a single message out by id, returns the post-cancel snapshot.
Idempotent (no-op if not found). Drops the key when the queue empties
(mirrors drain).
- message-queue service: `MessageQueueService.cancel()` — wraps the pure op,
pushes a surface update ONLY on a real change (queue shrank); a missing-id
cancel is a no-op with no surface push (mirrors drain's no-notify-on-empty).
- session-orchestrator: `cancelQueuedMessage({ conversationId, messageId })`
-> `{ cancelled, queue }`. The single entry transports call; resolves the
queue lazily (same as enqueue). Degrades to `{ cancelled: false, queue: [] }`
when the message-queue extension isn't loaded. `cancelled` is derived from
the queue length delta (true iff a message was removed).
API contract (documented for the frontend agent; see
frontend-cancel-queued-message-handoff.md):
HTTP: DELETE /conversations/:id/queue/:messageId
-> 200 QueueCancelResponse { conversationId, cancelled, queue }
(cancelled:false is a 200 idempotent no-op, not an error)
WS: chat.queue.cancel { type:"chat.queue.cancel", conversationId, messageId }
(additive to WsClientMessage). Fire-and-forget like chat.queue: success
is confirmed by the message-queue SURFACE updating (the cancelled
message leaves payload.messages). A missing-id cancel is a silent
no-op (no surface update, no error). Malformed (empty conversationId/
messageId) -> chat.error.
No new AgentEvent: a cancelled message never appears in the transcript (it
never runs). The existing message-queue surface already reflects the
post-cancel snapshot. Race-safe by construction: if the kernel drains the
queue (steering) or carries it into a new turn before the cancel runs, the
message is already gone -> cancel returns cancelled:false (a no-op).
Version bump: @dispatch/transport-contract 0.23.0 -> 0.24.0 (additive:
QueueCancelResponse + ChatQueueCancelMessage added to WsClientMessage).
@dispatch/wire unchanged (QueuedMessage.id is the cancel target).
No CLI command added: the CLI has no queue-listing affordance to discover a
messageId, so a CLI cancel would have no input source. The HTTP DELETE is
available for any non-WS client that knows the id (e.g. from a prior enqueue
response's queue[]).
Verification: tsc -b EXIT 0; vitest 2024 passed / 6 skipped (25 new tests);
biome EXIT 0 (0 errors).
|
|
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
|
|
|
|
service
Hands the user a one-command script to apply the systemd MemoryMax/MemoryHigh
cgroup limits to the live dispatch.service without a full reinstall.
Patches in the real user (same as bin/install), daemon-reloads, and restarts.
|
|
The server has a memory leak (~2.5 GB/h) that eventually triggers a Bun
runtime segfault. These cgroup limits turn the uncontrolled crash into a
controlled OOM-kill → clean restart via Restart=on-failure.
MemoryHigh=20G (soft throttle) + MemoryMax=24G (hard cap).
Machine has 33.24 GB total; 24G leaves OS headroom.
|
|
|
|
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
|
|
|
|
backend, dispatch-web → frontend)
|
|
|
|
|
|
|
|
|
|
|