The 4.0.0 major release is a top-to-bottom hardening pass over the whole runtime. The dependency bump to functools-kit v4 (strict sequential queues) surfaced a class of latent deadlocks and lost-output bugs that the old v3 queue quietly papered over. Every one of them is now fixed, every point where the library calls into user code is guarded against throwing, and the behavior is locked down by a suite that grew from 42 to 298 tests (all green, tsc --noEmit clean).
The whole src/ tree was read line-by-line and audited for races, broken tool chains, dangling agent references, and history-integrity loss. 35 bugs were found and fixed along the way. If you build multi-agent flows with nested tool calls, navigation, operators, or persistence, upgrading removes a long tail of "the session just hangs" and "the wrong client got the answer" failure modes.
⚠️ Breaking changes
- Typo'd names removed.
addEmbeding/getEmbeding/overrideEmbeding(single-d) are gone — useaddEmbedding/getEmbedding/overrideEmbedding. No deprecated aliases are kept; update imports directly. - Duplicate
tool_callids now throw instead of silently executing both calls.session.complete/executereject withduplicate tool call id(delivered viaerrorSubject) and the flow recovers with a placeholder rather than corrupting history.
🔒 Security
- Path traversal in persistence (fixed).
PersistBasebuilt file paths by joiningentityId(aclientId/stateName/ …, possibly attacker-controlled) directly onto the dump directory, soclientId="../../x"could escapedump/and overwrite arbitrary files.entityIdis now sanitized (/,\,..→_). - Concurrent-ban lost update (fixed).
ClientPolicy.banClient/unbanClientwere an unserialized read-modify-write over the shared ban set: two concurrent bans of different clients could lose one update in memory and in the persisted store — a banned client would then passhasBanand keep interacting. The mutations are now serialized through a queue so each observes the previous one.
🐛 Notable fixes
waitForOutputdeadlock (the big one). Underfunctools-kitv4's strict queue, a nestedexecutetriggered from inside a tool subscribed to its output after the emit and hung forever — this alone accounted for 39 failing tests. Rewritten as a FIFO awaiter chain with ajoinOutput()path so nested tool executions attach to the parent waiter instead of queuing behind it.- Late tool errors & mid-flight teardown no longer hang the session. A tool that throws after
commitToolOutput, a server-sidechangeToAgentwhile a completion is still running, anddispose()during your own in-flightcomplete()all now resolve the pending waiter (placeholder / empty output) instead of leaving it — and the busy lock — stuck forever. - Stale output can't poison the next exchange.
cancelOutputandemit-substitution used to resolve the current waiter while the execution kept running and later emitted into the next message's waiter. An output-epoch guard now drops those stale results. - Every user-code entry point is guarded. Throwing
transform/map/mapToolCalls/ prompt functions, tool callbacks (onValidate/onBeforeCall/onAfterCall/validate), history adapters, MCPlistTools, persistencewaitForInit, custom resque functions, bus listeners, and schema hooks no longer hang the flow or crash the host process — errors are delivered to the caller (viaerrorSubject) or logged for observers, with graceful degradation. - Multi-directional routers work. Navigation tools are no longer deduplicated to one, so a triage/router agent exposing several
navigate_to_*tools can actually call any of them; only commit-action tools remain limited to one per turn. - Navigation "go back" is correct.
changeToPrevAgentno longer navigates into the current agent (off-by-one on the navigation stack); operator agents are now reachable through navigation (tool-modeexecuteforwards to the human instead of silently returning). - Edge-value fixes.
maxToolCalls=0now runs no tools (was running all —slice(-0)trap),keepMessages=0shows the model zero prior messages (same trap), andgetStatecalled from inside asetStatedispatch no longer deadlocks. createNumericIndexno longer overwrites live rows after deletions (index ismax(numeric id) + 1, notlength + 1).
⚙️ Configuration
- Previously hard-coded timers are now in
GLOBAL_CONFIG(tunable viasetConfig, defaults unchanged):CC_OPERATOR_SIGNAL_TIMEOUT,CC_CHAT_INACTIVITY_CHECK,CC_CHAT_INACTIVITY_TIMEOUT. TOOL_PROTOCOL_PROMPTandgetLastToolMessageare now exported from the package root.
✅ Tests
The suite went from 42 to 298 tests across ~40 new spec files, covering tool-call sequencing, MCP, resque strategies, multi-client isolation, navigation branching, ban mechanics, crash recovery across processes, and a systematic "throw at every user-code boundary" sweep. See TODO.md for the full 32-round audit log and per-bug detail.