chore: merge develop into hare/develop

This commit is contained in:
2026-08-29 13:25:32 +09:00
88 changed files with 8005 additions and 1472 deletions
+46 -11
View File
@@ -22,7 +22,7 @@ use crate::records::{
TicketEvidenceEvent, TicketEvidenceSummary, TicketListPageRequest, TicketMergeRequestSummary,
TicketQueryItem, TicketQueryRequest, TicketQueryResponse, TicketRelationView,
TicketRoleAssignmentSummary, TicketShowRequest, TicketSummary, TicketSummaryPage,
summarize_body, truncate_body, validate_project_id,
summarize_body, truncate_body,
};
use crate::store::{
ControlPlaneStore, MemoryDocumentRecord, MemoryStagingRecord, MemoryStagingResolutionRecord,
@@ -633,7 +633,15 @@ impl SqliteWorkspaceAuthority {
predicates.push(format!("o.updated_at<{value}"));
}
if let Some(value) = &query.linked_ticket_id {
let value = bind(SqlValue::Text(value.clone()));
let resolved = self
.store
.resolve_resource_reference(
&self.workspace_id,
WorkspaceResourceKind::Ticket,
value,
)?
.ok_or_else(|| invalid_objective_error("linked Ticket was not found"))?;
let value = bind(SqlValue::Text(resolved));
predicates.push(format!("EXISTS (SELECT 1 FROM objective_ticket_links link WHERE link.workspace_id=o.workspace_id AND link.objective_id=o.objective_id AND link.ticket_id={value})"));
}
let relevance_rank = if let Some(text) =
@@ -1237,6 +1245,10 @@ impl ObjectiveAuthority for SqliteWorkspaceAuthority {
.into_iter()
.map(|link| link.ticket_id)
.collect::<Vec<_>>();
let linked_ticket_keys = linked_tickets
.iter()
.map(|ticket_id| self.resource_key(WorkspaceResourceKind::Ticket, ticket_id))
.collect::<Result<Vec<_>>>()?;
let body_md = record.body_md.clone();
let objective = ObjectiveSummary {
resource_key: self
@@ -1253,6 +1265,7 @@ impl ObjectiveAuthority for SqliteWorkspaceAuthority {
items.push(objective_query_item(
objective,
linked_tickets,
linked_ticket_keys,
query.query.as_deref(),
&body_md,
));
@@ -1339,9 +1352,19 @@ impl ObjectiveAuthority for SqliteWorkspaceAuthority {
fn create_objective(&self, input: ObjectiveCreateInput) -> Result<ObjectiveDetail> {
validate_objective_title(&input.title)?;
validate_objective_state(&input.state)?;
for ticket_id in &input.linked_tickets {
validate_project_id(ticket_id)?;
}
let linked_tickets = input
.linked_tickets
.iter()
.map(|ticket_reference| {
self.store
.resolve_resource_reference(
&self.workspace_id,
WorkspaceResourceKind::Ticket,
ticket_reference,
)?
.ok_or_else(|| invalid_objective_error("linked Ticket was not found"))
})
.collect::<Result<Vec<_>>>()?;
let now = now_rfc3339();
let objective_id = allocate_record_id(
unix_epoch_millis_now().map_err(|err| {
@@ -1367,8 +1390,7 @@ impl ObjectiveAuthority for SqliteWorkspaceAuthority {
updated_at: now.clone(),
};
self.store.upsert_objective(&record)?;
let links = input
.linked_tickets
let links = linked_tickets
.into_iter()
.map(|ticket_id| ObjectiveTicketLinkRecord {
workspace_id: self.workspace_id.clone(),
@@ -2283,12 +2305,18 @@ fn ticket_query_item(
.iter()
.map(|objective| objective.id.clone())
.collect(),
linked_objective_keys: detail
.linked_objectives
.iter()
.map(|objective| objective.resource_key.clone())
.collect(),
relation_count: detail.relations.outgoing.len() + detail.relations.incoming.len(),
blocker_count: detail.relations.blockers.len(),
unresolved_blocker_count: detail.relations.blockers.len(),
unresolved_review_count: usize::from(detail.evidence.unresolved_request_changes),
evidence: detail.evidence.clone(),
merge_request: detail.merge_request.clone(),
current_coder: detail.current_coder.clone(),
}
}
@@ -2399,6 +2427,7 @@ fn ticket_item_after_cursor(
fn objective_query_item(
objective: ObjectiveSummary,
linked_tickets: Vec<String>,
linked_ticket_keys: Vec<String>,
text: Option<&str>,
body_md: &str,
) -> ObjectiveQueryItem {
@@ -2426,6 +2455,7 @@ fn objective_query_item(
snippet,
linked_ticket_count: linked_tickets.len(),
linked_tickets,
linked_ticket_keys,
}
}
@@ -3308,16 +3338,21 @@ VALUES ('workspace-test', 'ticket', 4);
assert!(!objective.revision.is_empty());
assert_eq!(objective.linked_ticket_summaries[0].id, "00000000001J2");
assert_eq!(objective.linked_ticket_summaries[0].state, "ready");
let linked_ticket_key = objective.linked_ticket_summaries[0].resource_key.clone();
let objective_query = authority
.query_objectives(ObjectiveQueryRequest {
query: Some("Control plane".to_string()),
linked_ticket_id: Some("00000000001J2".to_string()),
linked_ticket_id: Some(linked_ticket_key.clone()),
limit: Some(1),
..ObjectiveQueryRequest::default()
})
.unwrap();
assert_eq!(objective_query.items.len(), 1);
assert_eq!(objective_query.items[0].linked_ticket_count, 1);
assert_eq!(
objective_query.items[0].linked_ticket_keys,
vec![linked_ticket_key]
);
assert_eq!(objective_query.page.limit, 1);
let body_query = authority
.query_objectives(ObjectiveQueryRequest {
@@ -3410,7 +3445,7 @@ VALUES ('workspace-test', 'ticket', 3);
title: "Create Objective".to_string(),
body_md: "Alpha body".to_string(),
state: "active".to_string(),
linked_tickets: vec!["00000000001J2".to_string()],
linked_tickets: vec!["T-1".to_string()],
})
.unwrap();
assert_eq!(created.title, "Create Objective");
@@ -3436,14 +3471,14 @@ VALUES ('workspace-test', 'ticket', 3);
assert_eq!(state.state, "paused");
assert_eq!(
authority
.link_objective_ticket(&created.id, "00000000001J3")
.link_objective_ticket(&created.id, "T-2")
.unwrap()
.linked_tickets,
vec!["00000000001J2", "00000000001J3"]
);
assert_eq!(
authority
.unlink_objective_ticket(&created.id, "00000000001J2")
.unlink_objective_ticket(&created.id, "T-1")
.unwrap()
.linked_tickets,
vec!["00000000001J3"]
+5 -7
View File
@@ -1,12 +1,9 @@
use project_record::validate_record_id;
use serde::{Deserialize, Serialize};
pub use workspace_api::{
ObjectiveDetail, ObjectiveEventDetail, ObjectiveLinkedTicketSummary, ObjectiveResourceSummary,
ObjectiveSummary, QueryPage,
};
use crate::{Error, Result};
const SUMMARY_BODY_LIMIT: usize = 240;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
@@ -387,12 +384,16 @@ pub struct TicketQueryItem {
pub snippet: Option<String>,
pub matching_event: Option<TicketEvidenceEvent>,
pub linked_objective_ids: Vec<String>,
#[ts(skip)]
pub linked_objective_keys: Vec<String>,
pub relation_count: usize,
pub blocker_count: usize,
pub unresolved_blocker_count: usize,
pub unresolved_review_count: usize,
pub evidence: TicketEvidenceSummary,
pub merge_request: Option<TicketMergeRequestSummary>,
#[ts(skip)]
pub current_coder: Option<TicketAssignmentSummary>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
@@ -435,6 +436,7 @@ pub struct ObjectiveQueryItem {
pub snippet: Option<String>,
pub linked_ticket_count: usize,
pub linked_tickets: Vec<String>,
pub linked_ticket_keys: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
@@ -521,10 +523,6 @@ mod typescript_tests {
}
}
pub(crate) fn validate_project_id(id: &str) -> Result<()> {
validate_record_id(id).map_err(|_| Error::InvalidRecordId(id.to_string()))
}
pub(crate) fn summarize_body(body: &str) -> String {
let summary = body
.lines()
+257 -66
View File
@@ -327,8 +327,6 @@ fn repository_local_path(source: &workspace_api::RepositorySource) -> Option<Pat
}
}
const ORCHESTRATOR_ATTENTION_TICKET_LIMIT: usize = 20;
const ORCHESTRATOR_ATTENTION_PROMPT_NAME: &str = "internal.workspace_orchestrator_queue_attention";
static EMBEDDED_RUNTIME_REQUEST_IDENTITY: std::sync::LazyLock<
worker_runtime::auth::RuntimeIdentityMaterial,
> = std::sync::LazyLock::new(|| {
@@ -7257,25 +7255,21 @@ fn dispatch_orchestrator_queue_attention(api: &WorkspaceApi) {
return;
}
let shown = queued
.iter()
.take(ORCHESTRATOR_ATTENTION_TICKET_LIMIT)
.map(|ticket| {
format!(
"- {} — {}",
bounded_orchestrator_attention_text(&ticket.id, 80),
bounded_orchestrator_attention_text(&ticket.title, 240)
)
})
.collect::<Vec<_>>()
.join("\n");
let omitted = queued
.len()
.saturating_sub(ORCHESTRATOR_ATTENTION_TICKET_LIMIT);
let omitted_line = if omitted == 0 {
String::new()
} else {
format!("Additional queued Tickets omitted from this notice: {omitted}\n")
let attention_context = match orchestrator_queue_attention_context(
&api.config.workspace_id,
&api.config.workspace_id,
&queued,
) {
Ok(context) => context,
Err(error) => {
tracing::warn!(
workspace_id = %api.config.workspace_id,
candidate_count = queued.len(),
diagnostic = error,
"orchestrator backlog attention projection rejected"
);
return;
}
};
let Ok(Some(config_state)) = api
.config_store
@@ -7292,16 +7286,19 @@ fn dispatch_orchestrator_queue_attention(api: &WorkspaceApi) {
let Ok(catalog) = worker::PromptCatalog::from_projection(projection.catalog().clone()) else {
return;
};
let content = match catalog.render_serializable(
ORCHESTRATOR_ATTENTION_PROMPT_NAME,
&BTreeMap::from([
("omitted_line", omitted_line.as_str()),
("workspace_id", api.config.workspace_id.as_str()),
("ticket_lines", shown.as_str()),
]),
let content = match catalog.orchestrator_queue_attention(
worker::OrchestratorQueueAttentionPrompt::Server,
&attention_context,
) {
Ok(content) => content,
Err(_) => return,
Err(error) => {
tracing::warn!(
workspace_id = %api.config.workspace_id,
diagnostic = %error,
"orchestrator backlog attention rendering failed"
);
return;
}
};
let accepted = api
.runtime
@@ -7321,20 +7318,26 @@ fn dispatch_orchestrator_queue_attention(api: &WorkspaceApi) {
}
}
fn bounded_orchestrator_attention_text(input: &str, max_chars: usize) -> String {
let mut output = String::new();
for (index, character) in input.chars().enumerate() {
if index == max_chars {
output.push('…');
break;
}
output.push(if character.is_control() {
' '
} else {
character
});
fn orchestrator_queue_attention_context(
expected_workspace_id: &str,
candidate_workspace_id: &str,
tickets: &[ticket::TicketSummary],
) -> std::result::Result<worker::OrchestratorQueueAttentionContext, &'static str> {
if candidate_workspace_id != expected_workspace_id {
return Err("foreign_workspace_ticket_projection");
}
output
let tickets = tickets
.iter()
.map(|ticket| {
let resource_key = ticket
.resource_key
.clone()
.ok_or("missing_ticket_resource_key")?;
worker::OrchestratorQueueAttentionTicket::new(resource_key, ticket.title.clone())
.map_err(|_| "invalid_ticket_resource_key")
})
.collect::<std::result::Result<Vec<_>, _>>()?;
Ok(worker::OrchestratorQueueAttentionContext::new(tickets))
}
fn require_online_workspace_orchestrator_source(
@@ -9258,9 +9261,8 @@ fn cleanup_working_directory_for_runtime(
result.diagnostics,
));
};
let record = workdir_record_from_summary(&api, runtime_id, &working_directory.summary);
api.store.upsert_workdir_registry(&record)?;
let mut summary = working_directory.summary;
persist_workdir_cleanup_observation(&api, runtime_id, &summary)?;
apply_workdir_occupancy_projection(&api, &mut summary)?;
Ok(Json(BrowserWorkingDirectoryDetailResponse {
workspace_id: api.config.workspace_id.clone(),
@@ -14151,11 +14153,7 @@ fn sync_runtime_workdir_observations(
api.store.upsert_workdir_registry(&updated)?;
}
} else {
record.materialization_status =
workdir_status_from_runtime_miss(result.diagnostics.as_slice()).to_string();
record.cleanliness = "unknown".to_string();
record.updated_at = now_registry_timestamp();
api.store.upsert_workdir_registry(&record)?;
persist_workdir_runtime_miss(api, record, result.diagnostics.as_slice())?;
}
}
Err(_) => {
@@ -14169,17 +14167,54 @@ fn sync_runtime_workdir_observations(
Ok(response.diagnostics)
}
fn workdir_status_from_runtime_miss(diagnostics: &[RuntimeDiagnostic]) -> &'static str {
if diagnostics
fn persist_workdir_cleanup_observation(
api: &WorkspaceApi,
runtime_id: &str,
summary: &WorkingDirectorySummary,
) -> ApiResult<()> {
if summary.status == WorkingDirectoryStatusKind::NotFound {
api.store.delete_workdir_registry(
&api.config.workspace_id,
summary.working_directory_id.as_str(),
)?;
} else {
let record = workdir_record_from_summary(api, runtime_id, summary);
api.store.upsert_workdir_registry(&record)?;
}
Ok(())
}
fn workdir_runtime_miss_is_not_found(diagnostics: &[RuntimeDiagnostic]) -> bool {
diagnostics
.iter()
.any(|diagnostic| diagnostic.code == "working_directory_not_found")
{
}
fn workdir_status_from_runtime_miss(diagnostics: &[RuntimeDiagnostic]) -> &'static str {
if workdir_runtime_miss_is_not_found(diagnostics) {
"not_found"
} else {
"unknown"
}
}
fn persist_workdir_runtime_miss(
api: &WorkspaceApi,
mut record: WorkdirRegistryRecord,
diagnostics: &[RuntimeDiagnostic],
) -> ApiResult<()> {
if workdir_runtime_miss_is_not_found(diagnostics) {
api.store
.delete_workdir_registry(&api.config.workspace_id, record.workdir_id.as_str())?;
} else {
record.materialization_status = "unknown".to_string();
record.cleanliness = "unknown".to_string();
record.updated_at = now_registry_timestamp();
api.store.upsert_workdir_registry(&record)?;
}
Ok(())
}
fn sync_all_runtime_workdir_observations(api: &WorkspaceApi) -> Vec<RuntimeDiagnostic> {
let mut diagnostics = Vec::new();
let runtimes = api.runtime.list_runtimes(api.config.max_records.min(200));
@@ -16977,22 +17012,24 @@ mod tests {
#[test]
fn workdir_runtime_miss_uses_exact_typed_code() {
let typed_not_found = [RuntimeDiagnostic {
code: "working_directory_not_found".to_string(),
severity: DiagnosticSeverity::Warning,
message: "missing".to_string(),
}];
assert_eq!(
workdir_status_from_runtime_miss(&[RuntimeDiagnostic {
code: "working_directory_not_found".to_string(),
severity: DiagnosticSeverity::Warning,
message: "missing".to_string(),
}]),
workdir_status_from_runtime_miss(&typed_not_found),
"not_found"
);
assert_eq!(
workdir_status_from_runtime_miss(&[RuntimeDiagnostic {
code: "some_other_not_found".to_string(),
severity: DiagnosticSeverity::Warning,
message: "not a typed workdir miss".to_string(),
}]),
"unknown"
);
assert!(workdir_runtime_miss_is_not_found(&typed_not_found));
let unrelated = [RuntimeDiagnostic {
code: "some_other_not_found".to_string(),
severity: DiagnosticSeverity::Warning,
message: "not a typed workdir miss".to_string(),
}];
assert_eq!(workdir_status_from_runtime_miss(&unrelated), "unknown");
assert!(!workdir_runtime_miss_is_not_found(&unrelated));
}
struct DeterministicExecutionBackend {
@@ -19615,7 +19652,8 @@ mod tests {
#[tokio::test]
async fn orchestrator_running_to_idle_recovers_queued_ticket_without_notification_memory() {
let dir = tempfile::tempdir().unwrap();
let api = test_api(dir.path()).await;
init_clean_git_workspace(dir.path());
let (api, execution) = test_api_with_recording_backend(dir.path()).await;
let backend = browser_ticket_backend(&api).unwrap();
let mut input = ticket::NewTicket::new("Recover queued work");
input.workflow_state = Some(TicketWorkflowState::Queued);
@@ -19634,6 +19672,8 @@ mod tests {
.await
.unwrap();
assert!(started.online);
let startup_inputs = execution.take_inputs();
assert_eq!(startup_inputs.len(), 1);
assert_eq!(
api.orchestrator_attention_fingerprint
.lock()
@@ -19669,6 +19709,76 @@ mod tests {
.as_deref(),
Some(ticket_ref.id.as_str())
);
let notifications = execution.take_inputs();
assert_eq!(notifications.len(), 1);
assert_eq!(notifications[0].0.worker_id.to_string(), worker_id);
let content = &notifications[0].1;
assert!(content.starts_with("Queued Tickets require attention:"));
assert!(
content.contains(&format!(
"- {} — Recover queued work",
ticket_ref.resource_key.as_deref().unwrap()
)),
"unexpected notification body: {content:?}"
);
assert!(content.contains("Reread the current Ticket state before acting"));
assert!(!content.contains(ticket_ref.id.as_str()));
assert!(!content.contains(TEST_WORKSPACE_ID));
assert!(!content.contains("bounded"));
assert!(!content.contains("omitted"));
let candidates = backend
.list(ticket::TicketListQuery::states([
ticket::TicketListState::Queued,
]))
.unwrap();
let mut truncated_candidates = (1..=worker::OrchestratorQueueAttentionContext::MAX_TICKETS
+ 1)
.map(|index| {
let mut candidate = candidates[0].clone();
candidate.id = format!("opaque-{index}");
candidate.resource_key = Some(format!("T-{index}"));
candidate.title = format!("Queued {index}");
candidate
})
.collect::<Vec<_>>();
let truncated = orchestrator_queue_attention_context(
TEST_WORKSPACE_ID,
TEST_WORKSPACE_ID,
&truncated_candidates,
)
.unwrap();
let rendered = worker::PromptCatalog::builtins_only()
.unwrap()
.orchestrator_queue_attention(
worker::OrchestratorQueueAttentionPrompt::Server,
&truncated,
)
.unwrap();
assert!(rendered.contains("- T-20 — Queued 20"));
assert!(!rendered.contains("T-21"));
assert!(rendered.contains("were omitted from this notice: 1"));
assert!(!rendered.contains("opaque-"));
truncated_candidates[0].resource_key = None;
assert_eq!(
orchestrator_queue_attention_context(
TEST_WORKSPACE_ID,
TEST_WORKSPACE_ID,
&truncated_candidates
)
.unwrap_err(),
"missing_ticket_resource_key"
);
assert_eq!(
orchestrator_queue_attention_context(
TEST_WORKSPACE_ID,
"foreign-workspace",
&truncated_candidates
)
.unwrap_err(),
"foreign_workspace_ticket_projection"
);
}
#[tokio::test]
@@ -21232,6 +21342,87 @@ mod tests {
.unwrap();
}
#[tokio::test]
async fn confirmed_runtime_miss_removes_registry_record_but_unknown_is_retained() {
let workspace = tempfile::tempdir().unwrap();
init_clean_git_workspace(workspace.path());
let api = test_api(workspace.path()).await;
seed_cleanup_workdir(&api, "deleted-workdir", "present", "clean");
let deleted = api
.store
.get_workdir_registry(TEST_WORKSPACE_ID, "deleted-workdir")
.unwrap()
.unwrap();
persist_workdir_runtime_miss(
&api,
deleted,
&[RuntimeDiagnostic {
code: "working_directory_not_found".to_string(),
severity: DiagnosticSeverity::Warning,
message: "missing".to_string(),
}],
)
.unwrap();
assert!(
api.store
.get_workdir_registry(TEST_WORKSPACE_ID, "deleted-workdir")
.unwrap()
.is_none()
);
seed_cleanup_workdir(&api, "unknown-workdir", "present", "clean");
let unknown = api
.store
.get_workdir_registry(TEST_WORKSPACE_ID, "unknown-workdir")
.unwrap()
.unwrap();
persist_workdir_runtime_miss(
&api,
unknown,
&[RuntimeDiagnostic {
code: "runtime_unavailable".to_string(),
severity: DiagnosticSeverity::Warning,
message: "temporarily unavailable".to_string(),
}],
)
.unwrap();
assert_eq!(
api.store
.get_workdir_registry(TEST_WORKSPACE_ID, "unknown-workdir")
.unwrap()
.unwrap()
.materialization_status,
"unknown"
);
}
#[tokio::test]
async fn cleanup_not_found_observation_removes_registry_record() {
let workspace = tempfile::tempdir().unwrap();
init_clean_git_workspace(workspace.path());
let api = test_api(workspace.path()).await;
let working_directory_id = "cleanup-existing";
seed_cleanup_workdir(&api, working_directory_id, "present", "clean");
let record = api
.store
.get_workdir_registry(TEST_WORKSPACE_ID, working_directory_id)
.unwrap()
.unwrap();
let mut summary = workdir_summary_from_record(&record);
summary.status = WorkingDirectoryStatusKind::NotFound;
persist_workdir_cleanup_observation(&api, "runtime-test", &summary).unwrap();
assert!(
api.store
.get_workdir_registry(TEST_WORKSPACE_ID, working_directory_id)
.unwrap()
.is_none()
);
}
fn seed_cleanup_link(api: &WorkspaceApi, runtime_worker_id: &str, workdir_id: &str) {
let runtime_worker_id = runtime_worker_id.parse::<u64>().unwrap();
api.store