Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
20 changes: 20 additions & 0 deletions .claude/settings.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

34 changes: 17 additions & 17 deletions .codex/hooks.json
Original file line number Diff line number Diff line change
@@ -1,46 +1,46 @@
{
"hooks": {
"UserPromptSubmit": [
"PostToolUse": [
{
"hooks": [
{
"type": "command",
"command": "root=\"$(git rev-parse --show-toplevel 2>/dev/null)\" || exit 0; exec bash \"$root/.codex/hooks/run-sce-or-show-install-guidance.sh\" sce hooks codex"
"command": "root=\"$(git rev-parse --show-toplevel 2>/dev/null)\" || exit 0; exec bash \"$root/.codex/hooks/run-sce-or-show-install-guidance.sh\" sce hooks codex",
"type": "command"
}
]
],
"matcher": "apply_patch"
}
],
"Stop": [
"PreToolUse": [
{
"hooks": [
{
"type": "command",
"command": "root=\"$(git rev-parse --show-toplevel 2>/dev/null)\" || exit 0; exec bash \"$root/.codex/hooks/run-sce-or-show-install-guidance.sh\" sce hooks codex"
"command": "root=\"$(git rev-parse --show-toplevel 2>/dev/null)\" || exit 0; exec bash \"$root/.codex/hooks/run-sce-or-show-install-guidance.sh\" sce hooks codex",
"type": "command"
}
]
],
"matcher": "Bash"
}
],
"PreToolUse": [
"Stop": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "root=\"$(git rev-parse --show-toplevel 2>/dev/null)\" || exit 0; exec bash \"$root/.codex/hooks/run-sce-or-show-install-guidance.sh\" sce hooks codex"
"command": "root=\"$(git rev-parse --show-toplevel 2>/dev/null)\" || exit 0; exec bash \"$root/.codex/hooks/run-sce-or-show-install-guidance.sh\" sce hooks codex",
"type": "command"
}
]
}
],
"PostToolUse": [
"UserPromptSubmit": [
{
"matcher": "apply_patch",
"hooks": [
{
"type": "command",
"command": "root=\"$(git rev-parse --show-toplevel 2>/dev/null)\" || exit 0; exec bash \"$root/.codex/hooks/run-sce-or-show-install-guidance.sh\" sce hooks codex"
"command": "root=\"$(git rev-parse --show-toplevel 2>/dev/null)\" || exit 0; exec bash \"$root/.codex/hooks/run-sce-or-show-install-guidance.sh\" sce hooks codex",
"type": "command"
}
]
}
]
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
CREATE TABLE IF NOT EXISTS claude_model_state (
session_id TEXT NOT NULL,
agent_id TEXT NOT NULL,
model_id TEXT NOT NULL,
observation_kind TEXT NOT NULL CHECK (observation_kind IN ('session_start', 'post_model_switch')),
source TEXT NOT NULL,
observed_at_ms INTEGER NOT NULL CHECK (observed_at_ms >= 0),
PRIMARY KEY (session_id, agent_id)
);
3 changes: 3 additions & 0 deletions cli/src/cli_schema.rs
Original file line number Diff line number Diff line change
Expand Up @@ -323,6 +323,9 @@ pub enum HooksSubcommand {

#[command(about = "Run Codex hook (reads JSON payload from STDIN)")]
Codex,

#[command(about = "Run Claude model-state hook (reads JSON payload from STDIN)")]
ClaudeModelState,
}

#[derive(Subcommand, Debug, Clone, PartialEq, Eq)]
Expand Down
134 changes: 134 additions & 0 deletions cli/src/services/agent_trace_db/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,49 @@ VALUES (?1, ?2, ?3, ?4, ?5)";
const SELECT_MESSAGE_EXISTS_SQL: &str =
"SELECT 1 FROM messages WHERE session_id = ?1 AND message_id = ?2 LIMIT 1";

pub const UPSERT_CLAUDE_MODEL_STATE_SQL: &str = "INSERT INTO claude_model_state (
session_id,
agent_id,
model_id,
observation_kind,
source,
observed_at_ms
) VALUES (?1, ?2, ?3, ?4, ?5, ?6)
ON CONFLICT (session_id, agent_id) DO UPDATE SET
model_id = excluded.model_id,
observation_kind = excluded.observation_kind,
source = excluded.source,
observed_at_ms = excluded.observed_at_ms
WHERE excluded.observed_at_ms > claude_model_state.observed_at_ms
OR (
excluded.observed_at_ms = claude_model_state.observed_at_ms
AND (
(excluded.observation_kind = 'post_model_switch'
AND claude_model_state.observation_kind = 'session_start')
OR (
excluded.observation_kind = claude_model_state.observation_kind
AND (
excluded.model_id > claude_model_state.model_id
OR (
excluded.model_id = claude_model_state.model_id
AND excluded.source > claude_model_state.source
)
)
)
)
)";

