Graph construction no longer raises on a bootstrap registry: build paths
bind a shared RegistryNotReadyChatModel placeholder that fails every call
with MODEL_REGISTRY_NOT_READY, so langgraph dev serves the Config API for
first-time configuration while run creation stays forbidden.
Run snapshot binding no longer compares configurable
'workspace_deployment_id' (the workspace-isolation scope id) against the
snapshot's issuing deployment — a mismatch that made every BFF run fail
with SNAPSHOT_NOT_FOUND. SnapshotService.get_for_run verifies thread_id
equality plus membership in the platform-registered deployment set
(local_deployment_id + webui_delegation_public_keys entries).
Blocking I/O moved off the event loop for langgraph dev's blockbuster:
Config API authentication (store mkdir/chmod, config.yaml read, jti
registration) and the message-budget snapshot read now run in threads,
with the immutable snapshot cached per run.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Remove legacy provider profiles, admin-token auth, /model command,
model picker widget, and config.yaml LLM fields (design doc section 10)
- Wire CLI/channels/cron and async sub-agents through the local snapshot
entry; run creation rejects model config outside runtime_snapshot_id
- Add periodic run-snapshot TTL cleanup to the config service lifespan
- Isolate tests from the real config dir and activate the registry where
run/model paths fail closed in bootstrap
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
POST /api/model-registry/test (model_config:test) runs the section 9.4
flow: resolve_for_test, per-test credential resolution, build_chat_model
with both safe clients, one minimal chat call, and per-capability probes
(tools/structured_output/vision) whose failures only mark that capability
unverified. Results upsert the model_verifications five-tuple inside a
BEGIN IMMEDIATE transaction that re-checks the registry revision and
configuration hash, returning 409 MODEL_CONFIGURATION_CHANGED on any
concurrent change. resolve_for_test now also relaxes the passing-
verification gate, which the provider test itself produces.
effective_request_options reuses the redacted adapter.build_request
output; the OpenAPI contract and checked-in openapi.json are updated.
- add the missing rule-8 counterexample test: declared capabilities
exceeding the adapter protocol are rejected with
CAPABILITY_UNSUPPORTED_BY_ADAPTER (all eight section 9.2 checks now
have at least one negative test)
- raise CREDENTIAL_NOT_CONFIGURED explicitly in _check_enabled_model
when a required credential reference is null instead of relying on
resolve_parameters call ordering
The abort path was read-then-write with an unconditional UPDATE, so a bind
committing between the two calls was clobbered back to aborted, losing its
langgraph_run_id. Add a conditional store-level abort_run_snapshot
(prepared-only UPDATE, rowcount-checked) and re-read on a lost race, matching
the bind loop. Also pin the inherit selection_hash test to a hardcoded
SHA-256 literal instead of reimplementing the serialization in the test.
Resolver (8.1): validates provider/model/credential/capability/limits and
the 6.5 four-mode input budget, freezes ResolvedModelConfig; resolve_for_test
relaxes only the enabled-visibility check (9.4); compute_availability is the
single 4.3 six-state judgement (stale beats configured, selectable only when
enabled).
SnapshotService (8.2, shared by the Task 5 HTTP API and Task 7 local entry):
freezes both roles' full ResolvedModelConfig with adapter spec revision,
fixed reserves, capabilities, and credential revisions; selection-hash
idempotency with pre-resolution semantics; prepared(15min)/bound(+24h)/
expired/aborted lifecycle with atomic bind; binding-checked reads that
revalidate frozen spec revisions; per-call credential resolution against the
frozen revision with no in-process secret cache (5.2); public diagnostic
view limited to the 8.2 safe subset.
Store gains additive helpers (credential pointer lookup, verification
listing, active-triplet lookup, conditional bind, due-expiry sweep) and the
taxonomy gains SNAPSHOT_NOT_FOUND (404) for missing snapshots.
Review fixes for the Task 3 contract layer:
- build_chat_model now accepts http_async_client alongside http_client
(at least one required) and wires it into ChatOpenAI
(http_async_client), ChatAnthropic (seeded _async_client), and
ChatOllama (async_client_kwargs transport), closing the unsafe
default-async-client gap.
- ChatOllama safe transports move from the shared client_kwargs to
sync_client_kwargs/async_client_kwargs; langchain-ollama merges shared
kwargs into both clients, which poisoned the async client with a sync
transport and crashed ainvoke.
- Unsupported parameters now actually execute the contract-declared
normalizer (reject_non_auto) instead of a hardcoded raise, with a
fallback rejection if a normalizer would let a value through.
- build_chat_model rejects overlapping client_options/request_options
keys instead of silently overwriting.
Add the Task 3 parameter contract layer (design doc 6.1-6.4):
- adapters.py: versioned built-in contracts for the five phase-1
adapters plus the openai-compatible/glm-5.2 model-specific contract
(verbatim section 6.2 values); exact > longest glob > generic
matching with spec_revision pinning; resolve_parameters implementing
the section 6.1 inherit/omit semantics, contract validation with
stable error codes, and named normalizers (identity,
clamp_to_model_limit, omit_when_none, omit_when_auto,
reject_non_auto); Adapter.build_request as the single entry point
mapping ResolvedModelConfig to {client_options, request_options};
compute_effective_capabilities (protocol AND declared AND verified).
- factory.py: build_chat_model(resolved_config, http_client, *,
credential=None) with no **kwargs and no setdefault merging; injects
the safe HTTP client into ChatOpenAI/ChatAnthropic/ChatOllama, never
reads provider API-key environment variables, and strips the
OLLAMA_API_KEY authorization header for mode=none adapters.
- tests: per-adapter request-capturing fakes plus an httpx.MockTransport
outbound capture proving registry resolution matches the wire request.
EndpointPolicy validates provider base URLs (section 4.3): public https
endpoints with hostname and optional port pass; loopback, private,
link-local, multicast, unspecified, and cloud-metadata addresses are
denied unless the normalized URL exactly matches a registered
development_endpoints entry (no prefix or wildcard matching). URLs with
user info, fragments, or non-http(s) schemes are rejected with the new
stable 422 code ENDPOINT_NOT_ALLOWED.
SafeHttpTransport is the single network egress for adapters: a custom
httpcore NetworkBackend resolves DNS under control on every connect
(retries included), filters denied ranges, and connects directly to the
selected IP, while TLS SNI/certificate checks and the HTTP Host header
keep the original hostname. Redirects and env proxies are disabled;
every request origin re-passes URL-layer validation before any I/O.
* chore: add pytest-asyncio in auto mode
* test: migrate channel and stream tests to native async
Convert run_async() wrapper tests to plain 'async def test_*' under
pytest-asyncio auto mode. collect_events() in stream_v3_fakes becomes a
coroutine awaited at every call site.
* test: migrate command and model/middleware tests to native async
Convert run_async() wrappers (import, alias, and fixture forms) to plain
'async def test_*'. Multi-call tests merge onto one loop as sequential
awaits; none asserted on loop identity.
* test: migrate TUI, notifier, gateway, and session tests to native async
TUI/notifier/gateway files convert run_async wrappers to plain async
tests. test_sessions.py's unittest.TestCase classes move to
unittest.IsolatedAsyncioTestCase (pytest-asyncio does not await async
methods on plain TestCase; converting blindly would have made ~70 tests
silently vacuous). Its setUpClass keeps a one-shot asyncio.run() since
IsolatedAsyncioTestCase has no async class-level hook. TestLoadingWidget
in test_tui_widgets.py drops its TestCase base for the same reason.
* test: replace direct asyncio.run() calls with native async tests
Convert tests that called asyncio.run() (directly or via a local _run
helper) to plain 'async def test_*'; delete the local helpers.
* test: drop undeclared anyio markers and delete run_async helper
The @pytest.mark.anyio tests relied on anyio being a transitive dep of
httpx; auto-mode pytest-asyncio collects them natively. run_async() and
its fixture are unreferenced after the migration, so remove them —
pytest-asyncio's per-test loop teardown covers the pending-task
cancellation the helper existed for (verified: full suite runs with no
'Event loop is closed' errors or destroyed-task warnings).
* refactor(onboard): shared flow for ccproxy providers
* feat(onboard): support oauth configuration for auxiliary models
* fix(onboard): reuse main model auth for same-provider auxiliary
* fix(onboard): reconcile oauth providers
* feat(cli): add --output-format stream-json for headless clients
Emit EvoScientist's native event stream as line-delimited JSON on stdout
in single-shot (-p) mode, with all human output redirected to stderr so
stdout stays pure JSONL. Intended as the integration surface for
programmatic clients (e.g. an agent runtime) that drive EvoSci headlessly.
- stream/json_sink.py: write_events_as_json + stream_json sink, plus
redirect_console_to_stderr helper for stdout purity
- cli/interactive.py: cmd_run gains output_format; stream-json branch runs
the sink instead of the Rich renderer
- cli/commands.py: --output-format option + validation (stream-json
requires -p; value must be text|stream-json)
- docs/stream-json.md: event-schema contract + example transcript
- tests: json sink serialization, CLI dispatch, console redirect, validation
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(cli): honor explicit --no-auto-mode over config in stream-json
Address CodeRabbit review (discussion_r3514041123): the auto-mode override
block only wrote to cli_overrides when the resolved value was True, so an
explicit --no-auto-mode silently fell back to a config that enables
auto-mode -- breaking "explicit flags always win" and leaving stream-json
running unattended despite the warning. Write auto_mode=False when the flag
is explicitly False. Add regression tests that capture the overrides.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* fix(tui): keep welcome banner at top after /new
PR #262 replaced scroll_end() with anchor() for free-scrolling.
When /new clears a long anchored conversation, the anchor kept
the viewport pinned to the (now empty) bottom, producing a
negative scroll_y and pushing the welcome banner out of view.
Reset the anchor and scroll to the top in clear_chat(), and
restore the follow/new-content flags so the fresh session starts
correctly.
Closes#301
* fix(tui): suppress anchor when chat content fits viewport
The previous fix for #301 only handled the /new path. din0s reported
that the banner still dropped to the bottom after a normal short turn
(user types 'hi', agent replies) — i.e. whenever the conversation
fit in the viewport. Root cause is in Textual's compositor
(textual._compositor): when a widget is anchored, scroll_y is
recomputed via set_reactive, which bypasses the validator. If the
anchored widget's content is shorter than the viewport, scroll_y
goes negative on the next layout pass and the welcome banner is
pushed below the visible region.
PR #262 made _stream_with_widgets re-engage the anchor at the end of
every turn via _anchor_chat, so the bug surfaced on any short reply
that fit in the viewport. Markdown re-renders, status-bar updates,
or any subsequent mount would then trip the compositor.
Fix in three places:
* _anchor_chat: only engage the anchor when max_scroll_y > 0;
otherwise release and scroll_home so the banner stays at the top.
* streaming anchor loop: if content shrinks below the viewport
mid-stream (e.g. loading widget removed), release the anchor
instead of leaving _anchored=True for the compositor to trip on.
* clear_chat: keep the unconditional reset (children are removed
asynchronously so a max_scroll_y check would be stale) but
document why.
Adds two regressions:
* test_short_turn_keeps_banner_at_top_after_layout_refresh — the
exact scenario din0s tested; fails with scroll_y=-10 on the
previous code, passes with the fix.
* test_long_turn_keeps_viewport_pinned_to_bottom — guards against
regressing free-scrolling for overflowing conversations.
Manually verified: 'hi' -> reply (banner stays at top) -> /new
(banner at top) -> another turn (banner stays at top).
* test(tui): address review feedback on banner-position regressions
- extract `_release_anchor_and_pin_top` helper for the 3-line
`anchor(False) + scroll_home(...)` pattern repeated in
`clear_chat`, `_anchor_chat`, and the streaming loop
- replace `pytest.skip` in `_capture_app` with a hard `RuntimeError`
so a broken capture never silently passes
- drop the redundant `load_agent` and `create_session_workspace`
monkeypatches (the factory is given those as parameters, so the
module-level symbols never run; added a comment explaining why)
- add a defensive `_FakeChannelRuntime` patch for symmetry with the
other module-level fakes
- drop the local `_run` helper and use the `run_async` fixture from
`conftest.py` (its teardown is better)
* test(tui): replace _FakeChannelRuntime with _auto_start_channel no-op
The _FakeChannelRuntime patch was ineffective because ChannelRuntime is
just a dataclass — the real channel manager still started via
_auto_start_channel, leaving pending tasks and non-hermetic test state.
Per review feedback, stub _auto_start_channel directly instead.
* feat: expose model registry at GET /api/models
* fix: include Ollama models in /api/models endpoint
* fix: honor env vars override in /api/models endpoint
* fix: offload get_effective_config to thread to satisfy blockbuster
---------
Co-authored-by: Xi Zhang <106144707+X-iZhang@users.noreply.github.com>
* feat: add scheduler functionality with cron-style task management
- Implemented a new scheduler subagent to automate recurring tasks using cron expressions.
- Enhanced the subagent factory to include the skill manager and auxiliary chat model for the scheduler.
- Created a YAML configuration for the scheduler with a detailed system prompt and toolset.
- Updated README files to include documentation on scheduled tasks and usage examples.
- Added tests for the scheduler, including command execution, scheduling tools, and middleware integration.
- Introduced new dependencies for timezone handling and ensured compatibility in the project configuration.
* fix(async-notifier): ensure fallback hint is used for unknown notification kinds
* feat: enhance scheduling functionality and improve system message handling
- Ensure 'task' is excluded from the default PTC allowlist to prevent ValueError in langchain-quickjs >=0.3.
- Verify that essential async dispatch tools remain in the allowlist.
- Confirm that the live quickjs filter accepts the default allowlist even with a 'task' tool present.
- Test the creation of the code_interpreter middleware to ensure it builds correctly.
* feat(gateway): graph gateway protocol
* refactor(cli): wire gateway in cli/tui
* refactor(gateway): centralize runtime gateway init
* chore(gateway): restrict RunRequest message type
* feat(gateway): add langgraph server gateway
* chore(cli): tighten serve runtime state typing
* refactor(cli): route async task state reads through graph gateway
* refactor(gateway): support graph targets in server gateway
* refactor(cli): route session commands through graph gateway
* refactor(cli): fold thread store under graph gateway
* refactor(gateway): route graph state access through gateway
* refactor(channels): wire graph gateway
* refactor(memory): preserve graph threads for cloning
* feat(gateway): add thread cloning
* fix(tui): pass effective workspace for thread creation
* chore(memory): add workspare dir to memory worker metadata
* fix(sessions): filter preloaded UUID registy entries by the current scope
* test(fakes): use https
* refactor(consumer): consolidate imports
* fix(stream): optional summarization event
* fix(gateway): resolve abbreviated thread IDs by search
* fix(gateway): page server thread listings
* fix(gateway): emit pending interrupt events
* style: fmt
* feat(gateway): persist workspace_dir & model in thread metadata
* fix(gateway): page server thread prefix resolution
* fix(gateway): expose server thread list metadata
* refactor: add back type def
* refactor: tighten types
* revert: add back worker thread deletion
The worker thread forking changes are out of scope for now, so to
maintain parity with the existing behavior we'll leave this intact.
* fix(gateway): apply compaction to server thread history
* refactor(stream): restore direct summary replay suppression
* fix(gateway): preserve compaction state and server stream output
* fix(gateway): close local stream generator on cancellation
---------
Co-authored-by: Xi Zhang <106144707+X-iZhang@users.noreply.github.com>
Typing `/model` and pressing Enter did nothing in the TUI; the picker
only opened via `/model --save` or `/model <name>`. The completion popup
matched both `/model` and `/model-fallback` by prefix, so the
exact-match-hide guard (which required a single match) never fired. With
the popup still visible, the TUI's Enter handler completed the text
instead of submitting the command, so it never executed.
Treat the typed prefix as an exact match whenever it equals any matched
command name, not only when it is the sole match. This hides the popup
on a complete command name so Enter submits it, even when a longer
command shares the prefix.
* feat(backends): use _platform_quote for Windows cmd.exe compatibility
Resolves the 3 skipped E2E tests in test_backends.py that exercised
the /skills/... mount path. The path-rewriter was wrapping resolved
absolute paths via shlex.quote (POSIX single-quote style); cmd.exe
doesn't strip single quotes, so the literal ' characters ended up in
the subprocess argv and the python script failed to find its file.
Replace the 3 shlex.quote call sites in _resolve_virtual_mount_path
with _platform_quote, a thin platform dispatcher:
- POSIX: shlex.quote (unchanged)
- Windows: _cmd_quote uses cmd.exe-compatible double-quote wrapping
and properly escapes embedded " and percent signs
Adds:
- backends.py: _is_windows, _cmd_quote, _platform_quote (~40 lines)
- test_backends.py: 6 TestPlatformQuote unit tests + _split_cmd
cross-platform tokenizer helper to replace shlex.split in the 8
sites that tokenize convert_virtual_paths_in_command results
(POSIX shlex strips backslashes from bare Windows paths, which
broke the 5 TestVirtualMountResolution assertions on Windows)
Removes:
- 3 @pytest.mark.skipif(sys.platform == "win32") markers on the
E2E tests for /skills/... mount resolution
Refs #274.
* fix: escape % as %% in _cmd_quote instead of relying on double-quoting
cmd.exe expands %VAR% before processing quotes, so double-quoting
cannot neutralize percent signs. Escape bare % as %% (the cmd.exe
idiom for a literal percent) before any other quoting logic.
Also updates _cmd_quote docstring and _resolve_virtual_mount_path
docstring to reflect the actual quoting strategy.
* style: fix ruff format (single → double quotes)
* fix: treat % as regular char in _cmd_quote, document limitation
%% escaping only collapses in .bat/.cmd files, not via cmd /c.
Since virtual-mount paths should never contain % in practice,
simpler to leave % alone and document the caveat.
* feat(cli): multi-stage slash command completions with subcommand awareness
Phase 1 of #82 — subcommand and argument awareness in completions.
- commands/base.py: add SubCommand dataclass and subcommands/category
ClassVars to the Command ABC. Each SubCommand has name, description,
and optional arguments.
- commands/manager.py: add get_subcommands() and list_subcommands()
methods to expose subcommand metadata for completion rendering.
- commands/implementation/mcp.py: declare 6 subcommands (list, config,
add, edit, remove, install).
- commands/implementation/model_fallback.py: declare 6 subcommands
(list, add, remove, clear, save, help).
- commands/implementation/channel.py: declare 2 subcommands
(status, stop).
- cli/tui_interactive.py: rewrite on_text_area_changed slash-completion
branch. When the user types a command name + trailing space and the
command has subcommands, show subcommand completions instead of hiding
the popup. Filter subcommands by typed prefix in multi-token input.
- commands/implementation/general.py: /help now lists subcommands
below each command that declares them.
Tests: 8 new tests covering SubCommand creation, CommandManager
subcommand lookup, and cross-command verification.
2293 passed baseline, no regressions.
* fix: subcommand completion preserves prefix + prompt_toolkit + tests
- _apply_selected_completion: preserve '/mcp ' prefix when completing
subcommands via _comp_is_subcommand flag
- SlashCommandCompleter (Rich CLI): add subcommand completion support
- Fix trailing-space bug: rstrip prefix before top-level matching
- test_tui_widgets.py: update stub on_input_changed to match multi-stage
logic; add 5 new subcommand tests
- test_command_manager.py: 8 tests for SubCommand + CommandManager
28 passed, 0 failed.
* style: ruff format tui_interactive.py + test_tui_widgets.py
* style: fix RUF012 ClassVar annotation on subcommands lists
* fix: sync test stub, add len>=3 guard, remove exact-match hide
- Sync test stub on_input_changed with real TUI code (remove exact-match
hide for subcommands, add len(parts)>=3 guard)
- Update test_input_changed_exact_subcommand_hides -> shows_confirmation
- Add test_input_changed_three_parts_hides
- Remove unused category ClassVar (din0s: what is this for)
* refactor(commands): extract shared completion engine
Per din0s feedback: one shared completion engine (commands/_completion_engine.py)
that parses text + cursor once, returns structured CompletionCandidate objects
with replace_start/replace_end ranges.
- SlashCommandCompleter (Rich CLI): thin adapter, delegates to engine
- on_text_area_changed (TUI): thin adapter, delegates to engine
- _apply_selected_completion: uses candidate.replace_start/replace_end instead
of _comp_is_subcommand flag
- Tests: engine tested directly (10 new tests), stub methods updated
29 passed, 0 failed.
* style: ruff format
* fix: preserve text after cursor when applying completion
CodeRabbit: replace_start only cuts from start to cursor,
dropping any suffix after the cursor. Use replace_start + replace_end
to correctly splice the replacement while preserving trailing text.
* fix(cli): repair slash-command completion (TUI crash, subcommand bugs, sort)
Apology + context: the previous push shipped a TUI-breaking change
(the new shared engine assumed ``event.text_area.cursor_position``
existed, but ``ChatTextArea`` / ``Changed`` don't expose it). User
caught the crash on ``/``; fixing that surfaced two more bugs in
the engine that din0s had already flagged. This commit addresses
all of them and drops a piece of dead stub code.
## Bug fixes
1. **TUI crash on ``/``** (``tui_interactive.py:2335``)
``event.cursor_position`` doesn't exist on the ``Changed`` event,
and ``ChatTextArea`` (Textual ``TextArea`` subclass) doesn't expose
``cursor_position`` either. Pass ``len(event.text_area.text)``
instead — the user types at the end of the input in practice.
2. **Subcommand trailing-space duplication** (``_completion_engine.py``)
Typing ``/mcp a `` + Tab produced ``/mcp aadd``. The engine
included the trailing space in ``replace_end``; the TUI apply
unconditionally appended ``" "``, producing double-space output.
Fix: ``replace_end`` excludes the trailing space; the TUI apply
checks ``current[replace_end:].startswith(" ")`` and skips the
separator when the suffix already has one.
3. **Subcommand exact-match confirmation noise** (``_completion_engine.py``)
Typing ``/mcp list`` + Tab re-inserted ``list`` and the popup
kept showing the same subcommand. Add a guard mirroring the
top-level exact-match rule: when the only subcommand match is
the prefix itself (no trailing space), return ``empty``.
4. **Alphabetical sort dropped in CLI** (``cli/interactive.py``)
The new completer iterated ``result.candidates`` in registration
order. Re-add ``sorted(result.candidates, key=lambda c: c.text)``.
Same sort added to the TUI for consistency.
## Cleanup
- Drop the dead ``on_input_changed`` method from the ``_StubApp``
test stub (0 call sites) plus the unused ``_slash_commands`` /
``_subcommands`` locals that fed it. This addresses din0s's
comment about the stub duplicating real TUI logic — the inlined
copy is no longer needed since the real completer now routes
through the shared engine.
## Tests
- ``test_engine_exact_subcommand_shows_confirmation`` → renamed to
``test_engine_exact_subcommand_hides`` to match new behavior.
- New: ``test_engine_subcommand_trailing_space_excludes_space_from_range``
and ``test_engine_subcommand_trailing_space_apply_does_not_double_space``.
- All 97 tests in ``test_tui_widgets.py`` pass.
- ``ruff check`` / ``ruff format`` clean.
- Local TUI smoke: ``/`` (no crash, top-level popup), ``/mcp ``
(subcommand popup), ``/mcp a `` + Tab → ``/mcp add ``.
Refs the din0s review comments on PR #273. CLI path tests and the
``category`` ClassVar follow-up are deferred to a separate PR (the
former is a test-suite addition; the latter is already absent from
``base.py`` on the current branch).
* fix: address remaining review items (help duplication, CLI tests, stub sync, docstrings)
- mcp.py: auto-generate help text from subcommands ClassVar (#1)
- tests/test_cli_completion.py: add 9 CLI completer tests (#2c)
- test_tui_widgets.py: sync _apply_selected_completion stub with real code (#4)
- mcp.py + interactive.py: add docstrings to key functions (#8)
* fix: hide completions on exact subcommand match regardless of trailing space
Remove the
ot has_trailing_space guard from the exact-subcommand
check. Previously /mcp list (with trailing space) would still
return candidates, causing Tab to oscillate between adding and removing
the trailing whitespace. Now the engine hides whenever the subcommand
is an exact match, same as the top-level rule.
Added test_engine_exact_subcommand_with_trailing_space_hides to cover
the scenario din0s flagged.
* refactor: use StrEnum for CompletionResult.kind
Replace plain str with CompletionKind(StrEnum) for type safety.
Backward-compatible with existing string comparisons.
* fix: normalize @file completion tuples to CompletionCandidate
complete_file_mention() returns list[tuple[str, str]] but the TUI
rendering/apply code expects objects with .text/.description.
Wrap tuples in CompletionCandidate to prevent AttributeError crash.
---------
Co-authored-by: Xi Zhang <106144707+X-iZhang@users.noreply.github.com>
* fix(backends): rewrite quoted virtual paths containing whitespace
The `convert_virtual_paths_in_command` regex
`(?<=\s)/[^\s;|&<>'"`]*` stopped at the first whitespace or quote,
so:
- `python "/skills/my skill/main.py"` was left completely
unchanged (the `(?<=\s)` lookbehind failed after the opening
`"`), and the shell then broke the inner unquoted path at the
embedded space.
- `python /skills/my skill/main.py` was truncated to
`python ./skills/my skill/main.py` (only `/skills/my` rewritten).
Replace the regex with `shlex.shlex(command, posix=True,
punctuation_chars=";|&<>")` so quoted regions stay whole, then
splice the rewrite back into the original command — extending the
splice span to include any matching quote chars around the path so
the fresh `shlex.quote` of the replacement isn't double-wrapped.
`_resolve_virtual_mount_path` now returns the unquoted path; the
caller owns shell-quoting, which avoids the previous
`shlex.quote` inside original `"…"` leaving literal `'` chars in
the argument value.
Unquoted paths with embedded whitespace remain a known limitation
(shlex has no way to know the user meant one path) — the
workaround of avoiding spaces in skill directory names still
applies, as flagged in the original issue.
Closes#237
* fix(backends): backslash-escaped paths, multi-path per token, subshell paths
- Fix backslash-escape handling: use unescape before rewriting
- Fix re.search→re.finditer: all /-paths in a token are rewritten
- Keep ( ) and backticks inside word tokens so paths spanning
\ or wrapped in backticks are matched correctly
- Add _try_rewrite helper with URL detection and unescape logic
- Add 10 contract tests pinning the din0s review cases
* fix(backends): restore () and backtick as shell operators for validate_command
- Restore ( ) and backtick to the operator set in _shell_token_spans.
Removing them caused a security regression: commands like (sudo ls)
would not detect sudo as a blocked command because (sudo became one
word token. With operators restored, validate_command correctly
catches blocked commands inside subshells and command substitutions.
- Fix _value_span_to_raw_span: the 'quoted' flag from the tokenizer
means the token *contains* a quoted segment (not necessarily starts
with a quote). Replace raw[0] assumption with a forward scan for
the first quote char, consuming unquoted prefix chars 1:1.
- Update test_system_path_with_shell_expansion: paths are now
partially rewritten because () are operators. Test updated to
reflect this known limitation (security > path rewriting).
* fix(test): cross-platform compatibility for pre-existing Windows failures
- python3 -> python in execute() calls (python is on PATH in any activated venv)
- sleep 10 -> _sleep_cmd(10) cross-platform helper
- str().endswith() -> Path().parts assertions (backslash-safe on Windows)
- shlex.quote exact-match assertions -> 'in' assertions (Windows quotes paths differently)
- mkdir -p E2E test -> preprocessor boundary test
- Skip 3 E2E tests on Windows: shlex.quote produces POSIX quoting incompatible with cmd.exe
141 passed, 3 skipped on Windows.
* fix: update docstring + strengthen shell-expansion test assertion
- Fix _value_span_to_raw_span docstring: no longer assumes raw[0] is
the opening quote, scans forward for first quote char
- Strengthen test_system_path_with_shell_expansion: verify
./workspace/notes is rewritten, not just notes in result
* style: ruff format backends.py + test_backends.py
* refactor(backends): simplify quoted virtual path rewriting
Replace 500+ line shlex tokenizer with 12-line pre-process step. Match quoted args via regex, unescape, rewrite via _rewrite_quoted_path, substitute with shlex.quote. 133 passed, 3 skipped.
* fix: guard bare absolute paths from double-rewrite by post-process regex
On POSIX, shlex.quote returns bare paths (e.g. /tmp/memories/note.md).
The pre-process substitutes these into the command, then the post-process
regex re-matches and incorrectly rewrites them.
Fix: _guard_bare_absolute wraps bare /-paths in single quotes so the
post-process regex''s character class stops at the quote char.
* style: ruff format
* fix(backends): narrow pre-process to exclude system-prefixed paths
Only rewrite quoted paths that are NOT known system prefixes.
* fix: narrow quoted-path pre-process to virtual mounts only
Only rewrite quoted /... paths that resolve to actual virtual mounts (/skills/..., /memories/...) or workspace-prefixed system paths. Remove catch-all that incorrectly rewrote bare paths like echo /hi.
Addresses din0s review feedback on #269.
* docs: update docstring for narrower quoted-path rewrite scope
* fix: session lost after evoscientist restart
* feat: Implement memory worker thread deletion on completion
- Added synchronous and asynchronous functions to delete memory worker threads after they finish execution, ensuring no residual checkpoints are left in the database.
- Enhanced `_watch_memory_worker_run_sync` and `_watch_memory_worker_run_async` to invoke deletion functions upon confirming worker completion.
- Introduced tests to verify that worker threads are deleted correctly upon successful completion and that failures in deletion do not affect the overall worker status.
- Updated session management to ensure that only relevant threads are restored from the database, preventing exposure of internal or unrelated workspace threads.
- Implemented a purge function to clean up leftover worker checkpoints during server startup.
* feat: Implement short thread ID display for CLI and session hints
* fix: ensure proper accounting and deletion order for memory worker threads
---------
Co-authored-by: z00827015 <zhoulun1@huawei.com>
* feat(dangerous-mode): implement real-filesystem access with safety checks
- Introduced a 'dangerous mode' allowing the agent to operate on the real filesystem.
- Updated command validation to bypass path confinement while enforcing a blocklist for privileged commands.
- Added warnings and guidelines for users when operating in dangerous mode.
- Enhanced configuration to support dangerous mode and ensure it implies auto-approval.
- Updated tests to verify the behavior of commands and configurations in dangerous mode.
* feat(dangerous-mode): enhance logging and environment management for dangerous mode
* feat(dangerous-mode): improve handling of dangerous mode with environment flags and enhance test isolation
* ci: add windows-latest to test matrix + fix 11 cross-platform test bugs
The test workflow ran on ``ubuntu-latest`` only. Per the issue's
first bullet — the maintainer's explicit #1 priority — add
``windows-latest`` to the matrix so the manager and related
modules are exercised on Windows on every PR.
The matrix addition surfaces 18 pre-existing Windows-only test
failures. Without fixes the new leg would be 18+ reds from
day one and the matrix would just produce a wall of
``fail-fast`` noise. This PR fixes 11 of them; each fix is
a real (cross-platform) bug, not a Windows-specific hack —
most were already flagged by CodeRabbit on PR #236 but never
acted on. The remaining 4 failures need code refactors
(``os.killpg`` → ``psutil`` in ``background.py``,
``convert_virtual_paths_in_command`` Windows-aware quoting,
tilde expansion) that are documented as out-of-scope
follow-ups below.
## What changed
* ``.github/workflows/test.yml``
- ``os: [ubuntu-latest, windows-latest]`` → 2 OS × 2 Python
= 4 cells.
- ``fail-fast: false`` so one bad cell doesn't cancel the
rest while the Windows leg is being brought up. Removable
in a future PR once the suite is fully green.
* ``tests/test_backends.py``
- Hard-coded ``"python3"`` → ``{sys.executable}`` in 7
test commands. Windows has no ``python3`` on PATH; using
``sys.executable`` is portable and matches what CodeRabbit
flagged on PR #236.
- Strict string comparisons → ``shlex.split`` round-trip in
5 resolver tests. ``shlex.quote`` adds single quotes
around backslash paths on Windows, which broke the
direct ``==`` compare.
- Cross-platform suffix checks in 2 path-resolution tests
(``Path(resolved).parts[-2:]`` instead of
``str(resolved).endswith("src/main.py")``).
- ``mkdir -p`` → ``sys.executable -c "import os;
os.makedirs(...)"`` in the cwd-sanitization test.
- ``skipif(sys.platform == "win32")`` on 3 e2e tests that
hit the underlying ``shlex.quote`` + ``cmd.exe`` quoting
bug (real, separate issue).
* ``tests/test_sessions.py``
- ``test_uses_data_dir``: check ``.evoscientist`` in the
long path form (via ``Path.resolve()``) rather than the
short-path form ``get_db_path`` returns on Windows.
* ``tests/test_mcp_client.py``
- ``endswith("python")`` → ``Path(result).stem.lower()`` so
``python.EXE`` matches on Windows.
- ``endswith("npx")`` also accepts ``npx.cmd`` so the npm
shim on Windows matches.
## Out of scope (follow-up issues to file)
* ``os.killpg`` doesn't exist on Windows
(``EvoScientist/background.py:248``) — 3 background tests
fail. Real fix is the same ``psutil`` walk pattern PR #200
shipped in ``langgraph_dev/manager.py``.
* Tilde expansion in file mentions.
* Windows-aware shell quoting in
``convert_virtual_paths_in_command``.
* Path conventions (``~/.config/evoscientist/`` vs
``%APPDATA%\EvoScientist``) — needs design discussion +
``platformdirs`` migration.
* Cross-module audit of
``EvoScientist/tools/execute.py``,
``EvoScientist/ccproxy_manager.py``,
``EvoScientist/config/onboard.py``.
Closes#207 (step 1 only — CI matrix + the easy test
fixes; remaining bullets tracked separately).
* fix: cross-platform compatibility for Windows CI runners
- background.py: replace POSIX-only os.killpg/os.getpgid with
cross-platform _kill_process_tree() helper. On Windows falls back
to Popen.terminate()/Popen.kill() (TerminateProcess); on POSIX
keeps existing os.killpg logic.
- test_backends.py: replace mkdir -p shell execution in
test_literal_workspace_path_replaced with preprocessing-boundary
assertion (patch LocalShellBackend.execute, capture command,
assert workspace path was rewritten to ./). Avoids POSIX-only
mkdir -p on Windows runners.
- test_file_mentions.py: monkeypatch USERPROFILE on Windows so
ntpath.expanduser() resolves ~ to tmp_path even when HOME is
unset on CI runners.
* fix(test): cross-platform sleep/true commands for Windows CI
Replace POSIX-only sleep/true with module-level helpers that use
ping -n / cmd /c on Windows. Also fix python3 -> sys.executable
in the non-timeout recovery test.
- test_background.py: 7 sleep/true fixes
- test_background_middleware.py: 6 sleep/true fixes
- test_backends.py: 4 sleep fixes + 1 python3 fix
2318 passed, 0 failed on Windows.
* fix(test): use shell-portable double quotes for python -c on Windows
cmd.exe does not treat single quotes as string delimiters, so
-c 'raise SystemExit(1)' was passed with literal quotes on Windows.
Switch to double quotes which work on both cmd.exe and POSIX sh.
* fix: use psutil for Windows process tree kill + avoid sys.executable under uv
- background.py: replace Popen.terminate()/kill() with psutil-based
process tree walking on Windows. TerminateProcess does NOT cascade
to grandchildren; psutil.Process.children(recursive=True) ensures
the entire tree is signaled.
- test_backends.py: replace sys.executable with 'python' in sandbox
execute() calls. Under uv, sys.executable is under the workspace
and gets rewritten to ./ by prepare_sandbox_command, breaking
Linux CI. The plain 'python' command resolves correctly in any
activated venv.
* fix: broaden try/except in _kill_process_tree to cover proc.children()
If the process exits between Process(popen.pid) and children(recursive=True),
the children call raises an uncaught exception escaping stop(). Move it inside
the existing try/except block.
* fix: narrow exception to ProcessLookupError in POSIX _kill_process_tree
OSError is too broad — would silently swallow EPERM on SIGKILL, leaving
the process alive when we report it as stopped. Match original behavior
which only caught ProcessLookupError (process already gone).
* style: ruff format test_backends.py
* ci: trigger re-run for flaky prompt_toolkit test
* style: fix ruff check (import order + RUF005 unpacking)
---------
Co-authored-by: Xi Zhang <106144707+X-iZhang@users.noreply.github.com>
* fix(langgraph-dev): rotate langgraph_dev.log when it exceeds 50MB
``_LOG_FILE`` (``~/.config/evoscientist/langgraph_dev.log``) was
opened in ``start_langgraph_dev`` with plain ``"ab"`` and never
rotated, so it grew unbounded over weeks/months of heavy use —
especially when chatty MCP servers spawned by langgraph dev
filled it, or when failure paths produced stack traces.
Implement the recommended option 1 from #209: filesize-based
rollover. When the active log exceeds 50MB on the next
``start_langgraph_dev`` invocation, rename it to
``langgraph_dev.log.1`` (overwriting any existing backup) via
``os.replace`` and start fresh. Single-backup policy keeps the
disk footprint bounded at roughly 2x threshold.
Rotation is best-effort: ``_rotate_log_if_needed`` logs and
swallows OSError so a permission error or racing rename can't
block langgraph dev from starting. The next ``start`` invocation
will try again — worst case the log grows for one more session.
Options 2 (timestamped per-session + 7-day sweep) and 3
(``RotatingFileHandler`` + pipe) are explicitly NOT done — option
1 is simplest, no async machinery, matches the issue's
recommendation.
Closes#209
* test(langgraph-dev): redirect _PID_DIR in rotate integration test
Address CodeRabbit review comment on #270: the
``TestStartLanggraphDevRotatesLog::test_rotate_called_before_open``
test patched only ``_LOG_FILE`` to a tmp path, but
``start_langgraph_dev`` also calls ``_PID_DIR.mkdir(...)`` as part
of its prelude, which would create a real directory under
``~/.config/evoscientist/`` on a dev machine. Redirect
``_PID_DIR`` to ``tmp_path / "pids"`` too so the test stays
fully isolated. Add a final assertion that ``pid_dir.is_dir()``
holds, proving the function reached past the mkdir call.
* refactor(langgraph-dev): bundle runtime paths into LanggraphRuntimePaths
@din0s review follow-up on #270: the previous test isolation patched
only ``_LOG_FILE`` (and after a second round, ``_PID_DIR``), but
``start_langgraph_dev`` still touches 5 distinct on-disk paths. Patching
any subset of those still leaves the others pointing at the user's real
``~/.config/evoscientist/`` — exactly the case that produced the
"Port 6174 cannot be bound after waiting 60s" symptom on the
reviewer's machine.
Replace the five free-floating module-level constants
(``_PID_DIR`` / ``_PID_FILE`` / ``_LOG_FILE`` / ``_WORKSPACE_SIDECAR``
/ ``_FILE_LOCK_PATH``) with a single ``LanggraphRuntimePaths`` frozen
dataclass exposed as a module-level ``RUNTIME`` instance. Production
code accesses ``RUNTIME.pid_file`` etc.; tests can now substitute the
*whole* bundle in one assignment:
monkeypatch.setattr(
manager, "RUNTIME",
manager.LanggraphRuntimePaths.for_directory(tmp_path / "runtime"),
)
The classmethod ``for_directory(pid_dir)`` builds an isolated bundle
rooted at a single dir, so the test author doesn't spell out every
path field. Tests that only care about one field (e.g. pid_file
during the stale-process kill path) use
``dataclasses.replace(manager.RUNTIME, pid_file=X)`` — frozen
dataclass-friendly, no need to enumerate the other four fields.
The dataclass's docstring records the migration rationale (the old
five-name layout invited inconsistent patches).
External callers of the old constants updated:
- ``EvoScientist/deploy/server.py`` and ``webui.py`` now import
``RUNTIME`` and use ``RUNTIME.log_file`` for the on-screen log
path hint. The other imports they had (``_DEFAULT_PORT``,
``_is_port_occupied``, ``_read_workspace_sidecar``) are still
module-level functions/values, untouched.
Test updates:
- ``tests/test_langgraph_manager.py``: ``patch.object(manager, "_XXX",
X)`` patterns now go through ``dataclasses.replace(manager.RUNTIME,
xxx=X)``; the ``TestStartLanggraphDevRotatesLog::test_rotate_called_before_open``
test (from the previous #270 review iteration) uses
``for_directory`` for one-shot isolation.
- ``tests/test_langgraph_dev_workspace_sidecar.py``: each test now
goes through a tiny ``_isolated_runtime(monkeypatch, tmp_path)``
helper that calls ``for_directory``.
- ``tests/test_langgraph_dev_deploy_mode.py``: same ``for_directory``
swap.
No production behavior change. All ``langgraph_dev``-side tests
(``test_langgraph_manager.py`` 26/26, ``test_langgraph_dev_workspace_sidecar.py``
14/14, ``test_langgraph_dev_deploy_mode.py`` 14/14, ``test_cli_deploy.py``
18/18 — which indirectly exercises deploy/server.py and deploy/webui.py
imports) pass. Full-project test count unchanged from baseline; the
remaining 22 Windows-only pre-existing failures (test_background
``os.killpg``, test_file_mentions tilde, mcp_client ``shutil.which``,
test_sessions 8.3 short path) are documented as out-of-scope for #207.
* style: apply ruff format to langgraph_dev test + module files
CI lint check on #270 failed:
Run ruff format --check .
Would reformat: EvoScientist/langgraph_dev/manager.py
Would reformat: tests/test_langgraph_manager.py
Plus two test files touched by the prior consolidation commit that
``ruff format`` hadn't seen yet:
tests/test_langgraph_dev_deploy_mode.py
tests/test_langgraph_dev_workspace_sidecar.py
Just formatting. No logic change. All 75 refactor-related tests pass.
* fix(test): use for_directory for full path isolation + patch _can_bind_port to skip real socket ops
Two fixes for TestStartLanggraphDevRotatesLog:
1. Replace dataclasses.replace(manager.RUNTIME, ...) with
LanggraphRuntimePaths.for_directory(pid_dir) so pid_file,
workspace_sidecar, and lock_file are also temp-rooted
(prevents leak to ~/.config/evoscientist/).
2. Monkeypatch _can_bind_port to always return True so the
bind-poll loop in _wait_for_port_bindable passes immediately
without touching real sockets (fixes 60s timeout on machines
where port 6174 is already in use).
* fix: cross-platform compatibility for Windows CI runners
- background.py: replace POSIX-only os.killpg/os.getpgid with
cross-platform _kill_process_tree() helper. On Windows falls back
to Popen.terminate()/Popen.kill() (TerminateProcess); on POSIX
keeps existing os.killpg logic.
- test_backends.py: replace mkdir -p shell execution in
test_literal_workspace_path_replaced with preprocessing-boundary
assertion (patch LocalShellBackend.execute, capture command,
assert workspace path was rewritten to ./). Avoids POSIX-only
mkdir -p on Windows runners.
- test_file_mentions.py: monkeypatch USERPROFILE on Windows so
ntpath.expanduser() resolves ~ to tmp_path even when HOME is
unset on CI runners.
* refactor(test): add runtime_paths fixture to isolate manager.RUNTIME
Adds a reusable fixture that monkeypatches manager.RUNTIME to a
temp-rooted LanggraphRuntimePaths.for_directory(). Tests that need
specific fields can still dataclasses.replace(runtime_paths, ...)
but the baseline is always temp-isolated, preventing leaks to
~/.config/evoscientist/.
Updated test_langgraph_dev_deploy_mode.py, test_langgraph_dev_workspace_sidecar.py,
and test_langgraph_manager.py to use the fixture, consolidating sequential
lock_file + pid_dir patches into single dataclasses.replace calls.
* Revert "fix: cross-platform compatibility for Windows CI runners"
This reverts commit eb025d24af32e195a982cd40f6d70dba885c4019.
* style: ruff format conftest.py
* fix: address review issues in log-rotation + runtime paths
- Use for_directory(tmp_path/pids) as base in ensure_langgraph_dev tests
so pid_file/log_file are co-located with pid_dir, not split across paths
- Remove unused runtime_paths param from test_no_existing_file_is_noop
- Replace manager.RUNTIME with runtime_paths in two sidecar tests
- Use for_directory(DEFAULT_PID_DIR) instead of explicit construction
- Fix stale _LOG_FILE reference in TestRotateLogIfNeeded docstring
* style: ruff format test files
---------
Co-authored-by: Xi Zhang <106144707+X-iZhang@users.noreply.github.com>
* refactor(agent): make create_cli_agent(config=, chat_model=) pure
Re-applies the #183 purity refactor on top of the observation-memory
lifecycle that landed in #259, integrating the two cleanly.
create_cli_agent gains a pure path: when both `config` and `chat_model`
are passed it builds the agent entirely from locals and writes none of
the cached module globals (`_config`, `_chat_model`, `_chat_model_key`,
`_EvoScientist_agent`). `/model` commits the switch via
`set_active_config` / `set_chat_model_instance` only after a successful
build, so a failed rebuild leaves the session on the original model
(replaces the old snapshot/restore rollback).
Supporting changes:
- Extract `set_active_config` (write-half of `_ensure_config`),
`_apply_env_from_config`, `_build_chat_model`, and
`set_chat_model_instance`.
- Thread `cfg` / `chat_model` through `_get_default_middleware`,
`_build_base_kwargs`, `load_mcp_and_build_kwargs`,
`_maybe_swap_async_subagents`, and `_inject_subagent_middleware` so the
pure path never falls back to the global-writing `_ensure_config()` /
`_ensure_chat_model()`.
- Integrate with #259's memory middleware: subagent context-editing
middleware binds the threaded `chat_model`, and the configured system
prompt / memory controls read the threaded `cfg` (new threading vs the
original #183, required because #259 made these paths read config).
- Consolidate `cfg` resolution to one `cfg if cfg is not None else
_ensure_config()` at the top of each kwargs builder, matching the
pattern already used in the other config-aware helpers.
* fix(agent): keep pure tool selector off global cache
* fix(model): apply config switch in place to preserve reference integrity
---------
Co-authored-by: Xi Zhang <106144707+X-iZhang@users.noreply.github.com>
Co-authored-by: X-iZhang <zacharyzhang2022@gmail.com>