fix: reconcile Memory Journal links with handoff fencing

This commit is contained in:
2026-10-17 03:52:11 +09:00
62 changed files with 7238 additions and 652 deletions
@@ -529,7 +529,7 @@ fn activation_oldest_and_previous_schema_migrate_with_same_constraints_and_prese
assert!(!store.is_user_pending_activation("old-user").unwrap());
store.with_conn(|conn| {
assert_eq!(schema(conn), expected);
assert_eq!(current_schema_version(conn)?, 100);
assert_eq!(current_schema_version(conn)?, LATEST_SCHEMA_VERSION);
assert!(conn.execute("INSERT INTO user_activations VALUES('missing','issue','hash','future',0)", []).is_err());
for attempts in [-1, 6] { assert!(conn.execute("INSERT INTO user_activations VALUES('old-user','issue','hash','future',?1)", [attempts]).is_err()); }
Ok(())
@@ -474,7 +474,8 @@ fn tracking_store_real_schema91_to92_retains_queue_cases_principals_audit_receip
(97, "MR notifications and authenticated implementation results".into()),
(98, "Assignment-independent implementation reports".into()),
(99, "Explicit hosting administrators and self-registration policy".into()),
(100, "Provisional users and scoped passkey activation".into())
(100, "Provisional users and scoped passkey activation".into()),
(101, "Durable explicit Subject body release".into())
]
);
for (table, rows) in retained.iter().zip(original.iter()) {
@@ -282,6 +282,10 @@ pub struct SubjektivConsolidationGrant {
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(deny_unknown_fields)]
pub struct SubjektivJournalGrant {
/// Frozen writer generation. Legacy absence preserves its original canonical wire/digest;
/// recovery must use only the immutable original Session and still-current body.
#[serde(default, skip_serializing_if = "String::is_empty")]
pub binding_id: String,
pub subject_id: String,
pub session_id: String,
pub journal_id: String,
+51 -1
View File
@@ -1288,6 +1288,13 @@ pub trait WorkspaceWorkerRuntime: Send + Sync {
})
}
fn subject_release(
&self,
_request: runtime_api::SubjectReleaseRequest,
) -> Result<runtime_api::SubjectReleaseResponse, String> {
Err("actual Subject Controller reservation is unavailable on this Runtime".into())
}
fn session_material_pin(
&self,
_request: runtime_api::SessionMaterialPinRequest,
@@ -2244,6 +2251,21 @@ impl RuntimeRegistry {
runtime.worker_session_attachment(workspace_id, &worker_ref, session_id, attachment_id)
}
pub fn subject_release(
&self,
runtime_id: &str,
request: runtime_api::SubjectReleaseRequest,
) -> Result<runtime_api::SubjectReleaseResponse, RuntimeRegistryError> {
validate_backend_identifier("runtime_id", runtime_id)?;
self.runtime(runtime_id)?
.subject_release(request)
.map_err(|message| RuntimeRegistryError::RuntimeOperationFailed {
runtime_id: runtime_id.into(),
code: "subject_release_unproven".into(),
message,
})
}
pub fn session_material_pin(
&self,
runtime_id: &str,
@@ -3297,6 +3319,19 @@ impl WorkspaceWorkerRuntime for EmbeddedWorkerRuntime {
})
}
fn subject_release(
&self,
request: runtime_api::SubjectReleaseRequest,
) -> Result<runtime_api::SubjectReleaseResponse, String> {
if request.workspace_id != self.workspace_id {
return Err("Subject release Workspace mismatch".into());
}
let scope = RuntimeWorkspaceScope::new(&request.workspace_id, "embedded-backend");
self.runtime
.subject_release_scoped(&scope, request)
.map_err(|e| e.to_string())
}
fn session_material_pin(
&self,
request: runtime_api::SessionMaterialPinRequest,
@@ -4114,7 +4149,10 @@ fn workspace_runtime_operation(method: &str, path_and_query: &str) -> &'static s
{
return "workers:input";
}
if path.ends_with("/stop") || path.ends_with("/cancel") {
if path.ends_with("/stop")
|| path.ends_with("/cancel")
|| (path == "/v1/subject-release" && method == "POST")
{
return "workers:stop";
}
if path == "/v1/protocol/ws" {
@@ -5523,6 +5561,18 @@ impl WorkspaceWorkerRuntime for RemoteWorkerRuntime {
})
}
fn subject_release(
&self,
request: runtime_api::SubjectReleaseRequest,
) -> Result<runtime_api::SubjectReleaseResponse, String> {
self.run_runtime_api(
self.request_timeout,
MAX_REMOTE_RUNTIME_RESPONSE_BYTES,
move |client| async move { client.subject_release(request).await },
)
.map_err(|diagnostic| diagnostic.message)
}
fn session_material_pin(
&self,
request: runtime_api::SessionMaterialPinRequest,
@@ -381,6 +381,8 @@ async fn implementation_result_v97_to_v98_preserves_legacy_bytes_receipts_and_no
DROP TABLE ticket_implementation_results;
DROP TABLE ticket_implementation_delegations;
DROP TABLE merge_request_source_observations;
DROP TABLE subject_worker_releases;
UPDATE __yoi_schema_migrations SET version=100 WHERE version=101;
DROP TABLE user_activations;
UPDATE __yoi_schema_migrations SET version=99 WHERE version=100;
DROP TABLE hosting_administrators; DROP TABLE hosting_settings;
@@ -513,6 +515,8 @@ async fn mr_notification_upgrade_recovers_events_not_comments_and_retains_unknow
// Reconstruct the prior composition before its new business capture existed.
conn.execute_batch("DROP TRIGGER notification_mr_event; DROP TRIGGER notification_mr_source; DROP TRIGGER notification_implementation_result; DROP TRIGGER IF EXISTS implementation_delegation_capture;
DROP TABLE ticket_implementation_results; DROP TABLE ticket_implementation_delegations; DROP TABLE merge_request_source_observations;
DROP TABLE subject_worker_releases;
UPDATE __yoi_schema_migrations SET version=100 WHERE version=101;
DROP TABLE user_activations;
UPDATE __yoi_schema_migrations SET version=99 WHERE version=100;
DROP TABLE hosting_administrators; DROP TABLE hosting_settings;
@@ -671,7 +675,9 @@ async fn mr_event_actors_preserve_self_suppression_in_capture_and_backfill() {
if historical {
conn.execute_batch("DROP TRIGGER notification_mr_event; DROP TRIGGER notification_mr_source; DROP TRIGGER notification_implementation_result; DROP TRIGGER IF EXISTS implementation_delegation_capture;
DROP TABLE ticket_implementation_results; DROP TABLE ticket_implementation_delegations; DROP TABLE merge_request_source_observations;
DROP TABLE user_activations;
DROP TABLE subject_worker_releases;
UPDATE __yoi_schema_migrations SET version=100 WHERE version=101;
DROP TABLE user_activations;
UPDATE __yoi_schema_migrations SET version=99 WHERE version=100;
DROP TABLE hosting_administrators; DROP TABLE hosting_settings;
UPDATE __yoi_schema_migrations SET version=98 WHERE version=99;
@@ -699,6 +699,22 @@ CREATE TABLE worker_create_reservations (
FOREIGN KEY (workspace_id) REFERENCES workspaces(workspace_id) ON DELETE CASCADE,
CHECK ((singleton_key IS NULL) = (singleton_generation IS NULL))
);
CREATE TABLE subject_worker_releases (
workspace_id TEXT NOT NULL,
runtime_id TEXT NOT NULL,
worker_id TEXT NOT NULL,
singleton_key TEXT NOT NULL,
singleton_generation INTEGER NOT NULL CHECK (singleton_generation > 0),
binding_id TEXT NOT NULL,
operation_id TEXT NOT NULL,
proof TEXT CHECK (proof IN ('runtime_disconnected','erased')),
completed INTEGER NOT NULL DEFAULT 0 CHECK (completed IN (0,1)),
cancelled INTEGER NOT NULL DEFAULT 0 CHECK (cancelled IN (0,1)),
PRIMARY KEY (workspace_id, operation_id),
CHECK (cancelled = 0 OR (completed = 0 AND proof IS NULL)),
FOREIGN KEY (workspace_id, worker_id) REFERENCES worker_create_reservations(workspace_id, worker_id) ON DELETE CASCADE
);
CREATE UNIQUE INDEX subject_worker_release_active ON subject_worker_releases(workspace_id,worker_id) WHERE cancelled=0;
CREATE TRIGGER worker_create_reservation_singleton_lease_insert
BEFORE INSERT ON worker_create_reservations
WHEN (NEW.singleton_key IS NULL AND NEW.singleton_generation IS NOT NULL)
@@ -57,6 +57,7 @@ fn fresh_and_upgraded_state_contract_schemas_enforce_the_same_constraints() {
"workspace_config_tree_history",
"workspace_memory_settings",
"worker_create_reservations",
"subject_worker_releases",
"backend_jobs",
"backend_job_attempts",
"flow_sources",
+38 -17
View File
@@ -1655,6 +1655,7 @@ pub struct WorkspaceApi {
Arc<Mutex<HashMap<RuntimeWorkerRef, Arc<Mutex<OrchestratorAttentionState>>>>>,
notification_delivery_lifecycle: Arc<notification_delivery::NotificationDeliveryLifecycle>,
backend_job_drain_requests: Arc<AtomicU64>,
journal_lifecycle: Arc<Mutex<HashMap<String, Arc<Mutex<()>>>>>,
journal_dispatch_owner: Option<Arc<WorkspaceApi>>,
observation_proxy: BackendObservationProxy,
runtime_subscription_broker: RuntimeSubscriptionBroker,
@@ -3380,6 +3381,7 @@ fn workspace_request_is_callback(method: &Method, path: &str, workspace_id: &str
|| path == format!("/api/runtime/v1/workspaces/{workspace_id}/resources/fetch")))
|| (*method == Method::GET
&& (path == format!("/api/w/{workspace_id}/runtime-config")
|| path == format!("/api/w/{workspace_id}/subjektiv/journal/startup")
|| path == format!("/api/w/{workspace_id}/worker-discovery/workers")))
}
@@ -4161,6 +4163,7 @@ impl WorkspaceApi {
notification_delivery_lifecycle: Arc::new(Default::default()),
orchestrator_attention: Arc::new(Mutex::new(HashMap::new())),
backend_job_drain_requests: Arc::new(AtomicU64::new(0)),
journal_lifecycle: Arc::new(Mutex::new(HashMap::new())),
journal_dispatch_owner: None,
observation_proxy,
runtime_subscription_broker,
@@ -4363,8 +4366,9 @@ impl WorkspaceApi {
resolved_worker_observation_grants: Vec::new(),
resolved_workspace_api: None,
resolved_memory_settings: None,
resolved_subjektiv_attached: request.grants.subjektiv_consolidation.is_some()
|| request.grants.subjektiv_journal.is_some(),
// Journal Jobs use their independent frozen Job Host grant, not the
// main-Subject/Memory attachment or its resident/extraction policy.
resolved_subjektiv_attached: request.grants.subjektiv_consolidation.is_some(),
resolved_control_operation: Some(control_operation),
},
) {
@@ -5469,6 +5473,22 @@ impl WorkspaceApi {
.map(|assignment| assignment.operation_id.clone())
})
.unwrap_or_else(|| format!("manual:{}", WorkerId::now_v7()));
let subject = singleton_key
.as_deref()
.and_then(|key| key.strip_prefix(SUBJEKTIV_SINGLETON_PREFIX));
let subject_gate = subject
.map(|s| subjektiv_journal::lifecycle_lock(self, s))
.transpose()?;
let subject_guard = subject_gate
.as_ref()
.map(|gate| {
gate.lock()
.map_err(|_| Error::Store("Journal lifecycle lock poisoned".into()))
})
.transpose()?;
if let Some(subject) = subject {
subjektiv_journal::check_start_allowed(self, subject)?;
}
let reservation = match self.config_store.reserve_worker_create(
&self.config.workspace_id,
runtime_id,
@@ -5491,6 +5511,7 @@ impl WorkspaceApi {
return Err(ApiError::with_diagnostics(error, diagnostics));
}
};
drop(subject_guard);
let worker_id = reservation.worker_id;
if let Some(assignment) = request.ticket_assignment.as_ref() {
self.store.bind_ticket_assignment_operation_worker(
@@ -5804,6 +5825,9 @@ impl WorkspaceApi {
diagnostics,
));
}
if let Err(error) = subjektiv_journal::reconcile_created_worker(self, &worker_ref) {
tracing::warn!(%error, worker_id=%worker_ref.worker_id, "Journal first-run reconciliation remains recoverable; saved history retained");
}
Ok(result)
}
@@ -13123,6 +13147,8 @@ fn build_inner_router(api: WorkspaceApi) -> Router {
"/api/w/{workspace_id}/workers/self/backend-job-result",
post(scoped_submit_backend_job_result),
)
.route("/api/w/{workspace_id}/subjektiv/journal/release", post(subjektiv_journal::release_route))
.route("/api/w/{workspace_id}/subjektiv/journal/startup", get(subjektiv_journal::startup_route))
.route("/api/w/{workspace_id}/subjektiv/journal/recording", post(subjektiv_journal::recording_route))
.route("/api/w/{workspace_id}/subjektiv/journal/memory", post(subjektiv_memory_journal::route))
.route("/api/w/{workspace_id}/subjektiv/journal/job", post(subjektiv_journal::job_route))
@@ -22369,6 +22395,7 @@ async fn scoped_record_subjektiv_session(
&request.session_id,
)?
};
subjektiv_journal::bind_host_session(&api, attribution.clone())?;
Ok(Json(server_api::SubjektivRecordSessionResponse {
subject_id: attribution.subject_id,
session_id: attribution.session_id,
@@ -46578,8 +46605,8 @@ mod tests {
let runtime = WorkdirlessFixtureRuntime::default();
api.runtime.register_or_replace(runtime.clone());
let request = |operation_id: &str| WorkerSpawnRequest {
requested_worker_name: Some("Subject Worker".to_string()),
singleton_key: Some("subjektiv:subject-retry".to_string()),
requested_worker_name: Some("Generic singleton Worker".to_string()),
singleton_key: Some("fixture:subject-retry".to_string()),
intent: WorkerSpawnIntent::WorkspaceCompanion,
acceptance: WorkerSpawnAcceptanceRequirement::RunAccepted {
expected_segments: 0,
@@ -46616,7 +46643,7 @@ mod tests {
// Runtime dispatch. The restarted request receives a fresh manual allocation key, yet its
// identical singleton intent replays the reserved Worker and dispatches that generation.
let mut restart_request = request("ignored-manual-operation");
restart_request.singleton_key = Some("subjektiv:restart-retry".to_string());
restart_request.singleton_key = Some("fixture:restart-retry".to_string());
restart_request.resolved_control_operation = None;
let restart_fingerprint = worker_spawn_create_fingerprint(&restart_request).unwrap();
let memory = api
@@ -46663,7 +46690,7 @@ mod tests {
assert_eq!(rejected.state, InternalWorkerOperationState::Rejected);
assert!(
api.store
.current_worker_singleton_owner(&api.config.workspace_id, "subjektiv:subject-retry")
.current_worker_singleton_owner(&api.config.workspace_id, "fixture:subject-retry")
.unwrap()
.is_none()
);
@@ -46677,14 +46704,14 @@ mod tests {
let worker = created.worker.unwrap();
let current_owner = api
.store
.current_worker_singleton_owner(&api.config.workspace_id, "subjektiv:subject-retry")
.current_worker_singleton_owner(&api.config.workspace_id, "fixture:subject-retry")
.unwrap()
.unwrap();
assert_eq!(current_owner.worker, worker.worker);
assert_eq!(current_owner.generation, 3);
assert_eq!(
worker.singleton_key,
Some("subjektiv:subject-retry".to_string())
Some("fixture:subject-retry".to_string())
);
// Idle and paused Runtime metadata never releases Backend ownership, and a conflict is
@@ -46714,7 +46741,7 @@ mod tests {
assert_eq!(restored.state, server_api::WorkerRestoreState::Accepted);
assert_eq!(
restored.worker.unwrap().singleton_key.as_deref(),
Some("subjektiv:subject-retry")
Some("fixture:subject-retry")
);
// Replacing the connected Runtime with an empty instance models an unavailable/restarted
@@ -46734,10 +46761,7 @@ mod tests {
);
assert_eq!(
api.store
.current_worker_singleton_owner(
&api.config.workspace_id,
"subjektiv:subject-retry",
)
.current_worker_singleton_owner(&api.config.workspace_id, "fixture:subject-retry",)
.unwrap()
.unwrap()
.worker,
@@ -46814,10 +46838,7 @@ mod tests {
));
assert_eq!(
api.store
.current_worker_singleton_owner(
&api.config.workspace_id,
"subjektiv:subject-retry",
)
.current_worker_singleton_owner(&api.config.workspace_id, "fixture:subject-retry",)
.unwrap()
.unwrap()
.worker,
File diff suppressed because it is too large Load Diff
@@ -110,7 +110,7 @@ async fn subject_callbacks_finish_before_waiting_host_parents_release_capacity()
));
}
// Resident memory is not an allowed deletion-drain callback. Preserve that
// admission fence; only the exact Session attribution route reenters below.
// admission fence; only exact Session attribution and startup read callbacks reenter below.
let mut deletion = Box::pin(gate.lock_deletion());
assert!(futures::poll!(&mut deletion).is_pending());
for (subject_id, worker_id, session_id) in &callbacks {
@@ -139,6 +139,27 @@ async fn subject_callbacks_finish_before_waiting_host_parents_release_capacity()
let stored = subjektiv.session_attribution(session_id).unwrap().unwrap();
assert_eq!(stored.worker_id, *worker_id);
assert_eq!(stored.subject_id, *subject_id);
let startup_path = format!("/api/w/{TEST_WORKSPACE_ID}/subjektiv/journal/startup");
let response = tokio::time::timeout(
std::time::Duration::from_secs(2),
app.clone().oneshot(runtime_source_request(
&identity,
Some(worker_id),
"GET",
&startup_path,
Vec::new(),
)),
)
.await
.expect("startup snapshot must not wait for Runtime's occupied Host parents")
.unwrap();
assert_eq!(response.status(), StatusCode::OK);
let snapshot: subjektiv::journal::startup::JournalStartupSnapshot =
serde_json::from_slice(&to_bytes(response.into_body(), usize::MAX).await.unwrap())
.unwrap();
assert_eq!(snapshot.subject_id, *subject_id);
assert!(snapshot.journals.is_empty());
assert!(!snapshot.omitted);
}
// Neither actual parent nor its admission lease was released to make these
// callbacks succeed. Only durable attribution completion releases them.
@@ -297,7 +318,7 @@ async fn only_exact_authenticated_callbacks_can_reenter_deletion_drain() {
}
#[tokio::test]
async fn rejected_create_releases_reservation_and_key_without_cleanup_errors() {
async fn rejected_create_releases_key_but_retains_subject_occupancy_without_release_proof() {
let workspace = tempfile::tempdir().unwrap();
let api = test_api(workspace.path()).await;
let memory = api
@@ -349,11 +370,13 @@ async fn rejected_create_releases_reservation_and_key_without_cleanup_errors() {
.unwrap()
.is_none()
);
assert!(
assert_eq!(
api.store
.current_worker_singleton_owner(TEST_WORKSPACE_ID, "subjektiv:rejected-create")
.unwrap()
.is_none()
.expect("failed creation is not explicit Subject release authority")
.worker,
worker,
);
}
}
@@ -0,0 +1,623 @@
//! Connected Backend → production Runtime factory → real Controller → Journal → signed
//! Host startup reader → persisted first input. Only the external model stream is scripted.
//! Browser authentication is outside this regression: creation enters the real scoped handler.
use super::*;
use agen::llm_client::{
LlmClient, Request,
error::ClientError,
event::{Event, StopReason},
types::Item,
};
use serde_json::{Value, json};
use std::sync::{Arc, Mutex};
#[derive(Clone, Default)]
struct ConnectedJournalModel {
dialogs: Arc<Mutex<Vec<Request>>>,
}
impl ConnectedJournalModel {
fn journal_response(request: &Request) -> Vec<Event> {
// Runtime Controllers deliberately retain alias-routed core schemas for their
// lifetime even with zero attachments. Verify invocation authority, not schema
// absence (standalone Internal Jobs use a different composition).
for (id, tool, input) in [
(
"denied-bash",
"Bash",
json!({"command":"printf JOURNAL_MUST_NOT_EXECUTE","cwd":"."}),
),
(
"denied-read",
"Read",
json!({"file_path":"journal-must-not-be-readable"}),
),
] {
if let Some(error) = request.items.iter().find_map(|item| match item {
Item::ToolResult {
call_id, is_error, ..
} if call_id == id => Some(*is_error),
_ => None,
}) {
assert!(
error,
"Journal Job unexpectedly gained {tool} Workdir authority"
);
} else {
return vec![
Event::tool_use_start(0, id, tool),
Event::tool_input_delta(0, input.to_string()),
Event::tool_use_stop(0),
];
}
}
let result = |id: &str| -> Option<Value> {
request.items.iter().find_map(|item| match item {
Item::ToolResult {
call_id,
content,
is_error,
summary,
..
} if call_id == id => {
assert!(!is_error, "{id}: {summary}; {content:?}");
Some(
content
.as_deref()
.map(|text| serde_json::from_str(text).unwrap())
.unwrap_or(Value::Null),
)
}
_ => None,
})
};
if result("submit").is_some() {
return vec![
Event::text_block_start(0),
Event::text_delta(0, "saved"),
Event::text_block_stop(0, Some(StopReason::EndTurn)),
];
}
let (tool, id, input) = if result("checkpoint").is_some() {
("SubmitBackendJobResult", "submit", json!({"result":{}}))
} else if let Some(position) = result("position") {
let state = result("state").unwrap();
(
"CheckpointJournal",
"checkpoint",
json!({
"request_id":format!("checkpoint:{}",state["value"]["generation_id"].as_str().unwrap()),
"expected_state":position["value"]["state_token"],
"action":{"action":"checkpoint","record_operation_ids":[format!("record:{}",state["value"]["generation_id"].as_str().unwrap())]},
"covered_ranges":state["value"]["selected_ranges"],
}),
)
} else if let Some(record) = result("record") {
let state = result("state").unwrap();
(
"UpdateJournalPosition",
"position",
json!({
"request_id":format!("position:{}",state["value"]["generation_id"].as_str().unwrap()),
"expected_state":record["value"]["state_token"],
"action":{"action":"update_position","current_position":"CONNECTED_HANDOFF: partially implemented; outcome unconfirmed"},
"covered_ranges":[],
}),
)
} else if let Some(state) = result("state") {
let state = &state["value"];
// Deliver the actual frozen Runtime source through authenticated Job tools, rather
// than injecting a fixture Journal or claiming unseen source was processed.
for (index, range) in state["selected_ranges"]
.as_array()
.unwrap()
.iter()
.enumerate()
{
let mut offset = range["bytes"]["start"].as_u64().unwrap();
let mut token = Value::Null;
loop {
let id = format!("source-{index}-{offset}");
if let Some(page) = result(&id) {
let page = &page["value"];
if let Some(next) = page["next_byte"].as_u64() {
assert!(next > offset);
offset = next;
token = page["content_token"].clone();
} else {
break;
}
} else {
assert!(
request
.tools
.iter()
.any(|tool| tool.name == "ReadJournalText")
);
return vec![Event::tool_use_start(0, &id, "ReadJournalText"), Event::tool_input_delta(0, json!({
"target":{"target":"source","pointer":range["source"]["pointer"]},
"offset":offset,"limit":MAX_JOURNAL_READ_BYTES,"content_token":token,
}).to_string()), Event::tool_use_stop(0)];
}
}
}
let (tool, action) = if state["current_open"].is_null() {
(
"CreateJournalEntry",
json!({"action":"create","title":"Connected handoff","body":"CONNECTED_HANDOFF: proposed A, user chose B; only partial work performed.","ongoing":"Remaining work; do not claim success","state":"open","source_end":null,"correction_refs":[]}),
)
} else {
(
"RewriteJournalEntry",
json!({"action":"rewrite","entry_id":state["current_open"]["id"],"title":"Connected handoff","body":"CONNECTED_HANDOFF: B selected; partial work remains.","ongoing":"Remaining work; result unknown"}),
)
};
(
tool,
"record",
json!({
"request_id":format!("record:{}",state["generation_id"].as_str().unwrap()),
"expected_state":state["current_journal"]["state_token"],
"action":action,"covered_ranges":state["selected_ranges"],
}),
)
} else {
("ReadJournalState", "state", json!({}))
};
assert!(
request
.tools
.iter()
.any(|definition| definition.name == tool),
"missing {tool}"
);
for forbidden in ["MemoryApplyCandidate", "SubjektivSessionRead"] {
assert!(
!request
.tools
.iter()
.any(|definition| definition.name == forbidden),
"Journal exposed forbidden {forbidden}: {:?}",
request
.tools
.iter()
.map(|tool| &tool.name)
.collect::<Vec<_>>()
);
}
vec![
Event::tool_use_start(0, id, tool),
Event::tool_input_delta(0, input.to_string()),
Event::tool_use_stop(0),
]
}
}
#[async_trait::async_trait]
impl LlmClient for ConnectedJournalModel {
async fn stream(
&self,
request: Request,
) -> std::result::Result<
std::pin::Pin<
Box<dyn futures::Stream<Item = std::result::Result<Event, ClientError>> + Send>,
>,
ClientError,
> {
let events = if request
.tools
.iter()
.any(|tool| tool.name == "ReadJournalState")
{
Self::journal_response(&request)
} else if request.tools.iter().any(|tool| {
matches!(
tool.name.as_str(),
"SubmitBackendJobResult" | "SubmitMemorySurface"
)
}) {
// The real companion also requests an empty Memory consolidation Job. Keep that
// lifecycle real without counting its model requests as main-body dialog requests.
if request.items.iter().any(|item| matches!(item, Item::ToolResult { call_id, is_error, .. } if call_id == "empty-memory" && !is_error)) {
vec![Event::text_block_start(0), Event::text_delta(0, "empty Memory acknowledged"), Event::text_block_stop(0, Some(StopReason::EndTurn))]
} else {
let (tool, input) = if request.tools.iter().any(|tool| tool.name == "SubmitMemorySurface") {
("SubmitMemorySurface", json!({"points":[]}))
} else {
let input: Value = request.items.iter().filter_map(Item::as_text).find_map(|text| text.lines().find_map(|line| line.strip_prefix("input_json: "))).map(|input| serde_json::from_str(input).unwrap()).expect("Backend Memory Job envelope");
assert_eq!(input["candidate_ids"], json!([]));
("SubmitBackendJobResult", json!({"result":{"subject_id":input["subject_id"],"candidate_ids":[]}}))
};
vec![Event::tool_use_start(0, "empty-memory", tool), Event::tool_input_delta(0, input.to_string()), Event::tool_use_stop(0)]
}
} else {
self.dialogs.lock().unwrap().push(request);
vec![
Event::text_block_start(0),
Event::text_delta(
0,
"Partial B implementation; remaining work is not confirmed.",
),
Event::text_block_stop(0, Some(StopReason::EndTurn)),
]
};
Ok(Box::pin(futures::stream::iter(events.into_iter().map(Ok))))
}
fn clone_boxed(&self) -> Box<dyn LlmClient> {
Box::new(self.clone())
}
}
async fn connected_create(api: &WorkspaceApi, subject: &str, name: &str) -> RuntimeWorkerRef {
let Json(created) = scoped_create_workspace_worker(
State(api.clone()),
AxumPath(ScopedWorkspacePath { workspace_id: TEST_WORKSPACE_ID.into() }),
HeaderMap::new(),
Json(CreateWorkspaceWorkerRequest {
runtime_id: EMBEDDED_WORKER_RUNTIME_ID.into(), display_name: name.into(),
singleton_key: None, profile: Some("builtin:companion".into()), ticket_assignment: None,
initial_submit: vec![protocol::Segment::Text { content: "User chose B rather than A. Continue the partial work without assuming success.".into() }],
workdir_attachments: vec![],
feature_connections: server_api::WorkspaceWorkerFeatureConnectionsRequest {
subjektiv: Some(server_api::SubjektivWorkerConnectionRequest { subject_id: subject.into() }),
}, control_operation_id: None,
}),
).await.unwrap();
RuntimeWorkerRef::new(created.runtime_id, created.worker_id)
}
async fn connected_wait_for_recording(
api: &WorkspaceApi,
store: &crate::subjektiv::SubjektivStore,
attr: &crate::subjektiv::SubjectSessionAttribution,
) {
let host = JournalHostContext::validated_session(store, attr.clone()).unwrap();
tokio::time::timeout(std::time::Duration::from_secs(30), async {
let mut tick = tokio::time::interval(std::time::Duration::from_millis(25));
loop {
tick.tick().await;
let jobs = api
.store
.list_backend_journal_jobs(TEST_WORKSPACE_ID, &attr.session_id, None, 100)
.unwrap();
let generation_closed =
store
.journal_for_session(&host)
.unwrap()
.is_some_and(|journal| {
store
.active_journal_generation(&host, &journal)
.unwrap()
.is_none()
});
if !jobs.is_empty()
&& generation_closed
&& jobs
.iter()
.all(|job| job.state == BackendJobState::Completed)
&& !store.journal_retention_required(&host).unwrap()
&& api
.runtime
.worker(&RuntimeWorkerRef::new(&attr.runtime_id, &attr.worker_id))
.unwrap()
.worker_state
.is_some_and(|state| state.catalog_status() == protocol::WorkerStatus::Idle)
{
break;
}
}
})
.await
.unwrap_or_else(|_| {
panic!(
"real Controller recording did not complete: {:?}",
api.store
.list_backend_journal_jobs(TEST_WORKSPACE_ID, &attr.session_id, None, 100)
.unwrap()
)
});
}
#[tokio::test(flavor = "multi_thread", worker_threads = 6)]
async fn backend_real_runtime_journal_release_and_next_body_persisted_startup() {
let dir = tempfile::tempdir().unwrap();
init_clean_git_workspace(dir.path());
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let mut config = test_server_config(dir.path());
config.backend_base_url = Some(format!("http://{}", listener.local_addr().unwrap()));
let control = Arc::new(test_control_store(&config));
seed_test_registered_workspace(control.as_ref(), &config)
.await
.unwrap();
let broker = BackendResourceBroker::default();
let dispatcher = Arc::new(
crate::worker_source::EmbeddedServerWorkerMutationDispatcher::new(
config.clone(),
control.clone(),
),
);
let model = ConnectedJournalModel::default();
let model_factory = model.clone();
let audience = embedded_runtime_request_audience(&config).unwrap();
let execution = worker_runtime::worker_backend::WorkerRuntimeExecutionBackend::new(
worker_runtime::worker_backend::ProfileRuntimeWorkerFactory::new(
config.workspace_execution_root.clone(),
)
.with_embedded_worker_mutation_dispatcher(EMBEDDED_WORKER_RUNTIME_ID, dispatcher.clone())
.with_workspace_request_client(
worker_runtime::workspace_request::RuntimeWorkspaceRequestClient::new(
TEST_WORKSPACE_ID,
audience.clone(),
EMBEDDED_WORKER_RUNTIME_ID,
)
.with_runtime_request_source(&EMBEDDED_RUNTIME_REQUEST_IDENTITY, audience),
)
.with_runtime_store_dir(config.embedded_runtime_store_root.clone())
.with_resource_client(Arc::new(broker.clone()))
.with_spawn_model_client_factory(move || Box::new(model_factory.clone())),
)
.unwrap();
let api = WorkspaceApi::new_with_execution_backend_and_broker(
config,
control,
Arc::new(execution),
broker,
HostExecutors::default(),
Some(dispatcher),
)
.await
.unwrap();
seed_test_delivery_binding(&api);
let requests = Arc::new(Mutex::new(Vec::new()));
let observed = requests.clone();
let app = build_router(api.clone()).layer(axum::middleware::from_fn(
move |request: axum::extract::Request, next: axum::middleware::Next| {
let observed = observed.clone();
async move {
let path = request.uri().path().to_string();
let response = next.run(request).await;
observed.lock().unwrap().push((path, response.status()));
response
}
},
));
let server = tokio::spawn(async move { axum::serve(listener, app).await.unwrap() });
let store = open_subjektiv_store(&api).unwrap();
let subject = store
.create_subject(crate::subjektiv::SubjectRole::new("connected-handoff").unwrap())
.unwrap();
let old = connected_create(&api, &subject.id, "connected-old-body").await;
let old_attr = store
.subject_session_attributions(&subject.id)
.unwrap()
.into_iter()
.find(|attr| attr.worker_id == old.worker_id)
.unwrap();
connected_wait_for_recording(&api, &store, &old_attr).await;
let old_host = JournalHostContext::validated_session(&store, old_attr.clone()).unwrap();
let old_binding = JournalWriterBinding {
subject_id: subject.id.clone(),
runtime_id: old.runtime_id.clone(),
worker_id: old.worker_id.clone(),
binding_id: old_host.writer_binding_id().unwrap().into(),
};
// Inject a durable result-acceptance gap after real Journal completion. This must
// reject before writer revocation and confirm cancellation of the actual Controller
// guard; merely returning Err would strand the body and its execution-admission fence.
let recorded = api
.store
.list_backend_journal_jobs(TEST_WORKSPACE_ID, &old_attr.session_id, None, 100)
.unwrap();
let uncertain_job = &recorded[0].request.job_id;
let conn = rusqlite::Connection::open(&api.config.database_path).unwrap();
let saved_result: (String, String) = conn.query_row(
"SELECT result_json,result_digest FROM backend_jobs WHERE workspace_id=?1 AND job_id=?2",
rusqlite::params![TEST_WORKSPACE_ID, uncertain_job], |r| Ok((r.get(0)?, r.get(1)?)),
).unwrap();
conn.execute(
"UPDATE backend_jobs SET state='unknown',result_json=NULL,result_digest=NULL WHERE workspace_id=?1 AND job_id=?2",
rusqlite::params![TEST_WORKSPACE_ID, uncertain_job],
)
.unwrap();
let incomplete = json!({
"worker":old,"binding_id":old_binding.binding_id,"operation_id":"connected-incomplete-release",
});
let error = subjektiv_journal::release_subject(
&api,
serde_json::from_value(incomplete.clone()).unwrap(),
)
.unwrap_err();
assert!(error.to_string().contains("Unknown"), "{error}");
assert!(store.journal_writer_active(&old_binding).unwrap());
assert!(
!api.config_store
.subject_release_pending(TEST_WORKSPACE_ID, &old)
.unwrap()
);
let cancelled: bool = conn.query_row("SELECT cancelled FROM subject_worker_releases WHERE workspace_id=?1 AND operation_id='connected-incomplete-release'", [TEST_WORKSPACE_ID], |r| r.get(0)).unwrap();
assert!(
cancelled,
"checked pre-revoke refusal needs a terminal cancellation receipt"
);
assert!(
subjektiv_journal::release_subject(&api, serde_json::from_value(incomplete).unwrap())
.is_err(),
"cancelled release identity must not rearm"
);
conn.execute(
"UPDATE backend_jobs SET state='completed',result_json=?3,result_digest=?4 WHERE workspace_id=?1 AND job_id=?2",
rusqlite::params![TEST_WORKSPACE_ID, uncertain_job, saved_result.0, saved_result.1],
)
.unwrap();
// The new release below must reacquire the genuine Controller guard. It cannot
// succeed if the failed pre-revoke operation left the body frozen.
let release = subjektiv_journal::release_subject(&api, serde_json::from_value(json!({
"worker":old,"binding_id":old_binding.binding_id,"operation_id":"connected-normal-release",
})).unwrap()).unwrap();
assert_eq!(
release.state,
runtime_api::SubjectReleaseState::Disconnected
);
assert!(!store.journal_writer_active(&old_binding).unwrap());
let key = crate::subjektiv::subject_worker_singleton_key(&subject.id).unwrap();
assert!(
api.store
.current_worker_singleton_owner(TEST_WORKSPACE_ID, &key)
.unwrap()
.is_none()
);
let stopped = api
.runtime
.stop_worker(&old, WorkerLifecycleRequest::default())
.unwrap();
assert_eq!(stopped.state, InternalWorkerOperationState::Accepted);
let removed = WorkerRemovalService::new(&api)
.execute_target_removal(
api.runtime.as_ref(),
&old,
"connected regression removes prior body",
None,
)
.await
.unwrap();
assert_eq!(removed.status, StatusCode::OK.as_u16(), "{}", removed.body);
assert!(api.runtime.worker(&old).is_err());
let original_root = api
.config
.embedded_runtime_store_root
.join("workers")
.join(&old.worker_id)
.join("session");
assert!(
!original_root.exists(),
"prior Worker original Session must actually be removed before startup"
);
// Purge retained original archive through Runtime, not fixture filesystem deletion.
// Journal text is independent and remains readable for new-body startup.
if let Some(archive) = api
.config_store
.worker_tombstone(TEST_WORKSPACE_ID, &old)
.unwrap()
.and_then(|t| t.archive_id)
{
api.runtime
.session_material_erase(
&old.runtime_id,
runtime_api::SessionMaterialEraseRequest {
workspace_id: TEST_WORKSPACE_ID.into(),
source: runtime_api::SessionPublicSource::Archived {
archive_id: archive.clone(),
},
expected_session_id: old_attr.session_id.clone(),
},
)
.unwrap();
assert!(
!api.config
.embedded_runtime_store_root
.join("archives/workers")
.join(archive)
.join("session")
.exists()
);
}
let next = connected_create(&api, &subject.id, "connected-next-body").await;
assert_ne!(next, old);
let next_attr = store
.subject_session_attributions(&subject.id)
.unwrap()
.into_iter()
.find(|attr| attr.worker_id == next.worker_id)
.unwrap();
assert_ne!(next_attr.session_id, old_attr.session_id);
connected_wait_for_recording(&api, &store, &next_attr).await;
// Read actual Session files produced by the real Controller; no fixture Session append,
// startup-item injection, fake disconnect or store-only simulation is used in this test.
use session_store::Store as _;
let session = session_store::WorkerSessionStore::open_read_only(
api.config
.embedded_runtime_store_root
.join("workers")
.join(&next.worker_id)
.join("session"),
)
.unwrap();
let session_id = next_attr.session_id.parse().unwrap();
let segments = session.list_segments(session_id).unwrap();
assert_eq!(segments.len(), 1);
let entries = session.read_all_read_only(session_id, segments[0]).unwrap();
let startup: Vec<_> = entries
.iter()
.enumerate()
.filter_map(|(index, log)| match log {
session_store::LogEntry::AnnotatedSystemItem { entry, .. } => match &entry.item {
session_store::SystemItem::JournalStartup {
snapshot,
snapshot_sha256,
body,
prompt_provenance,
} => Some((index, snapshot, snapshot_sha256, body, prompt_provenance)),
_ => None,
},
_ => None,
})
.collect();
assert_eq!(startup.len(), 1, "startup must be durably committed once");
let (startup_index, snapshot, digest, body, provenance) = startup[0];
let first_input = entries
.iter()
.position(|entry| matches!(entry, session_store::LogEntry::AnnotatedUserInput { .. }))
.expect("new body persisted its initial human input");
assert!(
startup_index < first_input,
"startup reference must be persisted before the first human input/model request"
);
assert_eq!(snapshot["subject_id"], subject.id);
assert_eq!(
snapshot["journals"][0]["source_session_id"], old_attr.session_id,
"Host-selected writer predecessor must lead the snapshot"
);
assert!(body.contains("CONNECTED_HANDOFF"));
assert_eq!(
provenance.logical_name,
"internal.subjektiv_journal_startup"
);
assert_eq!(
*digest,
Sha256::digest(serde_json::to_vec(snapshot).unwrap())
.iter()
.map(|byte| format!("{byte:02x}"))
.collect::<String>()
);
let dialogs = model.dialogs.lock().unwrap();
assert!(dialogs.len() >= 2);
assert!(
dialogs[1]
.items
.iter()
.filter_map(Item::as_text)
.any(|text| text.contains(body)),
"first new-body model request must contain the same persisted startup body"
);
drop(dialogs);
let requests = requests.lock().unwrap();
assert_eq!(
requests
.iter()
.filter(
|(path, status)| path.ends_with("/subjektiv/journal/startup")
&& *status == StatusCode::OK
)
.count(),
2,
"both bodies must use the authenticated HTTP Host startup reader"
);
assert!(
requests.iter().any(
|(path, status)| path.ends_with("/subjektiv/sessions") && *status == StatusCode::OK
)
);
drop(requests);
server.abort();
}
@@ -8,6 +8,19 @@ use sha2::{Digest, Sha256};
use subjektiv::journal::transport::*;
use subjektiv::journal::*;
#[path = "subjektiv_journal_connected_tests.rs"]
mod connected;
fn bind_fixture_session(
store: &crate::subjektiv::SubjektivStore,
attr: crate::subjektiv::SubjectSessionAttribution,
) -> JournalHostContext {
let binding = store
.claim_journal_writer(&attr.subject_id, &attr.runtime_id, &attr.worker_id)
.unwrap();
store.bind_journal_session(&binding, attr).unwrap()
}
#[tokio::test]
async fn journal_job_reserves_before_binding_and_retains_partial_receipts_on_cancel() {
let Fixture {
@@ -195,7 +208,7 @@ impl Fixture {
.unwrap(),
)
.unwrap();
let host = JournalHostContext::validated_session(&store, attr.clone()).unwrap();
let host = bind_fixture_session(&store, attr.clone());
let text = "original public input";
let range = JournalSourceRange {
source: JournalSource {
@@ -228,6 +241,7 @@ impl Fixture {
profile: "builtin:subjektiv-journal-recording".into(),
grants: BackendJobGrants {
subjektiv_journal: Some(SubjektivJournalGrant {
binding_id: host.writer_binding_id().unwrap().into(),
subject_id: subject.id,
session_id: attr.session_id,
journal_id: id.clone(),
@@ -288,13 +302,14 @@ async fn journal_completion_replays_only_saved_checkpoint_and_fences_late_operat
panic!("receipt required")
};
assert!(subjektiv_journal::finish(&f.api, worker).is_err());
let position = save_fixture_position(&f.api, worker, receipt.state_token);
subjektiv_journal::execute(
&f.api,
worker,
JournalJobRequest::Execute {
operation: JournalOperation {
request_id: "checkpoint".into(),
expected_state: receipt.state_token,
expected_state: position,
action: JournalAction::Checkpoint {
record_operation_ids: vec!["no-change".into()],
},
@@ -332,6 +347,69 @@ async fn journal_completion_replays_only_saved_checkpoint_and_fences_late_operat
);
assert!(subjektiv_journal::execute(&f.api, worker, JournalJobRequest::ReadState).is_err());
assert!(!f.store.journal_retention_required(&f.host).unwrap());
// Store authority boundary: Host Job-state gating is separate from revocation itself.
let grant = f.request.grants.subjektiv_journal.as_ref().unwrap();
let attr = f
.store
.session_attribution(&grant.session_id)
.unwrap()
.unwrap();
let binding = JournalWriterBinding {
subject_id: attr.subject_id.clone(),
runtime_id: attr.runtime_id.clone(),
worker_id: attr.worker_id.clone(),
binding_id: grant.binding_id.clone(),
};
f.store.release_journal_writer(&binding).unwrap();
let next = f
.store
.claim_journal_writer(&attr.subject_id, &attr.runtime_id, &attr.worker_id)
.unwrap();
assert_ne!(next.binding_id, binding.binding_id);
assert!(f.store.bind_journal_session(&next, attr).is_err());
let mut stale = dispatched.clone();
stale
.job
.request
.grants
.subjektiv_journal
.as_mut()
.unwrap()
.binding_id = next.binding_id;
assert!(subjektiv_journal::prepare(&f.api, &stale).is_err());
assert!(
f.store
.query_journal_operation(&f.host, &f.id, "no-change")
.unwrap()
.is_some()
);
// Exact saved completion is still recoverable; it is not a new write grant.
subjektiv_journal::validate_result(&f.api, &f.request, &dispatched.attempt, &value).unwrap();
}
fn save_fixture_position(
api: &WorkspaceApi,
worker: &RuntimeWorkerRef,
expected_state: String,
) -> String {
let JournalJobResponse::Saved(receipt) = subjektiv_journal::execute(
api,
worker,
JournalJobRequest::Execute {
operation: JournalOperation {
request_id: "position".into(),
expected_state,
action: JournalAction::UpdatePosition {
current_position: "No activity change; work remains pending.".into(),
},
covered_ranges: vec![],
},
},
)
.unwrap() else {
panic!("saved position receipt required")
};
receipt.state_token
}
fn deliver_fixture_source(
@@ -483,7 +561,7 @@ impl RuntimeFixture {
.unwrap(),
)
.unwrap();
let host = JournalHostContext::validated_session(&store, attr.clone()).unwrap();
let host = bind_fixture_session(&store, attr.clone());
let src = runtime_api::SessionPublicSource::Retained {
worker_id: worker_runtime::identity::WorkerId::parse(&source.worker_id).unwrap(),
};
@@ -543,13 +621,15 @@ impl RuntimeFixture {
.accept_journal_sources(&host, "source", &[range.clone()])
.unwrap();
let request = subjektiv_journal::recording_request(
&host,
attr,
&id,
vec![subjektiv::journal::recording::JournalMaterial {
range: range.clone(),
text: SOURCE_SENTINEL.into(),
}],
);
)
.unwrap();
assert!(
!serde_json::to_string(&request)
.unwrap()
@@ -650,13 +730,14 @@ impl RuntimeFixture {
.unwrap() else {
panic!("receipt")
};
let position = save_fixture_position(&self.api, worker, receipt.state_token);
subjektiv_journal::execute(
&self.api,
worker,
JournalJobRequest::Execute {
operation: JournalOperation {
request_id: "checkpoint".into(),
expected_state: receipt.state_token,
expected_state: position,
action: JournalAction::Checkpoint {
record_operation_ids: vec!["record".into()],
},
@@ -1661,7 +1742,7 @@ impl SubjectRemovalFixture {
.unwrap(),
)
.unwrap();
let host = JournalHostContext::validated_session(&store, attr.clone()).unwrap();
let host = bind_fixture_session(&store, attr.clone());
seed_worker_control_grant(&api, &controller, &target, "subject-removal-fixture");
api.runtime
.stop_worker(
@@ -1696,6 +1777,43 @@ impl SubjectRemovalFixture {
registry.profile = Some("builtin:default".into());
self.api.store.upsert_worker_registry(&registry).unwrap();
}
/// Seed the historical release boundary for retention-recovery component tests.
/// This deterministic stopped-Worker fixture has no Controller and does NOT
/// prove disconnect. The connected test below exercises actual release proof.
fn seed_completed_release_for_archive_recovery(&self) {
let lease = self
.api
.store
.require_current_worker_singleton_owner(TEST_WORKSPACE_ID, &self.target)
.unwrap()
.unwrap();
let binding = JournalWriterBinding {
subject_id: self.attr.subject_id.clone(),
runtime_id: self.target.runtime_id.clone(),
worker_id: self.target.worker_id.clone(),
binding_id: self.host.writer_binding_id().unwrap().into(),
};
let operation = "historical-release-retention-fixture";
self.api
.config_store
.reserve_subject_release(TEST_WORKSPACE_ID, &lease, &binding.binding_id, operation)
.unwrap();
self.store.release_journal_writer(&binding).unwrap();
self.api
.config_store
.confirm_subject_release_proof(
TEST_WORKSPACE_ID,
&lease,
&binding.binding_id,
operation,
false,
)
.unwrap();
self.api
.config_store
.release_subject_singleton(TEST_WORKSPACE_ID, &lease, &binding.binding_id, operation)
.unwrap();
}
fn remove(&self) -> worker::WorkspaceResponse {
let key = self
.api
@@ -1714,6 +1832,37 @@ impl SubjectRemovalFixture {
}
}
#[tokio::test]
async fn subject_release_never_substitutes_catalog_idle_or_stopped_for_controller_reservation() {
let f = SubjectRemovalFixture::new().await;
let input = serde_json::from_value(serde_json::json!({
"worker":f.target,"binding_id":f.host.writer_binding_id().unwrap(),"operation_id":"actual-controller-required",
})).unwrap();
let error = subjektiv_journal::release_subject(&f.api, input).unwrap_err();
assert!(
error.to_string().contains("actual") || error.to_string().contains("Controller"),
"{error}"
);
let key = crate::subjektiv::subject_worker_singleton_key(&f.attr.subject_id).unwrap();
assert_eq!(
f.api
.store
.current_worker_singleton_owner(TEST_WORKSPACE_ID, &key)
.unwrap()
.unwrap()
.worker,
f.target
);
let binding = JournalWriterBinding {
subject_id: f.attr.subject_id.clone(),
runtime_id: f.target.runtime_id.clone(),
worker_id: f.target.worker_id.clone(),
binding_id: f.host.writer_binding_id().unwrap().into(),
};
assert!(f.store.journal_writer_active(&binding).unwrap());
assert!(f.store.journal_for_session(&f.host).unwrap().is_none());
}
#[tokio::test]
async fn disabled_subject_removal_does_not_admit_new_public_history_or_reserve_a_job() {
let f = SubjectRemovalFixture::new().await;
@@ -1721,7 +1870,7 @@ async fn disabled_subject_removal_does_not_admit_new_public_history_or_reserve_a
let response = f.remove();
assert_eq!(
response.status,
StatusCode::OK.as_u16(),
StatusCode::CONFLICT.as_u16(),
"{}",
response.body
);
@@ -1730,7 +1879,7 @@ async fn disabled_subject_removal_does_not_admit_new_public_history_or_reserve_a
.store
.get_worker_registry(TEST_WORKSPACE_ID, &f.target)
.unwrap()
.is_none()
.is_some()
);
assert!(f.store.journal_for_session(&f.host).unwrap().is_none());
assert!(
@@ -1795,7 +1944,7 @@ async fn disabled_subject_removal_retains_only_the_already_admitted_prefix() {
let response = f.remove();
assert_eq!(
response.status,
StatusCode::OK.as_u16(),
StatusCode::CONFLICT.as_u16(),
"{}",
response.body
);
@@ -1804,62 +1953,25 @@ async fn disabled_subject_removal_retains_only_the_already_admitted_prefix() {
.store
.get_worker_registry(TEST_WORKSPACE_ID, &f.target)
.unwrap()
.is_none()
.is_some()
);
let jobs = f
.api
.store
.list_backend_journal_jobs(TEST_WORKSPACE_ID, &f.attr.session_id, None, 100)
.unwrap();
assert_eq!(jobs.len(), 1);
assert_eq!(
jobs[0]
.request
.grants
.subjektiv_journal
.as_ref()
assert!(
f.api
.store
.list_backend_journal_jobs(TEST_WORKSPACE_ID, &f.attr.session_id, None, 100)
.unwrap()
.selected_ranges,
ranges[..1]
.is_empty()
);
assert_eq!(
f.store.journal_for_session(&f.host).unwrap().as_deref(),
Some(id.as_str())
);
assert!(f.store.journal_retention_required(&f.host).unwrap());
let archive = f
.api
.config_store
.worker_session_archive(TEST_WORKSPACE_ID, &f.target, &f.attr.session_id)
.unwrap()
.unwrap();
assert!(!session_root(f.dir.path(), &f.target).exists());
let source = runtime_api::SessionPublicSource::Archived {
archive_id: archive.archive_id,
};
let response = f
.api
.runtime
.session_material_catalog(
&f.target.runtime_id,
runtime_api::SessionMaterialCatalogRequest {
workspace_id: TEST_WORKSPACE_ID.into(),
source,
expected_session_id: f.attr.session_id.clone(),
expected_catalog_sha256: None,
offset: 0,
limit: 100,
},
)
.unwrap();
assert!(matches!(
response,
runtime_api::SessionMaterialCatalogAvailability::Page { .. }
));
assert!(session_root(f.dir.path(), &f.target).exists());
}
#[tokio::test]
async fn attributed_unmaterialized_subject_removal_needs_no_journal_job() {
async fn attributed_unmaterialized_subject_still_requires_actual_idle_release() {
let f = SubjectRemovalFixture::with_materialized_source(false).await;
let retention = source_retention(f.dir.path());
let session = f.attr.session_id.parse().unwrap();
@@ -1882,28 +1994,21 @@ async fn attributed_unmaterialized_subject_removal_needs_no_journal_job() {
.unwrap();
assert_eq!(retention.pins(session).unwrap().len(), 1);
let response = f.remove();
assert!(retention.pins(session).unwrap().is_empty());
assert_eq!(
response.status,
StatusCode::OK.as_u16(),
StatusCode::CONFLICT.as_u16(),
"{}",
response.body
);
assert_eq!(retention.pins(session).unwrap().len(), 1);
assert!(
f.api
.store
.get_worker_registry(TEST_WORKSPACE_ID, &f.target)
.unwrap()
.is_none()
.is_some()
);
assert!(f.store.journal_for_session(&f.host).unwrap().is_none());
assert!(
f.api
.store
.list_backend_journal_jobs(TEST_WORKSPACE_ID, &f.attr.session_id, None, 100)
.unwrap()
.is_empty()
);
}
#[tokio::test]
@@ -2237,6 +2342,7 @@ async fn assert_erased_archive_plan_replans_without_republishing_sources(
) {
let mut f = SubjectRemovalFixture::new().await;
f.disable();
f.seed_completed_release_for_archive_recovery();
let policy = f
.api
.config_store
@@ -2331,6 +2437,7 @@ journal_recovery_cases! {
async fn assert_erased_completed_archive_receipt_is_audited_without_republishing(restart: bool) {
let mut f = SubjectRemovalFixture::new().await;
f.disable();
f.seed_completed_release_for_archive_recovery();
let conn = rusqlite::Connection::open(&f.api.config.database_path).unwrap();
// Runtime has committed source_removed, but Backend commit/response is lost.
conn.execute_batch("CREATE TRIGGER interrupt_removal BEFORE DELETE ON worker_registry BEGIN SELECT RAISE(ABORT,'injected metadata commit interruption'); END;").unwrap();
@@ -2406,6 +2513,7 @@ journal_recovery_cases! {
async fn erased_archive_without_receipt_or_inventory_remains_unknown_without_replanning() {
let f = SubjectRemovalFixture::new().await;
f.disable();
f.seed_completed_release_for_archive_recovery();
let conn = rusqlite::Connection::open(&f.api.config.database_path).unwrap();
conn.execute_batch("CREATE TRIGGER interrupt_removal BEFORE UPDATE OF state ON worker_removal_operations WHEN NEW.state='executing' BEGIN SELECT RAISE(ABORT,'injected interruption'); END;").unwrap();
assert_ne!(f.remove().status, StatusCode::OK.as_u16());
@@ -2465,6 +2573,7 @@ async fn assert_erased_archive_retained_diagnostics_receipt_finalizes_only_after
) {
let mut f = SubjectRemovalFixture::new().await;
f.disable();
f.seed_completed_release_for_archive_recovery();
let policy = f
.api
.config_store
@@ -2662,3 +2771,36 @@ journal_recovery_cases! {
erased_archive_retained_diagnostics_reopen_finalizes_only_after_physical_ack => (true, false),
erased_archive_retained_diagnostics_interrupted_reopen_finalizes_only_after_physical_ack => (true, true),
}
#[tokio::test]
async fn legacy_journal_grant_preserves_old_json_and_only_recovers_original_current_body() {
let f = SubjectRemovalFixture::new().await;
let old = json!({"subject_id":f.attr.subject_id,"session_id":f.attr.session_id,"journal_id":"legacy-journal","selected_ranges":[]});
let wire = serde_json::to_string(&old).unwrap();
let grant: SubjektivJournalGrant = serde_json::from_str(&wire).unwrap();
assert!(grant.binding_id.is_empty());
assert_eq!(
serde_json::to_value(&grant).unwrap(),
old,
"old canonical request/digest must not acquire a new empty binding_id field"
);
assert_eq!(
serde_json::to_string(&serde_json::to_value(&grant).unwrap()).unwrap(),
wire
);
assert!(subjektiv_journal::host(&f.api, &f.store, &grant).is_ok());
let writer = JournalWriterBinding {
subject_id: f.attr.subject_id.clone(),
runtime_id: f.attr.runtime_id.clone(),
worker_id: f.attr.worker_id.clone(),
binding_id: f.host.writer_binding_id().unwrap().into(),
};
f.store.release_journal_writer(&writer).unwrap();
f.store
.claim_journal_writer(&f.attr.subject_id, &f.attr.runtime_id, "different-body")
.unwrap();
assert!(
subjektiv_journal::host(&f.api, &f.store, &grant).is_err(),
"legacy grant must never map to a later writer"
);
}
@@ -126,7 +126,10 @@ impl Fixture {
.unwrap(),
)
.unwrap();
let host = JournalHostContext::validated_session(&store, attr.clone()).unwrap();
let writer = store
.claim_journal_writer(&subject.id, RUNTIME, &source_worker.worker_id)
.unwrap();
let host = store.bind_journal_session(&writer, attr.clone()).unwrap();
let journal_id = store
.accept_journal_sources(&host, "original activity", &ranges)
.unwrap();
+368 -4
View File
@@ -119,7 +119,7 @@ pub use ticket::{
};
const OLDEST_SCHEMA_VERSION: i64 = 50;
const LATEST_SCHEMA_VERSION: i64 = 100;
const LATEST_SCHEMA_VERSION: i64 = 101;
const WORKSPACE_DRIVE_GRANTS_MIGRATION_NAME: &str = "durable Workspace Drive grants";
const WORKSPACE_CONFIG_GRANTS_MIGRATION_NAME: &str = "Workspace config grants and logical Workdirs";
const SCHEMA_BASELINE_NAME: &str = "workspace schema baseline";
@@ -427,6 +427,11 @@ const MIGRATIONS: &[Migration] = &[
name: "Provisional users and scoped passkey activation",
apply: activation::migrate,
},
Migration {
version: 101,
name: "Durable explicit Subject body release",
apply: migrate_subject_release_v100_to_v101,
},
];
#[derive(Clone, Copy)]
@@ -1954,6 +1959,15 @@ pub trait ControlPlaneStore: Send + Sync + WorkspaceDeletionStore {
workspace_id: &str,
singleton_key: &str,
) -> Result<Option<WorkerSingletonLease>>;
fn retained_worker_singleton_lease(
&self,
_workspace_id: &str,
_worker: &RuntimeWorkerRef,
) -> Result<Option<WorkerSingletonLease>> {
Err(Error::Store(
"retained singleton lookup is unavailable".into(),
))
}
fn require_current_worker_singleton_owner(
&self,
workspace_id: &str,
@@ -3164,6 +3178,156 @@ impl SqliteWorkspaceStore {
})
}
/// Retained identity for lifecycle/retention reconciliation only, never execution authority.
pub(crate) fn retained_worker_singleton_lease(
&self,
workspace_id: &str,
worker: &RuntimeWorkerRef,
) -> Result<Option<WorkerSingletonLease>> {
self.with_conn(|conn| {
let row: Option<(Option<String>, Option<i64>)> = conn.query_row(
"SELECT singleton_key,singleton_generation FROM worker_create_reservations WHERE workspace_id=?1 AND runtime_id=?2 AND worker_id=?3",
params![workspace_id,worker.runtime_id,worker.worker_id], |r| Ok((r.get(0)?,r.get(1)?)),
).optional()?;
match row {
Some((Some(key),Some(generation))) => Ok(Some(WorkerSingletonLease {
key, generation: generation.try_into().map_err(|_| Error::Store("invalid singleton generation".into()))?, worker:worker.clone(),
})),
Some((None,None)) | None => Ok(None),
_ => Err(Error::Store("incomplete singleton lease".into())),
}
})
}
pub(crate) fn retained_subject_release_operation(
&self,
workspace: &str,
lease: &WorkerSingletonLease,
binding: &str,
) -> Result<Option<String>> {
self.with_conn(|c| Ok(c.query_row("SELECT operation_id FROM subject_worker_releases WHERE workspace_id=?1 AND worker_id=?2 AND runtime_id=?3 AND singleton_generation=?4 AND binding_id=?5 AND cancelled=0",params![workspace,lease.worker.worker_id,lease.worker.runtime_id,lease.generation as i64,binding],|r|r.get(0)).optional()?))
}
pub(crate) fn subject_release_pending(
&self,
workspace: &str,
worker: &RuntimeWorkerRef,
) -> Result<bool> {
self.with_conn(|c| Ok(c.query_row("SELECT EXISTS(SELECT 1 FROM subject_worker_releases WHERE workspace_id=?1 AND runtime_id=?2 AND worker_id=?3 AND cancelled=0 AND completed=0)",params![workspace,worker.runtime_id,worker.worker_id],|r|r.get(0))?))
}
pub(crate) fn require_subject_release_intent(
&self,
workspace: &str,
lease: &WorkerSingletonLease,
binding: &str,
operation: &str,
) -> Result<()> {
self.with_conn(|c| {
let active: bool = c.query_row("SELECT EXISTS(SELECT 1 FROM subject_worker_releases WHERE workspace_id=?1 AND runtime_id=?2 AND worker_id=?3 AND singleton_key=?4 AND singleton_generation=?5 AND binding_id=?6 AND operation_id=?7 AND cancelled=0)",params![workspace,lease.worker.runtime_id,lease.worker.worker_id,lease.key,lease.generation as i64,binding,operation],|r|r.get(0))?;
if !active {return Err(Error::RepositoryConflict("Subject release intent absent, cancelled or mismatched".into()));} Ok(())
})
}
pub(crate) fn reserve_subject_release(
&self,
workspace: &str,
lease: &WorkerSingletonLease,
binding: &str,
operation: &str,
) -> Result<()> {
if binding.trim().is_empty()
|| operation.trim().is_empty()
|| binding.len() > 512
|| operation.len() > 512
{
return Err(Error::InvalidInput(
"invalid Subject release identity".into(),
));
}
self.with_conn_mut(|c| {
let tx=c.transaction_with_behavior(TransactionBehavior::Immediate)?;
let saved: Option<(String,String,String,u64,String,bool)> = tx.query_row("SELECT runtime_id,worker_id,singleton_key,singleton_generation,binding_id,cancelled FROM subject_worker_releases WHERE workspace_id=?1 AND operation_id=?2",params![workspace,operation],|r|Ok((r.get(0)?,r.get(1)?,r.get(2)?,r.get(3)?,r.get(4)?,r.get(5)?))).optional()?;
if let Some(saved)=saved {
if saved != (lease.worker.runtime_id.clone(),lease.worker.worker_id.clone(),lease.key.clone(),lease.generation,binding.into(),false) {return Err(Error::RepositoryConflict("release operation mismatched or terminally cancelled; use a new operation".into()));} return Ok(());
}
require_current_worker_singleton_lease(&tx,workspace,lease)?;
let busy: bool=tx.query_row("SELECT EXISTS(SELECT 1 FROM subject_worker_releases WHERE workspace_id=?1 AND worker_id=?2 AND cancelled=0)",params![workspace,lease.worker.worker_id],|r|r.get(0))?;
if busy {return Err(Error::RepositoryConflict("another Subject release intent is active".into()));}
tx.execute("INSERT INTO subject_worker_releases(workspace_id,runtime_id,worker_id,singleton_key,singleton_generation,binding_id,operation_id) VALUES(?1,?2,?3,?4,?5,?6,?7)",params![workspace,lease.worker.runtime_id,lease.worker.worker_id,lease.key,lease.generation as i64,binding,operation])?;
tx.commit()?; Ok(())
})
}
pub(crate) fn cancel_subject_release(
&self,
workspace: &str,
lease: &WorkerSingletonLease,
binding: &str,
operation: &str,
) -> Result<()> {
self.with_conn(|c| {
let changed=c.execute("UPDATE subject_worker_releases SET cancelled=1 WHERE workspace_id=?1 AND runtime_id=?2 AND worker_id=?3 AND singleton_generation=?4 AND binding_id=?5 AND operation_id=?6 AND proof IS NULL AND completed=0",params![workspace,lease.worker.runtime_id,lease.worker.worker_id,lease.generation as i64,binding,operation])?;
if changed!=1 {return Err(Error::RepositoryConflict("cannot confirm cancellation of different or committed Subject release".into()));} Ok(())
})
}
pub(crate) fn subject_release_proven(
&self,
workspace: &str,
lease: &WorkerSingletonLease,
binding: &str,
operation: &str,
) -> Result<bool> {
self.with_conn(|c|Ok(c.query_row("SELECT EXISTS(SELECT 1 FROM subject_worker_releases WHERE workspace_id=?1 AND runtime_id=?2 AND worker_id=?3 AND singleton_key=?4 AND singleton_generation=?5 AND binding_id=?6 AND operation_id=?7 AND proof IS NOT NULL AND cancelled=0)",params![workspace,lease.worker.runtime_id,lease.worker.worker_id,lease.key,lease.generation as i64,binding,operation],|r|r.get(0))?))
}
pub(crate) fn confirm_subject_release_proof(
&self,
workspace: &str,
lease: &WorkerSingletonLease,
binding: &str,
operation: &str,
erased: bool,
) -> Result<()> {
let proof = if erased {
"erased"
} else {
"runtime_disconnected"
};
self.with_conn(|c| {
let changed=c.execute("UPDATE subject_worker_releases SET proof=?8 WHERE workspace_id=?1 AND runtime_id=?2 AND worker_id=?3 AND singleton_key=?4 AND singleton_generation=?5 AND binding_id=?6 AND operation_id=?7 AND cancelled=0 AND (proof IS NULL OR proof=?8)",params![workspace,lease.worker.runtime_id,lease.worker.worker_id,lease.key,lease.generation as i64,binding,operation,proof])?;
if changed!=1 {return Err(Error::RepositoryConflict("Subject release proof does not match active intent".into()));} Ok(())
})
}
pub(crate) fn release_subject_singleton(
&self,
workspace: &str,
lease: &WorkerSingletonLease,
binding: &str,
operation: &str,
) -> Result<()> {
self.require_subject_release_intent(workspace, lease, binding, operation)?;
if !lease
.key
.starts_with(crate::subjektiv::SUBJECT_WORKER_SINGLETON_PREFIX)
{
return Err(Error::InvalidInput("not a Subject singleton".into()));
}
self.with_conn_mut(|c| {
let tx=c.transaction_with_behavior(TransactionBehavior::Immediate)?;
let (proof,completed,cancelled):(Option<String>,bool,bool)=tx.query_row("SELECT proof,completed,cancelled FROM subject_worker_releases WHERE workspace_id=?1 AND operation_id=?2",params![workspace,operation],|r|Ok((r.get(0)?,r.get(1)?,r.get(2)?)))?;
if cancelled || proof.is_none() {return Err(Error::RepositoryConflict("Subject release lacks confirmed proof; retain occupancy".into()));}
if completed {return Ok(());}
require_current_worker_singleton_lease(&tx,workspace,lease)?;
tx.execute("UPDATE subject_worker_releases SET completed=1 WHERE workspace_id=?1 AND operation_id=?2 AND cancelled=0",params![workspace,operation])?;
let changed=tx.execute("UPDATE worker_create_reservations SET state='removed',updated_at=?6 WHERE workspace_id=?1 AND runtime_id=?2 AND worker_id=?3 AND singleton_key=?4 AND singleton_generation=?5 AND state IN ('reserved','created','removed')",params![workspace,lease.worker.runtime_id,lease.worker.worker_id,lease.key,lease.generation as i64,chrono::Utc::now().to_rfc3339()])?;
if changed!=1 {return Err(Error::RepositoryConflict("Subject owner changed before release".into()));}
tx.commit()?; Ok(())
})
}
pub(crate) fn current_worker_singleton_owner(
&self,
workspace_id: &str,
@@ -7931,6 +8095,14 @@ impl ControlPlaneStore for SqliteWorkspaceStore {
SqliteWorkspaceStore::current_worker_singleton_owner(self, workspace_id, singleton_key)
}
fn retained_worker_singleton_lease(
&self,
workspace_id: &str,
worker: &RuntimeWorkerRef,
) -> Result<Option<WorkerSingletonLease>> {
SqliteWorkspaceStore::retained_worker_singleton_lease(self, workspace_id, worker)
}
fn require_current_worker_singleton_owner(
&self,
workspace_id: &str,
@@ -14840,6 +15012,22 @@ fn migrate_result_reports_v97_to_v98(conn: &Connection) -> Result<()> {
Ok(())
}
fn migrate_subject_release_v100_to_v101(conn: &Connection) -> Result<()> {
if current_schema_version(conn)? != 100 {
return Err(Error::Store(
"Subject release migration requires schema 100".into(),
));
}
let tx = conn.unchecked_transaction()?;
tx.execute_batch(include_str!("subject_release_schema.sql"))?;
tx.execute(
"INSERT INTO __yoi_schema_migrations(version,name) VALUES(101,?1)",
["Durable explicit Subject body release"],
)?;
tx.commit()?;
Ok(())
}
fn migrate_hosting_policy_v98_to_v99(conn: &Connection) -> Result<()> {
if current_schema_version(conn)? != 98 {
return Err(Error::Store(
@@ -15521,7 +15709,10 @@ fn current_worker_singleton_owner(
) -> Result<Option<(WorkerSingletonLease, Option<String>)>> {
let owner = conn
.query_row(
"SELECT owner.runtime_id, owner.worker_id, owner.generation, reservation.state \
"SELECT owner.runtime_id, owner.worker_id, owner.generation, \
CASE WHEN substr(owner.singleton_key,1,10)='subjektiv:' AND reservation.state='removed' \
AND NOT EXISTS(SELECT 1 FROM subject_worker_releases r WHERE r.workspace_id=owner.workspace_id AND r.worker_id=owner.worker_id AND r.runtime_id=owner.runtime_id AND r.singleton_key=owner.singleton_key AND r.singleton_generation=owner.generation AND r.completed=1) \
THEN 'created' ELSE reservation.state END \
FROM worker_singleton_owners owner \
LEFT JOIN worker_create_reservations reservation \
ON reservation.workspace_id = owner.workspace_id \
@@ -18547,6 +18738,7 @@ CREATE UNIQUE INDEX ticket_current_worker_role_idx
WorkspaceSchemaMigrationStep {version:98, name:"Assignment-independent implementation reports".into()},
WorkspaceSchemaMigrationStep {version:99, name:"Explicit hosting administrators and self-registration policy".into()},
WorkspaceSchemaMigrationStep {version:100, name:"Provisional users and scoped passkey activation".into()},
WorkspaceSchemaMigrationStep {version:101, name:"Durable explicit Subject body release".into()},
]
);
@@ -18657,6 +18849,7 @@ CREATE UNIQUE INDEX ticket_current_worker_role_idx
(98, "Assignment-independent implementation reports".to_string()),
(99, "Explicit hosting administrators and self-registration policy".to_string()),
(100, "Provisional users and scoped passkey activation".to_string()),
(101, "Durable explicit Subject body release".to_string()),
]
);
assert!(!table_exists(conn, "trusted_runtime_records")?);
@@ -20734,7 +20927,8 @@ CREATE UNIQUE INDEX ticket_current_worker_role_idx
.await
.unwrap();
}
let key = "subjektiv:主体 id/?!";
// Generic singleton lifecycle; Subject-prefixed keys have a separate explicit release contract.
let key = "fixture:主体 id/?!";
let memory_a = store.get_workspace_memory_settings("workspace-a").unwrap();
let first = store
.reserve_worker_create(
@@ -20842,6 +21036,175 @@ CREATE UNIQUE INDEX ticket_current_worker_role_idx
);
}
#[test]
fn subject_release_migration_rolls_back_schema_and_marker_then_reopens() {
let dir = tempfile::tempdir().unwrap();
let database = dir.path().join("server.db");
prepare_retained_schema(&database, 100);
let conn = Connection::open(&database).unwrap();
configure_sqlite(&conn).unwrap();
conn.execute_batch("CREATE TRIGGER interrupt_subject_release_migration BEFORE INSERT ON __yoi_schema_migrations WHEN NEW.version=101 BEGIN SELECT RAISE(ABORT,'injected Subject release migration interruption'); END;").unwrap();
assert!(migrate_subject_release_v100_to_v101(&conn).is_err());
assert_eq!(current_schema_version(&conn).unwrap(), 100);
assert!(!table_exists(&conn, "subject_worker_releases").unwrap());
conn.execute_batch("DROP TRIGGER interrupt_subject_release_migration")
.unwrap();
migrate_subject_release_v100_to_v101(&conn).unwrap();
assert_eq!(current_schema_version(&conn).unwrap(), 101);
assert!(table_exists(&conn, "subject_worker_releases").unwrap());
assert!(migrate_subject_release_v100_to_v101(&conn).is_err());
drop(conn);
let reopened = SqliteWorkspaceStore::open_for_test(database).unwrap();
reopened
.with_conn(|conn| verify_schema_history(conn, 101))
.unwrap();
}
#[tokio::test]
async fn subject_generic_removal_retains_occupancy_until_exact_explicit_release_receipt() {
let dir = tempfile::tempdir().unwrap();
let database = dir.path().join("server.db");
let store = SqliteWorkspaceStore::open_api_fixture(&database).unwrap();
store
.upsert_workspace(&WorkspaceRecord {
workspace_id: "workspace-a".into(),
owner_account_id: "owner-account".into(),
display_name: "Workspace A".into(),
state: "active".into(),
created_at: "2026-10-01T00:00:00Z".into(),
updated_at: "2026-10-01T00:00:00Z".into(),
})
.await
.unwrap();
let memory = store.get_workspace_memory_settings("workspace-a").unwrap();
let key = "subjektiv:subject-a";
let first = store
.reserve_worker_create(
"workspace-a",
"runtime-a",
"first",
"sha256:first",
Some(key),
&memory,
)
.unwrap();
let lease = first.singleton.unwrap();
store
.fail_worker_create_reservation(
"workspace-a",
"runtime-a",
first.worker_id,
&first.create_fingerprint,
)
.unwrap();
assert_eq!(
store
.current_worker_singleton_owner("workspace-a", key)
.unwrap(),
Some(lease.clone())
);
assert!(
store
.reserve_worker_create(
"workspace-a",
"runtime-b",
"early",
"sha256:early",
Some(key),
&memory
)
.is_err()
);
store
.reserve_subject_release("workspace-a", &lease, "binding-a", "cancelled-first")
.unwrap();
assert!(
store
.subject_release_pending("workspace-a", &lease.worker)
.unwrap()
);
store
.cancel_subject_release("workspace-a", &lease, "binding-a", "cancelled-first")
.unwrap();
assert!(
!store
.subject_release_pending("workspace-a", &lease.worker)
.unwrap()
);
assert!(
store
.reserve_subject_release("workspace-a", &lease, "binding-a", "cancelled-first")
.is_err()
);
store
.reserve_subject_release("workspace-a", &lease, "binding-a", "release-a")
.unwrap();
drop(store);
let reopened = SqliteWorkspaceStore::open_for_test(&database).unwrap();
assert_eq!(
reopened
.current_worker_singleton_owner("workspace-a", key)
.unwrap(),
Some(lease.clone())
);
assert!(
reopened
.reserve_subject_release("workspace-a", &lease, "binding-later", "release-a")
.is_err()
);
// SQL component boundary: the Host's actual Controller and durable writer-revoke proof
// are tested at their integration boundaries, not certified by this private SQL helper.
assert!(
reopened
.release_subject_singleton("workspace-a", &lease, "binding-a", "release-a")
.is_err()
);
reopened
.confirm_subject_release_proof("workspace-a", &lease, "binding-a", "release-a", false)
.unwrap();
reopened
.release_subject_singleton("workspace-a", &lease, "binding-a", "release-a")
.unwrap();
assert!(
reopened
.current_worker_singleton_owner("workspace-a", key)
.unwrap()
.is_none()
);
let replacement = reopened
.reserve_worker_create(
"workspace-a",
"runtime-b",
"next",
"sha256:next",
Some(key),
&memory,
)
.unwrap();
assert_eq!(
replacement.singleton.unwrap().generation,
lease.generation + 1
);
reopened
.release_subject_singleton("workspace-a", &lease, "binding-a", "release-a")
.unwrap();
assert!(
reopened
.release_subject_singleton(
"workspace-a",
&lease,
"binding-a",
"different-operation"
)
.is_err()
);
assert!(
reopened
.require_current_worker_singleton_owner("workspace-a", &lease.worker)
.is_err()
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn concurrent_worker_singleton_reservations_have_one_winner() {
let dir = tempfile::tempdir().unwrap();
@@ -21272,7 +21635,8 @@ CREATE UNIQUE INDEX ticket_current_worker_role_idx
(97, "MR notifications and authenticated implementation results".into()),
(98, "Assignment-independent implementation reports".into()),
(99, "Explicit hosting administrators and self-registration policy".into()),
(100, "Provisional users and scoped passkey activation".into())]);
(100, "Provisional users and scoped passkey activation".into()),
(101, "Durable explicit Subject body release".into())]);
for (table, rows) in unchanged.iter().zip(before.iter()) {
assert_eq!(&stored_fixture_rows(conn, table), rows, "preserve {table}");
}
@@ -0,0 +1,16 @@
CREATE TABLE subject_worker_releases (
workspace_id TEXT NOT NULL,
runtime_id TEXT NOT NULL,
worker_id TEXT NOT NULL,
singleton_key TEXT NOT NULL,
singleton_generation INTEGER NOT NULL CHECK (singleton_generation > 0),
binding_id TEXT NOT NULL,
operation_id TEXT NOT NULL,
proof TEXT CHECK (proof IN ('runtime_disconnected','erased')),
completed INTEGER NOT NULL DEFAULT 0 CHECK (completed IN (0,1)),
cancelled INTEGER NOT NULL DEFAULT 0 CHECK (cancelled IN (0,1)),
PRIMARY KEY (workspace_id, operation_id),
CHECK (cancelled = 0 OR (completed = 0 AND proof IS NULL)),
FOREIGN KEY (workspace_id, worker_id) REFERENCES worker_create_reservations(workspace_id, worker_id) ON DELETE CASCADE
);
CREATE UNIQUE INDEX subject_worker_release_active ON subject_worker_releases(workspace_id,worker_id) WHERE cancelled=0;
@@ -94,6 +94,7 @@ const WORKSPACE_DELETION_PURGE_TABLES: &[&str] = &[
"workdir_registry",
"workdir_removal_operations",
"worker_control_grants",
"subject_worker_releases",
"worker_create_reservations",
"worker_diagnostics_archives",
"worker_mutation_source_proof_jtis",