diff --git a/crates/client/src/workspace_product.rs b/crates/client/src/workspace_product.rs index c4a41f20..70b6a6af 100644 --- a/crates/client/src/workspace_product.rs +++ b/crates/client/src/workspace_product.rs @@ -10,10 +10,10 @@ use ticket::{ }; use workspace_api::{ BrowserCreateWorkerResponse, BrowserWorkspaceOrchestratorResponse, - CreateWorkspaceWorkerRequest, ListResponse, ObjectiveCreateRequest, ObjectiveDetail, - ObjectiveEditRequest, ObjectiveLinkTicketRequest, ObjectiveStateRequest, ObjectiveSummary, - TICKET_ORCHESTRATION_PLANS_QUERY_PATH, TICKET_RELATIONS_QUERY_PATH, - WorkerLaunchOptionsResponse, + CreateWorkspaceWorkerRequest, ListResponse, MemoryDocumentResponse, MemoryStagingListResponse, + ObjectiveCreateRequest, ObjectiveDetail, ObjectiveEditRequest, ObjectiveLinkTicketRequest, + ObjectiveStateRequest, ObjectiveSummary, TICKET_ORCHESTRATION_PLANS_QUERY_PATH, + TICKET_RELATIONS_QUERY_PATH, WorkerLaunchOptionsResponse, }; use crate::{BackendApiClient, BackendWorkspaceClientError}; @@ -241,6 +241,17 @@ impl BackendWorkspaceProductClient { ) } + pub fn memory_document(&self) -> Result { + self.get_json("/memory") + } + + pub fn list_memory_staging( + &self, + limit: usize, + ) -> Result { + self.get_json(&format!("/memory/staging?limit={limit}")) + } + pub fn launch_ticket_intake( &self, ticket_id: &str, @@ -668,6 +679,82 @@ mod tests { (format!("http://{address}"), receiver, handle) } + #[test] + fn memory_document_uses_shared_workspace_scoped_response() { + let body = r##"{"body_md":"# Memory\\n","created_at":"2026-09-01T00:00:00Z","updated_at":"2026-09-02T00:00:00Z","bytes":10,"record_source":"workspace-sqlite"}"##; + let (base_url, request, handle) = one_response_server("200 OK", body); + let client = BackendWorkspaceProductClient::new_with_access_token( + base_url, + "workspace-a", + "test-backend-token", + ) + .unwrap(); + + let response = client.memory_document().unwrap(); + + assert_eq!(response.record_source, "workspace-sqlite"); + assert!( + request + .recv() + .unwrap() + .starts_with("GET /api/w/workspace-a/memory ") + ); + handle.join().unwrap(); + } + + #[test] + fn memory_staging_uses_shared_dto_with_typed_origin() { + let body = r#"{"limit":10,"returned_count":1,"total_valid_count":1,"invalid_count":0,"truncated":false,"order":"imported_at_desc_candidate_id_asc","record_authority":"sqlite_workspace_authority.memory_staging","items":[{"id":"candidate-1","byte_len":128,"record":{"schema_version":1,"id":"candidate-1","extract_run_id":"run-1","source":{"segment_id":"segment-1","range":[1,2]},"kind":"decision","claim":"Keep typed provenance.","why_useful":"Prevents trust loss.","staleness":null,"evidence":[],"source_refs":[{"session_id":"session-1","segment_id":"segment-1","entry_range":[1,2],"evidence_id":"evidence-1","origin":{"kind":"worker_input","workspace_id":"workspace-a","runtime_id":"runtime-1","worker_id":"worker-1"},"evidence_kind":"worker_session_entry","label":null,"summary":null}]}}],"diagnostics":[]}"#; + let (base_url, request, handle) = one_response_server("200 OK", body); + let client = BackendWorkspaceProductClient::new_with_access_token( + base_url, + "workspace-a", + "test-backend-token", + ) + .unwrap(); + + let response = client.list_memory_staging(10).unwrap(); + + assert_eq!( + response.items[0].record.source_refs[0] + .origin + .as_ref() + .unwrap() + .kind, + workspace_api::MemoryEvidenceOriginKind::WorkerInput + ); + assert!( + request + .recv() + .unwrap() + .starts_with("GET /api/w/workspace-a/memory/staging?limit=10 ") + ); + handle.join().unwrap(); + } + + #[test] + fn memory_staging_rejects_unknown_origin_kind() { + let body = r#"{"limit":10,"returned_count":1,"total_valid_count":1,"invalid_count":0,"truncated":false,"order":"order","record_authority":"authority","items":[{"id":"candidate-1","byte_len":1,"record":{"schema_version":1,"id":"candidate-1","extract_run_id":"run-1","source":{"segment_id":"segment-1","range":[1,2]},"kind":"decision","claim":"claim","why_useful":"useful","staleness":null,"evidence":[],"source_refs":[{"session_id":null,"segment_id":null,"entry_range":null,"evidence_id":null,"origin":{"kind":"future_origin"},"evidence_kind":null,"label":null,"summary":null}]}}],"diagnostics":[]}"#; + let (base_url, request, handle) = one_response_server("200 OK", body); + let client = BackendWorkspaceProductClient::new_with_access_token( + base_url, + "workspace-a", + "test-backend-token", + ) + .unwrap(); + + let error = client.list_memory_staging(10).unwrap_err(); + + assert!(matches!(error, BackendWorkspaceClientError::Http(_))); + assert!( + request + .recv() + .unwrap() + .starts_with("GET /api/w/workspace-a/memory/staging?limit=10 ") + ); + handle.join().unwrap(); + } + #[test] fn objective_list_uses_workspace_scoped_backend_route() { let body = r#"{"workspace_id":"workspace-a","limit":1000,"items":[],"source":"sqlite","diagnostics":[]}"#; diff --git a/crates/memory/src/extract/payload.rs b/crates/memory/src/extract/payload.rs index 42f87a29..74571285 100644 --- a/crates/memory/src/extract/payload.rs +++ b/crates/memory/src/extract/payload.rs @@ -74,6 +74,7 @@ impl ExtractedPayload { /// Bounded evidence snippet copied into a flat staging record. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] pub struct StagingEvidence { pub id: String, pub kind: EvidenceKind, @@ -89,6 +90,7 @@ pub struct StagingEvidence { /// One flat staging record. One record is one consolidation decision unit. #[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] pub struct StagingRecord { pub schema_version: u32, pub id: String, diff --git a/crates/memory/src/schema/common.rs b/crates/memory/src/schema/common.rs index a5396632..65f520ef 100644 --- a/crates/memory/src/schema/common.rs +++ b/crates/memory/src/schema/common.rs @@ -22,6 +22,7 @@ impl<'de> Deserialize<'de> for SourceRef { D: serde::Deserializer<'de>, { #[derive(Deserialize)] + #[serde(deny_unknown_fields)] struct RawSourceRef { #[serde(default)] segment_id: Option, @@ -83,6 +84,7 @@ pub enum EvidenceOriginKind { /// Bounded origin snapshot attached to extraction evidence. This is audit /// metadata only and cannot authorize Workspace operations. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +#[serde(deny_unknown_fields)] pub struct EvidenceOrigin { pub kind: EvidenceOriginKind, #[serde(default, skip_serializing_if = "Option::is_none")] @@ -107,6 +109,7 @@ pub struct EvidenceOrigin { /// ranges, and short labels/summaries. It must not carry raw message bodies or /// full tool result content. #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +#[serde(deny_unknown_fields)] pub struct SourceEvidenceRef { /// Stable session id when the anchor crosses or disambiguates segments. #[serde(default, skip_serializing_if = "Option::is_none")] diff --git a/crates/workspace-api/Cargo.toml b/crates/workspace-api/Cargo.toml index 979231a2..5b222e35 100644 --- a/crates/workspace-api/Cargo.toml +++ b/crates/workspace-api/Cargo.toml @@ -33,6 +33,10 @@ required-features = ["typescript"] name = "generate_companion_api_types" required-features = ["typescript"] +[[example]] +name = "generate_memory_api_types" +required-features = ["typescript"] + [[example]] name = "generate_repository_access_types" required-features = ["typescript"] diff --git a/crates/workspace-api/examples/generate_memory_api_types.rs b/crates/workspace-api/examples/generate_memory_api_types.rs new file mode 100644 index 00000000..87a9de91 --- /dev/null +++ b/crates/workspace-api/examples/generate_memory_api_types.rs @@ -0,0 +1,3 @@ +fn main() { + print!("{}", workspace_api::memory_api_typescript()); +} diff --git a/crates/workspace-api/src/lib.rs b/crates/workspace-api/src/lib.rs index f6aa4e78..3ab42838 100644 --- a/crates/workspace-api/src/lib.rs +++ b/crates/workspace-api/src/lib.rs @@ -1291,6 +1291,197 @@ pub struct WorkerRestoreResponse { pub result: WorkerRestoreResult, } +/// Public Workspace Memory document projection. +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] +#[serde(deny_unknown_fields)] +pub struct MemoryDocumentResponse { + pub body_md: String, + pub created_at: String, + pub updated_at: String, + pub bytes: usize, + pub record_source: String, +} + +/// Candidate kinds exposed by the Memory staging resource. +#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] +#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] +#[serde(rename_all = "snake_case")] +pub enum MemoryCandidateKind { + Preference, + WorkingAssumption, + Constraint, + Decision, + OpenQuestion, + Lesson, +} + +/// Typed, bounded provenance classification for public Memory evidence anchors. +#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] +#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] +#[serde(rename_all = "snake_case")] +pub enum MemoryEvidenceOriginKind { + HumanInput, + WorkerInput, + FlowInstruction, + BackendInstruction, + ModelOutput, + ToolOutput, + DerivedSummary, + LegacyUnknown, +} + +/// Bounded origin metadata copied from one typed Memory evidence anchor. +/// +/// This is provenance only. It carries no message body, prompt, reasoning, +/// secret, tool output, or authorization authority. +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] +#[cfg_attr(feature = "typescript", ts(optional_fields = nullable))] +#[serde(deny_unknown_fields)] +pub struct MemoryEvidenceOrigin { + pub kind: MemoryEvidenceOriginKind, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub account_id: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub workspace_id: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub runtime_id: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub worker_id: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub flow_selector: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub flow_definition_id: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + #[cfg_attr(feature = "typescript", ts(optional, type = "number | null"))] + pub flow_definition_revision: Option, +} + +/// Record-level source range for one Memory staging candidate. +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] +#[serde(deny_unknown_fields)] +pub struct MemorySourceRef { + pub segment_id: String, + #[cfg_attr(feature = "typescript", ts(type = "[number, number]"))] + pub range: [u64; 2], +} + +/// Bounded evidence snippet included in one Memory staging record. +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] +#[serde(deny_unknown_fields)] +pub struct MemoryStagingEvidence { + pub id: String, + pub kind: String, + #[cfg_attr(feature = "typescript", ts(type = "[number, number] | null"))] + pub entry_range: Option<[u64; 2]>, + #[serde(default, skip_serializing_if = "Option::is_none")] + #[cfg_attr( + feature = "typescript", + ts(optional, type = "MemoryEvidenceOrigin | null") + )] + pub origin: Option, + pub excerpt: Option, + pub summary: Option, +} + +/// Bounded source anchor included in one Memory staging record. +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] +#[serde(deny_unknown_fields)] +pub struct MemorySourceEvidenceRef { + pub session_id: Option, + pub segment_id: Option, + #[cfg_attr(feature = "typescript", ts(type = "[number, number] | null"))] + pub entry_range: Option<[u64; 2]>, + pub evidence_id: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + #[cfg_attr( + feature = "typescript", + ts(optional, type = "MemoryEvidenceOrigin | null") + )] + pub origin: Option, + pub evidence_kind: Option, + pub label: Option, + pub summary: Option, +} + +/// Public projection of one valid Memory staging record. +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] +#[serde(deny_unknown_fields)] +pub struct MemoryStagingRecord { + pub schema_version: u32, + pub id: String, + pub extract_run_id: String, + pub source: MemorySourceRef, + pub kind: MemoryCandidateKind, + pub claim: String, + pub why_useful: String, + pub staleness: Option, + pub evidence: Vec, + pub source_refs: Vec, +} + +/// Public list entry for one valid Memory staging record. +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] +#[serde(deny_unknown_fields)] +pub struct MemoryStagingEntry { + pub id: String, + #[cfg_attr(feature = "typescript", ts(type = "number"))] + pub byte_len: u64, + pub record: MemoryStagingRecord, +} + +/// Public response returned by the Workspace Memory staging list resource. +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] +#[serde(deny_unknown_fields)] +pub struct MemoryStagingListResponse { + pub limit: usize, + pub returned_count: usize, + pub total_valid_count: usize, + pub invalid_count: usize, + pub truncated: bool, + pub order: String, + pub record_authority: String, + pub items: Vec, + pub diagnostics: Vec, +} + +#[cfg(feature = "typescript")] +pub fn memory_api_typescript() -> String { + use ts_rs::TS; + + let config = ts_rs::Config::default(); + let declarations = [ + DiagnosticSeverity::decl(&config), + Diagnostic::decl(&config), + MemoryDocumentResponse::decl(&config), + MemoryCandidateKind::decl(&config), + MemoryEvidenceOriginKind::decl(&config), + MemoryEvidenceOrigin::decl(&config), + MemorySourceRef::decl(&config), + MemoryStagingEvidence::decl(&config), + MemorySourceEvidenceRef::decl(&config), + MemoryStagingRecord::decl(&config), + MemoryStagingEntry::decl(&config), + MemoryStagingListResponse::decl(&config), + ]; + + format!( + "// Generated from workspace-api. Do not edit by hand.\n// Regenerate: cargo run -q -p workspace-api --features typescript --example generate_memory_api_types > web/workspace/src/lib/generated/memory-api.ts\n\n{}\n", + declarations + .into_iter() + .map(|declaration| format!("export {declaration}")) + .collect::>() + .join("\n\n") + ) +} + /// Workspace-owned Memory settings returned by the shared Server API. #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] #[serde(deny_unknown_fields)] @@ -1603,6 +1794,36 @@ mod worker_launch_typescript_tests { } } +#[cfg(all(test, feature = "typescript"))] +mod memory_typescript_tests { + #[test] + fn generated_memory_api_contract_is_current() { + let expected = super::memory_api_typescript(); + let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../../web/workspace/src/lib/generated/memory-api.ts"); + let actual = std::fs::read_to_string(&path) + .unwrap_or_else(|error| panic!("failed to read {}: {error}", path.display())); + assert_eq!( + normalize(&actual), + normalize(&expected), + "regenerate Memory API TypeScript types with `cargo run -q -p workspace-api --features typescript --example generate_memory_api_types > web/workspace/src/lib/generated/memory-api.ts` and format the generated file", + ); + } + + fn normalize(value: &str) -> String { + value + .chars() + .filter_map(|character| match character { + character if character.is_whitespace() => None, + ',' => Some(';'), + character => Some(character), + }) + .collect::() + .replace("=|", "=") + .replace(";}", "}") + } +} + #[cfg(all(test, feature = "typescript"))] mod workdir_typescript_tests { #[test] @@ -1636,6 +1857,52 @@ mod workdir_typescript_tests { mod tests { use super::*; + #[test] + fn memory_evidence_origins_round_trip_as_typed_provenance() { + let kinds = [ + MemoryEvidenceOriginKind::HumanInput, + MemoryEvidenceOriginKind::WorkerInput, + MemoryEvidenceOriginKind::FlowInstruction, + MemoryEvidenceOriginKind::BackendInstruction, + MemoryEvidenceOriginKind::ModelOutput, + MemoryEvidenceOriginKind::ToolOutput, + MemoryEvidenceOriginKind::DerivedSummary, + MemoryEvidenceOriginKind::LegacyUnknown, + ]; + for kind in kinds { + let origin = MemoryEvidenceOrigin { + kind, + account_id: Some("account-1".to_string()), + workspace_id: Some("workspace-1".to_string()), + runtime_id: Some("runtime-1".to_string()), + worker_id: Some("worker-1".to_string()), + flow_selector: Some("builtin:coder-review".to_string()), + flow_definition_id: Some("flow-1".to_string()), + flow_definition_revision: Some(7), + }; + let encoded = serde_json::to_value(&origin).unwrap(); + let decoded: MemoryEvidenceOrigin = serde_json::from_value(encoded).unwrap(); + assert_eq!(decoded, origin); + } + } + + #[test] + fn memory_evidence_origin_rejects_unknown_kind_and_fields() { + assert!( + serde_json::from_value::( + serde_json::json!({"kind": "future_origin"}) + ) + .is_err() + ); + assert!( + serde_json::from_value::(serde_json::json!({ + "kind": "human_input", + "future_field": "not current schema" + })) + .is_err() + ); + } + fn worker_launch_summary() -> WorkerLaunchWorkerSummary { WorkerLaunchWorkerSummary { runtime_id: "runtime-a".to_string(), diff --git a/crates/workspace-server/src/memory_staging.rs b/crates/workspace-server/src/memory_staging.rs index f3478847..5ab7e01a 100644 --- a/crates/workspace-server/src/memory_staging.rs +++ b/crates/workspace-server/src/memory_staging.rs @@ -1,6 +1,10 @@ -use memory::extract::StagingRecord; -use memory::schema::{SourceEvidenceRef, SourceRef}; -use serde::{Deserialize, Serialize}; +use memory::extract::{CandidateKind, StagingRecord}; +use memory::schema::{EvidenceOrigin, EvidenceOriginKind, SourceEvidenceRef, SourceRef}; +use workspace_api::{ + Diagnostic, DiagnosticSeverity, MemoryCandidateKind, MemoryEvidenceOrigin, + MemoryEvidenceOriginKind, MemorySourceEvidenceRef, MemorySourceRef, MemoryStagingEntry, + MemoryStagingEvidence, MemoryStagingListResponse, MemoryStagingRecord, +}; use crate::Result; use crate::authority::MemoryAuthority; @@ -8,59 +12,6 @@ use crate::authority::MemoryAuthority; const DEFAULT_MEMORY_STAGING_LIMIT: usize = 100; const MAX_MEMORY_STAGING_LIMIT: usize = 500; -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -pub struct MemoryStagingListResponse { - pub limit: usize, - pub returned_count: usize, - pub total_valid_count: usize, - pub invalid_count: usize, - pub truncated: bool, - pub order: String, - pub record_authority: String, - pub items: Vec, -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -pub struct MemoryStagingEntrySummary { - pub id: String, - pub byte_len: u64, - pub record: MemoryStagingRecordSummary, -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -pub struct MemoryStagingRecordSummary { - pub schema_version: u32, - pub id: String, - pub extract_run_id: String, - pub source: SourceRef, - pub kind: String, - pub claim: String, - pub why_useful: String, - pub staleness: Option, - pub evidence: Vec, - pub source_refs: Vec, -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -pub struct MemoryStagingEvidenceSummary { - pub id: String, - pub kind: String, - pub entry_range: Option<[u64; 2]>, - pub excerpt: Option, - pub summary: Option, -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -pub struct MemorySourceEvidenceRefSummary { - pub session_id: Option, - pub segment_id: Option, - pub entry_range: Option<[u64; 2]>, - pub evidence_id: Option, - pub evidence_kind: Option, - pub label: Option, - pub summary: Option, -} - #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct MemoryStagingBacklogSummary { pub candidate_count: usize, @@ -90,14 +41,24 @@ pub fn list_memory_staging_from_authority( }; total_valid_count += 1; if valid_items.len() < limit { - valid_items.push(MemoryStagingEntrySummary { + valid_items.push(MemoryStagingEntry { id: entry.candidate_id, byte_len: entry.raw_json.len() as u64, - record: memory_staging_record_summary(record), + record: memory_staging_record_projection(record), }); } } let returned_count = valid_items.len(); + let diagnostics = (invalid_count > 0) + .then(|| Diagnostic { + code: "memory_staging_record_invalid".to_string(), + message: format!( + "{invalid_count} Memory staging record(s) were excluded because they did not match the current schema." + ), + severity: DiagnosticSeverity::Error, + }) + .into_iter() + .collect(); Ok(MemoryStagingListResponse { limit, returned_count, @@ -107,6 +68,7 @@ pub fn list_memory_staging_from_authority( order: "imported_at_desc_candidate_id_asc".to_string(), record_authority: "sqlite_workspace_authority.memory_staging".to_string(), items: valid_items, + diagnostics, }) } @@ -133,23 +95,24 @@ pub fn memory_staging_backlog_from_authority( }) } -fn memory_staging_record_summary(record: StagingRecord) -> MemoryStagingRecordSummary { - MemoryStagingRecordSummary { +fn memory_staging_record_projection(record: StagingRecord) -> MemoryStagingRecord { + MemoryStagingRecord { schema_version: record.schema_version, id: record.id, extract_run_id: record.extract_run_id, - source: record.source, - kind: record.kind.as_str().to_string(), + source: memory_source_ref_projection(record.source), + kind: memory_candidate_kind_projection(record.kind), claim: record.claim, why_useful: record.why_useful, staleness: record.staleness, evidence: record .evidence .into_iter() - .map(|evidence| MemoryStagingEvidenceSummary { + .map(|evidence| MemoryStagingEvidence { id: evidence.id, kind: evidence.kind.as_str().to_string(), entry_range: evidence.entry_range, + origin: evidence.origin.map(memory_evidence_origin_projection), excerpt: evidence.excerpt, summary: evidence.summary, }) @@ -157,19 +120,36 @@ fn memory_staging_record_summary(record: StagingRecord) -> MemoryStagingRecordSu source_refs: record .source_refs .into_iter() - .map(memory_source_evidence_ref_summary) + .map(memory_source_evidence_ref_projection) .collect(), } } -fn memory_source_evidence_ref_summary( - source_ref: SourceEvidenceRef, -) -> MemorySourceEvidenceRefSummary { - MemorySourceEvidenceRefSummary { +fn memory_source_ref_projection(source_ref: SourceRef) -> MemorySourceRef { + MemorySourceRef { + segment_id: source_ref.segment_id, + range: source_ref.range, + } +} + +fn memory_candidate_kind_projection(kind: CandidateKind) -> MemoryCandidateKind { + match kind { + CandidateKind::Preference => MemoryCandidateKind::Preference, + CandidateKind::WorkingAssumption => MemoryCandidateKind::WorkingAssumption, + CandidateKind::Constraint => MemoryCandidateKind::Constraint, + CandidateKind::Decision => MemoryCandidateKind::Decision, + CandidateKind::OpenQuestion => MemoryCandidateKind::OpenQuestion, + CandidateKind::Lesson => MemoryCandidateKind::Lesson, + } +} + +fn memory_source_evidence_ref_projection(source_ref: SourceEvidenceRef) -> MemorySourceEvidenceRef { + MemorySourceEvidenceRef { session_id: source_ref.session_id, segment_id: source_ref.segment_id, entry_range: source_ref.entry_range, evidence_id: source_ref.evidence_id, + origin: source_ref.origin.map(memory_evidence_origin_projection), evidence_kind: source_ref .evidence_kind .map(|evidence_kind| evidence_kind.as_str().to_string()), @@ -178,12 +158,35 @@ fn memory_source_evidence_ref_summary( } } +fn memory_evidence_origin_projection(origin: EvidenceOrigin) -> MemoryEvidenceOrigin { + MemoryEvidenceOrigin { + kind: match origin.kind { + EvidenceOriginKind::HumanInput => MemoryEvidenceOriginKind::HumanInput, + EvidenceOriginKind::WorkerInput => MemoryEvidenceOriginKind::WorkerInput, + EvidenceOriginKind::FlowInstruction => MemoryEvidenceOriginKind::FlowInstruction, + EvidenceOriginKind::BackendInstruction => MemoryEvidenceOriginKind::BackendInstruction, + EvidenceOriginKind::ModelOutput => MemoryEvidenceOriginKind::ModelOutput, + EvidenceOriginKind::ToolOutput => MemoryEvidenceOriginKind::ToolOutput, + EvidenceOriginKind::DerivedSummary => MemoryEvidenceOriginKind::DerivedSummary, + EvidenceOriginKind::LegacyUnknown => MemoryEvidenceOriginKind::LegacyUnknown, + }, + account_id: origin.account_id, + workspace_id: origin.workspace_id, + runtime_id: origin.runtime_id, + worker_id: origin.worker_id, + flow_selector: origin.flow_selector, + flow_definition_id: origin.flow_definition_id, + flow_definition_revision: origin.flow_definition_revision, + } +} + #[cfg(test)] mod tests { use super::*; use crate::authority::{MemoryAuthority, SqliteWorkspaceAuthority}; use crate::store::{ControlPlaneStore, SqliteWorkspaceStore, WorkspaceRecord}; use memory::extract::{CandidateKind, ExtractedCandidate, StagingRecord}; + use memory::schema::{EvidenceOrigin, EvidenceOriginKind, SourceEvidenceRef}; use tempfile::TempDir; fn source() -> SourceRef { @@ -258,7 +261,114 @@ mod tests { response.record_authority, "sqlite_workspace_authority.memory_staging" ); - assert_eq!(response.items[0].record.kind, "decision"); + assert_eq!(response.items[0].record.kind, MemoryCandidateKind::Decision); + assert!(response.diagnostics.is_empty()); + } + + #[test] + fn projects_every_typed_evidence_origin_without_flattening() { + let cases = [ + ( + EvidenceOriginKind::HumanInput, + MemoryEvidenceOriginKind::HumanInput, + ), + ( + EvidenceOriginKind::WorkerInput, + MemoryEvidenceOriginKind::WorkerInput, + ), + ( + EvidenceOriginKind::FlowInstruction, + MemoryEvidenceOriginKind::FlowInstruction, + ), + ( + EvidenceOriginKind::BackendInstruction, + MemoryEvidenceOriginKind::BackendInstruction, + ), + ( + EvidenceOriginKind::ModelOutput, + MemoryEvidenceOriginKind::ModelOutput, + ), + ( + EvidenceOriginKind::ToolOutput, + MemoryEvidenceOriginKind::ToolOutput, + ), + ( + EvidenceOriginKind::DerivedSummary, + MemoryEvidenceOriginKind::DerivedSummary, + ), + ( + EvidenceOriginKind::LegacyUnknown, + MemoryEvidenceOriginKind::LegacyUnknown, + ), + ]; + + for (domain_kind, api_kind) in cases { + let projected = memory_source_evidence_ref_projection(SourceEvidenceRef { + session_id: Some("session-1".to_string()), + origin: Some(EvidenceOrigin { + kind: domain_kind, + account_id: Some("account-1".to_string()), + workspace_id: Some("workspace-test".to_string()), + runtime_id: Some("runtime-1".to_string()), + worker_id: Some("worker-1".to_string()), + flow_selector: Some("builtin:coder-review".to_string()), + flow_definition_id: Some("flow-1".to_string()), + flow_definition_revision: Some(7), + }), + ..SourceEvidenceRef::default() + }); + let origin = projected.origin.unwrap(); + assert_eq!(origin.kind, api_kind); + assert_eq!(origin.account_id.as_deref(), Some("account-1")); + assert_eq!(origin.workspace_id.as_deref(), Some("workspace-test")); + assert_eq!(origin.runtime_id.as_deref(), Some("runtime-1")); + assert_eq!(origin.worker_id.as_deref(), Some("worker-1")); + assert_eq!( + origin.flow_selector.as_deref(), + Some("builtin:coder-review") + ); + assert_eq!(origin.flow_definition_id.as_deref(), Some("flow-1")); + assert_eq!(origin.flow_definition_revision, Some(7)); + } + } + + #[tokio::test] + async fn invalid_or_newer_origin_shapes_are_excluded_with_bounded_diagnostic() { + let (_temp, authority) = authority().await; + for (id, origin) in [ + ( + "unknown-origin-kind", + serde_json::json!({"kind": "future_origin_kind"}), + ), + ( + "newer-origin-shape", + serde_json::json!({"kind": "human_input", "future_field": "do not echo me"}), + ), + ] { + let mut record: serde_json::Value = + serde_json::from_str(&record_json(id, "claim")).unwrap(); + record["source_refs"] = serde_json::json!([{"origin": origin}]); + authority + .upsert_memory_staging_record(id, &serde_json::to_string(&record).unwrap(), None) + .unwrap(); + } + + let response = list_memory_staging_from_authority(&authority, None).unwrap(); + + assert_eq!(response.returned_count, 0); + assert_eq!(response.invalid_count, 2); + assert_eq!(response.diagnostics.len(), 1); + assert_eq!( + response.diagnostics[0].code, + "memory_staging_record_invalid" + ); + assert_eq!(response.diagnostics[0].severity, DiagnosticSeverity::Error); + assert!( + !response.diagnostics[0] + .message + .contains("future_origin_kind") + ); + assert!(!response.diagnostics[0].message.contains("do not echo me")); } #[tokio::test] diff --git a/crates/workspace-server/src/server.rs b/crates/workspace-server/src/server.rs index b5a1b428..aea8f4e3 100644 --- a/crates/workspace-server/src/server.rs +++ b/crates/workspace-server/src/server.rs @@ -62,10 +62,11 @@ use workspace_api::{ CreateRepositorySshCredentialRequest, CreateWorkspaceRepositoryRequest, CreateWorkspaceRepositoryResponse, CreateWorkspaceWorkerRequest, CreateWorkspaceWorkerTicketAssignmentRequest, DeleteRepositorySshCredentialRequest, - DeleteRepositorySshHostTrustRequest, ObjectiveCreateRequest, ObjectiveEditRequest, - ObjectiveLinkTicketRequest, ObjectiveStateRequest, ProfileSettingsResponse, - PutRepositorySshHostTrustRequest, RepositoryAccessProjection, RepositoryDetailResponse, - RepositoryListResponse, RepositoryLogResponse, RepositorySshCredential, RepositorySshHostTrust, + DeleteRepositorySshHostTrustRequest, MemoryDocumentResponse, MemoryStagingListResponse, + ObjectiveCreateRequest, ObjectiveEditRequest, ObjectiveLinkTicketRequest, + ObjectiveStateRequest, ProfileSettingsResponse, PutRepositorySshHostTrustRequest, + RepositoryAccessProjection, RepositoryDetailResponse, RepositoryListResponse, + RepositoryLogResponse, RepositorySshCredential, RepositorySshHostTrust, RotateRepositorySshCredentialRequest, RuntimeConnectionTestResponse, RuntimeManagementSummary, TICKET_ORCHESTRATION_PLANS_QUERY_PATH, TICKET_RELATIONS_QUERY_PATH, UpdateWorkspaceMetadataRequest, WorkerLaunchOptionsResponse, WorkerLaunchProfileCandidate, @@ -113,8 +114,7 @@ use crate::hosts::{ use crate::identity::WorkspaceIdentity; use crate::memory_backend::execute_memory_backend_operation_with_authority; use crate::memory_staging::{ - MemoryStagingListResponse, list_memory_staging_from_authority, - memory_staging_backlog_from_authority, + list_memory_staging_from_authority, memory_staging_backlog_from_authority, }; use crate::observation::{ BackendObservationProxy, ObservationProxyError, RuntimeObservationClient, @@ -7540,15 +7540,6 @@ fn find_workspace_orchestrator(api: &WorkspaceApi) -> Option { None } -#[derive(Debug, Clone, Serialize)] -struct MemoryDocumentResponse { - body_md: String, - created_at: String, - updated_at: String, - bytes: usize, - record_source: String, -} - async fn scoped_get_memory_document( State(api): State, AxumPath(path): AxumPath, @@ -25981,6 +25972,8 @@ mod tests { let memory_document = get_json(app.clone(), &format!("/api/w/{TEST_WORKSPACE_ID}/memory")).await; + let _: workspace_api::MemoryDocumentResponse = + serde_json::from_value(memory_document.clone()).unwrap(); assert_eq!(memory_document["created_at"], "2026-01-01T00:00:00Z"); assert_eq!(memory_document["updated_at"], "2026-01-02T00:00:00Z"); assert_eq!(memory_document["bytes"], 63); @@ -25997,6 +25990,8 @@ mod tests { &format!("/api/w/{TEST_WORKSPACE_ID}/memory/staging?limit=10"), ) .await; + let _: workspace_api::MemoryStagingListResponse = + serde_json::from_value(memory_staging.clone()).unwrap(); assert_eq!( memory_staging["record_authority"], "sqlite_workspace_authority.memory_staging" diff --git a/web/workspace/src/lib/generated/memory-api.ts b/web/workspace/src/lib/generated/memory-api.ts new file mode 100644 index 00000000..f6247280 --- /dev/null +++ b/web/workspace/src/lib/generated/memory-api.ts @@ -0,0 +1,100 @@ +// Generated from workspace-api. Do not edit by hand. +// Regenerate: cargo run -q -p workspace-api --features typescript --example generate_memory_api_types > web/workspace/src/lib/generated/memory-api.ts + +export type DiagnosticSeverity = "info" | "warning" | "error"; + +export type Diagnostic = { + code: string; + severity: DiagnosticSeverity; + message: string; +}; + +export type MemoryDocumentResponse = { + body_md: string; + created_at: string; + updated_at: string; + bytes: number; + record_source: string; +}; + +export type MemoryCandidateKind = + | "preference" + | "working_assumption" + | "constraint" + | "decision" + | "open_question" + | "lesson"; + +export type MemoryEvidenceOriginKind = + | "human_input" + | "worker_input" + | "flow_instruction" + | "backend_instruction" + | "model_output" + | "tool_output" + | "derived_summary" + | "legacy_unknown"; + +export type MemoryEvidenceOrigin = { + kind: MemoryEvidenceOriginKind; + account_id?: string | null; + workspace_id?: string | null; + runtime_id?: string | null; + worker_id?: string | null; + flow_selector?: string | null; + flow_definition_id?: string | null; + flow_definition_revision?: number | null; +}; + +export type MemorySourceRef = { segment_id: string; range: [number, number] }; + +export type MemoryStagingEvidence = { + id: string; + kind: string; + entry_range: [number, number] | null; + origin?: MemoryEvidenceOrigin | null; + excerpt: string | null; + summary: string | null; +}; + +export type MemorySourceEvidenceRef = { + session_id: string | null; + segment_id: string | null; + entry_range: [number, number] | null; + evidence_id: string | null; + origin?: MemoryEvidenceOrigin | null; + evidence_kind: string | null; + label: string | null; + summary: string | null; +}; + +export type MemoryStagingRecord = { + schema_version: number; + id: string; + extract_run_id: string; + source: MemorySourceRef; + kind: MemoryCandidateKind; + claim: string; + why_useful: string; + staleness: string | null; + evidence: Array; + source_refs: Array; +}; + +export type MemoryStagingEntry = { + id: string; + byte_len: number; + record: MemoryStagingRecord; +}; + +export type MemoryStagingListResponse = { + limit: number; + returned_count: number; + total_valid_count: number; + invalid_count: number; + truncated: boolean; + order: string; + record_authority: string; + items: Array; + diagnostics: Array; +}; diff --git a/web/workspace/src/lib/workspace/memory/api.ts b/web/workspace/src/lib/workspace/memory/api.ts new file mode 100644 index 00000000..5a209740 --- /dev/null +++ b/web/workspace/src/lib/workspace/memory/api.ts @@ -0,0 +1,392 @@ +import type { + Diagnostic, + DiagnosticSeverity, + MemoryCandidateKind, + MemoryDocumentResponse, + MemoryEvidenceOrigin, + MemoryEvidenceOriginKind, + MemorySourceEvidenceRef, + MemorySourceRef, + MemoryStagingEntry, + MemoryStagingEvidence, + MemoryStagingListResponse, + MemoryStagingRecord, +} from "$lib/generated/memory-api"; + +const MAX_STAGING_ITEMS = 500; +const MAX_EVIDENCE_PER_RECORD = 500; +const MAX_SOURCE_REFS_PER_RECORD = 500; +const MAX_ORIGIN_VALUE_LENGTH = 512; + +const candidateKinds = new Set([ + "preference", + "working_assumption", + "constraint", + "decision", + "open_question", + "lesson", +]); +const originKinds = new Set([ + "human_input", + "worker_input", + "flow_instruction", + "backend_instruction", + "model_output", + "tool_output", + "derived_summary", + "legacy_unknown", +]); +const diagnosticSeverities = new Set([ + "info", + "warning", + "error", +]); + +export function parseMemoryDocumentResponse( + value: unknown, +): MemoryDocumentResponse { + const record = strictRecord( + value, + ["body_md", "created_at", "updated_at", "bytes", "record_source"], + "Memory document response", + ); + return { + body_md: requiredString(record, "body_md"), + created_at: requiredString(record, "created_at"), + updated_at: requiredString(record, "updated_at"), + bytes: requiredNonNegativeInteger(record, "bytes"), + record_source: requiredString(record, "record_source"), + }; +} + +export function parseMemoryStagingListResponse( + value: unknown, +): MemoryStagingListResponse { + const record = strictRecord( + value, + [ + "limit", + "returned_count", + "total_valid_count", + "invalid_count", + "truncated", + "order", + "record_authority", + "items", + "diagnostics", + ], + "Memory staging list response", + ); + const items = boundedArray(record.items, MAX_STAGING_ITEMS, "items").map( + parseStagingEntry, + ); + const diagnostics = boundedArray( + record.diagnostics, + MAX_STAGING_ITEMS, + "diagnostics", + ).map(parseDiagnostic); + const returnedCount = requiredNonNegativeInteger(record, "returned_count"); + if (returnedCount !== items.length) { + invalid("returned_count does not match items"); + } + return { + limit: requiredNonNegativeInteger(record, "limit"), + returned_count: returnedCount, + total_valid_count: requiredNonNegativeInteger(record, "total_valid_count"), + invalid_count: requiredNonNegativeInteger(record, "invalid_count"), + truncated: requiredBoolean(record, "truncated"), + order: requiredString(record, "order"), + record_authority: requiredString(record, "record_authority"), + items, + diagnostics, + }; +} + +function parseStagingEntry(value: unknown): MemoryStagingEntry { + const record = strictRecord( + value, + ["id", "byte_len", "record"], + "Memory staging entry", + ); + return { + id: requiredString(record, "id"), + byte_len: requiredNonNegativeInteger(record, "byte_len"), + record: parseStagingRecord(record.record), + }; +} + +function parseStagingRecord(value: unknown): MemoryStagingRecord { + const record = strictRecord( + value, + [ + "schema_version", + "id", + "extract_run_id", + "source", + "kind", + "claim", + "why_useful", + "staleness", + "evidence", + "source_refs", + ], + "Memory staging record", + ); + const kind = requiredString(record, "kind") as MemoryCandidateKind; + if (!candidateKinds.has(kind)) { + invalid("unknown Memory candidate kind"); + } + return { + schema_version: requiredNonNegativeInteger(record, "schema_version"), + id: requiredString(record, "id"), + extract_run_id: requiredString(record, "extract_run_id"), + source: parseSourceRef(record.source), + kind, + claim: requiredString(record, "claim"), + why_useful: requiredString(record, "why_useful"), + staleness: nullableString(record, "staleness"), + evidence: boundedArray( + record.evidence, + MAX_EVIDENCE_PER_RECORD, + "evidence", + ).map(parseStagingEvidence), + source_refs: boundedArray( + record.source_refs, + MAX_SOURCE_REFS_PER_RECORD, + "source_refs", + ).map(parseSourceEvidenceRef), + }; +} + +function parseSourceRef(value: unknown): MemorySourceRef { + const record = strictRecord( + value, + ["segment_id", "range"], + "Memory source ref", + ); + return { + segment_id: requiredString(record, "segment_id"), + range: parseEntryRange(record.range, "range"), + }; +} + +function parseStagingEvidence(value: unknown): MemoryStagingEvidence { + const record = strictRecord( + value, + ["id", "kind", "entry_range", "origin", "excerpt", "summary"], + "Memory staging evidence", + ["origin"], + ); + const result: MemoryStagingEvidence = { + id: requiredString(record, "id"), + kind: requiredString(record, "kind"), + entry_range: parseNullableEntryRange(record.entry_range, "entry_range"), + excerpt: nullableString(record, "excerpt"), + summary: nullableString(record, "summary"), + }; + if ("origin" in record) { + result.origin = record.origin === null + ? null + : parseEvidenceOrigin(record.origin); + } + return result; +} + +function parseSourceEvidenceRef(value: unknown): MemorySourceEvidenceRef { + const record = strictRecord( + value, + [ + "session_id", + "segment_id", + "entry_range", + "evidence_id", + "origin", + "evidence_kind", + "label", + "summary", + ], + "Memory source evidence ref", + ["origin"], + ); + const result: MemorySourceEvidenceRef = { + session_id: nullableString(record, "session_id"), + segment_id: nullableString(record, "segment_id"), + entry_range: parseNullableEntryRange(record.entry_range, "entry_range"), + evidence_id: nullableString(record, "evidence_id"), + evidence_kind: nullableString(record, "evidence_kind"), + label: nullableString(record, "label"), + summary: nullableString(record, "summary"), + }; + if ("origin" in record) { + result.origin = record.origin === null + ? null + : parseEvidenceOrigin(record.origin); + } + return result; +} + +function parseEvidenceOrigin(value: unknown): MemoryEvidenceOrigin { + const optional = [ + "account_id", + "workspace_id", + "runtime_id", + "worker_id", + "flow_selector", + "flow_definition_id", + "flow_definition_revision", + ] as const; + const record = strictRecord( + value, + ["kind", ...optional], + "Memory evidence origin", + [...optional], + ); + const kind = requiredString(record, "kind") as MemoryEvidenceOriginKind; + if (!originKinds.has(kind)) { + invalid("unknown Memory evidence origin kind"); + } + const result: MemoryEvidenceOrigin = { kind }; + for ( + const key of [ + "account_id", + "workspace_id", + "runtime_id", + "worker_id", + "flow_selector", + "flow_definition_id", + ] as const + ) { + if (key in record) { + const text = nullableString(record, key); + if (text !== null && text.length > MAX_ORIGIN_VALUE_LENGTH) { + invalid(`${key} exceeds the Memory origin limit`); + } + result[key] = text; + } + } + if ("flow_definition_revision" in record) { + result.flow_definition_revision = record.flow_definition_revision === null + ? null + : nonNegativeInteger( + record.flow_definition_revision, + "flow_definition_revision", + ); + } + return result; +} + +function parseDiagnostic(value: unknown): Diagnostic { + const record = strictRecord( + value, + ["code", "severity", "message"], + "Memory diagnostic", + ); + const severity = requiredString(record, "severity") as DiagnosticSeverity; + if (!diagnosticSeverities.has(severity)) { + invalid("unknown diagnostic severity"); + } + return { + code: requiredString(record, "code"), + severity, + message: requiredString(record, "message"), + }; +} + +function strictRecord( + value: unknown, + keys: readonly string[], + label: string, + optionalKeys: readonly string[] = [], +): Record { + if (typeof value !== "object" || value === null || Array.isArray(value)) { + invalid(`${label} must be an object`); + } + const record = value as Record; + const allowed = new Set(keys); + for (const key of Object.keys(record)) { + if (!allowed.has(key)) { + invalid(`${label} has an unknown field`); + } + } + const optional = new Set(optionalKeys); + for (const key of keys) { + if (!optional.has(key) && !(key in record)) { + invalid(`${label} is missing a required field`); + } + } + return record; +} + +function boundedArray( + value: unknown, + maximum: number, + label: string, +): unknown[] { + if (!Array.isArray(value) || value.length > maximum) { + invalid(`${label} must be a bounded array`); + } + return value; +} + +function requiredString(record: Record, key: string): string { + if (typeof record[key] !== "string") { + invalid(`${key} must be a string`); + } + return record[key]; +} + +function nullableString( + record: Record, + key: string, +): string | null { + const value = record[key]; + if (value !== null && typeof value !== "string") { + invalid(`${key} must be a string or null`); + } + return value; +} + +function requiredBoolean( + record: Record, + key: string, +): boolean { + if (typeof record[key] !== "boolean") { + invalid(`${key} must be a boolean`); + } + return record[key]; +} + +function requiredNonNegativeInteger( + record: Record, + key: string, +): number { + return nonNegativeInteger(record[key], key); +} + +function nonNegativeInteger(value: unknown, label: string): number { + if (!Number.isSafeInteger(value) || (value as number) < 0) { + invalid(`${label} must be a non-negative safe integer`); + } + return value as number; +} + +function parseEntryRange(value: unknown, label: string): [number, number] { + if (!Array.isArray(value) || value.length !== 2) { + invalid(`${label} must be a two-item entry range`); + } + return [ + nonNegativeInteger(value[0], label), + nonNegativeInteger(value[1], label), + ]; +} + +function parseNullableEntryRange( + value: unknown, + label: string, +): [number, number] | null { + return value === null ? null : parseEntryRange(value, label); +} + +function invalid(message: string): never { + throw new Error(`Invalid Memory API response: ${message}`); +} diff --git a/web/workspace/src/lib/workspace/sidebar/types.ts b/web/workspace/src/lib/workspace/sidebar/types.ts index ec758cd7..67d7aa10 100644 --- a/web/workspace/src/lib/workspace/sidebar/types.ts +++ b/web/workspace/src/lib/workspace/sidebar/types.ts @@ -206,75 +206,6 @@ export type RepositoryListResponse = SharedRepositoryListResponse; export type RepositoryDetailResponse = SharedRepositoryDetailResponse; export type RepositoryLogResponse = SharedRepositoryLogResponse; -export type MemoryDocumentResponse = { - body_md: string; - created_at: string; - updated_at: string; - bytes: number; - record_source: string; -}; - -export type MemoryCandidateKind = - | "preference" - | "working_assumption" - | "constraint" - | "decision" - | "open_question" - | "lesson"; - -export type MemorySourceRef = { - segment_id: string; - range: [number, number]; -}; - -export type MemoryStagingEvidence = { - id: string; - kind: string; - entry_range?: [number, number] | null; - excerpt?: string | null; - summary?: string | null; -}; - -export type MemorySourceEvidenceRef = { - session_id?: string | null; - segment_id?: string | null; - entry_range?: [number, number] | null; - evidence_id?: string | null; - evidence_kind?: string | null; - label?: string | null; - summary?: string | null; -}; - -export type MemoryStagingRecord = { - schema_version: number; - id: string; - extract_run_id: string; - source: MemorySourceRef; - kind: MemoryCandidateKind; - claim: string; - why_useful: string; - staleness?: string | null; - evidence?: MemoryStagingEvidence[]; - source_refs?: MemorySourceEvidenceRef[]; -}; - -export type MemoryStagingEntry = { - id: string; - byte_len: number; - record: MemoryStagingRecord; -}; - -export type MemoryStagingListResponse = { - limit: number; - returned_count: number; - total_valid_count: number; - invalid_count: number; - truncated: boolean; - order: string; - record_authority: string; - items: MemoryStagingEntry[]; -}; - export type { DerivedTicketRelation, TicketDetail, diff --git a/web/workspace/src/routes/w/[workspaceId]/memory/+page.ts b/web/workspace/src/routes/w/[workspaceId]/memory/+page.ts index 21ce4191..9e25c841 100644 --- a/web/workspace/src/routes/w/[workspaceId]/memory/+page.ts +++ b/web/workspace/src/routes/w/[workspaceId]/memory/+page.ts @@ -1,13 +1,15 @@ import { loadJson, workspaceApiPath } from "$lib/workspace/api/http"; -import type { MemoryDocumentResponse } from "$lib/workspace/sidebar/types"; +import { parseMemoryDocumentResponse } from "$lib/workspace/memory/api"; import type { PageLoad } from "./$types"; export const load: PageLoad = async ({ fetch, params }) => { return { workspaceId: params.workspaceId, - memory: await loadJson( + memory: await loadJson( fetch, workspaceApiPath(params.workspaceId, "/memory"), + undefined, + parseMemoryDocumentResponse, ), }; }; diff --git a/web/workspace/src/routes/w/[workspaceId]/memory/staging/+page.svelte b/web/workspace/src/routes/w/[workspaceId]/memory/staging/+page.svelte index 3cc98778..1cdee6ed 100644 --- a/web/workspace/src/routes/w/[workspaceId]/memory/staging/+page.svelte +++ b/web/workspace/src/routes/w/[workspaceId]/memory/staging/+page.svelte @@ -1,5 +1,5 @@