Project Files
CHANGELOG.md
All notable changes to vibeLM will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
~ in file-tool paths instead of treating it as a directory name โ a model passing filePath: '~/Desktop/...' now correctly resolves the home directory instead of writing into a literal ~/ subdirectory within the workspace..claude/ and .agents/ directories in .gitignore.bootstrapSessionState now detects when a new
conversation is dramatically shorter than the previously persisted history length and discards the
stale plan instead of carrying it forward. Previously, a brand-new chat could inherit an unrelated
plan from runtime-state.json.preprocessMessageCore now auto-appends a new
pending step so the model actually executes it instead of replying in plain text and stopping.sessionId changing across some turns even without a
process reload. Suspected contributing factors, not yet isolated: vibe_bridge's background tick
shares the same module-level session state via its own separate model.act() call (built from a
minimal handover summary, not the real conversation history), and/or having two plugin instances
(a lms dev instance and a stale production install) simultaneously enabled in LM Studio, each with
independent in-memory state. Needs a clean, uncounfounded live session (fresh chat, vibe_bridge
disabled from the start, single plugin instance) to isolate further.create_plan, update_plan_step, and get_plan give the model a structured,
ordered step list instead of letting it narrate a plan in prose and stop. amend now refuses to
close out a session while the plan has untouched (pending) steps. create_plan can auto-start
so unattended ticks keep making progress on the plan's next pending step.loaded_context_length, read from LM Studio's REST API) instead of its larger max ceiling.
Previously a model loaded at ~40K but reporting a 256K max was budgeted against 256K, so
auto-compaction (30% trigger) and the budget warning (50%) never fired before the session overflowed
its real window and died โ confirmed live on a remote M5 around 40โ60K tokens.vibe_bridge now auto-starts as soon as the "Vibe Bridge" toggle is enabled in plugin settings,
using the configured default prompt/interval/maxDuration โ no chat message needed to explicitly
call vibe_bridge({action:"start"}) first. An already-running bridge is left alone if the tool
provider is invoked again (e.g. a new session on an already-running plugin process).vibe_bridge's keep-alive tick called model.act(chat, bridgeTickTools) as a standalone call
outside the main orchestrator, so it never inherited maxOrchestratorTurns and had no round or
time cap of its own. A model that got stuck reasoning without calling a tool could loop
indefinitely โ confirmed live, one tick ran 43 minutes straight on repeating "Plan... Wait...
Actually..." text after a set_workspace error it never recovered from, blocking all subsequent
ticks. The tick now passes maxPredictionRounds (8) and a 3-minute timeout signal, so a stuck
generation is canceled and flows into the existing consecutive-failure handling instead of
hanging.vibe_bridge tool for autonomous session keep-alive via self-recalling timer loop
tools.vibe_bridge.*)model.complete() for handover summarization and model.act() to drive
autonomous responses, using a rolling window of the last 5 user messages as context, instead of
passive prompt injection.explore_workspace command for quick workspace inspectionrespond_to_user to amendPlan steps can now override the session's reasoningEffort setting individually. create_plan
accepts either a plain string or { description, thinking } per step (thinking is
"off"|"low"|"medium"|"high"), and update_plan_step can set/change it later. When resolving the
thinking directive for a bridge tick, the current step's override (the first in_progress step, or
failing that the first pending one) wins over the session-wide config. This lets one plan mark
mechanical steps "off" and a tricky step "high" instead of applying one uniform reasoning level to
every step โ the per-model-family directive mapping (reasoningDirectiveFor) already handled Qwen's
binary /think//no_think switch, gpt-oss's native Harmony tiers, and a generic natural-language
fallback for everything else, so this only needed a new source for which effort to apply, not new
per-family logic.
vibe_bridge ticks now give a generous maxTokens floor (6000) to architectures whose reasoning
can't actually be turned off, instead of leaving it uncapped or subject to whatever ambient
per-model limit LM Studio applies. Live-tested against real loaded models across gpt-oss-20b,
qwen3-4b, google/gemma-4-e4b, microsoft/phi-4-mini-reasoning, and nvidia/nemotron-3-nano-omni:
qwen3's native /no_think switch works correctly (reasoning_tokens: 1), but gemma-4 and
Nemotron-H kept producing full reasoning_content regardless of vibeLM's directive or NVIDIA's
own documented "detailed thinking off"/<thought off> conventions, and LM Studio's own native
reasoning REST setting (/api/v1/chat) outright rejected "off" for phi-4-mini-reasoning with
"Supported settings: 'on'" โ i.e. LM Studio itself confirms there is no off-switch for that model.
Under a tight token budget this reasoning can consume the entire allowance before the model reaches
its answer โ reproduced live on phi-4-mini-reasoning, which returned empty content with
finish_reason: "length" after burning 396 of 400 tokens on reasoning alone. resolveBridgeTickMaxTokens(arch)
now detects this class of architecture (gemma.?4|phi.?[34]|nemotron) and gives it explicit headroom
so a verbose reasoning phase can't silently starve the tick of room for its actual answer/tool call.
A mid-conversation lms dev hot reload (or plugin process restart) could silently drop an
in-progress plan instead of resuming it. bootstrapSessionState() had two paths for "the raw
conversation history can't be matched against what was persisted": one for "history exists but its
fingerprint doesn't match" (already correctly carried the last-known plan/managedContextBlocks
forward) and one for "no history could be read at all" (hard-reset to a blank session, no carryover).
The second path fires whenever the very first bootstrap call after a reload comes from a controller
with no pullHistory() โ which is exactly what happens when vibe_bridge's background tick runs
before any real preprocessMessage() call re-establishes history. Caught live: editing toolsProvider.ts
while a real vibe_bridge-driven session was mid-plan triggered esbuild's watch rebuild, and the next
tick came up with a brand-new sessionId and an empty plan โ even though the plan was still sitting in
runtime-state.json on disk the whole time. The no-history path now carries over the persisted plan
and managedContextBlocks the same way the fingerprint-mismatch path already did.
Every real conversation turn silently started a brand-new session, resetting turnCounter to 0 and
making auto-compaction, session-scoped fact-dedup, and the context-spine resume mechanism unreachable
in production. toolsProvider() forced a session-state re-bootstrap on every call using its own
ToolsProviderController, which โ unlike the PromptPreprocessorController โ has no pullHistory()
method (confirmed against the SDK's own type definitions). The forced bootstrap's history read always
silently failed, so it always concluded "history unreadable, must be a fresh/rolled session" and
manufactured a new random sessionId, discarding whatever preprocessMessage() had correctly
established moments earlier in the same turn. Caught live driving a real multi-turn LM Studio
conversation: runtime-state.json's sessionId changed on every single turn and turnCounter never
advanced past 0, even though the working-window/spine/compaction machinery all assume sessionId
identifies one whole conversation, not one exchange. toolsProvider() no longer forces the bootstrap;
it reuses the state preprocessMessage() already established for the turn.
bash_terminal reported genuinely installed tools (node, npm, anything managed by nvm/Homebrew/
asdf/volta) as "not found." Commands ran via a bare exec() inheriting LM Studio.app's own
environment โ launched via Launch Services (Dock/Finder), never an interactive login shell โ so
PATH never picked up anything a version manager adds by sourcing .zshrc/.zprofile. Live testing
caught the agent confidently telling the user to reinstall Node.js that was already installed via
nvm. Commands now run through ${SHELL} -ilc (interactive + login), sourcing the same profile a
real Terminal session would โ generically, without hardcoding any tool's install path.delete_file's parameter was named path while read_file/write_file use filePath. Live
testing caught the model calling delete_file({ filePath: ... }) โ consistent with every other file
tool in the same conversation โ and getting rejected by the schema. Renamed to filePath for
consistency; the tool's own inconsistency was silently degrading tool-call reliability with no
logged bug up to now.Importance-tiered context budgeting (Layer 3). Replaces the blunt fixed limits with tiers keyed on importance:
plan field from the first substantive
request, so the pinned head has a goal to anchor to even when the model never calls create_plan.
parsePersistedPlan now accepts a goal-only plan (previously it silently dropped any plan with
zero steps, which would have discarded the auto-goal on the next read).Cut-the-middle context retention via a pinned "context spine". Previously, when the LM Studio host rolled raw history it dropped the oldest turns first โ the goal and everything the agent had established โ and vibeLM only re-asserted its last directive, so early context was effectively lost (a rolling/tail strategy). vibeLM now rebuilds a pinned head from durable state โ the plan (goal + step statuses) plus the top distilled facts โ and re-injects it on resume after a detected roll. The head survives, the recent tail is whatever the host still holds, and the middle is deliberately not reproduced (only its distilled facts carry forward). Facts fall back to the most recent across sessions when a roll has regenerated the session id, so the "what we've learned" block isn't empty exactly when it's needed most.
called X โ result ok=true checkpoint, so a probing loop produced dozens of near-identical entries
that made search_memory return noise and told the model nothing it could act on (observed live: 32
blob memories + 27 empty checkpoints in one session). Results are now distilled into a compact
one-line fact keyed by a coarse signature (shell program + outcome), and equivalent facts are
deduped within a recent window โ so 24 failing ls <path> probes collapse to a single
bash_terminal \ls โฆ` โ failed: โฆfact, and checkpoints carry that same distilled line. The verbatim result is still kept on the turn entry, so recent-context fidelity is unchanged. Deduplication is coarse only for failures/info (the noise a probing storm produces); successful calls are keyed on their exact signature so distinct results (e.g.cat a.txtvscat b.txt) are each retained rather than collapsed. That success key is a **hash** of the signature, never the raw args, so secret arguments such as ssh_exec's password can't leak into the persisted fact:`
memory tag.ls <different node/npm path>bash_terminalssh_execls Als Bls Camendvibe_bridgereasoningEffort was a silent no-op for most users. medium/high produced an empty directive
for every architecture except gpt-oss (Llama/Mistral/Gemma/DeepSeek/GLM/Phi/etc. all got nothing),
and Qwen's low/medium/high all resolved to the identical /think โ the setting only ever did
anything for off vs. "on". Every level now produces a distinct directive per model family.managedContextBlocks recovery state (meant
to re-assert its own instructions after the host truncates/rolls raw history) was only ever
populated by scraping the model's own final answer for a marker it had no reason to echo back, and
even when populated it only rehydrated if the next user message happened to match a
continuation-style regex. It's now captured at the point vibeLM itself emits a directive, and
rehydrates unconditionally on the very next turn after a detected roll, regardless of wording.<|channel|>..., GLM/Qwen <think>...</think>) could
pollute vibeLM's own stored/reused data โ the vibe_bridge tick handover summary, amend
handoffs, and save_memory content. Added stripModelArtifacts and applied it at those call sites.
(The chat-bubble leak itself is LM Studio's own Harmony-parser bug and out of this plugin's reach โ
it has no hook into the assistant-response render path.)reasoningEffort setting (off/low/medium/high) โ calibrates model thinking, mapped per
model family: gpt-oss uses its native Harmony Reasoning: low/medium/high tiers, Qwen uses the
/no_thinkยท/think soft switches, and other models get an equivalent natural-language directive.
Applied to both interactive turns and unattended vibe_bridge ticks.compactionTriggerPercent setting (10โ90, default 30) โ how full the context gets before vibeLM
auto-compacts older history into memory. Higher keeps more live context; lower compacts earlier.maxThinkingSteps setting (1โ50, default 8) โ the per-tick prediction-round cap for vibe_bridge
is now user-configurable (previously hardcoded at 8).maxEffectiveContextTokens setting (default 0 = disabled) โ optional hard cap on the token budget
vibeLM plans against, for machines that can't sustain even the configured loaded context length.vibe_bridge's keep-alive tick called model.act(chat, []) with an empty tools array, so it
ran on schedule but could never actually call a tool โ confirmed live, the autonomous loop was
functionally inert. It now gets a curated tool set (workspace explore/list/read/write/append/
search, memory save/search, web fetch/search); bash_terminal stays excluded pending a command
allowlist.config.json, runtime-state.json, and session-log.jsonl lived inside the plugin's own
install directory, which lms dev --install wipes on every deploy โ a routine rebuild was
silently destroying all persisted memory, session state, and the configured workspace path.
Persistent data now lives under extensions/data, outside the path that gets wiped.npm test was independently corrupting the same real, production runtime files on every run
(deleting the live runtime-state.json, overwriting the live config.json with test fixtures
and never restoring it). Tests now use an isolated VIBE_LM_DATA_DIR override.vibe_bridge prompt ("adjast" โ "adjust").PROMPT_BUDGET_RATIO into hardPromptBudgetLimit, remove dead isReadOnlyTool, fix appendBatch offset bugset_workspace, get_configlist_files, read_file, write_file, append_file, rename_file, search_files, delete_filebash_terminalsave_memory, search_memory, list_memories, update_memory, delete_memory, clear_memoriescompact_contextweb_fetch, web_searchcalculate, get_current_datetimegenerate_uuid, generate_password, encode_base64, decode_base64ssh_exec, check_serviceconsult_expert (coder, debugger, architect, reviewer, writer, analyst, researcher, data_scientist, knowledge_keeper)respond_to_user (later renamed to amend)tool_choice required to prevent garbage outputread_filerequireWorkspace guard + remove hardcoded workspaceorchestratorLoop finalText now includes tool resultsrequireWorkspace infinite recursion