The live smoke exposed a harness trap: driving each turn through its own
asyncio.run() binds the engine asyncio primitives to the first loop, and
every later stream silently takes the interrupted path - full provider
replies persisted as empty assistant messages. The scripted smoke had
the same latent artifact and did not assert reply content, so it stayed
green. Now the whole scenario runs on ONE loop (like the real server)
and every turn asserts a real reply.
A long multi-turn session driven through the real SessionManager with a
forced 3k-token cap: repeated compactions advance the boundary, later
summaries fold the previous one in, the provider verifiably receives the
compacted view (summary block + verbatim tail, bounded) while the
canonical transcript keeps every turn, state survives a mid-conversation
rebuild, and the persisted record round-trips the final boundary.
Scripted stand-in for the live-model smoke: intent survival across a
real summarizer (prompt tuning) still needs a configured provider key.
Settings -> Models grows a Context compaction card next to Token savings:
the trigger % of the context window (10-95), the absolute token cap
(clamped 10k-2M), and the summarizer-model pin (default: the session's
own model). POST /v1/settings/compaction persists them; engines read the
knobs live per check, so changes apply to running sessions immediately.
The "context compacted" divider rides the existing notice machinery: the
persisted `compacted` notice replays on reload (itemsFromMessages) and
the live COMPACTED event appends the same info notice mid-turn. The
transcript itself stays intact - outbound-only by construction.
Covered by vitest (marker replay), a settings-card e2e (defaults +
clamped POSTs + model pin), and a mid-session divider e2e driven by the
fixtures' scripted `compacted` event.
Minimal engine footprint: a checkpoint at each iteration top (between tool
turns and before a new turn), the usage signal captured per round-trip
(context_tokens; chars/4 estimate when never reported), and
_outbound_messages consulting the boundary. The summarizer runs off-loop
through the normal provider router, so the Settings model pin is just an
id.
Failure policy per spec: retry once in both modes; attended sessions get
the Retry / Trim-oldest-10% prompt (via the ask_user plumbing, gated by an
is_attended callback the WS surface wires); unattended runs auto-trim and
continue — never parked on internal bookkeeping. Raw context-overflow 400s
from the main model route into the same policy, progress-guarded so a
still-overflowing model terminates in the error path.
CompactionState persists on the session record (new sqlite column, same
defensive parse as grants), so reloads keep the compacted view. A
persisted compacted notice + a new COMPACTED event mark the spot for
the GUI divider (rendered in commit 3).
Trigger math (usage signal, chars/4 estimate fallback, min(80% x window,
250k cap) with overridable knobs), boundary picking that never splits a
turn (user-message starts preferred, iteration starts inside a giant tool
loop), the 8-section summarizer prompt with the continuation contract,
mechanical working-state extraction from tool records, deterministic
user-message preservation, the trim-oldest fallback, outbound-view
application, and context-overflow detection. Injectable provider seam;
no engine changes yet.
Two ephemeral breakpoints per request (last system block, final message's
last block) so append-only history re-reads the prior turns' cache;
outbound-only, persisted history stays clean.
Normalized TokenUsage (input/output/cache split) captured in every provider's
stream and complete paths, persisted as an assistant-message sidecar and sent
on the assistant_message event; matrix gains verified context-window sizes.
Model listing 403/404s under plain ADC; countTokens is free and proves
project+location+API in one call. Verified live: Gemini (global), Qwen MaaS (us-south1).
Segmented track + inset per-method panel with its own Test & save footer.
Vertex gains the same treatment: Google Cloud login (default), service account,
and API key (express mode, Gemini-only with a clear error elsewhere).
'Connect with' segmented choice (API key / profile / IAM keys) shows only that
method's fields; non-selected fields are dropped at build so stale values can't leak.
New optional field: paste the console-generated key, no CLI/IAM setup needed.
Takes precedence over SigV4 credentials, matching boto3; live-tested on Converse.
gemini/ and claude/ ids reuse the native providers; openweight/ goes through the
MaaS OpenAI-compat endpoint with an auto-refreshed google-auth bearer.
Credentials: service-account JSON or Application Default Credentials.
claude/ ids use Anthropic's native Bedrock client; everything else goes via Converse.
Credentials: explicit keys, named profile (incl. SSO), or the ambient AWS chain.
The picker gates ollama:* on a live local probe, so this passed only where Ollama runs and failed in CI.
The probe's own behaviour stays covered by test_ollama_models_gated_on_liveness.
Boundary-hardening pass addressing three audit findings on the local sidecar.
Shell command allowlist (andrewyng/openworker#28):
- Replace prefix-string matching in PermissionEngine._command_allowed with
argv-aware matching: reject any command containing shell operators
(; & | > < ` $( ( and newlines) before consulting the allowlist, then require
the allowlisted entry's tokens to be an exact argv prefix. This closes the
auto-run bypass where an allowlisted "git status" also auto-ran
"git status && rm -rf ~", pipes, redirection, and command substitution.
- Drop language interpreters / package managers (python, python3, node, npm,
npx) from DEFAULT_ALLOWED_COMMANDS — allowlisting an interpreter allowlists
arbitrary code (python3 -c "..."), defeating approval gating. Read-only
inspection commands and pytest remain.
MCP OAuth loopback (andrewyng/openworker#29):
- Verify the OAuth state at the loopback boundary. The MCP SDK already validates
state (compare_digest), so this is not a CSRF fix but defense-in-depth: capture
the state from the authorize URL and have deliver_callback ignore a callback
whose state does not match WITHOUT consuming the pending future, so a stray or
forged local hit can no longer abort a user's in-progress sign-in. Falls back to
prior accept-any behavior when no state was captured.
WebSocket ingestion caps (andrewyng/openworker#38):
- Bound a single user_message frame in the session WS loop: max text length,
max attachment count, and max total attachment bytes. Oversized frames get a
visible error frame and are dropped instead of being buffered into a turn; the
socket stays alive. Guards the unauthenticated loopback socket against cheap
memory spikes.
Tests:
- Allowlist: reject operator chaining (8 variants), argv-boundary matching, and
interpreters-not-auto-allowed-by-default.
- OAuth: state extraction, and mismatched/missing state ignored without consuming
the flow while the matching state still resolves it.
- WS: oversized text and too-many-attachments rejected with an error frame, and a
normal message still works afterwards.
Full suite: 865 passed (1 pre-existing unrelated failure in
test_provider_router::test_manager_curated_models, present on origin/main).
Switch notices no longer consume the retry guard or hide the button.
Error, switch model, Retry is the intended recovery path; retry runs on the new model.
Gmail/Calendar/Drive show a disabled Coming-soon button; the server refuses the flow too.
Manual token connect and already-connected accounts are untouched.
New reasoning_delta event; traces persist as a display sidecar stripped from provider feeds.
Sources: compat vendors' reasoning_content and Gemini thought summaries (include_thoughts).
Live-verified on GLM via Together and Gemini 3; Gemini tool loops stay healthy.
Picker stays live for the session; switches persist a model_switch notice (§17 revised).
Rebinds refused mid-turn; images become placeholders for non-vision targets at send time.
Signatures ride the assistant message as a _gemini sidecar and are reattached in tool loops.
Thought-flagged parts are filtered from answer text; foreign sidecars stripped on the OpenAI wire.
Live-verified both Gemini 3 models end-to-end; without the echo they 400 on every tool loop.
Engine appends a display-only notice message on error/interrupted; providers never see it.
New retry frame re-runs a failed turn with no new user message, guarded on the error tail.
GUI renders persisted notices on reload and a Retry button on the trailing error.
GUI flushes the streaming buffer into a durable item on interrupted/error.
Engine persists partial text on the provider-error path like the stop path.
e2e red-green verified; full suites pass.
Stream drops between chunks (+ the pre-first-token wait); pending approvals/questions/plans resolve as interrupted.
Running shell commands die via an executor interrupt hook; skipped tool calls still get results (no orphans).
Also fixes delete_session calling a nonexistent engine.interrupt().
Keyless is not configured — ollama:* picker entries render only while /api/tags answers (30s cached probe).
Stops phantom local models on machines without Ollama.
A top-level arguments key named "items" shadows minijinja's .items() in Together's GLM-5.2 chat template — every replayed turn 400s.
Old key still executes; GUI renders both; regression tests added.
Slack installer joins the workspace allow-list on managed connect; MCP interactive oauth only from explicit connects.
Run-started toast over a new app-wide /ws/events socket; Automations e2e locators scoped to the account menu.
Imported from andrewyng/aisuite@1b4bbf303e
(contents of its platform/ directory, hoisted to the repo root).
Development history prior to this commit lives in that repository.
Co-authored-by: Devika <devikaverma11@gmail.com>