mirror of
https://github.com/codewhale-hq/Codewhale.git
synced 2026-11-07 17:09:13 -10:00
refactor(core,state): delete the dead core::JobManager and Runtime.jobs (#6513)
The app-server /jobs route was the only reader of core::JobManager and was dropped in8642ccbdc5because it always returned an empty list. After that, nothing in the workspace enqueued, ran or read a core job: JobManager had no caller outside its own tests, so Runtime::new loaded up to 500 rows from the jobs table into a list nobody read. That is a second, unused job-bookkeeping system beside the shell job manager the Runtime API actually serves (GET /v1/jobs), which the one-store rule forbids. Removed: - codewhale-core: JobManager, JobRecord, JobStatus, JobRetryMetadata, JobHistoryEntry, PersistedJobDetail, job_record_to_agent_run, the job encode/parse helpers, Runtime.jobs and Runtime::app_status (its only payload was the job list). Runtime keeps config, hooks and read-only state-store access. anyhow, chrono and tracing were only used by the deleted code and leave core's dependencies; uuid moves to dev-dependencies (the test temp-dir helper still uses it). Cargo.lock drops the three entries from codewhale-core. - codewhale-state: JobStateRecord, JobStateStatus, StateStore::upsert_job, StateStore::list_jobs and their status string helpers. The jobs table CREATE TABLE/INDEX in the schema migration is kept unchanged so existing databases and the schema version are untouched. Tests changed because this made them wrong: - core: the 35 JobManager/JobRecord/helper tests are deleted with the code. runtime_bookkeeping_retains_jobs_and_read_only_legacy_store becomes runtime_bookkeeping_keeps_the_legacy_store_read_only, keeping its no-thread-writes assertions and dropping the job ones. - state: second_connection_waits_for_active_writer (#4734) wrote and counted rows through upsert_job/list_jobs; it now does the same insert and count with raw SQL on the store's own connection, so the busy-timeout proof is unchanged. - app-server: runtime_read_paths_run_concurrently read app_status through two simultaneous read guards; it now reads config.provider through both. - The app-server AppState comment no longer says the Runtime holds jobs, and the state module/StateStore docs no longer list jobs as an owned store. Before deleting, every removed public item was grepped across the whole CW workspace (all crates incl. tests, benches, examples, build.rs, docs, and the other repos): no consumer outside crates/core, crates/state and the one app-server test. After the change, git grep for every removed name returns nothing, and no crate glob-imports codewhale_core or codewhale_state. codewhale-models and codewhale-runtime depend on core normally but use only its request, role and fragments modules, which are unchanged; codewhale-tui and codewhale-cli reference none of the removed names. Not done: protocol RunSource::CoreJob now has no producer. It is a serialized wire variant in the staging agent_run read model and removing it touches every protocol consumer (including codewhale-tui), so it is left for a separate decision. Verification (source32fbcddc17+ this diff): - scripts/dev-cargo.sh test -p codewhale-core -p codewhale-state -p codewhale-app-server: app-server lib 177 passed, daemon_socket 15 passed, core lib 62 passed, single_turn_loop 14 passed, state lib 26 passed, parity_state 10 passed; 0 failed (1 ignored doc-test). The three edited tests ran and passed. - clippy -p codewhale-core -p codewhale-state -p codewhale-app-server --all-targets --all-features --locked with the CI flags: clean. - cargo fmt --check on the three crates: clean. - scripts/check-dead-code-budget.py: 214 (budget 215), unchanged; the deleted code carried no dead_code suppressions, so the count did not move either way. - npm test: root 101/101, runtime-sdk 19/19 + type check, extension-host 413 passed / 7 skipped / 0 failed of 420, web dependency-security 7/7, web vitest 74 files / 772 passed. npm run check:web: exit 0 (facts, docs, tokens, eslint, tsc, next build). - Review check on this tree: clippy -p codewhale-tui -p codewhale-cli -p codewhale-models -p codewhale-runtime -p codewhale-core -p codewhale-state -p codewhale-app-server --all-targets --all-features --locked with the CI flags: clean (0 warnings, 9m12s), so every downstream crate and its test targets compile. Not verified: the test suites of codewhale-tui, codewhale-cli, codewhale-models and codewhale-runtime were not run. No CHANGELOG entry: nothing user-visible changes; the /jobs route was already gone. Refs #6513 Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01NBKbhx19BRbZtvWK8rVKjz
This commit is contained in:
1 parent
32fbcddc17
commit
2045e6e28a
5 files changed
+22
-1288
No files matched your search
Generated
-3
@@ -982,8 +982,6 @@ dependencies = [
|
||||
name = "codewhale-core"
|
||||
version = "0.10.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"chrono",
|
||||
"codewhale-config",
|
||||
"codewhale-hooks",
|
||||
"codewhale-protocol",
|
||||
@@ -995,7 +993,6 @@ dependencies = [
|
||||
"tempfile",
|
||||
"thiserror 2.0.21",
|
||||
"tokio",
|
||||
"tracing",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
|
||||
@@ -247,7 +247,7 @@ pub struct AppState {
|
||||
owner_frontend: Option<Arc<dyn RuntimeOwnerFrontend>>,
|
||||
config_path: Option<PathBuf>,
|
||||
config: Arc<RwLock<codewhale_config::ConfigToml>>,
|
||||
/// Bookkeeping config/jobs and read-only historical archive access.
|
||||
/// Bookkeeping config and read-only historical archive access.
|
||||
/// Actual turns and transcript writes belong to the captured owner.
|
||||
runtime: Arc<RwLock<Runtime>>,
|
||||
registry: ModelRegistry,
|
||||
@@ -4983,8 +4983,7 @@ mod tests {
|
||||
let (state, _tmp) = capability_test_state();
|
||||
let first = state.runtime.read().await;
|
||||
let second = state.runtime.read().await;
|
||||
assert!(first.app_status().ok);
|
||||
assert!(second.app_status().ok);
|
||||
assert_eq!(first.config.provider, second.config.provider);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
|
||||
@@ -11,8 +11,6 @@ description = "Core runtime boundaries for Codewhale"
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
chrono.workspace = true
|
||||
serde.workspace = true
|
||||
thiserror.workspace = true
|
||||
codewhale-config = { path = "../config", version = "0.10.2" }
|
||||
@@ -22,9 +20,8 @@ codewhale-state = { path = "../state", version = "0.10.2" }
|
||||
regex = "1.11"
|
||||
sha2.workspace = true
|
||||
serde_json = { workspace = true, features = ["preserve_order"] }
|
||||
tracing.workspace = true
|
||||
uuid.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile.workspace = true
|
||||
uuid.workspace = true
|
||||
tokio = { workspace = true, features = ["macros", "rt", "time"] }
|
||||
+4
-1141
File diff suppressed because it is too large.
Load diff
+15
-137
@@ -1,10 +1,10 @@
|
||||
//! Historical thread recovery, canonical owner receipts, metadata and jobs.
|
||||
//! Historical thread recovery, canonical owner receipts and metadata.
|
||||
//!
|
||||
//! [`StateStore`] retains legacy conversation graphs as read-only archives.
|
||||
//! A bounded absent-only restore can admit a complete historical graph in one
|
||||
//! SQLite transaction; live transcript writes belong to Runtime's Engine and
|
||||
//! SessionManager. Canonical aliases and durable operations use this same
|
||||
//! SQLite connection. Thread metadata, jobs, dynamic tools and the session
|
||||
//! SQLite connection. Thread metadata, dynamic tools and the session
|
||||
//! index retain their existing ownership.
|
||||
|
||||
mod runtime_aliases;
|
||||
@@ -156,45 +156,6 @@ pub struct CheckpointRecord {
|
||||
pub created_at: i64,
|
||||
}
|
||||
|
||||
/// Status of a background job.
|
||||
///
|
||||
/// Serialized as lowercase snake_case strings.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum JobStateStatus {
|
||||
/// Job is waiting to be executed.
|
||||
Queued,
|
||||
/// Job is currently executing.
|
||||
Running,
|
||||
/// Job has been temporarily paused.
|
||||
Paused,
|
||||
/// Job has finished successfully.
|
||||
Completed,
|
||||
/// Job has failed with an error.
|
||||
Failed,
|
||||
/// Job was cancelled before completion.
|
||||
Cancelled,
|
||||
}
|
||||
|
||||
/// Persisted state of a background job.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct JobStateRecord {
|
||||
/// Unique identifier for the job.
|
||||
pub id: String,
|
||||
/// Human-readable name describing the job.
|
||||
pub name: String,
|
||||
/// Current lifecycle status of the job.
|
||||
pub status: JobStateStatus,
|
||||
/// Completion progress as a percentage (0--100), if available.
|
||||
pub progress: Option<u8>,
|
||||
/// Optional detail message providing additional status information.
|
||||
pub detail: Option<String>,
|
||||
/// Unix timestamp (seconds) when the job was created.
|
||||
pub created_at: i64,
|
||||
/// Unix timestamp (seconds) of the most recent status update.
|
||||
pub updated_at: i64,
|
||||
}
|
||||
|
||||
/// Persisted lifecycle status for a thread goal.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
@@ -282,7 +243,7 @@ fn session_index_compact_line_threshold() -> usize {
|
||||
/// The `user_version` the last step of `StateStore::migrate_schema` writes.
|
||||
const SCHEMA_VERSION: u32 = 7;
|
||||
|
||||
/// Persistent storage for conversation threads, messages, checkpoints, and jobs.
|
||||
/// Persistent storage for conversation threads, messages, and checkpoints.
|
||||
///
|
||||
/// Backed by a SQLite database and an append-only JSONL session index file.
|
||||
/// The database schema is automatically initialized and migrated on [`open`](Self::open).
|
||||
@@ -1431,66 +1392,6 @@ impl StateStore {
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// Insert or update a background job record.
|
||||
pub fn upsert_job(&self, job: &JobStateRecord) -> Result<()> {
|
||||
let conn = self.conn()?;
|
||||
conn.execute(
|
||||
r#"
|
||||
INSERT INTO jobs(id, name, status, progress, detail, created_at, updated_at)
|
||||
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)
|
||||
ON CONFLICT(id) DO UPDATE SET
|
||||
name = excluded.name,
|
||||
status = excluded.status,
|
||||
progress = excluded.progress,
|
||||
detail = excluded.detail,
|
||||
created_at = excluded.created_at,
|
||||
updated_at = excluded.updated_at
|
||||
"#,
|
||||
params![
|
||||
job.id,
|
||||
job.name,
|
||||
job_state_status_to_str(&job.status),
|
||||
job.progress.map(i64::from),
|
||||
job.detail,
|
||||
job.created_at,
|
||||
job.updated_at
|
||||
],
|
||||
)
|
||||
.with_context(|| format!("failed to upsert job {}", job.id))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// List jobs ordered by most recently updated.
|
||||
///
|
||||
/// The `limit` parameter caps the number of results and defaults to 100.
|
||||
pub fn list_jobs(&self, limit: Option<usize>) -> Result<Vec<JobStateRecord>> {
|
||||
let conn = self.conn()?;
|
||||
let limit = i64::try_from(limit.unwrap_or(100)).unwrap_or(100);
|
||||
let mut stmt = conn
|
||||
.prepare(
|
||||
"SELECT id, name, status, progress, detail, created_at, updated_at FROM jobs ORDER BY updated_at DESC LIMIT ?1",
|
||||
)
|
||||
.context("failed to prepare job list query")?;
|
||||
let mut rows = stmt
|
||||
.query(params![limit])
|
||||
.context("failed to query persisted jobs")?;
|
||||
let mut out = Vec::new();
|
||||
while let Some(row) = rows.next().context("failed to iterate persisted jobs")? {
|
||||
let status_raw: String = row.get(2).context("failed to read job status")?;
|
||||
let progress: Option<i64> = row.get(3).context("failed to read job progress")?;
|
||||
out.push(JobStateRecord {
|
||||
id: row.get(0).context("failed to read job id")?,
|
||||
name: row.get(1).context("failed to read job name")?,
|
||||
status: job_state_status_from_str(&status_raw),
|
||||
progress: progress.and_then(|v| u8::try_from(v).ok()),
|
||||
detail: row.get(4).context("failed to read job detail")?,
|
||||
created_at: row.get(5).context("failed to read job created_at")?,
|
||||
updated_at: row.get(6).context("failed to read job updated_at")?,
|
||||
});
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// Look up the rollout file path for a thread by its ID.
|
||||
pub fn find_rollout_path_by_id(&self, id: &str) -> Result<Option<PathBuf>> {
|
||||
let conn = self.conn()?;
|
||||
@@ -2023,29 +1924,6 @@ fn parse_checkpoint_state(state_json: &str) -> Result<Value> {
|
||||
serde_json::from_str(state_json).context("failed to parse checkpoint state json")
|
||||
}
|
||||
|
||||
fn job_state_status_to_str(status: &JobStateStatus) -> &'static str {
|
||||
match status {
|
||||
JobStateStatus::Queued => "queued",
|
||||
JobStateStatus::Running => "running",
|
||||
JobStateStatus::Paused => "paused",
|
||||
JobStateStatus::Completed => "completed",
|
||||
JobStateStatus::Failed => "failed",
|
||||
JobStateStatus::Cancelled => "cancelled",
|
||||
}
|
||||
}
|
||||
|
||||
fn job_state_status_from_str(value: &str) -> JobStateStatus {
|
||||
match value {
|
||||
"queued" => JobStateStatus::Queued,
|
||||
"running" => JobStateStatus::Running,
|
||||
"paused" => JobStateStatus::Paused,
|
||||
"completed" => JobStateStatus::Completed,
|
||||
"failed" => JobStateStatus::Failed,
|
||||
"cancelled" => JobStateStatus::Cancelled,
|
||||
_ => JobStateStatus::Queued,
|
||||
}
|
||||
}
|
||||
|
||||
fn thread_goal_status_to_str(status: &ThreadGoalStatus) -> &'static str {
|
||||
match status {
|
||||
ThreadGoalStatus::Active => "active",
|
||||
@@ -2605,17 +2483,13 @@ mod tests {
|
||||
|
||||
locked_rx.recv().expect("wait for active writer");
|
||||
let (attempting_tx, attempting_rx) = mpsc::sync_channel(0);
|
||||
let writer_b = thread::spawn(move || {
|
||||
let writer_b = thread::spawn(move || -> Result<()> {
|
||||
attempting_tx.send(()).expect("announce second write");
|
||||
store_b.upsert_job(&JobStateRecord {
|
||||
id: "job-b".to_string(),
|
||||
name: "writer-b".to_string(),
|
||||
status: JobStateStatus::Running,
|
||||
progress: None,
|
||||
detail: Some("waited for writer a".to_string()),
|
||||
created_at: 1,
|
||||
updated_at: 1,
|
||||
})
|
||||
store_b.conn()?.execute(
|
||||
"INSERT INTO jobs(id, name, status, created_at, updated_at) VALUES ('job-b', 'writer-b', 'running', 1, 1)",
|
||||
[],
|
||||
)?;
|
||||
Ok(())
|
||||
});
|
||||
|
||||
attempting_rx.recv().expect("wait for second write attempt");
|
||||
@@ -2632,8 +2506,12 @@ mod tests {
|
||||
.expect("writer b should succeed after the lock is released");
|
||||
|
||||
let store = StateStore::open(Some(db_path)).expect("reopen for verify");
|
||||
let listed = store.list_jobs(Some(2)).expect("list jobs");
|
||||
assert_eq!(listed.len(), 2, "both writers should persist their jobs");
|
||||
let listed: i64 = store
|
||||
.conn()
|
||||
.expect("connection")
|
||||
.query_row("SELECT COUNT(*) FROM jobs", [], |row| row.get(0))
|
||||
.expect("count jobs");
|
||||
assert_eq!(listed, 2, "both writers should persist their jobs");
|
||||
|
||||
let _ = fs::remove_dir_all(dir);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user