web: expose memory staging view
This commit is contained in:
parent
b9067a8110
commit
b8cc58ac29
|
|
@ -9,6 +9,7 @@ pub mod companion;
|
||||||
pub mod config;
|
pub mod config;
|
||||||
pub mod hosts;
|
pub mod hosts;
|
||||||
pub mod identity;
|
pub mod identity;
|
||||||
|
pub mod memory_staging;
|
||||||
pub mod observation;
|
pub mod observation;
|
||||||
pub mod profile_settings;
|
pub mod profile_settings;
|
||||||
pub mod records;
|
pub mod records;
|
||||||
|
|
|
||||||
186
crates/workspace-server/src/memory_staging.rs
Normal file
186
crates/workspace-server/src/memory_staging.rs
Normal file
|
|
@ -0,0 +1,186 @@
|
||||||
|
use memory::WorkspaceLayout;
|
||||||
|
use memory::consolidate::{StagingEntry, list_staging_entries_snapshot};
|
||||||
|
use memory::extract::StagingRecord;
|
||||||
|
use memory::schema::{SourceEvidenceRef, SourceRef};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
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<MemoryStagingEntrySummary>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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<String>,
|
||||||
|
pub evidence: Vec<MemoryStagingEvidenceSummary>,
|
||||||
|
pub source_refs: Vec<MemorySourceEvidenceRefSummary>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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<String>,
|
||||||
|
pub summary: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
pub struct MemorySourceEvidenceRefSummary {
|
||||||
|
pub session_id: Option<String>,
|
||||||
|
pub segment_id: Option<String>,
|
||||||
|
pub entry_range: Option<[u64; 2]>,
|
||||||
|
pub evidence_id: Option<String>,
|
||||||
|
pub evidence_kind: Option<String>,
|
||||||
|
pub label: Option<String>,
|
||||||
|
pub summary: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn list_memory_staging(
|
||||||
|
layout: &WorkspaceLayout,
|
||||||
|
requested_limit: Option<usize>,
|
||||||
|
) -> MemoryStagingListResponse {
|
||||||
|
let limit = requested_limit
|
||||||
|
.unwrap_or(DEFAULT_MEMORY_STAGING_LIMIT)
|
||||||
|
.min(MAX_MEMORY_STAGING_LIMIT);
|
||||||
|
let snapshot = list_staging_entries_snapshot(layout);
|
||||||
|
let total_valid_count = snapshot.entries.len();
|
||||||
|
let items = snapshot
|
||||||
|
.entries
|
||||||
|
.into_iter()
|
||||||
|
.take(limit)
|
||||||
|
.map(memory_staging_entry_summary)
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
let returned_count = items.len();
|
||||||
|
MemoryStagingListResponse {
|
||||||
|
limit,
|
||||||
|
returned_count,
|
||||||
|
total_valid_count,
|
||||||
|
invalid_count: snapshot.invalid_count,
|
||||||
|
truncated: total_valid_count > returned_count,
|
||||||
|
order: "uuidv7_ascending".to_string(),
|
||||||
|
record_authority: "workspace_memory_staging".to_string(),
|
||||||
|
items,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn memory_staging_entry_summary(entry: StagingEntry) -> MemoryStagingEntrySummary {
|
||||||
|
MemoryStagingEntrySummary {
|
||||||
|
id: entry.id.to_string(),
|
||||||
|
byte_len: entry.bytes,
|
||||||
|
record: memory_staging_record_summary(entry.record),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn memory_staging_record_summary(record: StagingRecord) -> MemoryStagingRecordSummary {
|
||||||
|
MemoryStagingRecordSummary {
|
||||||
|
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(),
|
||||||
|
claim: record.claim,
|
||||||
|
why_useful: record.why_useful,
|
||||||
|
staleness: record.staleness,
|
||||||
|
evidence: record
|
||||||
|
.evidence
|
||||||
|
.into_iter()
|
||||||
|
.map(|evidence| MemoryStagingEvidenceSummary {
|
||||||
|
id: evidence.id,
|
||||||
|
kind: evidence.kind.as_str().to_string(),
|
||||||
|
entry_range: evidence.entry_range,
|
||||||
|
excerpt: evidence.excerpt,
|
||||||
|
summary: evidence.summary,
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
source_refs: record
|
||||||
|
.source_refs
|
||||||
|
.into_iter()
|
||||||
|
.map(memory_source_evidence_ref_summary)
|
||||||
|
.collect(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn memory_source_evidence_ref_summary(
|
||||||
|
source_ref: SourceEvidenceRef,
|
||||||
|
) -> MemorySourceEvidenceRefSummary {
|
||||||
|
MemorySourceEvidenceRefSummary {
|
||||||
|
session_id: source_ref.session_id,
|
||||||
|
segment_id: source_ref.segment_id,
|
||||||
|
entry_range: source_ref.entry_range,
|
||||||
|
evidence_id: source_ref.evidence_id,
|
||||||
|
evidence_kind: source_ref
|
||||||
|
.evidence_kind
|
||||||
|
.map(|evidence_kind| evidence_kind.as_str().to_string()),
|
||||||
|
label: source_ref.label,
|
||||||
|
summary: source_ref.summary,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use memory::extract::{CandidateKind, ExtractedCandidate, ExtractedPayload, write_staging};
|
||||||
|
use tempfile::TempDir;
|
||||||
|
|
||||||
|
fn source() -> SourceRef {
|
||||||
|
SourceRef {
|
||||||
|
segment_id: "segment-1".to_string(),
|
||||||
|
range: [0, 10],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn payload(claim: &str) -> ExtractedPayload {
|
||||||
|
ExtractedPayload {
|
||||||
|
candidates: vec![ExtractedCandidate {
|
||||||
|
kind: CandidateKind::Decision,
|
||||||
|
claim: claim.to_string(),
|
||||||
|
why_useful: "useful for future work".to_string(),
|
||||||
|
staleness: None,
|
||||||
|
evidence_ids: Vec::new(),
|
||||||
|
}],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn lists_memory_staging_records_with_cap() {
|
||||||
|
let temp = TempDir::new().unwrap();
|
||||||
|
let layout = WorkspaceLayout::new(temp.path().to_path_buf());
|
||||||
|
write_staging(&layout, source(), payload("first claim")).unwrap();
|
||||||
|
write_staging(&layout, source(), payload("second claim")).unwrap();
|
||||||
|
|
||||||
|
let response = list_memory_staging(&layout, Some(1));
|
||||||
|
|
||||||
|
assert_eq!(response.limit, 1);
|
||||||
|
assert_eq!(response.returned_count, 1);
|
||||||
|
assert_eq!(response.total_valid_count, 2);
|
||||||
|
assert!(response.truncated);
|
||||||
|
assert_eq!(response.record_authority, "workspace_memory_staging");
|
||||||
|
assert_eq!(response.items[0].record.kind, "decision");
|
||||||
|
assert_eq!(response.items[0].record.claim, "first claim");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -56,6 +56,7 @@ use crate::hosts::{
|
||||||
WorkerSpawnResult, WorkerSpawnWorkingDirectoryRequest, WorkerSummary, WorkerWorkspaceSummary,
|
WorkerSpawnResult, WorkerSpawnWorkingDirectoryRequest, WorkerSummary, WorkerWorkspaceSummary,
|
||||||
};
|
};
|
||||||
use crate::identity::WorkspaceIdentity;
|
use crate::identity::WorkspaceIdentity;
|
||||||
|
use crate::memory_staging::{MemoryStagingListResponse, list_memory_staging};
|
||||||
use crate::observation::{
|
use crate::observation::{
|
||||||
BackendObservationProxy, ObservationProxyError, RuntimeObservationClient,
|
BackendObservationProxy, ObservationProxyError, RuntimeObservationClient,
|
||||||
RuntimeObservationSource, RuntimeObservationSourceConfig,
|
RuntimeObservationSource, RuntimeObservationSourceConfig,
|
||||||
|
|
@ -479,6 +480,10 @@ pub fn build_router(api: WorkspaceApi) -> Router {
|
||||||
"/api/w/{workspace_id}/tickets/backend",
|
"/api/w/{workspace_id}/tickets/backend",
|
||||||
post(scoped_ticket_backend_operation),
|
post(scoped_ticket_backend_operation),
|
||||||
)
|
)
|
||||||
|
.route(
|
||||||
|
"/api/w/{workspace_id}/memory/staging",
|
||||||
|
get(scoped_list_memory_staging),
|
||||||
|
)
|
||||||
.route(
|
.route(
|
||||||
"/api/w/{workspace_id}/memory/backend",
|
"/api/w/{workspace_id}/memory/backend",
|
||||||
post(scoped_memory_backend_operation),
|
post(scoped_memory_backend_operation),
|
||||||
|
|
@ -1139,6 +1144,11 @@ struct TicketKanbanQuery {
|
||||||
limit: Option<usize>,
|
limit: Option<usize>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
struct MemoryStagingQuery {
|
||||||
|
limit: Option<usize>,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
struct TranscriptQuery {
|
struct TranscriptQuery {
|
||||||
start: Option<usize>,
|
start: Option<usize>,
|
||||||
|
|
@ -1432,6 +1442,17 @@ async fn scoped_ticket_backend_operation(
|
||||||
Ok(Json(response))
|
Ok(Json(response))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn scoped_list_memory_staging(
|
||||||
|
State(api): State<WorkspaceApi>,
|
||||||
|
AxumPath(path): AxumPath<ScopedWorkspacePath>,
|
||||||
|
Query(query): Query<MemoryStagingQuery>,
|
||||||
|
) -> ApiResult<Json<MemoryStagingListResponse>> {
|
||||||
|
validate_workspace_scope(&api, &path.workspace_id)?;
|
||||||
|
let memory_config = manifest::MemoryConfig::default();
|
||||||
|
let layout = memory::WorkspaceLayout::resolve(&memory_config, &api.config.workspace_root);
|
||||||
|
Ok(Json(list_memory_staging(&layout, query.limit)))
|
||||||
|
}
|
||||||
|
|
||||||
async fn scoped_memory_backend_operation(
|
async fn scoped_memory_backend_operation(
|
||||||
State(api): State<WorkspaceApi>,
|
State(api): State<WorkspaceApi>,
|
||||||
AxumPath(path): AxumPath<ScopedWorkspacePath>,
|
AxumPath(path): AxumPath<ScopedWorkspacePath>,
|
||||||
|
|
|
||||||
|
|
@ -56,6 +56,7 @@ Deno.test("workspace Worker list lives on the dedicated Workers page", async ()
|
||||||
assert(
|
assert(
|
||||||
!sidebar.includes("CompanionNavSection") &&
|
!sidebar.includes("CompanionNavSection") &&
|
||||||
sidebar.includes("TicketsNavSection") &&
|
sidebar.includes("TicketsNavSection") &&
|
||||||
|
sidebar.includes("MemoryNavSection") &&
|
||||||
sidebar.includes("WorkersNavSection"),
|
sidebar.includes("WorkersNavSection"),
|
||||||
"standalone Companion/Console navigation should not remain canonical and Tickets should be primary workspace navigation",
|
"standalone Companion/Console navigation should not remain canonical and Tickets should be primary workspace navigation",
|
||||||
);
|
);
|
||||||
|
|
@ -109,6 +110,32 @@ Deno.test("workspace Tickets surface uses read-only Backend Ticket APIs", async
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Deno.test("workspace Memory Staging surface uses read-only scoped memory API", async () => {
|
||||||
|
const memoryNav = await Deno.readTextFile(
|
||||||
|
new URL("../sidebar/MemoryNavSection.svelte", import.meta.url),
|
||||||
|
);
|
||||||
|
const memoryLoad = await Deno.readTextFile(
|
||||||
|
new URL("./../../../routes/w/[workspaceId]/memory/staging/+page.ts", import.meta.url),
|
||||||
|
);
|
||||||
|
const memoryPage = await Deno.readTextFile(
|
||||||
|
new URL("./../../../routes/w/[workspaceId]/memory/staging/+page.svelte", import.meta.url),
|
||||||
|
);
|
||||||
|
|
||||||
|
assert(
|
||||||
|
memoryNav.includes("workspaceRoute(workspaceId, '/memory/staging')") &&
|
||||||
|
memoryNav.includes("pending extraction candidates"),
|
||||||
|
"Memory sidebar section should link to the workspace Memory Staging surface",
|
||||||
|
);
|
||||||
|
assert(
|
||||||
|
memoryLoad.includes("workspaceApiPath(params.workspaceId, '/memory/staging')") &&
|
||||||
|
memoryPage.includes("Memory Staging") &&
|
||||||
|
memoryPage.includes("Workspace Server memory authority") &&
|
||||||
|
memoryPage.includes("data.staging.data.invalid_count") &&
|
||||||
|
memoryPage.includes("entry.record.evidence"),
|
||||||
|
"Memory Staging page should read the scoped API and expose staged records without mutation controls",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
Deno.test("root layout does not keep legacy unscoped route compatibility", async () => {
|
Deno.test("root layout does not keep legacy unscoped route compatibility", async () => {
|
||||||
const layoutLoad = await Deno.readTextFile(
|
const layoutLoad = await Deno.readTextFile(
|
||||||
new URL("./../../../routes/+layout.ts", import.meta.url),
|
new URL("./../../../routes/+layout.ts", import.meta.url),
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { workspaceRoute } from '$lib/workspace/api/http';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
currentPath?: string;
|
||||||
|
workspaceId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
let { currentPath = '/', workspaceId }: Props = $props();
|
||||||
|
let stagingHref = $derived(workspaceId ? workspaceRoute(workspaceId, '/memory/staging') : '/');
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<section class="nav-section">
|
||||||
|
<header class="section-header">
|
||||||
|
<span>Memory</span>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<a class="objective-link" class:active={currentPath.startsWith(stagingHref)} href={stagingHref}>
|
||||||
|
<span class="item-title">Staging</span>
|
||||||
|
<span class="item-meta">pending extraction candidates</span>
|
||||||
|
</a>
|
||||||
|
</section>
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
import { workspaceRoute } from '$lib/workspace/api/http';
|
import { workspaceRoute } from '$lib/workspace/api/http';
|
||||||
import './sidebar.css';
|
import './sidebar.css';
|
||||||
import ObjectivesNavSection from './ObjectivesNavSection.svelte';
|
import ObjectivesNavSection from './ObjectivesNavSection.svelte';
|
||||||
|
import MemoryNavSection from './MemoryNavSection.svelte';
|
||||||
import RepositoriesNavSection from './RepositoriesNavSection.svelte';
|
import RepositoriesNavSection from './RepositoriesNavSection.svelte';
|
||||||
import TicketsNavSection from './TicketsNavSection.svelte';
|
import TicketsNavSection from './TicketsNavSection.svelte';
|
||||||
import WorkersNavSection from './WorkersNavSection.svelte';
|
import WorkersNavSection from './WorkersNavSection.svelte';
|
||||||
|
|
@ -78,6 +79,7 @@
|
||||||
<nav class="sidebar-sections" aria-label="Workspace sections">
|
<nav class="sidebar-sections" aria-label="Workspace sections">
|
||||||
<RepositoriesNavSection {repositories} {repositoriesError} {currentPath} {workspaceId} />
|
<RepositoriesNavSection {repositories} {repositoriesError} {currentPath} {workspaceId} />
|
||||||
<ObjectivesNavSection {currentPath} {workspaceId} />
|
<ObjectivesNavSection {currentPath} {workspaceId} />
|
||||||
|
<MemoryNavSection {currentPath} {workspaceId} />
|
||||||
<TicketsNavSection {currentPath} {workspaceId} />
|
<TicketsNavSection {currentPath} {workspaceId} />
|
||||||
<WorkersNavSection {currentPath} {workspaceId} />
|
<WorkersNavSection {currentPath} {workspaceId} />
|
||||||
</nav>
|
</nav>
|
||||||
|
|
|
||||||
|
|
@ -318,6 +318,67 @@ export type RepositoryLogResponse = {
|
||||||
diagnostics: Diagnostic[];
|
diagnostics: Diagnostic[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
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 TicketSummary = {
|
export type TicketSummary = {
|
||||||
id: string;
|
id: string;
|
||||||
title: string;
|
title: string;
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,298 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import type { MemoryStagingEntry, MemoryStagingRecord } from '$lib/workspace/sidebar/types';
|
||||||
|
import type { PageProps } from './$types';
|
||||||
|
|
||||||
|
let { data }: PageProps = $props();
|
||||||
|
|
||||||
|
const entries = $derived(data.staging.data?.items ?? []);
|
||||||
|
|
||||||
|
function kindLabel(kind: string): string {
|
||||||
|
return kind.replaceAll('_', ' ');
|
||||||
|
}
|
||||||
|
|
||||||
|
function sourceLabel(record: MemoryStagingRecord): string {
|
||||||
|
const source = record.source;
|
||||||
|
return `${source.segment_id}:${source.range[0]}-${source.range[1]}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function evidenceCount(entry: MemoryStagingEntry): number {
|
||||||
|
return entry.record.evidence?.length ?? 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function sourceRefCount(entry: MemoryStagingEntry): number {
|
||||||
|
return entry.record.source_refs?.length ?? 0;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:head>
|
||||||
|
<title>Memory Staging · Yoi Workspace</title>
|
||||||
|
<meta name="description" content="Workspace Memory Staging" />
|
||||||
|
</svelte:head>
|
||||||
|
|
||||||
|
<section class="card memory-staging-card">
|
||||||
|
<div class="detail-heading">
|
||||||
|
<div>
|
||||||
|
<p class="eyebrow">Workspace memory</p>
|
||||||
|
<h2>Memory Staging</h2>
|
||||||
|
</div>
|
||||||
|
{#if data.staging.data}
|
||||||
|
<span>{data.staging.data.returned_count} / {data.staging.data.total_valid_count} staged</span>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="section-note">
|
||||||
|
Pending Memory extraction candidates staged for consolidation. This view is read-only and uses the Workspace Server memory authority.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{#if data.staging.data}
|
||||||
|
<div class="staging-summary-grid" aria-label="Memory staging summary">
|
||||||
|
<div>
|
||||||
|
<span>Valid records</span>
|
||||||
|
<strong>{data.staging.data.total_valid_count}</strong>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span>Invalid records</span>
|
||||||
|
<strong>{data.staging.data.invalid_count}</strong>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span>Order</span>
|
||||||
|
<strong>{data.staging.data.order}</strong>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span>Authority</span>
|
||||||
|
<strong>{data.staging.data.record_authority}</strong>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if data.staging.data.truncated}
|
||||||
|
<p class="section-note">Showing first {data.staging.data.limit} staged record(s).</p>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if entries.length === 0}
|
||||||
|
<p>No Memory Staging records are present.</p>
|
||||||
|
{:else}
|
||||||
|
<div class="staging-list">
|
||||||
|
{#each entries as entry (entry.id)}
|
||||||
|
<article class="staging-entry">
|
||||||
|
<header>
|
||||||
|
<div>
|
||||||
|
<span class="kind-pill">{kindLabel(entry.record.kind)}</span>
|
||||||
|
<h3>{entry.record.claim}</h3>
|
||||||
|
</div>
|
||||||
|
<code>{entry.id}</code>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<p>{entry.record.why_useful}</p>
|
||||||
|
|
||||||
|
{#if entry.record.staleness}
|
||||||
|
<p class="staleness">Staleness: {entry.record.staleness}</p>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<dl class="staging-meta">
|
||||||
|
<div>
|
||||||
|
<dt>Extract run</dt>
|
||||||
|
<dd>{entry.record.extract_run_id}</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt>Source</dt>
|
||||||
|
<dd>{sourceLabel(entry.record)}</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt>Evidence</dt>
|
||||||
|
<dd>{evidenceCount(entry)}</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt>Source refs</dt>
|
||||||
|
<dd>{sourceRefCount(entry)}</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt>Bytes</dt>
|
||||||
|
<dd>{entry.byte_len}</dd>
|
||||||
|
</div>
|
||||||
|
</dl>
|
||||||
|
|
||||||
|
{#if entry.record.evidence && entry.record.evidence.length > 0}
|
||||||
|
<details>
|
||||||
|
<summary>Evidence</summary>
|
||||||
|
<ul class="evidence-list">
|
||||||
|
{#each entry.record.evidence as evidence (evidence.id)}
|
||||||
|
<li>
|
||||||
|
<strong>{evidence.id}</strong>
|
||||||
|
<span>{evidence.kind}</span>
|
||||||
|
{#if evidence.summary}
|
||||||
|
<p>{evidence.summary}</p>
|
||||||
|
{/if}
|
||||||
|
{#if evidence.excerpt}
|
||||||
|
<blockquote>{evidence.excerpt}</blockquote>
|
||||||
|
{/if}
|
||||||
|
</li>
|
||||||
|
{/each}
|
||||||
|
</ul>
|
||||||
|
</details>
|
||||||
|
{/if}
|
||||||
|
</article>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{:else if data.staging.error}
|
||||||
|
<p class="error">{data.staging.error}</p>
|
||||||
|
{:else}
|
||||||
|
<p>Waiting for <code>/api/w/{data.workspaceId}/memory/staging</code>…</p>
|
||||||
|
{/if}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.memory-staging-card {
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.staging-summary-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||||
|
gap: 0.75rem;
|
||||||
|
margin: 1rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.staging-summary-grid div {
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: 0.75rem;
|
||||||
|
background: var(--bg-raised);
|
||||||
|
padding: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.staging-summary-grid span,
|
||||||
|
.staging-meta dt {
|
||||||
|
color: var(--text-muted);
|
||||||
|
display: block;
|
||||||
|
font-size: 0.76rem;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.staging-summary-grid strong {
|
||||||
|
color: var(--text);
|
||||||
|
display: block;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
margin-top: 0.25rem;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
}
|
||||||
|
|
||||||
|
.staging-list {
|
||||||
|
display: grid;
|
||||||
|
gap: 0.9rem;
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.staging-entry {
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: 0.9rem;
|
||||||
|
background: var(--bg-raised);
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.staging-entry header {
|
||||||
|
align-items: flex-start;
|
||||||
|
display: flex;
|
||||||
|
gap: 1rem;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.staging-entry h3 {
|
||||||
|
color: var(--text-strong);
|
||||||
|
font-size: 1rem;
|
||||||
|
margin: 0.35rem 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.staging-entry p {
|
||||||
|
color: var(--text);
|
||||||
|
margin: 0.75rem 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.staging-entry code,
|
||||||
|
.staging-meta dd {
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kind-pill {
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: 999px;
|
||||||
|
color: var(--accent);
|
||||||
|
display: inline-flex;
|
||||||
|
font-size: 0.72rem;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
padding: 0.18rem 0.55rem;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.staleness {
|
||||||
|
color: var(--warning);
|
||||||
|
}
|
||||||
|
|
||||||
|
.staging-meta {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(5, minmax(0, 1fr));
|
||||||
|
gap: 0.75rem;
|
||||||
|
margin: 1rem 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.staging-meta div {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.staging-meta dd {
|
||||||
|
color: var(--text);
|
||||||
|
margin: 0.25rem 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
details {
|
||||||
|
border-top: 1px solid var(--line);
|
||||||
|
margin-top: 1rem;
|
||||||
|
padding-top: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
summary {
|
||||||
|
color: var(--text-muted);
|
||||||
|
cursor: pointer;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.evidence-list {
|
||||||
|
display: grid;
|
||||||
|
gap: 0.75rem;
|
||||||
|
list-style: none;
|
||||||
|
margin: 0.75rem 0 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.evidence-list li {
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: 0.7rem;
|
||||||
|
background: var(--bg-subtle);
|
||||||
|
padding: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.evidence-list span {
|
||||||
|
color: var(--text-muted);
|
||||||
|
margin-left: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
blockquote {
|
||||||
|
border-left: 3px solid var(--line-strong);
|
||||||
|
color: var(--text-muted);
|
||||||
|
margin: 0.75rem 0 0;
|
||||||
|
padding-left: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 900px) {
|
||||||
|
.staging-summary-grid,
|
||||||
|
.staging-meta {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.staging-entry header {
|
||||||
|
display: grid;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
import { loadJson, workspaceApiPath } from '$lib/workspace/api/http';
|
||||||
|
import type { MemoryStagingListResponse } from '$lib/workspace/sidebar/types';
|
||||||
|
import type { PageLoad } from './$types';
|
||||||
|
|
||||||
|
export const load: PageLoad = async ({ fetch, params }) => {
|
||||||
|
return {
|
||||||
|
workspaceId: params.workspaceId,
|
||||||
|
staging: await loadJson<MemoryStagingListResponse>(
|
||||||
|
fetch,
|
||||||
|
`${workspaceApiPath(params.workspaceId, '/memory/staging')}?limit=200`,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
};
|
||||||
Loading…
Reference in New Issue
Block a user