Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
108e761
feat(artifact): export durable message timestamps in run artifacts
henrypark133 Aug 18, 2026
fd36b8b
refactor(artifact): add timing projection types for run artifacts
henrypark133 Aug 18, 2026
aeadd6a
refactor(artifact): narrow the timings re-export to types only
henrypark133 Aug 18, 2026
834d6fe
refactor(artifact): read run timings from the diagnostic store
henrypark133 Aug 18, 2026
d02dfb2
fix(artifact): correct stale line citation in timings_source debug co…
henrypark133 Aug 18, 2026
1a64a68
feat(artifact): attach run timings to downloadable artifacts
henrypark133 Aug 18, 2026
a8ab820
fix(artifact): scope per-run wall-clock timings to their own run
henrypark133 Aug 18, 2026
a16b025
test(integration): share one diagnostic store between loop and services
henrypark133 Aug 18, 2026
6aed043
test(integration): cover run artifact timings end to end
henrypark133 Aug 18, 2026
cabe72e
Address PR review feedback (#7735)
henrypark133 Aug 18, 2026
a48c45c
Fix CI failures for run artifact timings
henrypark133 Aug 18, 2026
736b46d
Match rustfmt for timing iterator
henrypark133 Aug 18, 2026
44b71ae
Fix clippy warning in timing grouping
henrypark133 Aug 18, 2026
276a15e
Match rustfmt for timing grouping
henrypark133 Aug 18, 2026
084bf24
Address follow-up timing review findings
henrypark133 Aug 18, 2026
56b5663
Match rustfmt for typed run fixture
henrypark133 Aug 19, 2026
7c3ec04
Fix clippy borrow in timing export
henrypark133 Aug 19, 2026
b196213
Preserve unavailable timing entries
henrypark133 Aug 19, 2026
be75b50
Cover every thread timing entry
henrypark133 Aug 19, 2026
2647025
Fix typed run keys in timing test
henrypark133 Aug 19, 2026
c1cb2f3
Chart thread timing grouping helper
henrypark133 Aug 19, 2026
d1dec57
Stabilize recurring trigger replay test
henrypark133 Aug 19, 2026
8ca38cd
fix(artifact): satisfy clippy in timing tests
henrypark133 Aug 19, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -506,6 +506,10 @@ path = "tests/integration/webui_v2_router_smoke.rs"
name = "reborn_integration_wiring_parity"
path = "tests/integration/wiring_parity.rs"

[[test]]
name = "reborn_integration_run_artifact_timings"
path = "tests/integration/run_artifact_timings.rs"

[[test]]
name = "reborn_integration_db_write_canonical"
path = "tests/integration/db_write_canonical.rs"
Expand Down
2 changes: 1 addition & 1 deletion crates/product/ironclaw_assistant/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -338,7 +338,7 @@ by file.
| `commands` | The product command grammar surface: listing declared commands, executing one, and the capability handler dispatch table | A command's *effect* — each handler delegates to the concern that owns it | `mod product_capability_handlers`, `mod product_commands`, `PRODUCT_COMMAND_LIST_COMMAND_ID`, `PRODUCT_COMMAND_LIST_COMMAND`, `PRODUCT_COMMAND_EXECUTE_COMMAND_ID`, `PRODUCT_COMMAND_EXECUTE_COMMAND`, `command_result_field`, `model_command_view`, `user_model_preference_command_view`, `idle_status_command_view`, `nothing_to_stop_command_view`, `new_conversation_started_view`, `reborn_services/types.rs::RebornProductCommandEffect`, `product_command_input`, `reborn_services/product_capability_handlers.rs::ProductCommandHandler`, `reborn_services/product_capability_handlers.rs::command_output`, `reborn_services/product_capability_handlers.rs::ProductCapabilityHandler`, `reborn_services/product_commands.rs::lifecycle_command_title`, `reborn_services/product_commands.rs::lifecycle_command_view`, `reborn_services/product_commands.rs::lifecycle_rows_view`, `reborn_services/product_commands.rs::lifecycle_confirmation_view`, `reborn_services/product_commands.rs::package_ref_fields`, `reborn_services/product_commands.rs::package_kind_label`, `reborn_services/product_commands.rs::blocker_line`, `reborn_services/product_commands.rs::capability_lines`, `reborn_services/product_commands.rs::extension_row`, `reborn_services/product_commands.rs::skill_row`, `reborn_services/product_commands.rs::yes_no`, `reborn_services/types.rs::RebornExecuteProductCommandResponse` |
| `admin-users` | Admin user CRUD, role/status, per-user secrets, and last-admin protection | Authorization of the caller — that is `dispatch`'s view gate | `mod admin_users`, `ADMIN_USER_UPDATE_CAPABILITY_ID`, `ADMIN_USER_UPDATE_CAPABILITY`, `ADMIN_USER_SET_STATUS_CAPABILITY_ID`, `ADMIN_USER_SET_STATUS_CAPABILITY`, `ADMIN_USER_SET_ROLE_CAPABILITY_ID`, `ADMIN_USER_SET_ROLE_CAPABILITY`, `ADMIN_USER_DELETE_CAPABILITY_ID`, `ADMIN_USER_DELETE_CAPABILITY`, `ADMIN_USER_PUT_SECRET_CAPABILITY_ID`, `ADMIN_USER_PUT_SECRET_CAPABILITY`, `ADMIN_USER_DELETE_SECRET_CAPABILITY_ID`, `ADMIN_USER_DELETE_SECRET_CAPABILITY`, `ADMIN_USER_CREATE_COMMAND_ID`, `ADMIN_USER_CREATE_COMMAND`, `ADMIN_USER_DELETE_SECRET_COMMAND_ID`, `ADMIN_USER_DELETE_SECRET_COMMAND`, `ADMIN_USERS_VIEW`, `ADMIN_USER_VIEW`, `ADMIN_USER_SECRETS_VIEW`, `map_admin_user_error`, `last_admin_error`, `reborn_services/admin_users.rs::RejectingAdminUserService` |
| `dispatch` | The `RebornServices` object itself, the caller/scope plumbing every concern shares (`ProductAgentBoundCaller`, resource scope, secret handles), the `ProductSurfaceError` shaping helpers, and the view-authorization gate | A concern's own logic — an item belongs here only when **more than one** sub-owner calls it | `mod views`, `ProductAgentBoundCaller`, `caller_resource_scope`, `product_view_forbidden`, `authorize_product_view`, `ProductCapabilityInvoker`, `UnavailableProductCapabilityInvoker`, `RebornServices`, `product_capability_input_error`, `product_secret_handle`, `segment`, `map_adapter_error`, `code_for_status`, `kind_for_surface_rejection`, `truncate_utf8_to_bytes`, `product_agent_bound_caller_from_webui`, `reborn_services/views.rs::EmptyViewParams`, `reborn_services/views.rs::parse_empty_view_params`, `reborn_services/views.rs::required_string_view_param`, `reborn_services/views.rs::view_page`, `reborn_services/views.rs::view_page_with_cursor`, `reborn_services/views.rs::UnavailableRebornViewProvider` |
| `run-artifact` | Run and thread artifact export views | Live run state — that is `runs` | `mod run_artifact`, `mod thread_artifact`, `ADMIN_THREAD_SCRAPE_THREADS_VIEW`, `ADMIN_THREAD_SCRAPE_ARTIFACT_VIEW`, `ADMIN_THREAD_SCRAPE_RUN_ARTIFACT_VIEW`, `reborn_services/run_artifact.rs::RUN_ARTIFACT_SCHEMA`, `reborn_services/run_artifact.rs::RUN_ARTIFACT_VIEW`, `reborn_services/run_artifact.rs::ARTIFACT_REDACTION_PIPELINE`, `reborn_services/run_artifact.rs::RebornRunArtifact`, `reborn_services/run_artifact.rs::RunArtifactMessage`, `reborn_services/run_artifact.rs::RunArtifactToolCall`, `reborn_services/run_artifact.rs::RunArtifactLogs`, `reborn_services/run_artifact.rs::RunArtifactRedaction`, `reborn_services/run_artifact.rs::context_messages_by_id`, `reborn_services/run_artifact.rs::artifact_messages`, `reborn_services/run_artifact.rs::redact_text`, `reborn_services/run_artifact.rs::redact_json`, `reborn_services/run_artifact.rs::redact_json_strings`, `reborn_services/thread_artifact.rs::THREAD_ARTIFACT_SCHEMA`, `reborn_services/thread_artifact.rs::THREAD_ARTIFACT_MAX_MESSAGES`, `reborn_services/thread_artifact.rs::THREAD_ARTIFACT_MAX_STORED_BYTES`, `reborn_services/thread_artifact.rs::THREAD_ARTIFACT_MAX_SERIALIZED_BYTES`, `reborn_services/thread_artifact.rs::THREAD_ARTIFACT_VIEW`, `reborn_services/thread_artifact.rs::RebornThreadArtifact`, `reborn_services/thread_artifact.rs::thread_artifact_too_large` |
| `run-artifact` | Run and thread artifact export views | Live run state — that is `runs` | `mod run_artifact`, `mod thread_artifact`, `mod timings_source`, `ADMIN_THREAD_SCRAPE_THREADS_VIEW`, `ADMIN_THREAD_SCRAPE_ARTIFACT_VIEW`, `ADMIN_THREAD_SCRAPE_RUN_ARTIFACT_VIEW`, `reborn_services/run_artifact.rs::RUN_ARTIFACT_SCHEMA`, `reborn_services/run_artifact.rs::RUN_ARTIFACT_VIEW`, `reborn_services/run_artifact.rs::ARTIFACT_REDACTION_PIPELINE`, `reborn_services/run_artifact.rs::RebornRunArtifact`, `reborn_services/run_artifact.rs::RunArtifactMessage`, `reborn_services/run_artifact.rs::RunArtifactToolCall`, `reborn_services/run_artifact.rs::RunArtifactLogs`, `reborn_services/run_artifact.rs::RunArtifactRedaction`, `reborn_services/run_artifact.rs::mod timings`, `reborn_services/run_artifact.rs::context_messages_by_id`, `reborn_services/run_artifact.rs::artifact_messages`, `reborn_services/run_artifact.rs::redact_text`, `reborn_services/run_artifact.rs::redact_json`, `reborn_services/run_artifact.rs::redact_json_strings`, `reborn_services/thread_artifact.rs::THREAD_ARTIFACT_SCHEMA`, `reborn_services/thread_artifact.rs::THREAD_ARTIFACT_MAX_MESSAGES`, `reborn_services/thread_artifact.rs::THREAD_ARTIFACT_MAX_STORED_BYTES`, `reborn_services/thread_artifact.rs::THREAD_ARTIFACT_MAX_SERIALIZED_BYTES`, `reborn_services/thread_artifact.rs::THREAD_ARTIFACT_VIEW`, `reborn_services/thread_artifact.rs::RebornThreadArtifact`, `reborn_services/thread_artifact.rs::RunArtifactRunTimings`, `reborn_services/thread_artifact.rs::group_messages_by_run`, `reborn_services/thread_artifact.rs::thread_artifact_too_large`, `reborn_services/timings_source.rs::derive_wall_clock_ms` |
| `skills` | Skill install/update/remove, search and content reads, auto-activation settings, and the activation recorder/clearer ports | Skill *selection* — that is `ironclaw_skills` | `SkillActivationRecorder`, `SkillActivationClearer`, `SKILL_INSTALL_CAPABILITY_ID`, `SKILL_INSTALL_CAPABILITY`, `SKILL_UPDATE_CAPABILITY_ID`, `SKILL_UPDATE_CAPABILITY`, `SKILL_REMOVE_CAPABILITY_ID`, `SKILL_REMOVE_CAPABILITY`, `SKILL_AUTO_ACTIVATE_SET_CAPABILITY_ID`, `SKILL_AUTO_ACTIVATE_SET_CAPABILITY`, `SKILL_AUTO_ACTIVATE_LEARNED_SET_CAPABILITY_ID`, `SKILL_AUTO_ACTIVATE_LEARNED_SET_CAPABILITY`, `SKILLS_VIEW`, `SKILL_SEARCH_VIEW`, `SKILL_CONTENT_VIEW`, `SkillsProductService`, `UnsupportedSkillsProductService` |
| `gates` | Approval and auth gate routing: which resolver a `gate_ref` reaches, stale/attacker-supplied ref rejection, and the blocked-gate notices | The approval or auth *policy* — that lives in `approval_interaction` / `auth_interaction`, not in this module | `mod approval_settings`, `RESOLVE_GATE_COMMAND_ID`, `RESOLVE_GATE_COMMAND`, `NOTICE_BLOCKED_APPROVAL`, `NOTICE_BLOCKED_AUTH`, `GateResolutionRoute`, `validate_current_gate_ref`, `participant_denied`, `assert_generic_run_parked_on_gate`, `reject_generic_auth_gate_resolution`, `persistent_approval_unavailable`, `blocked_approval_unavailable`, `blocked_authentication_unavailable`, `map_auth_interaction_error`, `reborn_services/types.rs::reborn_resume_gate_response` |
| `traces` | Trace credits, trace holds, and the trace account login link | Trace *content* — that is `ironclaw_trace_commons` | `mod trace_credits`, `TRACE_ACCOUNT_LOGIN_LINK_COMMAND_ID`, `TRACE_ACCOUNT_LOGIN_LINK_COMMAND`, `TRACE_HOLD_AUTHORIZE_COMMAND_ID`, `TRACE_HOLD_AUTHORIZE_COMMAND`, `reborn_services/trace_credits.rs::TRACE_CREDITS_VIEW`, `reborn_services/trace_credits.rs::TRACE_ACCOUNT_TRACES_VIEW`, `reborn_services/trace_credits.rs::TRACE_CREDITS_NOTE`, `reborn_services/trace_credits.rs::AccountLoginLinkMintError`, `reborn_services/trace_credits.rs::account_login_link_for_user`, `reborn_services/trace_credits.rs::AccountTracesError`, `reborn_services/trace_credits.rs::account_traces_for_user`, `reborn_services/trace_credits.rs::authorize_trace_hold_for_user`, `reborn_services/trace_credits.rs::local_trace_credits_for_user` |
Expand Down
153 changes: 153 additions & 0 deletions crates/product/ironclaw_assistant/src/inspector_store.rs
Original file line number Diff line number Diff line change
Expand Up @@ -349,6 +349,77 @@ pub struct InMemoryDiagnosticStore {
state: RwLock<DiagnosticStoreState>,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DiagnosticTimingModelCall {
pub call_id: DiagnosticModelCallId,
pub iteration: u32,
pub requested_model: BoundedDiagnosticText,
pub effective_model: Option<BoundedDiagnosticText>,
pub started_at: chrono::DateTime<Utc>,
pub completed_at: Option<chrono::DateTime<Utc>>,
pub duration_ms: Option<u64>,
pub status: InspectorModelCallStatus,
}

impl From<&ModelCallDiagnostic> for DiagnosticTimingModelCall {
fn from(call: &ModelCallDiagnostic) -> Self {
Self {
call_id: call.call_id,
iteration: call.iteration,
requested_model: call.requested_model.clone(),
effective_model: call.effective_model.clone(),
started_at: call.started_at,
completed_at: call.completed_at,
duration_ms: call.duration_ms,
status: call.status,
}
}
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DiagnosticTimingToolExecution {
pub model_call_id: Option<DiagnosticModelCallId>,
pub capability_name: BoundedDiagnosticText,
pub status: ToolExecutionStatus,
pub duration_ms: Option<u64>,
}

impl From<&ToolExecutionDiagnostic> for DiagnosticTimingToolExecution {
fn from(tool: &ToolExecutionDiagnostic) -> Self {
Self {
model_call_id: tool.model_call_id,
capability_name: tool.capability_name.clone(),
status: tool.status,
duration_ms: tool.duration_ms,
}
}
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DiagnosticTimingSnapshot {
pub model_calls: Vec<DiagnosticTimingModelCall>,
pub tool_executions: Vec<DiagnosticTimingToolExecution>,
pub stats: SessionDiagnosticStats,
}

impl From<DiagnosticSnapshot> for DiagnosticTimingSnapshot {
fn from(snapshot: DiagnosticSnapshot) -> Self {
Self {
model_calls: snapshot
.model_calls
.iter()
.map(DiagnosticTimingModelCall::from)
.collect(),
tool_executions: snapshot
.tool_executions
.iter()
.map(DiagnosticTimingToolExecution::from)
.collect(),
stats: snapshot.stats,
}
}
}

/// Operator inspection store surface exposed by product composition.
///
/// Capture remains behind [`HostManagedPromptDiagnosticSink`]; consumers of
Expand All @@ -365,6 +436,17 @@ pub trait DiagnosticStorePort: Send + Sync {
scope: &DiagnosticScope,
) -> Result<Option<DiagnosticSnapshot>, DiagnosticStoreError>;

/// Read only the data needed by timing projections. Implementations with
/// larger snapshots should override this to avoid cloning prompt and
/// activity payloads under the store lock.
fn timing_snapshot(
&self,
scope: &DiagnosticScope,
) -> Result<Option<DiagnosticTimingSnapshot>, DiagnosticStoreError> {
self.snapshot(scope)
.map(|snapshot| snapshot.map(DiagnosticTimingSnapshot::from))
}

fn prompt(
&self,
scope: &DiagnosticScope,
Expand Down Expand Up @@ -592,6 +674,32 @@ impl InMemoryDiagnosticStore {
}))
}

pub fn timing_snapshot(
&self,
scope: &DiagnosticScope,
) -> Result<Option<DiagnosticTimingSnapshot>, DiagnosticStoreError> {
let state = self
.state
.read()
.map_err(|_| DiagnosticStoreError::StateUnavailable)?;
let Some(run) = state.run(scope) else {
return Ok(None);
};
Ok(Some(DiagnosticTimingSnapshot {
model_calls: run
.model_calls
.iter()
.map(DiagnosticTimingModelCall::from)
.collect(),
tool_executions: run
.tool_executions
.iter()
.map(DiagnosticTimingToolExecution::from)
.collect(),
stats: run.stats.clone(),
}))
}

pub fn prompt(
&self,
scope: &DiagnosticScope,
Expand Down Expand Up @@ -884,6 +992,13 @@ impl DiagnosticStorePort for InMemoryDiagnosticStore {
InMemoryDiagnosticStore::snapshot(self, scope)
}

fn timing_snapshot(
&self,
scope: &DiagnosticScope,
) -> Result<Option<DiagnosticTimingSnapshot>, DiagnosticStoreError> {
InMemoryDiagnosticStore::timing_snapshot(self, scope)
}

fn prompt(
&self,
scope: &DiagnosticScope,
Expand Down Expand Up @@ -2531,6 +2646,44 @@ mod tests {
);
}

#[test]
fn timing_snapshot_omits_prompt_and_activity_payloads() {
let store = InMemoryDiagnosticStore::default();
let scope = scope("tenant", "user", "thread", TurnRunId::new());
let call_id = DiagnosticModelCallId::new();
store
.record_model_call(
scope.clone(),
ModelCallDiagnostic::new(
call_id,
1,
"requested-model",
None,
Utc::now(),
None,
None,
InspectorModelCallStatus::Started,
None,
None,
),
)
.expect("model call");
store
.record_activity(scope.clone(), activity("not part of timings"))
.expect("activity");

let full = store.snapshot(&scope).expect("full snapshot").expect("run");
assert_eq!(full.activity.len(), 1);

let timings = store
.timing_snapshot(&scope)
.expect("timing snapshot")
.expect("run");
assert_eq!(timings.model_calls.len(), 1);
assert!(timings.tool_executions.is_empty());
assert_eq!(timings.stats.total_model_calls, 1);
}

#[test]
fn session_eviction_is_deterministic_and_write_lru() {
let store = InMemoryDiagnosticStore::new(tiny_limits()).expect("store");
Expand Down
5 changes: 4 additions & 1 deletion crates/product/ironclaw_assistant/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,9 @@ pub use run_delivery::{
// Adapter, projection, and event DTOs are re-exported from
// `ironclaw_host_api::product_adapter` above so product terminals consume a
// single product service.
pub use reborn_services::run_artifact::timings::{
RunArtifactIterationTiming, RunArtifactTimingTotals, RunArtifactTimings, RunArtifactToolTiming,
};
pub use reborn_services::{
ADMIN_CONFIGURATION_REPLACE_CAPABILITY, ADMIN_CONFIGURATION_REPLACE_CAPABILITY_ID,
ADMIN_CONFIGURATION_VIEW, ADMIN_THREAD_SCRAPE_ARTIFACT_VIEW,
Expand Down Expand Up @@ -370,7 +373,7 @@ pub use reborn_services::{
RebornTimelineResponse, RebornTraceCreditsResponse, RebornTraceHoldAuthorizeProductRequest,
RebornTraceHoldAuthorizeResponse, RebornUpdateMemberRoleRequest, RebornUpdateProjectRequest,
RebornVendorAuthAccounts, RegistrationChannelNotificationSetupService, RunArtifactLogs,
RunArtifactMessage, RunArtifactRedaction, RunArtifactToolCall,
RunArtifactMessage, RunArtifactRedaction, RunArtifactRunTimings, RunArtifactToolCall,
SKILL_AUTO_ACTIVATE_LEARNED_SET_CAPABILITY, SKILL_AUTO_ACTIVATE_LEARNED_SET_CAPABILITY_ID,
SKILL_AUTO_ACTIVATE_SET_CAPABILITY, SKILL_AUTO_ACTIVATE_SET_CAPABILITY_ID, SKILL_CONTENT_VIEW,
SKILL_INSTALL_CAPABILITY, SKILL_INSTALL_CAPABILITY_ID, SKILL_REMOVE_CAPABILITY,
Expand Down
10 changes: 7 additions & 3 deletions crates/product/ironclaw_assistant/src/reborn_services.rs
Original file line number Diff line number Diff line change
Expand Up @@ -148,8 +148,12 @@ mod product_capability_handlers;
mod product_commands;
mod project_fs;
mod projects;
mod run_artifact;
// pub(crate): lib.rs re-exports `reborn_services::run_artifact::timings`
// directly, which needs this segment of the path visible crate-wide; the
// module's own contents stay unexported except through that re-export.
pub(crate) mod run_artifact;
mod thread_artifact;
mod timings_source;
mod trace_credits;
mod types;
mod views;
Expand Down Expand Up @@ -303,8 +307,8 @@ pub use run_artifact::{
RunArtifactLogs, RunArtifactMessage, RunArtifactRedaction, RunArtifactToolCall,
};
pub use thread_artifact::{
RebornThreadArtifact, RebornThreadArtifactRequest, THREAD_ARTIFACT_MAX_MESSAGES,
THREAD_ARTIFACT_SCHEMA, THREAD_ARTIFACT_VIEW,
RebornThreadArtifact, RebornThreadArtifactRequest, RunArtifactRunTimings,
THREAD_ARTIFACT_MAX_MESSAGES, THREAD_ARTIFACT_SCHEMA, THREAD_ARTIFACT_VIEW,
};
pub use types::{
RebornAuthAccount, RebornCreateThreadResponse, RebornExecuteProductCommandResponse,
Expand Down
Loading
Loading