requires-python declares >=3.10 and the README says "Python 3.10+", but
coworker/config.py imports tomllib at module top and tomllib only landed in
the 3.11 stdlib — on 3.10 the package cannot even be imported (every test
module fails collection through the coworker.config import chain), so the
advertised floor is broken in practice.
Fall back to the tomli package (the pre-stdlib implementation of the same API)
and declare it as a dependency only for python_version < '3.11', so 3.11+
installs are unchanged.
Verified on 3.10: importing coworker.config and load_config() parsing a
workspace config.toml both work through the fallback; the config suite still
passes on 3.12.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The GUI README tells contributors to run `npx tsc --noEmit` and `npm run build`
starts with `tsc`, but CI never runs either — a type error ships green today
and only surfaces when someone next builds a bundle locally. Add the check as
one step inside the existing gui-unit job so it reuses that job's npm ci and
costs no extra runner.
Verified locally: `npx tsc --noEmit` exits 0 on current main, and the vitest
suite (82 tests) still passes.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The GUI README still described the pre-split monorepo layout: every command
cd'd into a platform/ directory that does not exist in this repository, so a
fresh contributor following it verbatim fails at the first step. The venv,
packaging scripts, and surfaces/gui all live at the repo root now — align the
commands and the server_bin() dev-fallback comment with the layout the root
README already documents.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
guard.get_checked previously resolved a name for the address check and then let
the client resolve it again to connect, so a ~0-TTL record could flip to
127.0.0.1 (or the metadata endpoint) between the two — the exact gap the module
docstring called out as "not covered".
Every hop now connects to the address that passed its check: the request URL
carries the vetted IP, Host keeps the original name (and explicit port) for
virtual hosting, and the sni_hostname extension keeps the TLS handshake — and
certificate verification — against the name. Literal-IP URLs are unchanged.
Redirect Locations are resolved against the logical URL, and the final logical
URL rides resp.extensions["logical_url"] so web_fetch reports the name, not the
pinned address.
Verified against a local TLS server whose cert carries only DNS:pinned.test:
the pinned request (URL=127.0.0.1, sni_hostname=pinned.test) verifies and the
server sees Host: pinned.test; the same request without the extension fails
with "IP address mismatch" — httpcore does verify certificates against
sni_hostname, not the URL host.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The artifacts scan used rglob and filtered after descending, so a home directory
workspace walked into ~/Library and triggered the macOS App Data consent prompt on
every turn. Walk with pruning instead, and skip Library / AppData in search too.
The composer chip now shows the session total by default, with the context window
bar behind a Settings toggle.
Follow up to #290. Add RFC 6598 shared space (100.64.0.0/10, used by
Tailscale) to the address guard, and run the same guard on the Playwright
browser_open_url before navigating.
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.
Options accept {label, description, recommended, preview} objects (plain
strings unchanged — old sessions render as today's pills), and `questions`
groups up to 4 questions into one call, rendered as a stepper via the
header chips. Any option preview switches the card to a two-pane layout:
options left, monospace pane right, following hover/focus.
Grouped calls resolve with a JSON map keyed by header-or-question and
return {answers: {...}} to the agent (single stays {answer: ...});
a grouped item's first question doubles as its title/options so channel
mirrors and legacy surfaces degrade sensibly. Channel buttons use option
labels; grouped items mirror as text with the open-the-app hint.