pub const SELECT_CLAUDE_MODEL_STATE_SQL: &str = "SELECT
session_id,
agent_id,
model_id,
observation_kind,
source,
observed_at_ms
FROM claude_model_state
WHERE session_id = ?1 AND agent_id = ?2
LIMIT 1";

const CONVERSATION_TEXT_EVENT_OPERATION_NAME: &str = "insert conversation text event";
const CONVERSATION_TEXT_EVENT_RETRY_HINT: &str = "retry after the database lock clears; if the issue persists, stop other SCE processes using this database and rerun the command";

Expand All @@ -81,6 +124,39 @@ pub struct DiffTraceInsert<'a> {
pub payload_type: &'a str,
}

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum ObservationKind {
SessionStart,
PostModelSwitch,
}

impl ObservationKind {
fn as_str(self) -> &'static str {
match self {
Self::SessionStart => "session_start",
Self::PostModelSwitch => "post_model_switch",
}
}

fn from_str(value: &str) -> Result<Self> {
match value {
"session_start" => Ok(Self::SessionStart),
"post_model_switch" => Ok(Self::PostModelSwitch),
other => anyhow::bail!("invalid Claude model-state observation kind: {other}"),
}
}
}

#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ClaudeModelStateObservation {
pub session_id: String,
pub agent_id: String,
pub model_id: String,
pub observation_kind: ObservationKind,
pub source: String,
pub observed_at_ms: i64,
}

/// Raw diff trace row read from the agent trace database.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct DiffTracePatchRow {
Expand Down Expand Up @@ -258,6 +334,64 @@ fn insert_agent_trace_with<M: DbSpec>(db: &TursoDb<M>, input: AgentTraceInsert<'
)
}

fn upsert_claude_model_state_with<M: DbSpec>(
db: &TursoDb<M>,
input: ClaudeModelStateObservation,
) -> Result<u64> {
db.execute(
UPSERT_CLAUDE_MODEL_STATE_SQL,
(
input.session_id,
input.agent_id,
input.model_id,
input.observation_kind.as_str(),
input.source,
input.observed_at_ms,
),
)
}

fn claude_model_state_by_session_and_agent_with<M: DbSpec>(
db: &TursoDb<M>,
session_id: &str,
agent_id: &str,
) -> Result<Option<ClaudeModelStateObservation>> {
let rows = db.query_map(
SELECT_CLAUDE_MODEL_STATE_SQL,
(session_id, agent_id),
claude_model_state_observation_from_turso,
)?;

Ok(rows.into_iter().next())
}

fn claude_model_state_observation_from_turso(
row: &turso::Row,
) -> Result<ClaudeModelStateObservation> {
Ok(ClaudeModelStateObservation {
session_id: row
.get(0)
.context("failed to read claude_model_state.session_id")?,
agent_id: row
.get(1)
.context("failed to read claude_model_state.agent_id")?,
model_id: row
.get(2)
.context("failed to read claude_model_state.model_id")?,
observation_kind: ObservationKind::from_str(
row.get::<String>(3)
.context("failed to read claude_model_state.observation_kind")?
.as_str(),
)?,
source: row
.get(4)
.context("failed to read claude_model_state.source")?,
observed_at_ms: row
.get(5)
.context("failed to read claude_model_state.observed_at_ms")?,
})
}

#[allow(dead_code)]
fn insert_message_with<M: DbSpec>(db: &TursoDb<M>, input: InsertMessageInsert) -> Result<u64> {
db.execute(
Expand Down
Loading
Loading