Tandem v0.4.43
See the assets below to download the installer for your platform.
v0.4.43 (Released 2026-04-27)
This release improves hosted Tandem server usability by making the Files page manage workspace repos directly, and fixes two regressions: a Slack-only duplicate-reply loop after engine restarts, and a planner wrapper that refused structurally valid workflow plans whenever a model emitted an off-label action discriminant.
Hosted Files page can manage workspace repos directly
Provisioned hosted installs now treat the container workspace as the primary Files destination. The control panel exposes a Workspace explorer rooted at /workspace/repos, resolved from TANDEM_CONTROL_PANEL_WORKSPACE_ROOT, and the hosted compose renderer mounts HOSTED_REPOS_ROOT there read-write for tandem-control-panel. The generated control-panel config also defaults repository.worktree_root to /workspace/repos while leaving repository.path empty until a specific repo is selected.
The new workspace file APIs list directories, read previews, download files, upload files or folders, create directories, and delete workspace entries. All workspace paths are resolved under the configured workspace root and reject traversal, null bytes, invalid relative paths, and absolute paths outside the root. The existing managed buckets (uploads, artifacts, exports) remain available as a secondary mode instead of being the hosted default.
The Files UI now includes a minimal explorer with breadcrumbs, root/up navigation, current-directory uploads, folder upload with browser relative paths preserved, create-folder controls, preview/download/delete actions, and a collapsible file panel. The KB upload surface now has a real collection dropdown for existing KB MCP collections plus a new-collection path, and document rows can be clicked again to close their preview. Files and KB tool controls now use registered Lucide icons instead of plain text-only buttons, and the per-page selectors were restyled so the numeric values no longer look clipped or misaligned in the dark control-panel chrome.
The Files page also degrades cleanly during mixed frontend/backend rollouts: it only opens Workspace by default when capabilities explicitly advertise the workspace file API, and it falls back to managed buckets if /api/workspace/files/list returns 404.
The Coding dashboard now has an explicit repository sync action for the selected ACA project. Operators can clone or fast-forward a managed checkout before launching a run, and ACA now initializes local non-git folders as local git repositories so local workboards/local files can still flow through the branch, commit, and review pipeline. Dirty git checkouts remain protected: ACA refuses to pull over uncommitted changes.
Slack channel adapter no longer replays recent messages on engine restart
The Slack channel adapter polls conversations.history every three seconds and tracks a last_ts cursor so it only forwards messages it has not already processed. The cursor lived only in the listener task's stack frame and was initialised to an empty string at startup, which meant every engine restart hit Slack with no oldest filter and pulled back the most recent ten messages. Anything still in that window — most visibly a @Tandem mention sent earlier in the day — was reprocessed and answered again, so users on hosted instances saw the same Tandem reply land two or three times across the same Slack thread as the engine cycled.
Discord and Telegram never had this problem: Discord streams events from the gateway, and Telegram long-polls with getUpdates offsets the server treats as acks, so neither replays history when the adapter reconnects. Slack's polling adapter has no equivalent server-side ack, which is why the empty-string cursor was visible only on Slack.
The Slack listener now seeds last_ts to the listener's startup wallclock formatted as a Slack seconds.microseconds timestamp, before the first poll, so only messages posted after the engine starts are picked up. The trade-off is that messages sent during the brief restart window are dropped instead of replayed; for hosted operator chat surfaces that is strongly preferable to spamming the same answer multiple times. A future change can persist last_ts per channel under the engine state directory if zero-loss semantics across restarts become important.
Wizard no longer falls back to a generic plan when the planner LLM hallucinates the wrapper action
The Simple Wizard, Mission Builder, and chat-native automation drafts all share try_llm_build_workflow_plan in tandem-plan-compiler. The planner LLM is told to return one of two top-level shapes — {"action":"build", ..., "plan":{...}} or {"action":"clarify", ..., "clarifier":{...}} — and the wrapper deserialised action straight into a strict two-variant enum.
After 0.4.41 the planner system prompt grew significantly: the approval-gate policy section, the phased-DAG decomposition guidance, and the teaching library now teach the planner step-level vocabulary like the discover / synthesize / validate / deliver phase ids and step ids such as synthesize_analysis_outline. Some planner models — most visibly gpt-5.4-mini selected via the wizard's planner model override — started writing those step-level labels into the wrapper action field, e.g. {"action":"synthesize_analysis_outline", "plan":{...valid plan...}}. serde_json rejected the wrapper with unknown variant 'synthesize_analysis_outline', expected 'build' or 'clarify', the planner reported invalid_json, the wizard hid the structurally valid plan behind the "Planner returned a fallback draft" banner, and operators could not create new automations from the wizard at all.
The fix has two halves:
- The wrapper enum now has a
#[serde(other)]Unknownvariant so off-label discriminants no longer fail deserialisation.PlannerBuildPayload::resolved_actioninfers the canonical action from the payload shape: aplanfield implies Build, aclarifierfield implies Clarify, and the empty case falls through to Build so the existing empty-plan branch can produce a fallback draft with the assistant's text instead of erroring on the wrapper. The plan body still has to validate against the sameWorkflowPlanschema as before, so this does not loosen any guardrail beyond the action-name string match. - The planner prompt now states explicitly that
actionMUST be the literal stringbuildorclarifyand never a step id or phase name, and that step-level concepts (discover,synthesize,validate,deliver, etc.) belong insideplan.steps, never in the wrapper.
Three new planner_build::tests unit tests cover unknown-action-with-plan, unknown-action-with-clarifier, and canonical-action pass-through.
Full Changelog: v0.4.42...v0.4.43