From 1440ec742259646f9c8eae944e301599764d217c Mon Sep 17 00:00:00 2001 From: Henry Park Date: Mon, 9 Mar 2026 16:58:56 -0700 Subject: [PATCH 1/5] fix(ci): secrets can't be used in step if conditions [skip-regression-check] (#787) GitHub Actions step-level `if:` doesn't have access to `secrets` context. Replace `if: secrets.X != ''` with `continue-on-error: true` and let the Set token step handle the fallback. Co-authored-by: Claude Sonnet 4.6 --- .github/workflows/staging-ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/staging-ci.yml b/.github/workflows/staging-ci.yml index 9e887436a01..22d70030133 100644 --- a/.github/workflows/staging-ci.yml +++ b/.github/workflows/staging-ci.yml @@ -115,7 +115,7 @@ jobs: - name: Generate GitHub App token id: app-token - if: ${{ secrets.GH_RELEASES_MANAGER_APP_ID != '' }} + continue-on-error: true uses: actions/create-github-app-token@v2 with: app-id: ${{ secrets.GH_RELEASES_MANAGER_APP_ID }} @@ -230,7 +230,7 @@ jobs: - name: Generate GitHub App token id: app-token - if: ${{ secrets.GH_RELEASES_MANAGER_APP_ID != '' }} + continue-on-error: true uses: actions/create-github-app-token@v2 with: app-id: ${{ secrets.GH_RELEASES_MANAGER_APP_ID }} From b53986f00b994060f36808ca18a60903cb8626ef Mon Sep 17 00:00:00 2001 From: Henry Park Date: Mon, 9 Mar 2026 17:35:06 -0700 Subject: [PATCH 2/5] =?UTF-8?q?fix(ci):=20clean=20up=20staging=20pipeline?= =?UTF-8?q?=20=E2=80=94=20remove=20hacks,=20skip=20redundant=20checks=20[s?= =?UTF-8?q?kip-regression-check]=20(#794)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove continue-on-error from staging-ci.yml app token steps (secrets are configured) - Skip test.yml and code_style.yml on PRs targeting staging (staging-ci.yml already runs tests before promoting, promotion PR gets full CI on main) - Allow ironclaw-ci[bot] in Claude Code review for bot-created promotion PRs Co-authored-by: Claude Opus 4.6 --- .github/workflows/claude-review.yml | 1 + .github/workflows/code_style.yml | 2 ++ .github/workflows/staging-ci.yml | 2 -- .github/workflows/test.yml | 2 ++ 4 files changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/workflows/claude-review.yml b/.github/workflows/claude-review.yml index 86d1bb2f4f3..24d2fe98f7e 100644 --- a/.github/workflows/claude-review.yml +++ b/.github/workflows/claude-review.yml @@ -28,6 +28,7 @@ jobs: uses: anthropics/claude-code-action@v1 with: anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} + allowed_bots: "ironclaw-ci[bot]" claude_args: "--max-turns 50 --model claude-haiku-4-5-20251001 --allowedTools 'Bash(gh pr comment:*),Bash(gh pr diff:*),Bash(gh pr view:*),Bash(gh pr list:*),Bash(gh issue view:*),Bash(gh issue list:*),Bash(gh search:*),Bash(git blame:*),Bash(git log:*),Bash(git diff:*)'" prompt: | Code review this pull request. Follow these steps precisely: diff --git a/.github/workflows/code_style.yml b/.github/workflows/code_style.yml index 526c7740813..c65aa0df1e2 100644 --- a/.github/workflows/code_style.yml +++ b/.github/workflows/code_style.yml @@ -1,6 +1,8 @@ name: Code Style on: pull_request: + branches: + - main jobs: format: diff --git a/.github/workflows/staging-ci.yml b/.github/workflows/staging-ci.yml index 22d70030133..8e3693b2ead 100644 --- a/.github/workflows/staging-ci.yml +++ b/.github/workflows/staging-ci.yml @@ -115,7 +115,6 @@ jobs: - name: Generate GitHub App token id: app-token - continue-on-error: true uses: actions/create-github-app-token@v2 with: app-id: ${{ secrets.GH_RELEASES_MANAGER_APP_ID }} @@ -230,7 +229,6 @@ jobs: - name: Generate GitHub App token id: app-token - continue-on-error: true uses: actions/create-github-app-token@v2 with: app-id: ${{ secrets.GH_RELEASES_MANAGER_APP_ID }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index efa286481e7..bb29dd2a665 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -2,6 +2,8 @@ name: Run Tests on: workflow_call: pull_request: + branches: + - main push: branches: - main From 6e12ce6f2db8e6632ff5c95bbd0b8285ba90d4ca Mon Sep 17 00:00:00 2001 From: Henry Park Date: Mon, 9 Mar 2026 18:43:16 -0700 Subject: [PATCH 3/5] fix(ci): run fmt + clippy on staging PRs, skip Windows clippy [skip-regression-check] (#802) - Remove branches:[main] filter from code_style.yml so it runs on all PRs - Gate clippy-windows with `if: github.base_ref == 'main'` (skip on staging PRs) - Update rollup job to allow skipped clippy-windows - Simplify claude-review.yml to only trigger on labeled event (avoids duplicate runs) Co-authored-by: Claude Opus 4.6 --- .github/workflows/claude-review.yml | 2 +- .github/workflows/code_style.yml | 10 +++++++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/.github/workflows/claude-review.yml b/.github/workflows/claude-review.yml index 24d2fe98f7e..3836a5f903e 100644 --- a/.github/workflows/claude-review.yml +++ b/.github/workflows/claude-review.yml @@ -2,7 +2,7 @@ name: Claude Code Review on: pull_request: - types: [opened, labeled] + types: [labeled] permissions: contents: read diff --git a/.github/workflows/code_style.yml b/.github/workflows/code_style.yml index c65aa0df1e2..620760aeca2 100644 --- a/.github/workflows/code_style.yml +++ b/.github/workflows/code_style.yml @@ -1,8 +1,6 @@ name: Code Style on: pull_request: - branches: - - main jobs: format: @@ -46,6 +44,7 @@ jobs: clippy-windows: name: Clippy Windows (${{ matrix.name }}) + if: github.base_ref == 'main' runs-on: windows-latest strategy: fail-fast: false @@ -78,7 +77,12 @@ jobs: needs: [format, clippy, clippy-windows] steps: - run: | - if [[ "${{ needs.format.result }}" != "success" || "${{ needs.clippy.result }}" != "success" || "${{ needs.clippy-windows.result }}" != "success" ]]; then + if [[ "${{ needs.format.result }}" != "success" || "${{ needs.clippy.result }}" != "success" ]]; then echo "One or more jobs failed" exit 1 fi + # clippy-windows only runs on main PRs, so skip/success are both acceptable + if [[ "${{ needs.clippy-windows.result }}" == "failure" ]]; then + echo "Windows clippy failed" + exit 1 + fi From bcef04b82108222c9041e733de459130badd4cd7 Mon Sep 17 00:00:00 2001 From: Illia Polosukhin Date: Tue, 10 Mar 2026 01:51:43 +0000 Subject: [PATCH 4/5] feat: persist user_id in save_job and expose job_id on routine runs (#709) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: persist worker events to DB and fix activity tab rendering In-process Worker (used by Scheduler::dispatch_job) now persists events via save_job_event at key execution points: plan creation, LLM responses, tool_use, tool_result, and job completion/failure/stuck. Event data shapes match the container worker format so the gateway activity tab renders them correctly. Frontend: tool_result errors now show a red X icon with danger styling instead of a silent empty output. The result event falls back to the error field when message is absent. Co-Authored-By: Claude Opus 4.6 * feat: wire RoutineEngine into gateway for direct manual trigger firing Replace the message-channel hack in routines_trigger_handler with a direct call to RoutineEngine::fire_manual(), ensuring FullJob routines dispatch correctly when triggered from the web UI. Inject the engine into GatewayState from Agent::run after construction. Also persists user_id in save_job for both PG and libSQL backends, removes the source='sandbox' filter so all jobs are visible, and exposes job_id on RoutineRunInfo for the frontend job link. Co-Authored-By: Claude Opus 4.6 * fix: remove stale gateway_state argument from Agent::new test call sites The gateway_state parameter was removed from Agent::new during rebase (replaced by post-construction set_routine_engine_slot), but three test call sites still passed the extra None argument. Co-Authored-By: Claude Opus 4.6 * fix: address PR review — restore sandbox source filter, remove blank lines - Revert removal of `source = 'sandbox'` filter in all SandboxStore queries (8 sites across PG and libSQL). Sandbox-specific APIs should stay scoped to sandbox jobs; unified job listing for the Jobs tab should use a separate query path. - Remove extra blank lines in agent_loop.rs and worker.rs that caused formatting CI failure. [skip-regression-check] Co-Authored-By: Claude Opus 4.6 * fix: address review — regenerate Cargo.lock, add user_id regression test - Regenerate Cargo.lock from main's lockfile to eliminate dependency version downgrades (anyhow, syn, etc.) that were churn from rebase. - Add regression test verifying user_id round-trips through save_job and get_job in the libSQL backend. Co-Authored-By: Claude Opus 4.6 * style: remove trailing blank line in libsql jobs.rs [skip-regression-check] Co-Authored-By: Claude Opus 4.6 * test: add Postgres-side regression test for user_id persistence in save_job Mirrors the existing libSQL test (test_save_job_persists_user_id) for the Postgres backend. Gated behind #[cfg(feature = "postgres")] + #[ignore] since it requires a running PostgreSQL instance (integration tier). Co-Authored-By: Claude Opus 4.6 --------- Co-authored-by: Claude Opus 4.6 --- src/channels/web/handlers/routines.rs | 2 ++ src/channels/web/server.rs | 2 ++ src/channels/web/types.rs | 1 + src/db/libsql/jobs.rs | 5 +++- src/db/libsql/mod.rs | 18 +++++++++++++ src/history/store.rs | 37 ++++++++++++++++++++++++++- 6 files changed, 63 insertions(+), 2 deletions(-) diff --git a/src/channels/web/handlers/routines.rs b/src/channels/web/handlers/routines.rs index d7c4f76400a..8fbcc97bc3e 100644 --- a/src/channels/web/handlers/routines.rs +++ b/src/channels/web/handlers/routines.rs @@ -108,6 +108,7 @@ pub async fn routines_detail_handler( status: format!("{:?}", run.status), result_summary: run.result_summary.clone(), tokens_used: run.tokens_used, + job_id: run.job_id, }) .collect(); @@ -252,6 +253,7 @@ pub async fn routines_runs_handler( status: format!("{:?}", run.status), result_summary: run.result_summary.clone(), tokens_used: run.tokens_used, + job_id: run.job_id, }) .collect(); diff --git a/src/channels/web/server.rs b/src/channels/web/server.rs index d6605eee650..f454363d3eb 100644 --- a/src/channels/web/server.rs +++ b/src/channels/web/server.rs @@ -2017,6 +2017,7 @@ async fn routines_detail_handler( status: format!("{:?}", run.status), result_summary: run.result_summary.clone(), tokens_used: run.tokens_used, + job_id: run.job_id, }) .collect(); @@ -2169,6 +2170,7 @@ async fn routines_runs_handler( status: format!("{:?}", run.status), result_summary: run.result_summary.clone(), tokens_used: run.tokens_used, + job_id: run.job_id, }) .collect(); diff --git a/src/channels/web/types.rs b/src/channels/web/types.rs index 4d85c671706..b6d0d05a7d2 100644 --- a/src/channels/web/types.rs +++ b/src/channels/web/types.rs @@ -776,6 +776,7 @@ pub struct RoutineRunInfo { pub status: String, pub result_summary: Option, pub tokens_used: Option, + pub job_id: Option, } // --- Settings --- diff --git a/src/db/libsql/jobs.rs b/src/db/libsql/jobs.rs index d51723604d9..0750873d6d8 100644 --- a/src/db/libsql/jobs.rs +++ b/src/db/libsql/jobs.rs @@ -28,14 +28,16 @@ impl JobStore for LibSqlBackend { r#" INSERT INTO agent_jobs ( id, conversation_id, title, description, category, status, source, + user_id, budget_amount, budget_token, bid_amount, estimated_cost, estimated_time_secs, actual_cost, repair_attempts, created_at, started_at, completed_at - ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15, ?16, ?17) + ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15, ?16, ?17, ?18) ON CONFLICT (id) DO UPDATE SET title = excluded.title, description = excluded.description, category = excluded.category, status = excluded.status, + user_id = excluded.user_id, estimated_cost = excluded.estimated_cost, estimated_time_secs = excluded.estimated_time_secs, actual_cost = excluded.actual_cost, @@ -51,6 +53,7 @@ impl JobStore for LibSqlBackend { opt_text(ctx.category.as_deref()), status, "direct", + ctx.user_id.as_str(), opt_text_owned(ctx.budget.map(|d| d.to_string())), opt_text(ctx.budget_token.as_deref()), opt_text_owned(ctx.bid_amount.map(|d| d.to_string())), diff --git a/src/db/libsql/mod.rs b/src/db/libsql/mod.rs index 2845c757cdb..404441e67f0 100644 --- a/src/db/libsql/mod.rs +++ b/src/db/libsql/mod.rs @@ -482,6 +482,24 @@ mod tests { assert_eq!(timeout, 5000); } + /// Regression test: save_job must persist user_id and get_job must return it. + #[tokio::test] + async fn test_save_job_persists_user_id() { + use crate::context::JobContext; + use crate::db::JobStore; + + let dir = tempfile::tempdir().unwrap(); + let db_path = dir.path().join("test_user_id.db"); + let backend = LibSqlBackend::new_local(&db_path).await.unwrap(); + backend.run_migrations().await.unwrap(); + + let ctx = JobContext::with_user("test-user-42", "Test Job", "A test job"); + backend.save_job(&ctx).await.unwrap(); + + let loaded = backend.get_job(ctx.job_id).await.unwrap().unwrap(); + assert_eq!(loaded.user_id, "test-user-42"); + } + #[tokio::test] async fn test_concurrent_writes_succeed() { // Use a temp file so connections share state (in-memory DBs are connection-local) diff --git a/src/history/store.rs b/src/history/store.rs index f0b0b144148..1153f3e40ff 100644 --- a/src/history/store.rs +++ b/src/history/store.rs @@ -149,14 +149,16 @@ impl Store { r#" INSERT INTO agent_jobs ( id, conversation_id, title, description, category, status, source, + user_id, budget_amount, budget_token, bid_amount, estimated_cost, estimated_time_secs, actual_cost, repair_attempts, created_at, started_at, completed_at - ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17) + ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18) ON CONFLICT (id) DO UPDATE SET title = EXCLUDED.title, description = EXCLUDED.description, category = EXCLUDED.category, status = EXCLUDED.status, + user_id = EXCLUDED.user_id, estimated_cost = EXCLUDED.estimated_cost, estimated_time_secs = EXCLUDED.estimated_time_secs, actual_cost = EXCLUDED.actual_cost, @@ -172,6 +174,7 @@ impl Store { &ctx.category, &status, &"direct", // source + &ctx.user_id, &ctx.budget, &ctx.budget_token, &ctx.bid_amount, @@ -2133,4 +2136,36 @@ mod tests { assert_eq!(summary.channel, ch); } } + + /// Regression test: save_job must persist user_id and get_job must return it. + /// Requires a running PostgreSQL instance (integration tier). + #[cfg(feature = "postgres")] + #[tokio::test] + #[ignore] + async fn test_save_job_persists_user_id() { + use crate::config::Config; + use crate::context::JobContext; + + let _ = dotenvy::dotenv(); + let config = Config::from_env().await.expect("Failed to load config"); + let store = Store::new(&config.database) + .await + .expect("Failed to connect to database"); + store + .run_migrations() + .await + .expect("Failed to run migrations"); + + let ctx = JobContext::with_user("test-user-42", "PG user_id test", "regression test"); + store.save_job(&ctx).await.unwrap(); + + let loaded = store.get_job(ctx.job_id).await.unwrap().unwrap(); + assert_eq!(loaded.user_id, "test-user-42"); + + // Clean up + let conn = store.conn().await.unwrap(); + conn.execute("DELETE FROM agent_jobs WHERE id = $1", &[&ctx.job_id]) + .await + .unwrap(); + } } From 715410b7b61ce1819e99ec415f2b3a9d83fefdf2 Mon Sep 17 00:00:00 2001 From: reidliu41 Date: Tue, 10 Mar 2026 11:02:32 +0800 Subject: [PATCH 5/5] feat(agent): add context size logging before LLM prompt --- FEATURE_PARITY.md | 2 +- src/agent/dispatcher.rs | 22 ++++++++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/FEATURE_PARITY.md b/FEATURE_PARITY.md index d6336e90346..ec0e3a143af 100644 --- a/FEATURE_PARITY.md +++ b/FEATURE_PARITY.md @@ -51,7 +51,7 @@ This document tracks feature parity between IronClaw (Rust implementation) and O | Trusted-proxy auth mode | ✅ | ❌ | Header-based auth for reverse proxies | | APNs push pipeline | ✅ | ❌ | Wake disconnected iOS nodes via push | | Oversized payload guard | ✅ | 🚧 | HTTP webhook has 64KB body limit + Content-Length check; no chat.history cap | -| Pre-prompt context diagnostics | ✅ | ❌ | Context size logging before prompt | +| Pre-prompt context diagnostics | ✅ | 🚧 | Token breakdown logged before LLM call (conversational dispatcher path); other LLM entry points not yet covered | ### Owner: _Unassigned_ diff --git a/src/agent/dispatcher.rs b/src/agent/dispatcher.rs index f53066447db..4b64b517db3 100644 --- a/src/agent/dispatcher.rs +++ b/src/agent/dispatcher.rs @@ -10,6 +10,7 @@ use tokio::task::JoinSet; use uuid::Uuid; use crate::agent::Agent; +use crate::agent::context_monitor::{ContextBreakdown, estimate_text_tokens}; use crate::agent::session::{PendingApproval, Session, ThreadState}; use crate::channels::{IncomingMessage, StatusUpdate}; use crate::context::JobContext; @@ -254,6 +255,27 @@ impl Agent { ); } + // Pre-prompt context diagnostics: log token breakdown before LLM call + { + let breakdown = ContextBreakdown::analyze(&context_messages); + let system_prompt_tokens = + estimate_text_tokens(context.system_prompt.as_deref().unwrap_or("")); + let total_tokens = breakdown.total_tokens + system_prompt_tokens; + tracing::debug!( + iteration, + messages = breakdown.message_count, + total_tokens, + system_prompt_tokens, + system_msg_tokens = breakdown.system_tokens, + user_tokens = breakdown.user_tokens, + assistant_tokens = breakdown.assistant_tokens, + tool_tokens = breakdown.tool_tokens, + tools_available = context.available_tools.len(), + force_text, + "Pre-prompt context diagnostics" + ); + } + let _ = self .channels .send_status(