v0.9.1 — Telegram bot whitelisting + access control, mid-stream send modes, scoped image-support toggle
Features
-
Telegram bot whitelisting + access control — Solves "anyone who finds the bot can talk to my Agent". Two-tier access model: (1) workspace owners at the platform level (
accessMode: 'open' | 'owner-only'), (2) per-binding access with'inherit' | 'allow-list' | 'open'modes. The first user who runs/pairin a paired supergroup is auto-seeded as workspace owner. Locking the bot to owner-only in Settings → Messaging → Telegram flips the workspace switch and migrates every legacy'open'binding to'inherit'so no public binding is left dangling. A new "Allowed users" collapsible (Usersicon) lists owners with remove controls; a "Pending requests" panel surfaces senders the gateway already rejected so the operator can Allow or Allow for this chat (per-row decision, never auto-promotes binding-level rejects to global owner). Inline buttons (bind:/perm:/plan:) run through the same access evaluator as text commands so a non-owner can't tap their way past the gate; bot senders are silent-dropped at the platform layer. The unlock path lives in the platform context menu (next to Reconfigure, only when locked). Composite-key pending-store (platform, userId, reason, bindingId) keeps a sender's workspace-level reject separate from their per-binding rejects so each can be resolved independently. Closes #672. (fd68c070,aa46043a,3f4fe418) -
Per-connection mid-stream send behavior (Steer vs Queue) — A new submenu in the Settings → AI connection context menu lets you choose what happens when you send a follow-up while the agent is mid-stream. Steer keeps today's behavior (redirect attempts to inject the new message into the live turn); Queue finishes the current turn cleanly and replays the queued message after
onProcessingStopped. The two modes have meaningfully different failure shapes — Claude's emulated steer can producesteer_undeliveredif no tool fires before the turn ends, paying tokens for nothing; Pi's.steer()is native and has no such failure mode. Defaults are picked per-provider at connection-create time (anthropic →queue, pi/pi_compat →steer) and stored inconnection.midStreamBehavior. 4 new i18n keys × 7 locales. Closes the queue-vs-steer half of conversations around mid-stream UX. (35a4ca2e,f346035a) -
Per-model image support toggle in the chat-input picker — Custom-endpoint (
pi_compat) connections now expose a one-click image toggle next to each model in the chat-input model picker. When you stage an image but the active model resolves tosupportsImages !== true, an inline pre-flight banner appears above the attachment preview with an "Enable image support" action. Both surfaces resolve through shared helpers (modelSupportsImages/setModelSupportsImages) so they can't drift. The toggle now also renders in the single-model picker branch (used forpi_compatconnections with 0 or 1 models + adefaultModel). Storage schema unchanged — flipsconnection.models[i].supportsImages. Built-inanthropic/picatalogs stay SDK-owned and not editable from this surface. Closes #679. (f36c0833,bba2d726,31ddd5aa) -
Tool-result threshold scales with model context window — The fixed 15k-token threshold for large-response handling burned 25 % of context per tool result on a 64k-window model, and three or four moderate Read/Grep results could overflow even after Pi's auto-compaction. New
tokenLimitFor(contextWindow)helper: floor 2k, ceiling 15k, linearcontextWindow * 0.10between (64k → 6.4k, 128k → 12.8k). Pi-side tool wrapper sourcesagent.state.model.contextWindowper call so the threshold tracksset_modelmid-session. ClaudeAgent reads from the usage tracker. Addresses gap 1 from #666. (0d3c2796)
Improvements
-
Settings → Messaging i18n consolidation — Settings → Messaging mixed English fragments into every non-en locale (Hungarian showed "Reconfigure" / "Disconnect" / "Connect" alongside translated copy) because shared verbs were duplicated across per-platform namespaces and three keys resolved via
t(..., { defaultValue: ... })with no real entry. Added 7common.*action verbs (configure / connect / disable / disconnect / more / reconfigure / reconnect) translated to all 6 non-en locales, replaced 8 call sites inMessagingSettingsPage.tsx, deleted 9 redundant per-platform keys × 7 locales (63 cells removed), and routed the hardcodedPLATFORM_API_DESCRIPTIONconstant through newapiTypekeys. Backfilled 8 platform-specific gap-fills that had been left as English. Net: 14–17 untranslated values per locale → 2–5 (only legitimate cognates like "Region" remain). Adding a new platform no longer requires duplicating verb translations. (bd575ed1) -
Locale-file sort guard — Across
en/de/es/hu/ja/pl/zh-Hans.json, 96 of 1393 keys per locale were out of alphabetical order — identical disorder confirmed past mass-translate scripts preserved insertion order. Newscripts/sort-locales.ts(run viabun run sort-locales, orbun run lint:i18n:sortedfor CI/check mode) normalizes in place. The pre-commit hook (scripts/lint-i18n-staged.sh) now blocks commits that stage unsorted locales with a "Fix: bun run sort-locales" hint, andvalidate:ciincludes the same check. Net change: zero keys added/removed; the locale-parity test suite goes 58 / 7 → 65 / 0. (3a40b5a5,3f4fe418) -
Pi SDK uplift to 0.72.1 — Bumps
@mariozechner/pi-ai,pi-agent-core, andpi-coding-agentfrom 0.70.2 to 0.72.1 across all fourpackage.jsonfiles. Drops the now-removedgoogle-gemini-cliandgoogle-antigravityentries fromPI_EXCLUDED_PROVIDERS. The 0.72.0 release replacedcompat.reasoningEffortMapwith model-levelthinkingLevelMap; verified Craft'sbuildCustomEndpointModelDefsets neither field so existing custom-endpoint registrations are unaffected. Adjacent fixes picked up: undici body/headers idle timeouts disabled for long streams (#3715), Anthropic stream-end-as-error handling (#3936), DeepSeek V4 reasoning compat, GPT-5.5 Codex support, websocket-cached transport for OpenAI Codex (#4083). (522a727f) -
HTTP MCP validation no longer spawns a subprocess —
validateMcpConnection()previously spawned a full Claude Code subprocess via the Agent SDK'squery()just to callmcpServerStatus(). On macOS the Electron sandbox killed that subprocess withSIGKILLbefore validation could finish, so every HTTP MCPsource_testfailed even when the server was healthy. Now usesCraftMcpClientdirectly (which already supports HTTP transport and runs alistTools()health check insideconnect()); drops the subprocess, the Anthropic env-var swapping, the Claude credential pre-check, and the deadclaudeApiKey/claudeOAuthToken/modelfields from the validation config. Net −231 / +119 lines. Fixes #697. (f0a12c4e) -
API source probe honors
testEndpoint.bodyand method; tool descriptions slimmed — POST-only API endpoints reliably 500'd from an empty probe body because the configuredtestEndpoint.bodyandtestEndpoint.headerswere ignored. The basic probe also now honorstestEndpoint.methodinstead of the HEAD→GET-on-405 dance that silently passed POST-only endpoints. Separately,guide.mdis no longer inlined into the API tool description on every LLM request — the agent'sPrerequisiteManageralready forces aReadofsources/{slug}/guide.mdbefore anyapi_{slug}call, so the guide always lands in conversation history once. For OpenAPI-imported sources this saves tens of KB per request and removes the bloat that pushed #683's payload past the relay limit. (589a51bf) -
IPC inventory script auto-updates the test snapshot —
scripts/ipc-inventory.tsnow rewrites the auto-generatedEXPECTED_CHANNELSblock inipc-channels.test.tsin place instead of just printing a migration report. Run it after adding/removing/renaming anyRPC_CHANNELSentry. (aa46043a)
Bug Fixes
-
Custom-endpoint
supportsImagestoggle now propagates to the live Pi subprocess — Toggling per-model image support wrote to disk but never reached the running Pi subprocess, so the model kept seeing attached images replaced by Pi SDK's "(image omitted: model does not support images)" placeholder. Two compounding issues:getOrCreateAgentgated the in-place runtime refresh onmanaged.isProcessing(whichsendMessagehad just flipped totrue, making the refresh branch dead code), and thellmConnections.SAVEhandler had no notification path to active sessions. Now SAVE fires the runtime push detached (so config persistence isn't gated onN × 15sper-session timeouts), the gate usesagent.isProcessing()only, and a per-sessionagentRefreshLocksmutex serializes concurrent refreshes soagent.chat()can't fire against a still-applying update. Restart-required field changes (piAuthProvider,slug, auth/provider routing — fields theupdate_runtime_configIPC can't propagate) now route straight to dispose + recreate via a separatebuildRestartRequiredSignature. (81a6f195,6c0fec02,ebf01805,f0bbfe96) -
Vision toggle no longer blanks the trigger button on string-shaped models — Promoting a
models: ['gpt-4o']-shaped string entry to an object via the new image toggle dropped the model name (nonamefield), so the picker's trigger button rendered blank.setModelSupportsImagesnow seedsname: id, shortName: idwhen promoting; defensivem.name ?? stripPiPrefixForDisplay(m.id)fallback at the three picker display sites also recovers any pre-existing partial entries from hand-editedconfig.json. (bba2d726) -
Cold-session metadata changes survive app restart —
persistSessionpreviously silently no-op'd when called on a session whose messages hadn't been lazy-loaded yet (the post-restart state of every session in the sidebar). Status / labels / rename changes made before the session was opened were never written to disk and were lost on the next restart. The cold path now hydrates messages + token usage synchronously from the JSONL before enqueueing — keeps the original guard's intent (don't overwrite real messages with[]) while removing the silent drop.setSessionLabelsis now async and awaits the flush to matchsetSessionStatus. (d5a31774) -
Pi+GPT context overflow no longer cascades into a 3-error pile — Pi+GPT context overflow produced Codex error: context_length_exceeded → Auto-compaction failed: undefined is not an object → Pi subprocess error: Agent is already processing a prompt in the UI. Three independent root causes: an upstream race in Pi SDK's
_runAutoCompaction(separate PR pending), a wrapper-side duplicate compact inpi-agent-server'shandlePromptracing with the SDK's internal_runAutoCompaction, andPiEventAdaptercompleting its iterator on everyagent_endso the recovered turn fromagent.continue()arrived in a closed iterator. Now: a 5-state overflow recovery state machine inPiEventAdapter(none/held/awaiting/compacting/recovering) holds the queue open across the SDK's compaction → continue flow with a 5s drain fallback; the duplicate manual compact path inpi-agent-serveris removed;waitForCompactiondefault timeout matches the RPC compact timeout (60s → 300s); a friendly fallback message replaces the raw AbortController stack until the upstream SDK fix ships. (f70d2b56) -
Browser-pane
hide()no longer crashes mid-load — The'close'listener could re-enterhide()viakeepAliveOnWindowClose, andemitStateChange()ran synchronously afterwin.hide()while Chromium was still tearing down — particularly fatal when the BrowserView was mid-load. Net effect: black-screen crash on "Hide Window" during agent tool execution. Adds anisHidingguard, defers the state-change callback to a microtask, and callswebContents.stop()beforewin.hide()when a load is in flight. Fixes #695. (a4038a28) -
RoutedClientno longer crashes on synchronous workspace switch —onConnectionStateChangedfires its callback synchronously when the new client is already in the'connected'state, so the previousconst unsub = …form putunsubin the TDZ and threwReferenceError: Cannot access 'unsub' before initialization. Production-impacting on any make-before-break workspace switch that lands on an already-connected new client. Switches tolet+ optional-chaining so the callback can self-unsubscribe in both the synchronous and async cases. (44f8f001)
Documentation
-
Google OAuth setup uses Web application client type, not Desktop app — Issue #585: users following the docs got
redirect_uri_mismatchfrom Google when using the recommended "Desktop app" client. Newer Google Cloud projects and Workspace accounts with stricter controls reject the loopback redirect URI on Desktop-app clients even though the desktop flow useshttp://localhost:<port>/callback. The "Web application" client type works in both cases. Updatedapps/online-docs/source-guides/google-oauth-setup.mdxto a single Web-application setup with both redirect URIs documented (http://localhost:6477/callbackfor desktop loopback;https://agents.craft.do/auth/callbackfor WebUI/headless). (70828cbc,34521a7d) -
Mid-stream send and chat-input image toggle docs —
core-concepts/interactions.mdxgains a "Sending While the Agent Is Responding" section explaining Steer vs Queue modes, the Queued chip cue, and the per-backend defaults.reference/config/llm-connections.mdxadds themidStreamBehaviorfield to the schema table.reference/config/custom-endpoint.mdxdocuments the chat-input image-support toggle forpi_compatconnections. (f346035a,31ddd5aa)
Breaking Changes
- None. The Telegram access-control work migrates legacy
'open'bindings to'inherit'automatically when the workspace is locked down — no manual config required.connection.midStreamBehaviorfalls back to a per-provider default for connections created before this release.
Notes
- A bot pre-paired with a workspace will not have any owners until the next
/paircommand, at which point the first user is auto-seeded. To audit current state from the UI, open Settings → Messaging → Telegram and check the "Allowed users" collapsible. - The new
bun run sort-locales(andbun run lint:i18n:sortedin CI) replaces ad-hoc manual locale sorting. If you contribute translations, append the new key anywhere in the file and run the sort script before committing — the pre-commit hook will block on unsorted locales otherwise. - Pi SDK uplift to 0.72.1 is internal — no config changes required. If you previously had
google-gemini-cliorgoogle-antigravityconfigured (both removed upstream), those entries are no longer recognized.