Add the orchestrator infrastructure for replacing the Rust execution
loop with versioned Python code. This commit adds the module and host
functions without switching over — the existing Rust loop is unchanged.
New files:
- orchestrator/default.py: v0 Python orchestrator (run_loop + helpers)
- executor/orchestrator.rs: host function dispatch, orchestrator
loading from Store with version selection, OrchestratorResult parsing
Host functions exposed to orchestrator Python via Monty suspension:
__llm_complete__, __execute_code_step__ (nested Monty VM),
__execute_action__, __check_signals__, __emit_event__,
__add_message__, __save_checkpoint__, __transition_to__,
__retrieve_docs__, __check_budget__, __get_actions__
Also makes json_to_monty, monty_to_json, monty_to_string pub(crate)
in scripting.rs for cross-module use.
Design doc: docs/plans/2026-03-25-python-orchestrator.md
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
* Fix REPL single-message hang and cap CI test duration
* Fix Clippy nested-if lint in REPL startup
* Fix single-message approval flow
* Handle empty single-message REPL exits
* Wait for one-shot event routines before exit
* Fix MCP lifecycle trace user scope
* Fix REPL single-message hang and cap CI test duration
* Fix Clippy nested-if lint in REPL startup
* Fix single-message approval flow
* Handle empty single-message REPL exits
* Wait for one-shot event routines before exit
* feat(agent): thread per-tool reasoning from LLM through to REPL, HTTP, SSE, and DB
Add end-to-end agent reasoning summaries so users can see *why* the
agent chose specific tools, not just what it did.
- Add `reasoning: Option<String>` to `ToolCall` (all providers)
- Populate from LLM response content in `Reasoning::respond_with_tools`
and `select_tools`, with per-tool override when providers supply it
- Extend `Turn` with `narrative` and `TurnToolCall` with `rationale` +
`tool_call_id` for identity-based result matching
- Persist reasoning in DB via existing tool_calls JSON (no migration)
- Add `StatusUpdate::ReasoningUpdate` and `SseEvent::ReasoningUpdate` +
`SseEvent::JobReasoning` for real-time streaming
- Emit reasoning events in both chat dispatcher and worker job path
- Add `/reasoning [N|all]` command for inspecting turn reasoning
- Surface `narrative` and `rationale` in HTTP `/api/chat/history`
Based on the design from #361 and #456, reconstructed cleanly with
Option<String> to minimize blast radius (vs mandatory String that broke
compilation in #456).
Closes#456
Co-Authored-By: panosAthDBX <[email protected]>
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
* fix: address PR review feedback from Gemini and Copilot
- Fix `_ => Ok(None)` in agent_loop.rs to avoid accidental shutdown
- Fix fallback in record_tool_result_for/record_tool_error_for to use
first pending call instead of last_mut (parallel execution safety)
- Include per-tool decisions in WASM channel reasoning messages
- Apply truncate_at_tool_tags + clean_response to shared_reasoning in
select_tools (parity with respond_with_tools)
- Persist turn-level narrative to DB in tool_calls JSON wrapper
- Parse both old (array) and new (object) tool_calls formats in
build_turns_from_db_messages for backward compatibility
- Populate reasoning from action.reasoning in execute_plan ToolCalls
[skip-regression-check]
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
* fix: address second round of review comments + merge fixes
- Add reasoning: None to new github_copilot.rs ToolCall sites (from staging merge)
- Run cargo fmt on 4 files with formatting diffs
- Truncate narrative to 1000 chars before DB persistence
- Clone turn data and drop session lock in /reasoning command
- Extract ToolDecisionDto::from_json_array shared helper (deduplicate
worker/job.rs and orchestrator/api.rs)
- Add unit tests for wrapped tool_calls JSON format with narrative
[skip-regression-check]
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
* fix: address third round of review comments (Copilot + serrrfirat)
- Reword ToolCall.reasoning docstring to reflect provider-supplied or
fallback contract
- Sanitize narrative through SafetyLayer before storage/emission
- Clean per-tool reasoning via truncate_at_tool_tags + clean_response
in select_tools (parity with shared reasoning)
- Convert 4 approval-path recording sites in thread_ops.rs to
identity-based record_tool_result_for/record_tool_error_for
- Preserve tool_call_id and reasoning through restore_from_messages
- Fix has_result/has_error to reject JSON null values
- Truncate tool_call_id to 128 chars before DB persistence
- Add 4 unit tests for record_tool_result_for/error_for edge cases
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
* fix: address zmanian review — sanitize JobDelegate reasoning + warn on dropped results
- Sanitize narrative and per-tool rationale through SafetyLayer in
JobDelegate reasoning events (parity with ChatDelegate)
- Add tracing::warn when record_tool_result_for/error_for drops a
result because no matching or pending tool call exists
- Add 3 unit tests for reasoning normalization (thinking tags,
tool tags, empty-after-cleaning)
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
* fix: address 4 remaining unreplied review comments
- Clean per-tool reasoning in respond_with_tools via truncate_at_tool_tags
+ clean_response (parity with select_tools)
- Handle wrapped JSON format in rebuild_chat_messages_from_db so cold
hydration works after persist_tool_calls format change
- Update persist_tool_calls doc comment to describe new JSON shape
- Sanitize per-tool rationale through SafetyLayer in ChatDelegate before
emission and storage (parity with JobDelegate)
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
* fix: address zmanian review round 2
- Add tracing::debug on fallback-to-pending path in record_tool_result_for
and record_tool_error_for (item 1)
- Add comment explaining why /reasoning is special-cased in agent_loop.rs
(item 4)
- Items 2 (narrative persistence), 3 (rationale sanitization), and 5
(catch-all fix) were already addressed in prior commits
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
---------
Co-authored-by: panosAthDBX <[email protected]>
Co-authored-by: Claude Opus 4.6 (1M context) <[email protected]>
* fix: ensure LLM calls always end with user message (closes#763)
Claude 4.6 models (claude-sonnet-4-6, claude-opus-4-6) no longer support
assistant message prefill — any LLM call where the conversation ends on an
assistant message is rejected with HTTP 400 "This model does not support
assistant message prefill".
The same root cause also triggers NEAR AI's "No user query found in messages"
400 error for the routine engine path.
Two fixes:
1. src/worker/container.rs — before_llm_call()
After poll_and_inject_prompt(), if no user follow-up arrived and
handle_text_response() left an assistant message at the end of the
conversation, inject a sentinel "Continue." user message before
the next LLM call.
2. src/agent/routine_engine.rs — execute_lightweight_with_tools()
Before the force_text final completion call, ensure messages end
with a user-role message. Tool result messages (Role::Tool) satisfy
Anthropic but not NEAR AI; assistant messages satisfy neither.
Also updates the worker system prompt to instruct the agent to include
the phrase "The job is complete" in its final message, so the agentic
loop can detect termination reliably.
Tested with claude-sonnet-4-6 and claude-opus-4-6.
Workaround: ANTHROPIC_MODEL=claude-sonnet-4-20250514 (still supports prefill).
* fix: broaden sentinel guard to any non-user message (per review)
Gemini suggested the Role::Assistant check in before_llm_call() is too
specific. Changed to !Role::User to match the routine_engine.rs fix and
cover tool results too.
* fix: address zmanian review — JobDelegate sentinel, shared helper, NearAI complete() flattening
- Extract ensure_ends_with_user_message() to src/util.rs with 4 unit tests
(empty list, after assistant, after tool result, no-op when already user)
- Add sentinel guard to JobDelegate::before_llm_call() in src/worker/job.rs
so scheduler jobs (CreateJob / /job path) no longer hit Claude 4.6 / NEAR AI 400s
- Replace inline guards in ContainerDelegate and routine_engine.rs with the
shared helper — all 3 call sites now use one implementation
- Fix complete() in nearai_chat.rs to apply flatten_tool_messages when
flatten_tool_messages=true — previously only complete_with_tools() flattened,
so force_text paths could still send role:"tool" messages to NEAR AI
- Update stale comment in container.rs: "assistant message" → "non-user message"
- Add flatten tests in nearai_chat.rs covering the complete() path
Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
* ci: fix fmt and tar advisory
---------
Co-authored-by: Jacob Lasky <[email protected]>
Co-authored-by: Claude Sonnet 4.6 <[email protected]>
Co-authored-by: Illia Polosukhin <[email protected]>
Co-authored-by: firat.sertgoz <[email protected]>
Wire the self-improvement loop as a Mission with OnSystemEvent cadence,
inspired by karpathy/autoresearch's program.md approach. The mission
fires when threads complete with issues, receives trace data as trigger
payload, and uses tools directly to diagnose and fix problems.
Key changes:
Engine self-improvement (Phase A+B from design doc):
- Add fire_on_system_event() to MissionManager for OnSystemEvent cadence
- Add start_event_listener() that subscribes to thread events and fires
matching missions when non-Mission threads complete with trace issues
- Add ensure_self_improvement_mission() with autoresearch-style goal
prompt (concrete loop steps, not vague instructions)
- Add process_self_improvement_output() for structured JSON fallback
- Seed fix pattern database with 8 known patterns from debugging
- Runtime prompt overlay via MemoryDoc (build_codeact_system_prompt now
async + Store-aware, appends learned rules from prompt_overlay docs)
- Pass Store to ExecutionLoop for overlay loading
Bridge review fixes (P1/P2):
- Scope engine v2 SSE events to requesting user (broadcast_for_user)
- Per-user pending approvals via HashMap instead of global Option
- Reset tool-call limit counter before each thread execution
- Only persist auto-approval when user chose "always", not one-off "yes"
- Remove dead store/mission_manager fields from EngineState
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
* refactor: extract AppEvent to crates/ironclaw_common
SseEvent was defined in src/channels/web/types.rs but imported by 12+
modules across agent, orchestrator, worker, tools, and extensions — it
had become the application-wide event protocol, not a web transport
concern.
Create crates/ironclaw_common as a shared workspace crate and move the
enum there as AppEvent. Also move the truncate_preview utility which
was similarly leaked from the web gateway into agent modules.
- New crate: crates/ironclaw_common (AppEvent, truncate_preview)
- Rename SseEvent → AppEvent, from_sse_event → from_app_event
- web/types.rs re-exports AppEvent for internal gateway use
- web/util.rs re-exports truncate_preview
- Wire format unchanged (serde renames are on variants, not the enum)
Aligned with the event bus direction on refactor/architectural-hardening
where DomainEvent (≡ AppEvent) is wrapped in a SystemEvent envelope.
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
* refactor: add AppEvent::event_type() helper, deduplicate match blocks
Address Gemini review: extract the variant→string match into a single
method on AppEvent, replacing the duplicated 22-arm matches in sse.rs
and types.rs.
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
* refactor: rename leftover sse vars/tests to match AppEvent rename
Address Copilot review: rename sse_event vars to app_event in
orchestrator/api.rs and ws.rs, rename test functions from
test_ws_server_from_sse_* to test_ws_server_from_app_event_*, and
update stale SSE comments.
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
* refactor: add Deserialize to AppEvent, round-trip test, fix stale comments
Address zmanian review:
- Add Deserialize derive to AppEvent so downstream consumers can
deserialize incoming events
- Add event_type_matches_serde_type_field test that round-trips every
variant through serde and asserts event_type() matches the serialized
"type" field — catches drift between serde renames and the manual match
- Add round_trip_deserialize test for basic Serialize/Deserialize parity
- Update remaining "SSE" references in comments across server.rs,
manager.rs, ws_gateway_integration.rs, and worker/job.rs
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <[email protected]>
* feat(cli): show credential auth status in `tool info`
`ironclaw tool info` now checks the secrets store and shows whether
each required credential is configured or missing, consolidated into
a single Auth section that deduplicates across http.credentials,
auth, and setup.required_secrets. Secrets already shown in Auth are
filtered from the Secrets section to avoid redundancy.
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
* fix(cli): address review feedback on tool info auth status
- Fix clippy collapsible-if by using `if let` + `&&`
- Use HashMap<String, usize> for O(1) dedup instead of HashSet + linear scan
- Add --user flag to `tool info` for checking non-default user credentials
- Show "? unknown" on secrets store errors instead of silently reporting missing
- Surface secrets store init failure via eprintln instead of silent .ok()
- Sort auth entries by secret name for deterministic output
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
* fix(cli): only filter secrets when auth section renders, add regression test
When the secrets store fails to initialize, the Auth section is not
rendered. Previously, secret names were still filtered from the Secrets
section, causing credential names to disappear entirely. Now secrets
are only filtered when the Auth section will actually be displayed.
Adds test verifying auth secret deduplication across auth, setup, and
http.credentials sections, plus secrets store existence checks.
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
* refactor(cli): extract collect_auth_secrets helper, always render Auth section
Address review feedback:
- Extract dedup logic into `collect_auth_secrets()` so the test exercises
the same code path as production (not a re-implementation)
- Always render the Auth section when auth secrets exist, showing
"? unknown" status when the secrets store is unavailable instead of
hiding credential names entirely
- Lazily init secrets store only when capabilities contain auth secrets,
avoiding spurious warnings for tools with no auth
- Add test for empty capabilities edge case
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
* style(cli): move HashMap/HashSet imports to top of file
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
* fix(cli): use correct tagged JSON format for credential location in test
The CredentialLocationSchema uses serde tagged enum format
({"type": "bearer"}), not a bare string ("AuthorizationBearer").
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <[email protected]>
When the model calls routine_create, routine_list, routine_fire,
routine_pause, routine_resume, or routine_delete, the bridge now
routes them to the MissionManager instead of blocking with an error.
Mapping:
routine_create → mission_create (with cadence parsing)
routine_list → mission_list
routine_fire → mission_fire
routine_pause → mission_pause
routine_resume → mission_resume
routine_update → mission_pause/resume (based on params)
routine_delete → mission_complete (marks as done)
Routine tools removed from v1-only blocklist and restored in
available_actions(). The model can use either "routine" or "mission"
vocabulary — both work.
Still blocked: create_job, cancel_job, build_software (need v1
Scheduler/ContainerJobManager refs).
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
* fix: remove stale stream_token gate from channel-relay activation
The relay architecture now uses instance-scoped bearer auth + webhook
callbacks, not streaming. The `relay:<name>:stream_token` secret was
never written by the current OAuth flow, so activation always failed
with AuthRequired.
Replace stream_token with the team_id setting (already stored by the
OAuth callback) as the persistent "auth completed" marker:
- is_relay_channel(): check team_id setting instead of stream_token secret
- activate_channel_relay(): gate on team_id emptiness, not stream_token
- removal flow: delete team_id setting + oauth_state secret
- configure(): return empty allowed-secrets set (relay is OAuth-only)
- configure_token(): return AuthRequired (no manual token entry)
- list(): surface activation_error for relay channels (was hardcoded None)
- Clean up stale comments referencing stream_token / "stored token"
- Update test to match OAuth-only model (no secrets to pass)
Made-with: Cursor
* fix: address CI and review feedback
- Fix pre-existing tunnel/mod.rs test compilation (missing GatewayConfig
fields: memory_layers, user_tokens, workspace_read_scopes)
- Log warnings on failed team_id/oauth_state cleanup during removal
instead of silently ignoring errors (gemini review)
- Also delete legacy stream_token secret during removal for backward
compatibility with pre-webhook installs (codex review)
Made-with: Cursor
Missions are now callable from CodeAct Python code:
```python
# Create a daily briefing mission
result = mission_create(
name="Tech News",
goal="Daily AI/crypto/software news briefing",
cadence="0 9 * * *"
)
# List all missions
missions = mission_list()
# Manually fire a mission
mission_fire(id="...")
# Pause/resume
mission_pause(id="...")
mission_resume(id="...")
```
Implementation:
- MissionManager created on engine init, cron ticker started
- EffectBridgeAdapter intercepts mission_* function calls before tool
lookup and routes to MissionManager
- parse_cadence() handles: "manual", cron expressions, "event:pattern",
"webhook:path"
- Mission functions documented in CodeAct system prompt
- MissionManager set on adapter via set_mission_manager() after init
(avoids circular dependency)
System prompt updated with mission_create, mission_list, mission_fire,
mission_pause, mission_resume documentation.
151 tests passing.
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
* fix(agent): case-insensitive channel match and user_id filter for event triggers (#1051, #1076)
Event-triggered routines had two bugs preventing them from firing:
1. Channel comparison was case-sensitive (e.g., "Telegram" != "telegram"),
while emit_system_event already used eq_ignore_ascii_case. Fixed to match.
2. No user_id scoping — routines from any user were evaluated against every
message. Added ownership check so routines only fire for their owner's
messages.
Also adds periodic event cache refresh (every ~60s) in the cron ticker so
web/CLI mutations are picked up without requiring the tool path. Upgrades
skip-reason logging from trace to debug for debuggability.
Closes#1051
Refs #1076
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
* fix: correct refresh_every from 6 to 4 to match 15s default interval
The default cron_check_interval_secs is 15s, not 10s. With refresh_every=6,
the cache would refresh every 90s instead of the intended ~60s. Fix to 4
ticks (4 * 15s = 60s).
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
* fix(agent): address #1211 review -- extract routine_matches_message, fix refresh interval
Extract user/channel filter logic from check_event_triggers into a
standalone pure function routine_matches_message(). Rewrite tests to
call this function directly with controlled Routine and IncomingMessage
values, so they exercise the real code path and would catch a revert.
Add test_no_channel_filter_matches_any_channel for the None channel case.
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
* ci: re-trigger CI with latest changes
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: add missing IncomingMessage fields in test helper
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix(agent): address review -- time-based refresh, trace-level user mismatch, scope guard (#1211)
- Use tokio::time::Instant for cache refresh instead of tick counting
- Downgrade user-mismatch log to trace to reduce noise
- Add early return false for non-Event triggers in routine_matches_message
- Fix doc comment to say 'user scope' instead of 'message sender'
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
* style: run cargo fmt on agent_loop.rs
https://claude.ai/code/session_01ABGWibdKVQ3b6pEKtxPPkM
* fix(agent): resolve clippy warnings for unused binding and needless borrow
Fix unused `content` variable in event trigger guard (use `content: _`)
and remove redundant `&` on `message` which was already a reference.
https://claude.ai/code/session_01PzBK21BbUAuZbrfLpoz4Xb
* fix(test): update check_event_triggers call sites to new single-arg signature
The staging merge brought e2e_routine_heartbeat tests that still used
the old 3-argument check_event_triggers(user_id, channel, content)
signature. Updated all 11 call sites to pass &IncomingMessage directly.
[skip-regression-check]
https://claude.ai/code/session_012GrkTDrtDFkpJos2hkgTcE
* fix(agent): address review feedback on event trigger handling
- Use post-hook content for event trigger matching so BeforeInbound
hooks that rewrite input are respected
- Set MissedTickBehavior::Skip on cron ticker to avoid burst catch-up
after delays
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
* style: cargo fmt
https://claude.ai/code/session_01Va9wwvATNWFAx35GG7Zek7
---------
Co-authored-by: Claude Opus 4.6 (1M context) <[email protected]>
Co-authored-by: firat.sertgoz <[email protected]>
* fix(routines): normalize status display across web and CLI surfaces (#1319)
- Use Display (lowercase) instead of Debug (PascalCase) for RunStatus serialization in web handler
- Update JavaScript status class mapping to match lowercase values from the API
- Enrich CLI `routines list` to show running/attention states by querying last run status
[skip-regression-check]
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
* fix(routines): address review -- batch last-run query, consistent status, simplify ternary (#1319)
- Parallelize last-run lookups with join_all to avoid N+1 sequential queries
- Normalize status in /api/routines/{id}/runs handler to match lowercase convention
- Remove redundant 'running' check in app.js runStatusClass logic
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
* fix(db): replace N+1 last-run-status queries with batch method
The CLI routines list was firing a separate list_routine_runs query per
routine to determine each one's last run status. For large routine sets
this overwhelms the connection pool.
Add batch_get_last_run_status to the Database trait with implementations
for both PostgreSQL (DISTINCT ON + ORDER BY) and libSQL (correlated
subquery + in-memory filter). Update the CLI to call the batch method
once instead of N times.
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
* style: cargo fmt
https://claude.ai/code/session_01Va9wwvATNWFAx35GG7Zek7
---------
Co-authored-by: Claude Opus 4.6 (1M context) <[email protected]>
Three changes to make engine v2 visible in the web gateway:
1. SSE event streaming (AppEvent broadcast):
- ThreadEvent → AppEvent conversion via thread_event_to_app_event()
- Events broadcast to SseManager during the poll loop
- Covers: Thinking, ToolCompleted (success/error), Status, Response
- Web gateway receives real-time progress without any gateway changes
2. Conversation persistence to v1 database:
- After thread completes, writes user message + agent response to
v1 ConversationStore via add_conversation_message()
- Uses get_or_create_assistant_conversation() for per-user per-channel
- Web gateway reads from DB as usual — chat history appears
3. Final response broadcast:
- AppEvent::Response with full text + thread_id sent via SSE
- Web gateway renders the response in the chat UI
New EngineState fields: sse (Option<Arc<SseManager>>),
db (Option<Arc<dyn Database>>). Both populated from Agent.deps.
Agent.deps visibility widened to pub(crate).
Depends on: ironclaw_common crate with AppEvent type (PR #1615).
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
* fix(tunnel): target webhook server port instead of gateway port
start_managed_tunnel() always used the gateway port (3000) for the
tunnel target. Webhook routes live on the webhook server (HTTP_PORT,
default 8080), not the gateway. The old code never read
config.channels.http — no configuration could work around this.
Extracts resolve_tunnel_target() with regression tests.
* fix(tunnel): prevent SIGPIPE and fix default port fallback
Two fixes for managed tunnel subprocess lifetime:
1. After extracting the public URL from stdout/stderr, the pipe reader
was dropped (Rust ownership). The tunnel binary's next log write hit
the closed pipe and got SIGPIPE — killing it silently. Fix: drain
pipes in background tasks stored in TunnelProcess. Storing without
reading isn't enough — the OS pipe buffer fills up and the process
blocks instead.
2. When neither HTTP_PORT nor gateway is configured, the tunnel fell
back to 127.0.0.1:3000. But the webhook server defaults to
0.0.0.0:8080 in this case. Now the tunnel matches that fallback.
Affects ngrok (stdout), cloudflare (stderr), and custom (stdout).
Tailscale uses a daemon and is not affected by SIGPIPE.
* fix(tunnel): simplify drain loops and suppress CI false positives
Simplify `while let Ok(Ok(Some(line)))` drain pattern to
`while let Ok(Some(line))` — the extra Ok wrapper was unnecessary.
Add `// safety: test-only` to assert_eq! lines in test module to
suppress the "No panics in production code" CI check which greps
the diff without understanding Rust's #[cfg(test)] module boundaries.
---------
Co-authored-by: firat.sertgoz <[email protected]>
Merges refactor/extract-app-event-to-ironclaw-common which extracts
SseEvent into crates/ironclaw_common as AppEvent. This is the
prerequisite for engine v2 gateway integration — the bridge can now
emit AppEvents without depending on web gateway types.
Conflict resolution: workspace members includes all three crates
(ironclaw_common, ironclaw_safety, ironclaw_engine).
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
SseEvent was defined in src/channels/web/types.rs but imported by 12+
modules across agent, orchestrator, worker, tools, and extensions — it
had become the application-wide event protocol, not a web transport
concern.
Create crates/ironclaw_common as a shared workspace crate and move the
enum there as AppEvent. Also move the truncate_preview utility which
was similarly leaked from the web gateway into agent modules.
- New crate: crates/ironclaw_common (AppEvent, truncate_preview)
- Rename SseEvent → AppEvent, from_sse_event → from_app_event
- web/types.rs re-exports AppEvent for internal gateway use
- web/util.rs re-exports truncate_preview
- Wire format unchanged (serde renames are on variants, not the enum)
Aligned with the event bus direction on refactor/architectural-hardening
where DomainEvent (≡ AppEvent) is wrapped in a SystemEvent envelope.
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Routines are entirely v1 — not hooked up to engine v2. When a user
asks "create a routine" as natural language, engine v2 tries to call
routine_create via CodeAct, but the tool needs RoutineEngine + Database
refs that the bridge's minimal JobContext doesn't provide. This caused
a SIGKILL crash during testing.
Options documented: block routine tools in v2 (short term), pass refs
through context (medium), replace with Mission system (long term).
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Documents three gaps between engine v2 and the web gateway:
1. No SSE streaming (engine emits ThreadEvent, gateway expects SseEvent)
2. No conversation persistence (engine uses HybridStore, gateway reads v1 DB)
3. No cross-channel visibility (REPL ↔ web messages invisible to each other)
Implementation plan: bridge ThreadEvent→AppEvent, write messages to v1
conversation tables after thread completion. Prerequisite: AppEvent
extraction PR (in progress separately).
Also updated DB persistence status: HybridStore with workspace-backed
MemoryDocs is now implemented (partial persistence).
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Staging merge changed execute_tool_with_safety to take params by value
instead of by reference (perf optimization from PR #926). Updated
bridge adapter to clone params before passing.
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
* fix(agent): persist /model selection to .env, TOML, and DB
The /model command only wrote selected_model to the DB and config.toml,
but env vars from ~/.ironclaw/.env (e.g. NEARAI_MODEL) have the highest
priority in LlmConfig::resolve_model(). The .env value was never
updated, so it always shadowed the new model on restart.
Now persist_selected_model updates all three persistence layers:
1. The backend-specific model env var in ~/.ironclaw/.env (only if the
var already exists, to avoid injecting new vars)
2. The config.toml file (created if absent, since TOML > DB priority)
3. The DB settings table (for completeness)
Also adds diagnostic logging when the DB store is unavailable.
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
* fix(agent): address PR review — backend from deps, exact .env match
Review feedback:
- Use resolved llm_backend from AgentDeps instead of re-reading from
disk/env (fixes DB-only backend detection, eliminates redundant I/O)
- Match .env var with exact "KEY=" prefix and skip commented lines
(prevents false matches on NEARAI_MODEL_VERSION etc.)
- TOML is now loaded once (no double-read for backend + model update)
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <[email protected]>
Replaces InMemoryStore with HybridStore:
- Ephemeral data (threads, steps, events, leases) stays in-memory
- MemoryDocs (lessons, specs, playbooks from reflection) persist to
the workspace at engine/docs/{type}/{id}.json
On engine init, load_docs_from_workspace() reads existing docs back
into the in-memory cache. This means:
- Lessons learned in session 1 are available in session 2
- The RetrievalEngine injects relevant past lessons into new threads
- The engine genuinely improves over time as reflection accumulates
Workspace paths:
engine/docs/lessons/{uuid}.json
engine/docs/specs/{uuid}.json
engine/docs/playbooks/{uuid}.json
engine/docs/summaries/{uuid}.json
engine/docs/issues/{uuid}.json
No new database tables. Uses existing workspace write/read/list.
workspace() accessor widened to pub(crate).
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
* fix(tools): add missing description, parameters, and improve credential prompts
Silence three categories of startup warnings emitted by
CapabilitiesFile::validate() and WasmToolLoader:
1. "description" field missing → add tool descriptions to all manifests
2. "parameters" field missing → add action-enum parameter schemas
3. Short credential prompts (<30 chars) → append source URLs
Affects: github, gmail, google-calendar, google-docs, google-drive,
google-sheets, google-slides, slack, telegram, llm-context, feishu.
[skip-regression-check]
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
* refactor(tools): auto-compact WASM tool schemas from module exports
Replace the manual `parameters` field in capabilities JSON with automatic
schema compaction. WasmToolSchemas::compact_schema() derives a compact
advertised schema from the WASM module's schema() export by keeping only
required and enum-constrained properties. The full schema remains
available via tool_info(detail: "schema").
This eliminates:
- The `parameters` field from CapabilitiesFile and all 11 sidecar JSONs
- The "missing parameters" startup warning from the loader
- Manual maintenance of duplicate schema data
The `description` field in capabilities JSON is retained.
[skip-regression-check]
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
* fix(tests): remove cap_file.parameters reference in test_rig
The parameters field was removed from CapabilitiesFile in the previous
commit. Update test_rig.rs to match — schema is now auto-compacted from
the WASM module export, no sidecar override needed.
[skip-regression-check]
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
* fix(tools): handle oneOf schemas in compact_schema, add tool name to warning
Address PR review feedback:
- compact_schema now collects properties from oneOf/anyOf/allOf variants,
fixing GitHub-style schemas that have no top-level properties
- Use HashSet for required lookup instead of Vec::contains
- Add tool name to "Capabilities file not found" warning for consistency
[skip-regression-check]
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
* fix(tools): merge oneOf const values into enum, cap property collection
Address review feedback from @serrrfirat:
1. Merge const values across oneOf variants into a single enum array,
so the LLM sees all valid actions (not just the first variant's const).
2. Cap property collection at 100 to bound allocations.
3. Also keep properties with const constraint (single-variant case).
4. Update doc comment to describe variant collection and design choices
around variant-level required fields.
[skip-regression-check]
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <[email protected]>
The model was answering "Suggested 45 improvements" as a brief text
summary from training data without actually searching or listing them.
The trace showed: no code block, no tool calls, no FINAL().
Prompt changes:
- Rule 1: "ALWAYS respond with a ```repl code block. NEVER answer with
plain text only." (was: "Always write code... plain text for brief
explanations")
- Rule 2 (NEW): "NEVER answer from memory or training data alone.
Always use tools to get real, current information before answering."
- Rule 3: FINAL answer "should be detailed and complete — not just a
summary like 'found 45 items'"
- Rule 8 (NEW): "Include the actual content in your FINAL() answer,
not just a count or summary. Users want to see the details."
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
The leak detector's Warn-action matches (high_entropy_hex pattern on
web search results containing commit SHAs, CSS colors, URL hashes)
were logging at warn! level, corrupting the REPL UI with lines like:
WARN Potential secret leak detected pattern=high_entropy_hex preview=a96f********cee5
These are informational false positives — real leaks use LeakAction::Redact
which silently modifies the content. Warn-action matches only log for
debugging purposes and should not appear in production output.
Changed to debug! level — visible with RUST_LOG=ironclaw_safety=debug.
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>