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 in 8642ccbdc5 because 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 (source 32fbcddc17 + 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:
CodeWhale BotandClaude Opus 5.5 committed 2026-10-16 03:56:54 -07:00
1 parent 32fbcddc17
commit 2045e6e28a
5 files changed
+22 -1288

No files matched your search

Generated
-3
View File
@@ -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",
]
+2 -3
View File
@@ -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]
+1 -4
View File
@@ -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
View File
File diff suppressed because it is too large. Load diff
+15 -137
View File
@@ -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);
}