feat: cut over Memory to subjektiv product surfaces

This commit is contained in:
2026-10-03 00:06:47 +09:00
parent 07fcc249fe
commit f4966b78ea
54 changed files with 7600 additions and 848 deletions
@@ -1,6 +1,6 @@
// @generated by yoi-companion-openapi-typescript@1. Do not edit.
// Input: openapi/server-api.json
// Input contract: 0.1.0 (sha256:85a55ead75ec0b0ddebc4dd45905db935c59520656b8490df9fb928daac58dd0)
// Input contract: 0.1.0 (sha256:c3466d73ec67447e6f8af0343b20dc90fcffc3620d2e58a4405870311aa8dfd3)
// Options: companion-operation-schema-closure,openapi-3.1,strict-integer-ranges,sorted-output
// Output: web/workspace/src/lib/generated/companion-api.ts
// Regenerate/check: cargo run -q -p server-api --example generate_companion_api_types -- --check
+43 -85
View File
@@ -11,98 +11,56 @@ export const MEMORY_API_LIMITS = {
export type DiagnosticSeverity = "info" | "warning" | "error";
export type Diagnostic = {
code: string;
severity: DiagnosticSeverity;
message: string;
};
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 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 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 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 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 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 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 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<MemoryStagingEvidence>;
source_refs: Array<MemorySourceEvidenceRef>;
};
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<MemoryStagingEvidence>, source_refs: Array<MemorySourceEvidenceRef>, };
export type MemoryStagingEntry = {
id: string;
byte_len: number;
record: MemoryStagingRecord;
};
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<MemoryStagingEntry>;
diagnostics: Array<Diagnostic>;
};
export type MemoryStagingListResponse = { limit: number, returned_count: number, total_valid_count: number, invalid_count: number, truncated: boolean, order: string, record_authority: string, items: Array<MemoryStagingEntry>, diagnostics: Array<Diagnostic>, };
export type SubjektivSubjectState = "active" | "retired";
export type SubjektivSubjectResponse = { id: string, role: string, state: SubjektivSubjectState, store_revision: number, created_at: string, updated_at: string, current_worker?: | import("./worker-launch-api").WorkerLaunchWorkerSummary | null, };
export type SubjektivSubjectListResponse = { limit: number, items: Array<SubjektivSubjectResponse>, has_more: boolean, };
export type SubjektivMemoryState = "active" | "resolved" | "retracted";
export type SubjektivMemoryRevisionRef = { memory_id: string, revision: number, };
export type SubjektivResidentSurfaceAvailability = "ungenerated" | "stale" | "failed" | "ready";
export type SubjektivResidentSurfaceSnapshot = { snapshot_id: string, body_md: string, memory_refs: Array<SubjektivMemoryRevisionRef>, built_from_store_revision: number, created_at: string, };
export type SubjektivResidentSurfaceResponse = { subject_id: string, availability: SubjektivResidentSurfaceAvailability, snapshot?: SubjektivResidentSurfaceSnapshot | null, };
export type SubjektivMemoryQueryItem = { id: string, revision: number, kind: MemoryCandidateKind, state: SubjektivMemoryState, claim: string, excerpt: string, updated_at: string, };
export type SubjektivMemoryQueryResponse = { items: Array<SubjektivMemoryQueryItem>, next_cursor?: string | null, has_more: boolean, };
export type SubjektivMemoryEvidence = { id: string, kind: string, entry_range?: [number, number] | null, origin?: MemoryEvidenceOrigin | null, excerpt?: string | null, summary?: string | null, };
export type SubjektivMemorySourceEvidenceRef = { 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 SubjektivMemoryEvidenceCandidate = { candidate_id: string, evidence: Array<SubjektivMemoryEvidence>, evidence_total: number, evidence_truncated: boolean, source_refs: Array<SubjektivMemorySourceEvidenceRef>, source_refs_total: number, source_refs_truncated: boolean, };
export type SubjektivMemoryReadResponse = { memory_id: string, revision: number, current_revision: number, kind: MemoryCandidateKind, state: SubjektivMemoryState, claim: string, body_md: string, why_useful: string, staleness?: string | null, change_reason: string, created_at: string, updated_at: string, body_offset: number, body_byte_offset: number, body_next_offset?: number | null, body_next_byte_offset?: number | null, body_truncated: boolean, source_candidate_ids: Array<string>, source_candidates: Array<SubjektivMemoryEvidenceCandidate>, derived_from: Array<SubjektivMemoryRevisionRef>, evidence_next_cursor?: string | null, evidence_has_more: boolean, };
export type SubjektivMemoryRevisionItem = { revision: number, kind: MemoryCandidateKind, state: SubjektivMemoryState, claim: string, change_reason: string, updated_at: string, };
export type SubjektivMemoryListRevisionsResponse = { memory_id: string, current_revision: number, items: Array<SubjektivMemoryRevisionItem>, next_cursor?: string | null, has_more: boolean, };
@@ -1,6 +1,6 @@
// @generated by yoi-repository-openapi-typescript@1. Do not edit.
// Input: openapi/server-api.json
// Input contract: 0.1.0 (sha256:85a55ead75ec0b0ddebc4dd45905db935c59520656b8490df9fb928daac58dd0)
// Input contract: 0.1.0 (sha256:c3466d73ec67447e6f8af0343b20dc90fcffc3620d2e58a4405870311aa8dfd3)
// Options: repository-operation-schema-closure,openapi-3.1,format:uint64=safe-number,strict-safe-integers,sorted-output
// Output: web/workspace/src/lib/generated/repository-api.ts
// Regenerate/check: cargo run -q -p server-api --example generate_repository_openapi_types -- --check
@@ -1,6 +1,6 @@
// @generated by yoi-runtime-openapi-typescript@1. Do not edit.
// Input: openapi/server-api.json
// Input contract: 0.1.0 (sha256:85a55ead75ec0b0ddebc4dd45905db935c59520656b8490df9fb928daac58dd0)
// Input contract: 0.1.0 (sha256:c3466d73ec67447e6f8af0343b20dc90fcffc3620d2e58a4405870311aa8dfd3)
// Options: runtime-operation-schema-closure,openapi-3.1,strict-integer-ranges,sorted-output
// Output: web/workspace/src/lib/generated/runtime-api.ts
// Regenerate/check: cargo run -q -p server-api --example generate_runtime_api_types -- --check
@@ -1,6 +1,6 @@
// @generated by yoi-ticket-browser-openapi-typescript@1. Do not edit.
// Input: openapi/server-api.json
// Input contract: 0.1.0 (sha256:85a55ead75ec0b0ddebc4dd45905db935c59520656b8490df9fb928daac58dd0)
// Input contract: 0.1.0 (sha256:c3466d73ec67447e6f8af0343b20dc90fcffc3620d2e58a4405870311aa8dfd3)
// Options: ticket-objective-merge-request-browser-operation-schema-closure,openapi-3.1,strict-integer-ranges,sorted-output
// Output: web/workspace/src/lib/generated/ticket-api.ts
// Regenerate/check: cargo run -q -p server-api --example generate_ticket_api_types -- --check
@@ -1,6 +1,6 @@
// @generated by yoi-worker-openapi-typescript@1. Do not edit.
// Input: openapi/server-api.json
// Input contract: 0.1.0 (sha256:85a55ead75ec0b0ddebc4dd45905db935c59520656b8490df9fb928daac58dd0)
// Input contract: 0.1.0 (sha256:c3466d73ec67447e6f8af0343b20dc90fcffc3620d2e58a4405870311aa8dfd3)
// Options: worker-operation-schema-closure,openapi-3.1,strict-integer-ranges,sorted-output
// Output: web/workspace/src/lib/generated/worker-launch-api.ts
// Regenerate/check: cargo run -q -p server-api --example generate_worker_launch_api_types -- --check
+20 -25
View File
@@ -96,11 +96,7 @@ function optional<T>(
label: string,
parse: (item: unknown, label: string) => T,
): T | null | undefined {
return value === undefined
? undefined
: value === null
? null
: parse(value, label);
return value === undefined ? undefined : value === null ? null : parse(value, label);
}
function diagnostic(value: unknown, label: string): Diagnostic {
@@ -388,17 +384,13 @@ export function parseWorkerSummary(
`${label}.singleton_key`,
string,
),
tags: item.tags === undefined
? []
: array(item.tags, `${label}.tags`, string),
tags: item.tags === undefined ? [] : array(item.tags, `${label}.tags`, string),
workspace: workspaceSummary(item.workspace, `${label}.workspace`),
availability,
state: string(item.state, `${label}.state`),
worker_state: workerState,
last_seen_at: optional(item.last_seen_at, `${label}.last_seen_at`, string),
pinned: item.pinned === undefined
? false
: boolean(item.pinned, `${label}.pinned`),
pinned: item.pinned === undefined ? false : boolean(item.pinned, `${label}.pinned`),
retention_state: item.retention_state === undefined
? ""
: string(item.retention_state, `${label}.retention_state`),
@@ -406,13 +398,11 @@ export function parseWorkerSummary(
item.implementation,
`${label}.implementation`,
),
workdir_attachments: item.workdir_attachments === undefined
? undefined
: array(
item.workdir_attachments,
`${label}.workdir_attachments`,
workerWorkdirAttachment,
),
workdir_attachments: item.workdir_attachments === undefined ? undefined : array(
item.workdir_attachments,
`${label}.workdir_attachments`,
workerWorkdirAttachment,
),
diagnostics: item.diagnostics === undefined
? []
: array(item.diagnostics, `${label}.diagnostics`, diagnostic),
@@ -622,17 +612,22 @@ function workerSummary(
item.implementation,
`${label}.implementation`,
),
workdir_attachments: item.workdir_attachments === undefined
? undefined
: array(
item.workdir_attachments,
`${label}.workdir_attachments`,
runtimeWorkdirAttachment,
),
workdir_attachments: item.workdir_attachments === undefined ? undefined : array(
item.workdir_attachments,
`${label}.workdir_attachments`,
runtimeWorkdirAttachment,
),
diagnostics: array(item.diagnostics, `${label}.diagnostics`, diagnostic),
};
}
export function parseWorkerLaunchWorkerSummary(
value: unknown,
label = "Worker launch summary",
): WorkerLaunchWorkerSummary {
return workerSummary(value, label);
}
export function parseBrowserCreateWorkerResponse(
value: unknown,
): BrowserCreateWorkerResponse {
@@ -448,27 +448,39 @@ Deno.test("workspace Memory surfaces use read-only scoped memory APIs", async ()
const memoryNav = await Deno.readTextFile(
new URL("../sidebar/MemoryNavSection.svelte", import.meta.url),
);
const memoryDocumentLoad = await Deno.readTextFile(
const subjectIndexLoad = await Deno.readTextFile(
new URL(
"./../../../routes/w/[workspaceId]/memory/+page.ts",
import.meta.url,
),
);
const memoryDocumentPage = await Deno.readTextFile(
const subjectIndexPage = await Deno.readTextFile(
new URL(
"./../../../routes/w/[workspaceId]/memory/+page.svelte",
import.meta.url,
),
);
const memoryStagingLoad = await Deno.readTextFile(
const subjectPageLoad = await Deno.readTextFile(
new URL(
"./../../../routes/w/[workspaceId]/memory/staging/+page.ts",
"./../../../routes/w/[workspaceId]/memory/[subjectId]/+page.ts",
import.meta.url,
),
);
const memoryStagingPage = await Deno.readTextFile(
const subjectPage = await Deno.readTextFile(
new URL(
"./../../../routes/w/[workspaceId]/memory/staging/+page.svelte",
"./../../../routes/w/[workspaceId]/memory/[subjectId]/+page.svelte",
import.meta.url,
),
);
const memoryDetailLoad = await Deno.readTextFile(
new URL(
"./../../../routes/w/[workspaceId]/memory/[subjectId]/[memoryId]/+page.ts",
import.meta.url,
),
);
const memoryDetailPage = await Deno.readTextFile(
new URL(
"./../../../routes/w/[workspaceId]/memory/[subjectId]/[memoryId]/+page.svelte",
import.meta.url,
),
);
@@ -478,35 +490,44 @@ Deno.test("workspace Memory surfaces use read-only scoped memory APIs", async ()
memoryNav.includes(
'<h2 class="sidebar-nav-section__header">Memory</h2>',
) &&
memoryNav.includes("Document</a>") &&
memoryNav.includes("Subjects</a>") &&
memoryNav.includes("currentPath.startsWith(`${subjectsHref}/`)") &&
memoryNav.includes("sidebar-nav-section--category") &&
memoryNav.includes('class="sidebar-link"') &&
memoryNav.includes('workspaceRoute(workspaceId, "/memory/staging")') &&
memoryNav.includes("Staging</a>") &&
!memoryNav.includes("item-meta") &&
!memoryNav.includes("durable workspace memory") &&
!memoryNav.includes("pending extraction candidates"),
"Memory sidebar section should show Document and Staging as compact single-line links",
!memoryNav.includes("Staging</a>") &&
!memoryNav.includes("Document</a>"),
"Memory sidebar section should expose one Subjects link for the full nested route tree",
);
assert(
memoryDocumentLoad.includes("workspaceApiPath(params.workspaceId") &&
memoryDocumentLoad.includes('"/memory"') &&
memoryDocumentPage.includes("DocumentMarkdown from") &&
memoryDocumentPage.includes("Read-only") &&
memoryDocumentPage.includes("data.memory.data.body_md") &&
memoryDocumentPage.includes("data.memory.data.updated_at") &&
memoryDocumentPage.includes('role="alert"') &&
!memoryDocumentPage.includes('<pre class="memory-document-body"'),
"Memory Document page should read the scoped API and expose safe Markdown states without mutation controls",
subjectIndexLoad.includes('"/subjektiv/subjects"') &&
subjectIndexLoad.includes("parseSubjektivSubjectListResponse") &&
subjectIndexPage.includes(">Subjects</h1>") &&
subjectIndexPage.includes("subject.store_revision") &&
subjectIndexPage.includes('data-memory-view="subjects"'),
"Memory product entry should list explicit typed subjects",
);
assert(
memoryStagingLoad.includes("workspaceApiPath(params.workspaceId") &&
memoryStagingLoad.includes('"/memory/staging"') &&
memoryStagingPage.includes("Memory Staging") &&
memoryStagingPage.includes("Workspace Server memory authority") &&
memoryStagingPage.includes("data.staging.data.invalid_count") &&
memoryStagingPage.includes("entry.record.evidence"),
"Memory Staging page should read the scoped API and expose staged records without mutation controls",
subjectPageLoad.includes("`${subjectPath}/surface`") &&
subjectPageLoad.includes("`${subjectPath}/memories`") &&
subjectPage.includes("DocumentMarkdown from") &&
subjectPage.includes("Resident surface") &&
subjectPage.includes("Current Memories") &&
subjectPage.includes("surface?.availability === 'ready'") &&
subjectPage.includes("surface?.availability === 'stale'") &&
subjectPage.includes("data-surface-ready-empty") &&
!subjectPage.includes("overflow-y"),
"Subject page should combine strict resident-surface and current-Memory reads without a nested vertical scroller",
);
assert(
memoryDetailLoad.includes("`${memoryPath}/revisions`") &&
memoryDetailLoad.includes("parseSubjektivMemoryReadResponse") &&
memoryDetailPage.includes("Candidate provenance") &&
memoryDetailPage.includes("Source refs") &&
memoryDetailPage.includes("Derived from") &&
memoryDetailPage.includes("Revision history") &&
memoryDetailPage.includes("DocumentMarkdown from") &&
!memoryDetailPage.includes("overflow-y"),
"Committed Memory detail should preserve bounded provenance and immutable revisions without mutation controls",
);
});
@@ -719,8 +740,12 @@ Deno.test("Worker Console renders Edit diffs without preformatted template gaps"
Deno.test("Console spacing and text metrics use existing design tokens", async () => {
const files = [
"ComposerInput.svelte", "ConsoleLineItem.svelte", "RichMarkdown.svelte",
"ConsoleTasks.svelte", "WorkerRunStatus.svelte", "WorkerContextStatus.svelte",
"ComposerInput.svelte",
"ConsoleLineItem.svelte",
"RichMarkdown.svelte",
"ConsoleTasks.svelte",
"WorkerRunStatus.svelte",
"WorkerContextStatus.svelte",
"ConsoleDisplayState.svelte",
"ConsoleTurnNavigation.svelte",
"./../../../routes/w/[workspaceId]/workers/[workerRef]/console/+page.svelte",
@@ -729,7 +754,8 @@ Deno.test("Console spacing and text metrics use existing design tokens", async (
const source = await Deno.readTextFile(new URL(file, import.meta.url));
const css = source.split("<style>")[1];
assert(
!/(?:gap|padding(?:-[\w]+)?|margin(?:-[\w]+)?)\s*:[^;]*\d(?:\.\d+)?rem/.test(css),
!/(?:gap|padding(?:-[\w]+)?|margin(?:-[\w]+)?)\s*:[^;]*\d(?:\.\d+)?rem/
.test(css),
`${file} should use shared spacing tokens, not a local rem spacing scale`,
);
assert(
@@ -737,7 +763,9 @@ Deno.test("Console spacing and text metrics use existing design tokens", async (
`${file} should use the body/compact line-height tokens`,
);
}
const navigation = await Deno.readTextFile(new URL("./ConsoleTurnNavigation.svelte", import.meta.url));
const navigation = await Deno.readTextFile(
new URL("./ConsoleTurnNavigation.svelte", import.meta.url),
);
assert(
navigation.includes("box-shadow: var(--shadow-overlay)") &&
navigation.includes("font-size: var(--font-size-body)") &&
@@ -764,7 +792,9 @@ Deno.test("Console and app shell keep compact layout spacing", async () => {
import.meta.url,
),
);
const mainRules = [...layout.matchAll(/^\s*\.app-shell__main\s*\{([^}]+)\}/gm)];
const mainRules = [
...layout.matchAll(/^\s*\.app-shell__main\s*\{([^}]+)\}/gm),
];
assert(
mainRules.length === 2 &&
mainRules.every((rule) => rule[1].includes("padding: var(--space-4);")),
@@ -785,7 +815,9 @@ Deno.test("Worker Console uses turn bars with hidden scrollbars and preserves sc
import.meta.url,
),
);
const navigation = await Deno.readTextFile(new URL("./ConsoleTurnNavigation.svelte", import.meta.url));
const navigation = await Deno.readTextFile(
new URL("./ConsoleTurnNavigation.svelte", import.meta.url),
);
assert(
!/timeline/i.test(consolePage) &&
consolePage.includes("conversationTurnPreviewsFromLines(lines)") &&
@@ -814,11 +846,17 @@ Deno.test("Worker Console uses turn bars with hidden scrollbars and preserves sc
consolePage.includes('class="console-scroll"') &&
consolePage.includes("overflow-y: auto") &&
consolePage.includes("onscroll={handleConsoleScroll}") &&
consolePage.includes("autoFollowConsole = isNearConsoleBottom(consoleBodyElement)") &&
consolePage.includes(
"autoFollowConsole = isNearConsoleBottom(consoleBodyElement)",
) &&
consolePage.includes("void scrollConsoleToBottom()") &&
consolePage.includes("rememberConsoleWorkerViewScroll()") &&
consolePage.includes("consoleBodyElement.scrollTop = resolveConsoleViewScrollTop(") &&
consolePage.includes('event.key === "PageUp" || event.key === "PageDown"') &&
consolePage.includes(
"consoleBodyElement.scrollTop = resolveConsoleViewScrollTop(",
) &&
consolePage.includes(
'event.key === "PageUp" || event.key === "PageDown"',
) &&
consolePage.includes("consoleBodyElement.scrollBy({"),
"Native scroll, PageUp/PageDown, conditional bottom-follow and per-Worker scroll restoration must remain",
);
@@ -896,7 +934,8 @@ Deno.test("Worker Console composer separates the editor from its icon toolbar",
const composerInput = await Deno.readTextFile(
new URL("./ComposerInput.svelte", import.meta.url),
);
const footerStyle = consolePage.match(/\.composer-input-footer\s*\{([^}]+)\}/)?.[1];
const footerStyle = consolePage.match(/\.composer-input-footer\s*\{([^}]+)\}/)
?.[1];
assert(
footerStyle?.includes("display: grid") &&
!/position:\s*(absolute|fixed)/.test(footerStyle) &&
@@ -905,7 +944,9 @@ Deno.test("Worker Console composer separates the editor from its icon toolbar",
);
assert(
composerInput.includes("min-height: 2.65rem") &&
composerInput.includes("padding: var(--space-2) var(--space-2) var(--space-1)") &&
composerInput.includes(
"padding: var(--space-2) var(--space-2) var(--space-1)",
) &&
!composerInput.includes("padding: 0.55rem 3.4rem 3rem 0.65rem"),
"Editor must not reserve an overlapping toolbar inside its scrolling content",
);
@@ -997,19 +1038,32 @@ Deno.test("Worker Console paste chips preserve typed draft and target authority"
});
Deno.test("Worker Console routes image paste through the existing attachment upload", async () => {
const consolePage = await Deno.readTextFile(new URL(
"./../../../routes/w/[workspaceId]/workers/[workerRef]/console/+page.svelte", import.meta.url,
));
const input = await Deno.readTextFile(new URL("./ComposerInput.svelte", import.meta.url));
const paste = input.slice(input.indexOf("function handlePasteEvent"), input.indexOf("function selectedClipboardContent"));
const consolePage = await Deno.readTextFile(
new URL(
"./../../../routes/w/[workspaceId]/workers/[workerRef]/console/+page.svelte",
import.meta.url,
),
);
const input = await Deno.readTextFile(
new URL("./ComposerInput.svelte", import.meta.url),
);
const paste = input.slice(
input.indexOf("function handlePasteEvent"),
input.indexOf("function selectedClipboardContent"),
);
assert(
paste.indexOf("if (disabled || view?.state.readOnly)") < paste.indexOf("handleComposerImagePaste(event, onpasteimages)") &&
paste.indexOf("handleComposerImagePaste(event, onpasteimages)") < paste.indexOf('getData("text/plain")') &&
paste.indexOf("if (disabled || view?.state.readOnly)") <
paste.indexOf("handleComposerImagePaste(event, onpasteimages)") &&
paste.indexOf("handleComposerImagePaste(event, onpasteimages)") <
paste.indexOf('getData("text/plain")') &&
consolePage.includes("onpasteimages={addPastedImages}") &&
consolePage.includes("addAttachmentFiles(files.map(namePastedImage))"),
"image paste must respect editability and take priority over text before using the attachment callback",
);
const attach = consolePage.slice(consolePage.indexOf("function addAttachmentFiles"), consolePage.indexOf("async function removeAttachment"));
const attach = consolePage.slice(
consolePage.indexOf("function addAttachmentFiles"),
consolePage.indexOf("async function removeAttachment"),
);
assert(
attach.includes("if (!composerEditable) return") &&
attach.includes("MAX_FILES_PER_SUBMISSION - attachments.length") &&
@@ -1488,43 +1542,98 @@ Deno.test("Web Console switches main and direct SubWorker views from the Tasks r
});
Deno.test("Composer owns TUI-style completion and replaces Compact/Rewind header buttons", async () => {
const page = await Deno.readTextFile(new URL("./../../../routes/w/[workspaceId]/workers/[workerRef]/console/+page.svelte", import.meta.url));
const input = await Deno.readTextFile(new URL("./ComposerInput.svelte", import.meta.url));
const header = page.slice(page.indexOf('<section class="console-header'), page.indexOf('{#if rewindTargets.length'));
assert(!header.includes("Compact") && !header.includes("Rewind") && !page.includes("requestRewindTargets"), "Header must defer Compact/Rewind to commands");
assert(input.includes('role="listbox"') && input.includes('role="option"') && input.includes("entry.description") && input.includes("completionStart + 6"), "Completion must expose a bounded selectable list with descriptions");
assert(input.indexOf("keydown: completionKeydown") < input.indexOf('key: "ArrowUp"'), "Completion navigation must precede input history");
assert(page.includes("fileCompletions.receive(event.data.entries)") && page.includes("fileCompletions.close()"), "File completion lifecycle must be scoped to the transport");
const page = await Deno.readTextFile(
new URL(
"./../../../routes/w/[workspaceId]/workers/[workerRef]/console/+page.svelte",
import.meta.url,
),
);
const input = await Deno.readTextFile(
new URL("./ComposerInput.svelte", import.meta.url),
);
const header = page.slice(
page.indexOf('<section class="console-header'),
page.indexOf("{#if rewindTargets.length"),
);
assert(
!header.includes("Compact") && !header.includes("Rewind") &&
!page.includes("requestRewindTargets"),
"Header must defer Compact/Rewind to commands",
);
assert(
input.includes('role="listbox"') && input.includes('role="option"') &&
input.includes("entry.description") &&
input.includes("completionStart + 6"),
"Completion must expose a bounded selectable list with descriptions",
);
assert(
input.indexOf("keydown: completionKeydown") <
input.indexOf('key: "ArrowUp"'),
"Completion navigation must precede input history",
);
assert(
page.includes("fileCompletions.receive(event.data.entries)") &&
page.includes("fileCompletions.close()"),
"File completion lifecycle must be scoped to the transport",
);
});
Deno.test("mini task summary owns pane toggling instead of the header", async () => {
const page = await Deno.readTextFile(new URL(
"./../../../routes/w/[workspaceId]/workers/[workerRef]/console/+page.svelte", import.meta.url,
));
const component = await Deno.readTextFile(new URL("./ConsoleTasks.svelte", import.meta.url));
const header = page.slice(page.indexOf('<section class="console-header'), page.indexOf('{#if rewindTargets.length'));
assert(!header.includes("taskPaneOpen = !taskPaneOpen") && page.includes("onTogglePane={() => {"), "Only the mini summary should own Tasks toggling");
assert(component.includes("aria-expanded={paneOpen}") && component.includes("aria-controls={paneId}") && component.includes("onclick={onTogglePane}"), "Tasks summary must be an accessible pane toggle");
assert(!/\.task-pane\s*\{[^}]*display:\s*none/.test(component), "Narrow layouts must not hide the opened Tasks pane");
assert(page.includes("grid-template-rows: minmax(0, 1fr) minmax(0, 1fr)"), "Narrow layouts should bound transcript and task scrolling separately");
const page = await Deno.readTextFile(
new URL(
"./../../../routes/w/[workspaceId]/workers/[workerRef]/console/+page.svelte",
import.meta.url,
),
);
const component = await Deno.readTextFile(
new URL("./ConsoleTasks.svelte", import.meta.url),
);
const header = page.slice(
page.indexOf('<section class="console-header'),
page.indexOf("{#if rewindTargets.length"),
);
assert(
!header.includes("taskPaneOpen = !taskPaneOpen") &&
page.includes("onTogglePane={() => {"),
"Only the mini summary should own Tasks toggling",
);
assert(
component.includes("aria-expanded={paneOpen}") &&
component.includes("aria-controls={paneId}") &&
component.includes("onclick={onTogglePane}"),
"Tasks summary must be an accessible pane toggle",
);
assert(
!/\.task-pane\s*\{[^}]*display:\s*none/.test(component),
"Narrow layouts must not hide the opened Tasks pane",
);
assert(
page.includes("grid-template-rows: minmax(0, 1fr) minmax(0, 1fr)"),
"Narrow layouts should bound transcript and task scrolling separately",
);
});
Deno.test("Pending inputs are single-line previews between Tasks and Composer", async () => {
const page = await Deno.readTextFile(new URL(
"./../../../routes/w/[workspaceId]/workers/[workerRef]/console/+page.svelte",
import.meta.url,
));
const page = await Deno.readTextFile(
new URL(
"./../../../routes/w/[workspaceId]/workers/[workerRef]/console/+page.svelte",
import.meta.url,
),
);
const pending = page.indexOf('<section class="pending-submissions"');
assert(
page.indexOf("<ConsoleTasks") < pending && pending < page.indexOf('<form class="console-composer"'),
page.indexOf("<ConsoleTasks") < pending &&
pending < page.indexOf('<form class="console-composer"'),
"Pending inputs should sit between Tasks and Composer",
);
const section = page.slice(pending, page.indexOf("</section>", pending));
assert(
!section.includes("<details") && !page.includes('method: "continue_pending"') &&
!section.includes("{submission.submission_id}") && !section.includes("segment_count") &&
!section.includes("<details") &&
!page.includes('method: "continue_pending"') &&
!section.includes("{submission.submission_id}") &&
!section.includes("segment_count") &&
section.includes('submission.preview || "Preview unavailable"') &&
section.includes('aria-label={`Cancel queued input ${index + 1}`}') &&
section.includes("aria-label={`Cancel queued input ${index + 1}`}") &&
section.includes('aria-label="Notifications"') &&
section.includes("pendingSubmissions.notification_previews ?? []") &&
!page.includes('method: "clear_pending_submissions"'),
@@ -1533,18 +1642,23 @@ Deno.test("Pending inputs are single-line previews between Tasks and Composer",
const css = page.split("<style>")[1];
const previewRule = css.match(/\.pending-submission-preview,[^{]+\{([^}]+)\}/)?.[1] ?? "";
assert(
previewRule.includes("white-space: nowrap") && previewRule.includes("min-width: 0") &&
previewRule.includes("overflow: hidden") && previewRule.includes("text-overflow: ellipsis"),
previewRule.includes("white-space: nowrap") &&
previewRule.includes("min-width: 0") &&
previewRule.includes("overflow: hidden") &&
previewRule.includes("text-overflow: ellipsis"),
"Preview text must truncate instead of wrapping or expanding the row",
);
assert(
/\.pending-queue li\s*\{[^}]*grid-template-columns: minmax\(0, 1fr\) auto/.test(css) &&
/\.pending-queue li\s*\{[^}]*grid-template-columns: minmax\(0, 1fr\) auto/
.test(css) &&
css.includes("grid-template-columns: repeat(2, minmax(0, 1fr))") &&
!section.includes("BevelLine") &&
/\.pending-submissions\s*\{[^}]*font-size: var\(--font-size-compact\);[^}]*line-height: var\(--line-height-compact\);/.test(css) &&
/\.pending-submissions\s*\{[^}]*font-size: var\(--font-size-compact\);[^}]*line-height: var\(--line-height-compact\);/
.test(css) &&
css.includes(".pending-queue li:hover .pending-icon-button") &&
css.includes(".pending-queue li:focus-within .pending-icon-button") &&
css.includes("@media (hover: none)") && section.includes('<path d="M5 12h14"'),
css.includes("@media (hover: none)") &&
section.includes('<path d="M5 12h14"'),
"Cancel icons must retain their column when preview text is long",
);
});
@@ -1581,18 +1695,24 @@ Deno.test("Web Console uses Notify while running and exposes queued input cancel
assert(
consolePage.includes("handleQueueSubmit") &&
consolePage.includes('delivery: "queue"') &&
consolePage.includes('disabled={!canQueueDraft}') &&
consolePage.includes("disabled={!canQueueDraft}") &&
consolePage.includes('aria-label="Queue Submit"') &&
consolePage.includes('class="composer-queue-icon"') &&
consolePage.includes('{#if workerRunning || workerPaused}') &&
consolePage.includes("{#if workerRunning || workerPaused}") &&
consolePage.includes('delivery === "queue" && request.kind !== "user"') &&
consolePage.includes('method.method === "submit" && delivery === "submit"'),
consolePage.includes(
'method.method === "submit" && delivery === "submit"',
),
"busy Console must offer explicit Queue without changing paused state or queueing commands",
);
const actions = consolePage.slice(consolePage.indexOf('class="composer-submit-actions"'));
const actions = consolePage.slice(
consolePage.indexOf('class="composer-submit-actions"'),
);
assert(
actions.indexOf('class="composer-queue-button"') < actions.indexOf('class="composer-notify-button"') &&
actions.indexOf('class="composer-notify-button"') < actions.indexOf('class="composer-send-button"'),
actions.indexOf('class="composer-queue-button"') <
actions.indexOf('class="composer-notify-button"') &&
actions.indexOf('class="composer-notify-button"') <
actions.indexOf('class="composer-send-button"'),
"Queue must sit beside Notify and Submit/Stop",
);
+629 -10
View File
@@ -12,7 +12,24 @@ import type {
MemoryStagingEvidence,
MemoryStagingListResponse,
MemoryStagingRecord,
SubjektivMemoryEvidence,
SubjektivMemoryEvidenceCandidate,
SubjektivMemoryListRevisionsResponse,
SubjektivMemoryQueryItem,
SubjektivMemoryQueryResponse,
SubjektivMemoryReadResponse,
SubjektivMemoryRevisionItem,
SubjektivMemoryRevisionRef,
SubjektivMemorySourceEvidenceRef,
SubjektivMemoryState,
SubjektivResidentSurfaceAvailability,
SubjektivResidentSurfaceResponse,
SubjektivResidentSurfaceSnapshot,
SubjektivSubjectListResponse,
SubjektivSubjectResponse,
SubjektivSubjectState,
} from "$lib/generated/memory-api";
import { parseWorkerLaunchWorkerSummary } from "$lib/workspace/api/workers";
const MAX_STAGING_ITEMS = MEMORY_API_LIMITS.maxCollectionItems;
const MAX_EVIDENCE_PER_RECORD = MEMORY_API_LIMITS.maxCollectionItems;
@@ -42,6 +59,27 @@ const diagnosticSeverities = new Set<DiagnosticSeverity>([
"warning",
"error",
]);
const subjectStates = new Set<SubjektivSubjectState>(["active", "retired"]);
const surfaceAvailabilities = new Set<SubjektivResidentSurfaceAvailability>([
"ungenerated",
"stale",
"failed",
"ready",
]);
const memoryStates = new Set<SubjektivMemoryState>([
"active",
"resolved",
"retracted",
]);
const MAX_SUBJECTS = 100;
const MAX_CURRENT_MEMORIES = 100;
const MAX_REVISIONS = 100;
const MAX_MEMORY_REFS = MEMORY_API_LIMITS.maxCollectionItems;
export const MEMORY_API_LOAD_POLICY = {
diagnosticLabel: "Memory API",
maxResponseBytes: MEMORY_API_LIMITS.maxResponseBytes,
} as const;
export function parseMemoryDocumentResponse(
value: unknown,
@@ -190,9 +228,7 @@ function parseStagingEvidence(value: unknown): MemoryStagingEvidence {
summary: nullableString(record, "summary"),
};
if ("origin" in record) {
result.origin = record.origin === null
? null
: parseEvidenceOrigin(record.origin);
result.origin = record.origin === null ? null : parseEvidenceOrigin(record.origin);
}
return result;
}
@@ -223,9 +259,7 @@ function parseSourceEvidenceRef(value: unknown): MemorySourceEvidenceRef {
summary: nullableString(record, "summary"),
};
if ("origin" in record) {
result.origin = record.origin === null
? null
: parseEvidenceOrigin(record.origin);
result.origin = record.origin === null ? null : parseEvidenceOrigin(record.origin);
}
return result;
}
@@ -283,6 +317,542 @@ function parseEvidenceOrigin(value: unknown): MemoryEvidenceOrigin {
return result;
}
export function parseSubjektivSubjectResponse(
value: unknown,
expectedSubjectId?: string,
): SubjektivSubjectResponse {
const record = strictRecord(
value,
[
"id",
"role",
"state",
"store_revision",
"created_at",
"updated_at",
"current_worker",
],
"Subject response",
["current_worker"],
);
const id = requiredIdentifier(record, "id");
if (expectedSubjectId !== undefined && id !== expectedSubjectId) {
invalid("Subject response identity does not match the requested subject");
}
const state = requiredString(record, "state") as SubjektivSubjectState;
if (!subjectStates.has(state)) invalid("unknown Subject state");
const result: SubjektivSubjectResponse = {
id,
role: requiredString(record, "role"),
state,
store_revision: requiredNonNegativeInteger(record, "store_revision"),
created_at: requiredString(record, "created_at"),
updated_at: requiredString(record, "updated_at"),
};
if ("current_worker" in record) {
result.current_worker = record.current_worker === null ? null : parseWorkerLaunchWorkerSummary(
record.current_worker,
"Subject current worker",
);
}
return result;
}
export function parseSubjektivSubjectListResponse(
value: unknown,
): SubjektivSubjectListResponse {
const record = strictRecord(
value,
["limit", "items", "has_more"],
"Subject list response",
);
const limit = positiveInteger(record.limit, "limit");
if (limit > MAX_SUBJECTS) invalid("limit exceeds the Subject list bound");
const items = boundedArray(record.items, MAX_SUBJECTS, "items").map((item) =>
parseSubjektivSubjectResponse(item)
);
if (items.length > limit) invalid("items exceeds the declared Subject limit");
assertUnique(items.map((item) => item.id), "Subject ids");
return {
limit,
items,
has_more: requiredBoolean(record, "has_more"),
};
}
export function parseSubjektivResidentSurfaceResponse(
value: unknown,
expectedSubjectId?: string,
): SubjektivResidentSurfaceResponse {
const record = strictRecord(
value,
["subject_id", "availability", "snapshot"],
"Resident surface response",
["snapshot"],
);
const subjectId = requiredIdentifier(record, "subject_id");
if (expectedSubjectId !== undefined && subjectId !== expectedSubjectId) {
invalid("Resident surface identity does not match the requested subject");
}
const availability = requiredString(
record,
"availability",
) as SubjektivResidentSurfaceAvailability;
if (!surfaceAvailabilities.has(availability)) {
invalid("unknown resident surface availability");
}
const snapshot = "snapshot" in record && record.snapshot !== null
? parseResidentSurfaceSnapshot(record.snapshot)
: null;
if (availability === "ready" && snapshot === null) {
invalid("ready resident surface requires a snapshot");
}
if (availability !== "ready" && snapshot !== null) {
invalid("non-ready resident surface must not include a snapshot");
}
const result: SubjektivResidentSurfaceResponse = {
subject_id: subjectId,
availability,
};
if (snapshot !== null) result.snapshot = snapshot;
return result;
}
function parseResidentSurfaceSnapshot(
value: unknown,
): SubjektivResidentSurfaceSnapshot {
const record = strictRecord(
value,
[
"snapshot_id",
"body_md",
"memory_refs",
"built_from_store_revision",
"created_at",
],
"Resident surface snapshot",
);
return {
snapshot_id: requiredIdentifier(record, "snapshot_id"),
body_md: requiredString(
record,
"body_md",
MEMORY_API_LIMITS.maxDocumentBytes,
),
memory_refs: boundedArray(
record.memory_refs,
MAX_MEMORY_REFS,
"memory_refs",
).map(parseMemoryRevisionRef),
built_from_store_revision: requiredNonNegativeInteger(
record,
"built_from_store_revision",
),
created_at: requiredString(record, "created_at"),
};
}
export function parseSubjektivMemoryQueryResponse(
value: unknown,
): SubjektivMemoryQueryResponse {
const record = strictRecord(
value,
["items", "next_cursor", "has_more"],
"Current Memory list response",
["next_cursor"],
);
const items = boundedArray(
record.items,
MAX_CURRENT_MEMORIES,
"items",
).map(parseMemoryQueryItem);
assertUnique(items.map((item) => item.id), "Memory ids");
const hasMore = requiredBoolean(record, "has_more");
const nextCursor = optionalNullableString(record, "next_cursor");
assertCursorInvariant(hasMore, nextCursor, "Memory list");
const result: SubjektivMemoryQueryResponse = { items, has_more: hasMore };
if (nextCursor !== undefined) result.next_cursor = nextCursor;
return result;
}
function parseMemoryQueryItem(value: unknown): SubjektivMemoryQueryItem {
const record = strictRecord(
value,
["id", "revision", "kind", "state", "claim", "excerpt", "updated_at"],
"Current Memory item",
);
return {
id: requiredIdentifier(record, "id"),
revision: requiredPositiveInteger(record, "revision"),
kind: parseCandidateKind(record.kind),
state: parseMemoryState(record.state),
claim: requiredString(record, "claim"),
excerpt: requiredString(record, "excerpt"),
updated_at: requiredString(record, "updated_at"),
};
}
export function parseSubjektivMemoryReadResponse(
value: unknown,
expectedMemoryId?: string,
): SubjektivMemoryReadResponse {
const record = strictRecord(
value,
[
"memory_id",
"revision",
"current_revision",
"kind",
"state",
"claim",
"body_md",
"why_useful",
"staleness",
"change_reason",
"created_at",
"updated_at",
"body_offset",
"body_byte_offset",
"body_next_offset",
"body_next_byte_offset",
"body_truncated",
"source_candidate_ids",
"source_candidates",
"derived_from",
"evidence_next_cursor",
"evidence_has_more",
],
"Memory detail response",
[
"staleness",
"body_next_offset",
"body_next_byte_offset",
"evidence_next_cursor",
],
);
const memoryId = requiredIdentifier(record, "memory_id");
if (expectedMemoryId !== undefined && memoryId !== expectedMemoryId) {
invalid("Memory detail identity does not match the requested Memory");
}
const revision = requiredPositiveInteger(record, "revision");
const currentRevision = requiredPositiveInteger(record, "current_revision");
if (revision > currentRevision) invalid("revision exceeds current_revision");
const bodyTruncated = requiredBoolean(record, "body_truncated");
const bodyNextOffset = optionalNonNegativeInteger(record, "body_next_offset");
const bodyNextByteOffset = optionalNonNegativeInteger(
record,
"body_next_byte_offset",
);
if (
bodyTruncated !==
(bodyNextOffset !== undefined && bodyNextByteOffset !== undefined)
) {
invalid("body continuation fields do not match body_truncated");
}
const sourceCandidateIds = boundedArray(
record.source_candidate_ids,
MAX_MEMORY_REFS,
"source_candidate_ids",
).map((item) => identifier(item, "source_candidate_ids item"));
assertUnique(sourceCandidateIds, "source candidate ids");
const sourceCandidates = boundedArray(
record.source_candidates,
MAX_MEMORY_REFS,
"source_candidates",
).map(parseMemoryEvidenceCandidate);
if (
sourceCandidateIds.length !== sourceCandidates.length ||
sourceCandidateIds.some((id, index) => sourceCandidates[index]?.candidate_id !== id)
) {
invalid("source candidate ids do not match source candidates");
}
const evidenceHasMore = requiredBoolean(record, "evidence_has_more");
const evidenceNextCursor = optionalNullableString(
record,
"evidence_next_cursor",
);
assertCursorInvariant(evidenceHasMore, evidenceNextCursor, "Memory evidence");
const result: SubjektivMemoryReadResponse = {
memory_id: memoryId,
revision,
current_revision: currentRevision,
kind: parseCandidateKind(record.kind),
state: parseMemoryState(record.state),
claim: requiredString(record, "claim"),
body_md: requiredString(
record,
"body_md",
MEMORY_API_LIMITS.maxDocumentBytes,
),
why_useful: requiredString(record, "why_useful"),
change_reason: requiredString(record, "change_reason"),
created_at: requiredString(record, "created_at"),
updated_at: requiredString(record, "updated_at"),
body_offset: requiredNonNegativeInteger(record, "body_offset"),
body_byte_offset: requiredNonNegativeInteger(record, "body_byte_offset"),
body_truncated: bodyTruncated,
source_candidate_ids: sourceCandidateIds,
source_candidates: sourceCandidates,
derived_from: boundedArray(
record.derived_from,
MAX_MEMORY_REFS,
"derived_from",
).map(parseMemoryRevisionRef),
evidence_has_more: evidenceHasMore,
};
if ("staleness" in record) {
result.staleness = nullableString(record, "staleness");
}
if (bodyNextOffset !== undefined) result.body_next_offset = bodyNextOffset;
if (bodyNextByteOffset !== undefined) {
result.body_next_byte_offset = bodyNextByteOffset;
}
if (evidenceNextCursor !== undefined) {
result.evidence_next_cursor = evidenceNextCursor;
}
return result;
}
function parseMemoryEvidenceCandidate(
value: unknown,
): SubjektivMemoryEvidenceCandidate {
const record = strictRecord(
value,
[
"candidate_id",
"evidence",
"evidence_total",
"evidence_truncated",
"source_refs",
"source_refs_total",
"source_refs_truncated",
],
"Memory evidence candidate",
);
const evidence = boundedArray(
record.evidence,
MAX_EVIDENCE_PER_RECORD,
"evidence",
).map(parseMemoryEvidence);
assertUnique(evidence.map((item) => item.id), "evidence ids");
const evidenceTotal = requiredNonNegativeInteger(record, "evidence_total");
const evidenceTruncated = requiredBoolean(record, "evidence_truncated");
assertCollectionTotal(
evidence.length,
evidenceTotal,
evidenceTruncated,
"evidence",
);
const sourceRefs = boundedArray(
record.source_refs,
MAX_SOURCE_REFS_PER_RECORD,
"source_refs",
).map(parseMemorySourceEvidenceRef);
const sourceRefsTotal = requiredNonNegativeInteger(
record,
"source_refs_total",
);
const sourceRefsTruncated = requiredBoolean(
record,
"source_refs_truncated",
);
assertCollectionTotal(
sourceRefs.length,
sourceRefsTotal,
sourceRefsTruncated,
"source refs",
);
return {
candidate_id: requiredIdentifier(record, "candidate_id"),
evidence,
evidence_total: evidenceTotal,
evidence_truncated: evidenceTruncated,
source_refs: sourceRefs,
source_refs_total: sourceRefsTotal,
source_refs_truncated: sourceRefsTruncated,
};
}
function parseMemoryEvidence(value: unknown): SubjektivMemoryEvidence {
const record = strictRecord(
value,
["id", "kind", "entry_range", "origin", "excerpt", "summary"],
"Memory evidence",
["entry_range", "origin", "excerpt", "summary"],
);
const result: SubjektivMemoryEvidence = {
id: requiredIdentifier(record, "id"),
kind: requiredString(record, "kind"),
};
if ("entry_range" in record) {
result.entry_range = record.entry_range === null
? null
: parseEntryRange(record.entry_range, "entry_range");
}
if ("origin" in record) {
result.origin = record.origin === null ? null : parseEvidenceOrigin(record.origin);
}
if ("excerpt" in record) result.excerpt = nullableString(record, "excerpt");
if ("summary" in record) result.summary = nullableString(record, "summary");
return result;
}
function parseMemorySourceEvidenceRef(
value: unknown,
): SubjektivMemorySourceEvidenceRef {
const keys = [
"session_id",
"segment_id",
"entry_range",
"evidence_id",
"origin",
"evidence_kind",
"label",
"summary",
] as const;
const record = strictRecord(
value,
keys,
"Memory source evidence ref",
keys,
);
const result: SubjektivMemorySourceEvidenceRef = {};
for (
const key of [
"session_id",
"segment_id",
"evidence_id",
"evidence_kind",
"label",
"summary",
] as const
) {
if (key in record) result[key] = nullableString(record, key);
}
if ("entry_range" in record) {
result.entry_range = record.entry_range === null
? null
: parseEntryRange(record.entry_range, "entry_range");
}
if ("origin" in record) {
result.origin = record.origin === null ? null : parseEvidenceOrigin(record.origin);
}
return result;
}
export function parseSubjektivMemoryListRevisionsResponse(
value: unknown,
expectedMemoryId?: string,
): SubjektivMemoryListRevisionsResponse {
const record = strictRecord(
value,
["memory_id", "current_revision", "items", "next_cursor", "has_more"],
"Memory revisions response",
["next_cursor"],
);
const memoryId = requiredIdentifier(record, "memory_id");
if (expectedMemoryId !== undefined && memoryId !== expectedMemoryId) {
invalid("Memory revisions identity does not match the requested Memory");
}
const currentRevision = requiredPositiveInteger(record, "current_revision");
const items = boundedArray(record.items, MAX_REVISIONS, "items").map(
parseMemoryRevisionItem,
);
if (items.some((item) => item.revision > currentRevision)) {
invalid("revision history contains a future revision");
}
assertUnique(
items.map((item) => String(item.revision)),
"Memory revisions",
);
const hasMore = requiredBoolean(record, "has_more");
const nextCursor = optionalNullableString(record, "next_cursor");
assertCursorInvariant(hasMore, nextCursor, "Memory revisions");
const result: SubjektivMemoryListRevisionsResponse = {
memory_id: memoryId,
current_revision: currentRevision,
items,
has_more: hasMore,
};
if (nextCursor !== undefined) result.next_cursor = nextCursor;
return result;
}
function parseMemoryRevisionItem(value: unknown): SubjektivMemoryRevisionItem {
const record = strictRecord(
value,
["revision", "kind", "state", "claim", "change_reason", "updated_at"],
"Memory revision item",
);
return {
revision: requiredPositiveInteger(record, "revision"),
kind: parseCandidateKind(record.kind),
state: parseMemoryState(record.state),
claim: requiredString(record, "claim"),
change_reason: requiredString(record, "change_reason"),
updated_at: requiredString(record, "updated_at"),
};
}
function parseMemoryRevisionRef(value: unknown): SubjektivMemoryRevisionRef {
const record = strictRecord(
value,
["memory_id", "revision"],
"Memory revision ref",
);
return {
memory_id: requiredIdentifier(record, "memory_id"),
revision: requiredPositiveInteger(record, "revision"),
};
}
function parseCandidateKind(value: unknown): MemoryCandidateKind {
if (
typeof value !== "string" ||
!candidateKinds.has(value as MemoryCandidateKind)
) {
invalid("unknown Memory candidate kind");
}
return value as MemoryCandidateKind;
}
function parseMemoryState(value: unknown): SubjektivMemoryState {
if (
typeof value !== "string" ||
!memoryStates.has(value as SubjektivMemoryState)
) {
invalid("unknown Memory state");
}
return value as SubjektivMemoryState;
}
function assertCollectionTotal(
returned: number,
total: number,
truncated: boolean,
label: string,
): void {
if (total < returned || truncated !== (total > returned)) {
invalid(`${label} total does not match its truncated state`);
}
}
function assertCursorInvariant(
hasMore: boolean,
cursor: string | null | undefined,
label: string,
): void {
if (hasMore !== (typeof cursor === "string" && cursor.length > 0)) {
invalid(`${label} cursor does not match has_more`);
}
}
function assertUnique(values: string[], label: string): void {
if (new Set(values).size !== values.length) {
invalid(`${label} must be unique`);
}
}
function parseDiagnostic(value: unknown): Diagnostic {
const record = strictRecord(
value,
@@ -384,6 +954,55 @@ function requiredNonNegativeInteger(
return nonNegativeInteger(record[key], key);
}
function requiredPositiveInteger(
record: Record<string, unknown>,
key: string,
): number {
return positiveInteger(record[key], key);
}
function positiveInteger(value: unknown, label: string): number {
const result = nonNegativeInteger(value, label);
if (result === 0) invalid(`${label} must be a positive safe integer`);
return result;
}
function optionalNonNegativeInteger(
record: Record<string, unknown>,
key: string,
): number | undefined {
if (!(key in record)) return undefined;
if (record[key] === null) return undefined;
return nonNegativeInteger(record[key], key);
}
function requiredIdentifier(
record: Record<string, unknown>,
key: string,
): string {
return identifier(record[key], key);
}
function identifier(value: unknown, label: string): string {
if (
typeof value !== "string" ||
value.length === 0 ||
new TextEncoder().encode(value).byteLength >
MEMORY_API_LIMITS.maxIdentifierBytes
) {
invalid(`${label} must be a non-empty bounded identifier`);
}
return value;
}
function optionalNullableString(
record: Record<string, unknown>,
key: string,
): string | null | undefined {
if (!(key in record)) return undefined;
return nullableString(record, 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`);
@@ -395,10 +1014,10 @@ 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),
];
const start = nonNegativeInteger(value[0], label);
const end = nonNegativeInteger(value[1], label);
if (start > end) invalid(`${label} must be ordered`);
return [start, end];
}
function parseNullableEntryRange(
@@ -7,8 +7,12 @@
};
let { currentPath = "/", workspaceId }: Props = $props();
let documentHref = $derived(workspaceId ? workspaceRoute(workspaceId, "/memory") : "/");
let stagingHref = $derived(workspaceId ? workspaceRoute(workspaceId, "/memory/staging") : "/");
let subjectsHref = $derived(
workspaceId ? workspaceRoute(workspaceId, "/memory") : "/",
);
let subjectsActive = $derived(
currentPath === subjectsHref || currentPath.startsWith(`${subjectsHref}/`),
);
</script>
<section class="sidebar-nav-section sidebar-nav-section--category">
@@ -16,15 +20,8 @@
<a
class="sidebar-link"
class:active={currentPath === documentHref}
href={documentHref}
aria-current={currentPath === documentHref ? "page" : undefined}
>Document</a>
<a
class="sidebar-link"
class:active={currentPath.startsWith(stagingHref)}
href={stagingHref}
aria-current={currentPath.startsWith(stagingHref) ? "page" : undefined}
>Staging</a>
class:active={subjectsActive}
href={subjectsHref}
aria-current={subjectsActive ? "page" : undefined}
>Subjects</a>
</section>
@@ -1,4 +1,4 @@
export const designLabBasePath = '/design-lab/workspace-web-ux';
export const designLabBasePath = "/design-lab/workspace-web-ux";
export type WorkspaceNavigationItem = {
label: string;
@@ -7,31 +7,36 @@ export type WorkspaceNavigationItem = {
};
export const workspaceNavigation: WorkspaceNavigationItem[] = [
{ label: 'Tickets', href: `${designLabBasePath}?resource=tickets` },
{ label: 'Objectives', href: `${designLabBasePath}?resource=objectives` },
{ label: 'Merge Requests', href: `${designLabBasePath}?resource=merge-requests` },
{ label: "Tickets", href: `${designLabBasePath}?resource=tickets` },
{ label: "Objectives", href: `${designLabBasePath}?resource=objectives` },
{
label: 'Memory',
label: "Merge Requests",
href: `${designLabBasePath}?resource=merge-requests`,
},
{
label: "Memory",
href: `${designLabBasePath}?resource=memory`,
children: [
{ label: 'Document', href: `${designLabBasePath}?resource=memory-document` },
{ label: 'Staging', href: `${designLabBasePath}?resource=memory-staging` },
{
label: "Subjects",
href: `${designLabBasePath}?resource=memory-subjects`,
},
],
},
{ label: 'Workers', href: `${designLabBasePath}?resource=workers` },
{ label: "Workers", href: `${designLabBasePath}?resource=workers` },
];
export const workspaceWorkers = [
{
key: 'wrk-language-review',
label: 'Language review',
state: 'Running',
repository: 'yoi',
key: "wrk-language-review",
label: "Language review",
state: "Running",
repository: "yoi",
},
{
key: 'wrk-accessibility-check',
label: 'Accessibility check with a deliberately long display name',
state: 'Idle',
repository: 'workspace-web',
key: "wrk-accessibility-check",
label: "Accessibility check with a deliberately long display name",
state: "Idle",
repository: "workspace-web",
},
] as const;
@@ -1,66 +1,74 @@
<script lang="ts">
import DocumentMarkdown from '$lib/workspace/markdown/DocumentMarkdown.svelte';
import { formatDate } from '$lib/workspace/api/http';
import { formatDate, workspaceRoute } from '$lib/workspace/api/http';
import type { PageProps } from './$types';
let { data }: PageProps = $props();
const subjects = $derived(data.subjects.data?.items ?? []);
function subjectHref(subjectId: string): string {
return workspaceRoute(data.workspaceId, `/memory/${encodeURIComponent(subjectId)}`);
}
</script>
<svelte:head>
<title>Memory · Yoi Workspace</title>
<meta name="description" content="Workspace Memory document" />
<title>Subjects · Memory · Yoi Workspace</title>
<meta name="description" content="Workspace Memory subjects" />
</svelte:head>
<section class="memory-document-page" aria-label="Memory document">
{#if data.memory.data}
<div class="memory-document-meta">
<p>
<span>Read-only</span>
<span aria-hidden="true">·</span>
<span>Updated <time datetime={data.memory.data.updated_at}>{formatDate(data.memory.data.updated_at)}</time></span>
</p>
<details>
<summary>Document details</summary>
<dl>
<div>
<dt>Created</dt>
<dd><time datetime={data.memory.data.created_at}>{formatDate(data.memory.data.created_at)}</time></dd>
</div>
<div>
<dt>Size</dt>
<dd>{data.memory.data.bytes} bytes</dd>
</div>
<div>
<dt>Source</dt>
<dd><code>{data.memory.data.record_source}</code></dd>
</div>
</dl>
</details>
<section class="memory-page memory-subject-index" aria-labelledby="memory-subjects-heading" data-memory-view="subjects">
<header class="memory-page-header">
<div>
<p class="memory-eyebrow">Memory</p>
<h1 id="memory-subjects-heading">Subjects</h1>
<p>Read-only durable context, organized by explicit subject.</p>
</div>
{#if data.subjects.data}
<span class="memory-count">{subjects.length}{data.subjects.data.has_more ? '+' : ''} subject{subjects.length === 1 ? '' : 's'}</span>
{/if}
</header>
{#if data.memory.data.body_md.trim().length === 0}
<div class="memory-document-state" role="status">
<strong>Memory document is empty.</strong>
<p>Durable Workspace context will appear here when it is available.</p>
{#if data.subjects.data}
{#if subjects.length === 0}
<div class="memory-state" role="status" data-memory-state="empty">
<strong>No Memory subjects.</strong>
<p>Subjects will appear here after they are created for this Workspace.</p>
</div>
{:else}
<article class="memory-document-content" aria-label="Memory document content">
<DocumentMarkdown text={data.memory.data.body_md} />
</article>
<div class="subject-list" aria-label="Memory subjects">
{#each subjects as subject (subject.id)}
<a class="subject-row" href={subjectHref(subject.id)}>
<div class="subject-copy">
<span class="memory-state-pill is-{subject.state}">{subject.state}</span>
<h2>{subject.role}</h2>
<code title={subject.id}>{subject.id}</code>
</div>
<dl class="subject-meta">
<div><dt>Store revision</dt><dd>{subject.store_revision}</dd></div>
<div><dt>Updated</dt><dd><time datetime={subject.updated_at}>{formatDate(subject.updated_at)}</time></dd></div>
<div><dt>Worker</dt><dd>{subject.current_worker?.display_name ?? 'None'}</dd></div>
</dl>
</a>
{/each}
</div>
{#if data.subjects.data.has_more}
<p class="memory-note">Only the first {data.subjects.data.limit} subjects are shown.</p>
{/if}
{/if}
{:else if data.memory.error}
<div class="memory-document-state memory-document-error">
<p role="alert"><strong>Memory document unavailable.</strong> {data.memory.error}</p>
{:else if data.subjects.error}
<div class="memory-state is-error" role="alert" data-memory-state="error">
<strong>Subjects unavailable.</strong>
<p>{data.subjects.error}</p>
</div>
{:else}
<div class="memory-document-state" role="status">
<p>Memory document data is unavailable.</p>
<div class="memory-state" role="status" data-memory-state="unavailable">
<p>Subject data is unavailable.</p>
</div>
{/if}
</section>
<style>
.memory-document-page {
.memory-page {
display: grid;
flex: 0 0 auto;
gap: var(--space-5);
@@ -70,96 +78,161 @@
margin-inline: auto;
}
.memory-document-meta {
.memory-page-header {
display: flex;
align-items: baseline;
align-items: flex-end;
justify-content: space-between;
gap: var(--space-4);
color: var(--text-muted);
font-size: var(--font-size-compact);
line-height: var(--line-height-compact);
gap: var(--space-5);
padding-bottom: var(--space-4);
border-bottom: 1px solid var(--line);
}
.memory-document-meta > p {
display: flex;
flex-wrap: wrap;
gap: var(--space-2);
.memory-page-header h1,
.subject-copy h2 {
margin: 0;
}
.memory-document-meta details {
flex: 0 0 auto;
}
.memory-document-meta summary {
color: var(--text-muted);
cursor: pointer;
}
.memory-document-meta dl {
display: grid;
gap: var(--space-2);
min-width: min(24rem, calc(100vw - (2 * var(--space-4))));
margin: var(--space-3) 0 0;
padding: var(--space-3) 0 0;
border-top: 1px solid var(--line);
}
.memory-document-meta dl > div {
display: grid;
grid-template-columns: 5rem minmax(0, 1fr);
gap: var(--space-3);
}
.memory-document-meta dt {
color: var(--text-faint);
}
.memory-document-meta dd {
min-width: 0;
margin: 0;
color: var(--text);
overflow-wrap: anywhere;
}
.memory-document-content {
min-width: 0;
}
.memory-document-state {
padding-block: var(--space-5);
color: var(--text-muted);
}
.memory-document-state strong {
color: var(--text-strong);
}
.memory-document-state p {
.memory-page-header h1 {
font-size: var(--font-size-title);
}
.memory-page-header p:not(.memory-eyebrow) {
max-width: 46rem;
margin: var(--space-2) 0 0;
color: var(--text-muted);
}
.memory-eyebrow {
margin: 0 0 var(--space-1);
color: var(--accent);
font-size: var(--font-size-compact);
font-weight: 800;
letter-spacing: 0.1em;
text-transform: uppercase;
}
.memory-count {
flex: 0 0 auto;
color: var(--text-muted);
font-size: var(--font-size-compact);
}
.subject-list {
display: grid;
border-top: 1px solid var(--line);
}
.subject-row {
display: grid;
grid-template-columns: minmax(0, 1fr) minmax(22rem, 0.8fr);
gap: var(--space-5);
min-width: 0;
padding: var(--space-4);
border-bottom: 1px solid var(--line);
color: inherit;
text-decoration: none;
}
.subject-row:hover,
.subject-row:focus-visible {
background: var(--interactive-hover);
}
.subject-copy {
display: grid;
justify-items: start;
gap: var(--space-1);
min-width: 0;
}
.subject-copy h2 {
font-size: var(--font-size-body);
overflow-wrap: anywhere;
}
.subject-copy code {
display: block;
max-width: 100%;
color: var(--text-muted);
font-size: var(--font-size-compact);
overflow-wrap: anywhere;
}
.memory-state-pill {
color: var(--success);
font-size: var(--font-size-compact);
font-weight: 800;
letter-spacing: 0.08em;
text-transform: uppercase;
}
.memory-state-pill.is-retired {
color: var(--text-faint);
}
.subject-meta {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: var(--space-3);
align-self: center;
}
.subject-meta div {
display: block;
min-width: 0;
}
.subject-meta dt {
white-space: normal;
}
.subject-meta dd {
margin-top: var(--space-1);
color: var(--text-muted);
font-size: var(--font-size-compact);
overflow-wrap: anywhere;
}
.memory-state {
padding: var(--space-5) 0;
color: var(--text-muted);
}
.memory-state strong {
color: var(--text-strong);
}
.memory-state p,
.memory-note {
margin: var(--space-1) 0 0;
color: var(--text-muted);
}
.memory-document-state > p:first-child {
margin-top: 0;
}
.memory-document-error,
.memory-document-error strong {
.memory-state.is-error,
.memory-state.is-error strong {
color: var(--danger);
}
@media (max-width: 900px) {
.subject-row {
grid-template-columns: minmax(0, 1fr);
gap: var(--space-3);
}
}
@media (max-width: 600px) {
.memory-document-meta {
.memory-page-header {
display: grid;
gap: var(--space-2);
}
.memory-document-meta details {
width: 100%;
.subject-row {
padding-inline: 0;
}
.memory-document-meta dl {
min-width: 0;
.subject-meta {
grid-template-columns: minmax(0, 1fr);
}
}
</style>
@@ -1,20 +1,19 @@
import { MEMORY_API_LIMITS } from "$lib/generated/memory-api";
import { loadJson, workspaceApiPath } from "$lib/workspace/api/http";
import { parseMemoryDocumentResponse } from "$lib/workspace/memory/api";
import {
MEMORY_API_LOAD_POLICY,
parseSubjektivSubjectListResponse,
} from "$lib/workspace/memory/api";
import type { PageLoad } from "./$types";
export const load: PageLoad = async ({ fetch, params }) => {
return {
workspaceId: params.workspaceId,
memory: await loadJson(
subjects: await loadJson(
fetch,
workspaceApiPath(params.workspaceId, "/memory"),
`${workspaceApiPath(params.workspaceId, "/subjektiv/subjects")}?limit=100`,
undefined,
parseMemoryDocumentResponse,
{
diagnosticLabel: "Memory API",
maxResponseBytes: MEMORY_API_LIMITS.maxResponseBytes,
},
parseSubjektivSubjectListResponse,
MEMORY_API_LOAD_POLICY,
),
};
};
@@ -0,0 +1,470 @@
<script lang="ts">
import DocumentMarkdown from '$lib/workspace/markdown/DocumentMarkdown.svelte';
import { formatDate, workspaceRoute } from '$lib/workspace/api/http';
import type { SubjektivMemoryState } from '$lib/generated/memory-api';
import type { PageProps } from './$types';
let { data }: PageProps = $props();
const surface = $derived(data.surface.data);
const snapshot = $derived(surface?.snapshot ?? null);
const memories = $derived(data.memories.data?.items ?? []);
function subjectsHref(): string {
return workspaceRoute(data.workspaceId, '/memory');
}
function memoryHref(memoryId: string): string {
return workspaceRoute(
data.workspaceId,
`/memory/${encodeURIComponent(data.subjectId)}/${encodeURIComponent(memoryId)}`,
);
}
function stateLabel(state: SubjektivMemoryState): string {
return state[0].toUpperCase() + state.slice(1);
}
function kindLabel(kind: string): string {
return kind.replaceAll('_', ' ');
}
</script>
<svelte:head>
<title>{data.subject.data?.role ?? data.subjectId} · Memory · Yoi Workspace</title>
<meta name="description" content="Subject resident surface and current committed Memories" />
</svelte:head>
<section class="memory-page memory-subject-page" aria-labelledby="subject-heading" data-memory-view="subject" data-memory-state={surface?.availability ?? (data.surface.error ? 'error' : 'unavailable')}>
<a class="memory-back-link" href={subjectsHref()}>← All subjects</a>
<header class="memory-page-header">
<div>
<p class="memory-eyebrow">Memory subject</p>
<h1 id="subject-heading">{data.subject.data?.role ?? data.subjectId}</h1>
<code class="subject-id" title={data.subjectId}>{data.subjectId}</code>
</div>
<span class="read-only-label">Read-only</span>
</header>
{#if data.subject.data}
<dl class="subject-facts" aria-label="Subject details">
<div><dt>Role</dt><dd>{data.subject.data.role}</dd></div>
<div><dt>State</dt><dd><span class="subject-state is-{data.subject.data.state}">{data.subject.data.state}</span></dd></div>
<div><dt>Store revision</dt><dd>{data.subject.data.store_revision}</dd></div>
<div><dt>Updated</dt><dd><time datetime={data.subject.data.updated_at}>{formatDate(data.subject.data.updated_at)}</time></dd></div>
<div><dt>Current worker</dt><dd>{data.subject.data.current_worker?.display_name ?? 'None'}</dd></div>
</dl>
{:else if data.subject.error}
<div class="memory-state is-error" role="alert">
<strong>Subject details unavailable.</strong>
<p>{data.subject.error}</p>
</div>
{:else}
<div class="memory-state" role="status"><p>Subject details are unavailable.</p></div>
{/if}
<section class="surface-section" aria-labelledby="resident-surface-heading">
<header class="section-heading">
<div>
<p class="memory-eyebrow">Resident context</p>
<h2 id="resident-surface-heading">Resident surface</h2>
</div>
{#if surface}
<span class="availability is-{surface.availability}">{surface.availability}</span>
{/if}
</header>
{#if surface?.availability === 'ready' && snapshot}
<div class="surface-meta">
<span>Built from store revision {snapshot.built_from_store_revision}</span>
<span aria-hidden="true">·</span>
<time datetime={snapshot.created_at}>{formatDate(snapshot.created_at)}</time>
<span aria-hidden="true">·</span>
<span>{snapshot.memory_refs.length} Memory ref{snapshot.memory_refs.length === 1 ? '' : 's'}</span>
</div>
{#if snapshot.body_md.trim().length === 0}
<div class="memory-state" role="status" data-surface-ready-empty>
<strong>Resident surface is ready and empty.</strong>
<p>This subject currently has no resident context to display.</p>
</div>
{:else}
<article class="surface-document" aria-label="Resident surface">
<DocumentMarkdown text={snapshot.body_md} />
</article>
{/if}
<details class="surface-details">
<summary>Surface provenance</summary>
<dl>
<div><dt>Snapshot id</dt><dd><code>{snapshot.snapshot_id}</code></dd></div>
<div>
<dt>Memory refs</dt>
<dd>
{#if snapshot.memory_refs.length === 0}
None
{:else}
<ul class="reference-list">
{#each snapshot.memory_refs as reference (`${reference.memory_id}:${reference.revision}`)}
<li><a href={memoryHref(reference.memory_id)}><code>{reference.memory_id}</code> · revision {reference.revision}</a></li>
{/each}
</ul>
{/if}
</dd>
</div>
</dl>
</details>
{:else if surface?.availability === 'ungenerated'}
<div class="memory-state" role="status">
<strong>Resident surface has not been generated.</strong>
<p>Committed Memories remain available below.</p>
</div>
{:else if surface?.availability === 'stale'}
<div class="memory-state is-warning" role="status">
<strong>Resident surface is stale.</strong>
<p>The latest committed Memories are not represented by a publishable snapshot.</p>
</div>
{:else if surface?.availability === 'failed'}
<div class="memory-state is-error" role="alert">
<strong>Resident surface generation failed.</strong>
<p>No failed or partial snapshot is displayed.</p>
</div>
{:else if data.surface.error}
<div class="memory-state is-error" role="alert">
<strong>Resident surface unavailable.</strong>
<p>{data.surface.error}</p>
</div>
{:else}
<div class="memory-state" role="status"><p>Resident surface data is unavailable.</p></div>
{/if}
</section>
<section class="memories-section" aria-labelledby="current-memories-heading">
<header class="section-heading">
<div>
<p class="memory-eyebrow">Committed records</p>
<h2 id="current-memories-heading">Current Memories</h2>
</div>
{#if data.memories.data}<span class="memory-count">{memories.length}{data.memories.data.has_more ? '+' : ''}</span>{/if}
</header>
{#if data.memories.data}
{#if memories.length === 0}
<div class="memory-state" role="status" data-memories-empty>
<strong>No committed Memories.</strong>
<p>This subject has no current Memory revisions.</p>
</div>
{:else}
<div class="memory-list" aria-label="Current committed Memories">
{#each memories as memory (memory.id)}
<a class="memory-row" href={memoryHref(memory.id)}>
<div class="memory-row-heading">
<div class="pill-row">
<span class="memory-pill is-{memory.state}">{stateLabel(memory.state)}</span>
<span class="memory-kind">{kindLabel(memory.kind)}</span>
</div>
<h3>{memory.claim}</h3>
<p>{memory.excerpt || 'No excerpt.'}</p>
</div>
<dl>
<div><dt>Memory id</dt><dd><code title={memory.id}>{memory.id}</code></dd></div>
<div><dt>Revision</dt><dd>{memory.revision}</dd></div>
<div><dt>Updated</dt><dd><time datetime={memory.updated_at}>{formatDate(memory.updated_at)}</time></dd></div>
</dl>
</a>
{/each}
</div>
{#if data.memories.data.has_more}
<p class="memory-note">More current Memories are available after this bounded page.</p>
{/if}
{/if}
{:else if data.memories.error}
<div class="memory-state is-error" role="alert">
<strong>Current Memories unavailable.</strong>
<p>{data.memories.error}</p>
</div>
{:else}
<div class="memory-state" role="status"><p>Current Memory data is unavailable.</p></div>
{/if}
</section>
</section>
<style>
.memory-page {
display: grid;
flex: 0 0 auto;
gap: var(--space-5);
width: 100%;
min-width: 0;
max-width: 78rem;
margin-inline: auto;
}
.memory-back-link {
width: fit-content;
color: var(--text-muted);
font-size: var(--font-size-compact);
}
.memory-page-header,
.section-heading {
display: flex;
align-items: flex-end;
justify-content: space-between;
gap: var(--space-4);
}
.memory-page-header {
padding-bottom: var(--space-4);
border-bottom: 1px solid var(--line);
}
.memory-page-header h1,
.section-heading h2,
.memory-row h3 {
margin: 0;
color: var(--text-strong);
overflow-wrap: anywhere;
}
.memory-page-header h1 {
font-size: var(--font-size-title);
}
.section-heading h2 {
font-size: var(--font-size-body);
}
.memory-eyebrow {
margin: 0 0 var(--space-1);
color: var(--accent);
font-size: var(--font-size-compact);
font-weight: 800;
letter-spacing: 0.1em;
text-transform: uppercase;
}
.subject-id {
display: block;
max-width: min(60rem, 100%);
margin-top: var(--space-2);
color: var(--text-muted);
overflow-wrap: anywhere;
}
.read-only-label,
.memory-count {
flex: 0 0 auto;
color: var(--text-muted);
font-size: var(--font-size-compact);
}
.subject-facts {
grid-template-columns: repeat(5, minmax(0, 1fr));
gap: var(--space-3);
}
.subject-facts > div {
display: block;
}
.subject-facts dt {
white-space: normal;
}
.subject-facts dd {
margin-top: var(--space-1);
color: var(--text);
overflow-wrap: anywhere;
}
.subject-state,
.availability,
.memory-pill,
.memory-kind {
font-size: var(--font-size-compact);
font-weight: 800;
letter-spacing: 0.07em;
text-transform: uppercase;
}
.subject-state,
.availability.is-ready,
.memory-pill.is-active {
color: var(--success);
}
.subject-state.is-retired,
.availability.is-ungenerated,
.memory-pill.is-retracted {
color: var(--text-faint);
}
.availability.is-stale,
.memory-pill.is-resolved {
color: var(--warning);
}
.availability.is-failed {
color: var(--danger);
}
.surface-section,
.memories-section {
display: grid;
gap: var(--space-4);
min-width: 0;
padding-top: var(--space-3);
}
.surface-meta,
.pill-row {
display: flex;
flex-wrap: wrap;
gap: var(--space-2);
color: var(--text-muted);
font-size: var(--font-size-compact);
}
.surface-document {
min-width: 0;
}
.surface-details {
padding-top: var(--space-3);
border-top: 1px solid var(--line);
}
.surface-details summary {
color: var(--text-muted);
cursor: pointer;
font-size: var(--font-size-compact);
font-weight: 700;
}
.surface-details dl {
margin-top: var(--space-3);
}
.surface-details dd,
.reference-list code {
overflow-wrap: anywhere;
}
.reference-list {
display: grid;
gap: var(--space-1);
margin: 0;
padding-left: 1.25rem;
}
.memory-list {
display: grid;
border-top: 1px solid var(--line);
}
.memory-row {
display: grid;
grid-template-columns: minmax(0, 1.4fr) minmax(19rem, 0.8fr);
gap: var(--space-5);
min-width: 0;
padding: var(--space-4);
border-bottom: 1px solid var(--line);
color: inherit;
text-decoration: none;
}
.memory-row:hover,
.memory-row:focus-visible {
background: var(--interactive-hover);
}
.memory-row-heading {
display: grid;
gap: var(--space-2);
min-width: 0;
}
.memory-row h3 {
font-size: var(--font-size-body);
}
.memory-row p,
.memory-note {
margin: 0;
color: var(--text-muted);
}
.memory-kind {
color: var(--accent);
}
.memory-row dl {
align-content: center;
}
.memory-row dl > div {
grid-template-columns: 6.5rem minmax(0, 1fr);
}
.memory-row dd,
.memory-row code {
min-width: 0;
overflow-wrap: anywhere;
}
.memory-state {
padding: var(--space-4) 0;
color: var(--text-muted);
}
.memory-state strong {
color: var(--text-strong);
}
.memory-state p {
margin: var(--space-1) 0 0;
}
.memory-state.is-warning,
.memory-state.is-warning strong {
color: var(--warning);
}
.memory-state.is-error,
.memory-state.is-error strong {
color: var(--danger);
}
@media (max-width: 900px) {
.subject-facts {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.memory-row {
grid-template-columns: minmax(0, 1fr);
gap: var(--space-3);
}
}
@media (max-width: 600px) {
.memory-page-header,
.section-heading {
display: grid;
gap: var(--space-2);
}
.subject-facts {
grid-template-columns: minmax(0, 1fr);
}
.memory-row {
padding-inline: 0;
}
.memory-row dl > div,
.surface-details dl > div {
grid-template-columns: minmax(0, 1fr);
gap: var(--space-1);
}
}
</style>
@@ -0,0 +1,44 @@
import { loadJson, workspaceApiPath } from "$lib/workspace/api/http";
import {
MEMORY_API_LOAD_POLICY,
parseSubjektivMemoryQueryResponse,
parseSubjektivResidentSurfaceResponse,
parseSubjektivSubjectResponse,
} from "$lib/workspace/memory/api";
import type { PageLoad } from "./$types";
export const load: PageLoad = async ({ fetch, params }) => {
const subjectId = params.subjectId;
const subjectPath = `/subjektiv/subjects/${encodeURIComponent(subjectId)}`;
const [subject, surface, memories] = await Promise.all([
loadJson(
fetch,
workspaceApiPath(params.workspaceId, subjectPath),
undefined,
(value) => parseSubjektivSubjectResponse(value, subjectId),
MEMORY_API_LOAD_POLICY,
),
loadJson(
fetch,
workspaceApiPath(params.workspaceId, `${subjectPath}/surface`),
undefined,
(value) => parseSubjektivResidentSurfaceResponse(value, subjectId),
MEMORY_API_LOAD_POLICY,
),
loadJson(
fetch,
`${workspaceApiPath(params.workspaceId, `${subjectPath}/memories`)}?limit=100`,
undefined,
parseSubjektivMemoryQueryResponse,
MEMORY_API_LOAD_POLICY,
),
]);
return {
workspaceId: params.workspaceId,
subjectId,
subject,
surface,
memories,
};
};
@@ -0,0 +1,626 @@
<script lang="ts">
import DocumentMarkdown from '$lib/workspace/markdown/DocumentMarkdown.svelte';
import { formatDate, workspaceRoute } from '$lib/workspace/api/http';
import type {
MemoryEvidenceOrigin,
SubjektivMemoryRevisionRef,
SubjektivMemorySourceEvidenceRef,
} from '$lib/generated/memory-api';
import type { PageProps } from './$types';
let { data }: PageProps = $props();
const memory = $derived(data.memory.data);
const revisions = $derived(data.revisions.data?.items ?? []);
function subjectHref(): string {
return workspaceRoute(data.workspaceId, `/memory/${encodeURIComponent(data.subjectId)}`);
}
function memoryPath(memoryId = data.memoryId): string {
return workspaceRoute(
data.workspaceId,
`/memory/${encodeURIComponent(data.subjectId)}/${encodeURIComponent(memoryId)}`,
);
}
function revisionHref(revision: number): string {
return `${memoryPath()}?revision=${revision}`;
}
function derivedHref(reference: SubjektivMemoryRevisionRef): string {
return `${memoryPath(reference.memory_id)}?revision=${reference.revision}`;
}
function bodyContinuationHref(): string | null {
if (!memory?.body_truncated || memory.body_next_offset == null || memory.body_next_byte_offset == null) return null;
const query = new URLSearchParams({
revision: String(memory.revision),
offset: String(memory.body_next_offset),
byte_offset: String(memory.body_next_byte_offset),
});
return `${memoryPath()}?${query}`;
}
function evidenceContinuationHref(): string | null {
if (!memory?.evidence_has_more || !memory.evidence_next_cursor) return null;
const query = new URLSearchParams({
revision: String(memory.revision),
evidence_cursor: memory.evidence_next_cursor,
});
return `${memoryPath()}?${query}`;
}
function label(value: string): string {
return value.replaceAll('_', ' ');
}
function rangeLabel(range: [number, number] | null | undefined): string {
return range ? `${range[0]}–${range[1]}` : 'None';
}
function originLabel(origin: MemoryEvidenceOrigin | null | undefined): string {
if (!origin) return 'Origin unavailable';
const identity = [
origin.account_id && `account ${origin.account_id}`,
origin.workspace_id && `workspace ${origin.workspace_id}`,
origin.runtime_id && `runtime ${origin.runtime_id}`,
origin.worker_id && `worker ${origin.worker_id}`,
origin.flow_selector && `flow ${origin.flow_selector}`,
origin.flow_definition_id && `definition ${origin.flow_definition_id}`,
origin.flow_definition_revision != null && `definition revision ${origin.flow_definition_revision}`,
].filter(Boolean);
return `${label(origin.kind)}${identity.length ? ` · ${identity.join(' · ')}` : ''}`;
}
function sourceRefTitle(reference: SubjektivMemorySourceEvidenceRef): string {
return reference.label || reference.summary || reference.evidence_id || reference.segment_id || 'Source reference';
}
</script>
<svelte:head>
<title>{memory?.claim ?? data.memoryId} · Memory · Yoi Workspace</title>
<meta name="description" content="Committed Memory detail, immutable revisions, and provenance" />
</svelte:head>
<section class="memory-page memory-detail-page" aria-labelledby="memory-detail-heading" data-memory-view="detail" data-memory-state={data.memory.error ? 'error' : memory ? memory.state : 'unavailable'}>
<a class="memory-back-link" href={subjectHref()}>← {data.subject.data?.role ?? 'Subject'}</a>
{#if memory}
<header class="memory-page-header">
<div>
<div class="pill-row">
<span class="memory-pill is-{memory.state}">{memory.state}</span>
<span class="memory-kind">{label(memory.kind)}</span>
{#if memory.revision !== memory.current_revision}<span class="historical-pill">Historical revision</span>{/if}
</div>
<h1 id="memory-detail-heading">{memory.claim}</h1>
<code class="memory-id" title={memory.memory_id}>{memory.memory_id}</code>
</div>
<span class="read-only-label">Read-only</span>
</header>
<dl class="memory-facts" aria-label="Memory details">
<div><dt>Revision</dt><dd>{memory.revision} of {memory.current_revision}</dd></div>
<div><dt>State</dt><dd>{memory.state}</dd></div>
<div><dt>Kind</dt><dd>{label(memory.kind)}</dd></div>
<div><dt>Created</dt><dd><time datetime={memory.created_at}>{formatDate(memory.created_at)}</time></dd></div>
<div><dt>Updated</dt><dd><time datetime={memory.updated_at}>{formatDate(memory.updated_at)}</time></dd></div>
</dl>
<section class="detail-section memory-body-section" aria-labelledby="memory-body-heading">
<header class="section-heading"><h2 id="memory-body-heading">Memory</h2></header>
<p class="why-useful"><strong>Why useful:</strong> {memory.why_useful}</p>
{#if memory.staleness}<p class="staleness"><strong>Staleness:</strong> {memory.staleness}</p>{/if}
<p class="change-reason"><strong>Change reason:</strong> {memory.change_reason}</p>
{#if memory.body_md.trim().length === 0}
<div class="memory-state" role="status"><strong>This Memory body is empty.</strong></div>
{:else}
<article class="memory-document" aria-label="Committed Memory body">
<DocumentMarkdown text={memory.body_md} />
</article>
{/if}
{#if bodyContinuationHref()}
<div class="continuation-note" role="status">
<p>This bounded body segment continues at line {memory.body_next_offset}, byte {memory.body_next_byte_offset}.</p>
<a href={bodyContinuationHref() ?? undefined}>View next body segment</a>
</div>
{/if}
</section>
<section class="detail-section provenance-section" aria-labelledby="provenance-heading">
<header class="section-heading">
<div><p class="memory-eyebrow">Audit context</p><h2 id="provenance-heading">Candidate provenance</h2></div>
<span>{memory.source_candidates.length} candidate{memory.source_candidates.length === 1 ? '' : 's'}</span>
</header>
{#if memory.source_candidates.length === 0}
<div class="memory-state" role="status"><p>No candidate evidence is included on this page.</p></div>
{:else}
<div class="candidate-list">
{#each memory.source_candidates as candidate, candidateIndex (candidate.candidate_id)}
<article class="candidate-record">
<header>
<h3>Candidate</h3>
<code title={candidate.candidate_id}>{candidate.candidate_id}</code>
</header>
<section aria-labelledby={`evidence-${candidateIndex}`}>
<h4 id={`evidence-${candidateIndex}`}>Evidence <span>{candidate.evidence.length} of {candidate.evidence_total}</span></h4>
{#if candidate.evidence.length === 0}
<p class="empty-copy">No evidence anchors on this page.</p>
{:else}
<ul class="evidence-list">
{#each candidate.evidence as evidence (evidence.id)}
<li>
<div class="evidence-heading"><strong>{evidence.id}</strong><span>{label(evidence.kind)}</span></div>
<dl>
<div><dt>Entry range</dt><dd>{rangeLabel(evidence.entry_range)}</dd></div>
<div><dt>Origin</dt><dd>{originLabel(evidence.origin)}</dd></div>
</dl>
{#if evidence.summary}<p>{evidence.summary}</p>{/if}
{#if evidence.excerpt}<blockquote>{evidence.excerpt}</blockquote>{/if}
</li>
{/each}
</ul>
{/if}
{#if candidate.evidence_truncated}<p class="bounded-note">Additional evidence anchors are available.</p>{/if}
</section>
<section aria-labelledby={`sources-${candidateIndex}`}>
<h4 id={`sources-${candidateIndex}`}>Source refs <span>{candidate.source_refs.length} of {candidate.source_refs_total}</span></h4>
{#if candidate.source_refs.length === 0}
<p class="empty-copy">No source refs on this page.</p>
{:else}
<ul class="source-list">
{#each candidate.source_refs as reference, index (`${candidate.candidate_id}:source:${index}`)}
<li>
<strong>{sourceRefTitle(reference)}</strong>
<dl>
<div><dt>Session</dt><dd><code>{reference.session_id ?? 'None'}</code></dd></div>
<div><dt>Segment</dt><dd><code>{reference.segment_id ?? 'None'}</code></dd></div>
<div><dt>Entry range</dt><dd>{rangeLabel(reference.entry_range)}</dd></div>
<div><dt>Evidence id</dt><dd><code>{reference.evidence_id ?? 'None'}</code></dd></div>
<div><dt>Evidence kind</dt><dd>{reference.evidence_kind ? label(reference.evidence_kind) : 'None'}</dd></div>
<div><dt>Origin</dt><dd>{originLabel(reference.origin)}</dd></div>
</dl>
{#if reference.summary}<p>{reference.summary}</p>{/if}
</li>
{/each}
</ul>
{/if}
{#if candidate.source_refs_truncated}<p class="bounded-note">Additional source refs are available.</p>{/if}
</section>
</article>
{/each}
</div>
{/if}
{#if evidenceContinuationHref()}
<div class="continuation-note" role="status">
<p>Candidate evidence continues on another bounded page.</p>
<a href={evidenceContinuationHref() ?? undefined}>View next evidence page</a>
</div>
{/if}
</section>
<section class="detail-section derived-section" aria-labelledby="derived-heading">
<header class="section-heading"><h2 id="derived-heading">Derived from</h2><span>{memory.derived_from.length}</span></header>
{#if memory.derived_from.length === 0}
<p class="empty-copy">No derivation references.</p>
{:else}
<ul class="derived-list">
{#each memory.derived_from as reference (`${reference.memory_id}:${reference.revision}`)}
<li><a href={derivedHref(reference)}><code>{reference.memory_id}</code><span>Revision {reference.revision}</span></a></li>
{/each}
</ul>
{/if}
</section>
<section class="detail-section revision-section" aria-labelledby="revision-heading">
<header class="section-heading"><div><p class="memory-eyebrow">Immutable record</p><h2 id="revision-heading">Revision history</h2></div>{#if data.revisions.data}<span>{revisions.length}{data.revisions.data.has_more ? '+' : ''}</span>{/if}</header>
{#if data.revisions.data}
{#if revisions.length === 0}
<p class="empty-copy">No revision history is available.</p>
{:else}
<ol class="revision-list">
{#each revisions as revision (revision.revision)}
<li class:current={revision.revision === memory.revision}>
<a href={revisionHref(revision.revision)} aria-current={revision.revision === memory.revision ? 'page' : undefined}>
<div><strong>Revision {revision.revision}</strong><span class="memory-pill is-{revision.state}">{revision.state}</span><span class="memory-kind">{label(revision.kind)}</span></div>
<p>{revision.claim}</p>
<small>{revision.change_reason} · <time datetime={revision.updated_at}>{formatDate(revision.updated_at)}</time></small>
</a>
</li>
{/each}
</ol>
{#if data.revisions.data.has_more}<p class="bounded-note">More immutable revisions are available after this bounded page.</p>{/if}
{/if}
{:else if data.revisions.error}
<div class="memory-state is-error" role="alert"><strong>Revision history unavailable.</strong><p>{data.revisions.error}</p></div>
{:else}
<div class="memory-state" role="status"><p>Revision history data is unavailable.</p></div>
{/if}
</section>
{:else if data.memory.error}
<header class="memory-page-header">
<div><p class="memory-eyebrow">Committed Memory</p><h1 id="memory-detail-heading">{data.memoryId}</h1></div>
<span class="read-only-label">Read-only</span>
</header>
<div class="memory-state is-error" role="alert">
<strong>Memory unavailable.</strong>
<p>{data.memory.error}</p>
</div>
{:else}
<header class="memory-page-header"><h1 id="memory-detail-heading">{data.memoryId}</h1></header>
<div class="memory-state" role="status"><p>Memory data is unavailable.</p></div>
{/if}
</section>
<style>
.memory-page {
display: grid;
flex: 0 0 auto;
gap: var(--space-5);
width: 100%;
min-width: 0;
max-width: 78rem;
margin-inline: auto;
}
.memory-back-link {
width: fit-content;
color: var(--text-muted);
font-size: var(--font-size-compact);
}
.memory-page-header,
.section-heading,
.candidate-record > header {
display: flex;
align-items: flex-end;
justify-content: space-between;
gap: var(--space-4);
min-width: 0;
}
.memory-page-header {
padding-bottom: var(--space-4);
border-bottom: 1px solid var(--line);
}
.memory-page-header > div,
.section-heading > div {
min-width: 0;
}
.memory-page-header h1,
.section-heading h2,
.candidate-record h3,
.candidate-record h4 {
margin: 0;
color: var(--text-strong);
overflow-wrap: anywhere;
}
.memory-page-header h1 {
margin-top: var(--space-2);
font-size: var(--font-size-title);
}
.section-heading h2 {
font-size: var(--font-size-body);
}
.section-heading > span,
.candidate-record h4 span,
.read-only-label {
flex: 0 0 auto;
color: var(--text-muted);
font-size: var(--font-size-compact);
}
.pill-row,
.revision-list a > div,
.evidence-heading {
display: flex;
flex-wrap: wrap;
align-items: baseline;
gap: var(--space-2);
}
.memory-pill,
.memory-kind,
.historical-pill,
.memory-eyebrow {
font-size: var(--font-size-compact);
font-weight: 800;
letter-spacing: 0.08em;
text-transform: uppercase;
}
.memory-pill.is-active {
color: var(--success);
}
.memory-pill.is-resolved,
.historical-pill {
color: var(--warning);
}
.memory-pill.is-retracted {
color: var(--text-faint);
}
.memory-kind,
.memory-eyebrow {
color: var(--accent);
}
.memory-eyebrow {
margin: 0 0 var(--space-1);
}
.memory-id {
display: block;
max-width: 100%;
margin-top: var(--space-2);
color: var(--text-muted);
overflow-wrap: anywhere;
}
.memory-facts {
grid-template-columns: repeat(5, minmax(0, 1fr));
gap: var(--space-3);
}
.memory-facts > div {
display: block;
}
.memory-facts dt {
white-space: normal;
}
.memory-facts dd {
margin-top: var(--space-1);
overflow-wrap: anywhere;
}
.detail-section {
display: grid;
gap: var(--space-4);
min-width: 0;
padding-top: var(--space-4);
border-top: 1px solid var(--line);
}
.why-useful,
.staleness,
.change-reason,
.empty-copy,
.bounded-note {
margin: 0;
color: var(--text-muted);
}
.why-useful strong,
.change-reason strong {
color: var(--text);
}
.staleness,
.staleness strong {
color: var(--warning);
}
.memory-document {
min-width: 0;
}
.continuation-note {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: var(--space-3);
padding: var(--space-3);
border-left: 3px solid var(--warning);
background: var(--bg-raised);
color: var(--text-muted);
}
.continuation-note p {
margin: 0;
}
.candidate-list {
display: grid;
gap: var(--space-4);
}
.candidate-record {
display: grid;
gap: var(--space-4);
min-width: 0;
padding: var(--space-4);
border: 1px solid var(--line);
background: var(--bg-raised);
}
.candidate-record > header {
align-items: baseline;
padding-bottom: var(--space-3);
border-bottom: 1px solid var(--line);
}
.candidate-record > header code {
min-width: 0;
color: var(--text-muted);
overflow-wrap: anywhere;
}
.candidate-record section {
display: grid;
gap: var(--space-3);
min-width: 0;
}
.evidence-list,
.source-list,
.derived-list,
.revision-list {
display: grid;
gap: var(--space-2);
margin: 0;
padding: 0;
list-style: none;
}
.evidence-list li,
.source-list li {
min-width: 0;
padding: var(--space-3);
border: 1px solid var(--line);
background: var(--bg-subtle);
}
.evidence-heading span {
color: var(--text-muted);
font-size: var(--font-size-compact);
}
.evidence-list dl,
.source-list dl {
margin-top: var(--space-2);
}
.evidence-list dd,
.source-list dd,
.source-list code {
overflow-wrap: anywhere;
}
.evidence-list p,
.source-list p {
margin: var(--space-2) 0 0;
color: var(--text-muted);
}
.evidence-list blockquote {
margin: var(--space-2) 0 0;
padding-left: var(--space-3);
border-left: 3px solid var(--line-strong);
color: var(--text-muted);
}
.derived-list {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.derived-list a {
display: grid;
gap: var(--space-1);
min-width: 0;
padding: var(--space-3);
border: 1px solid var(--line);
color: inherit;
text-decoration: none;
}
.derived-list a:hover,
.derived-list a:focus-visible,
.revision-list a:hover,
.revision-list a:focus-visible {
background: var(--interactive-hover);
}
.derived-list code {
overflow-wrap: anywhere;
}
.derived-list span {
color: var(--text-muted);
font-size: var(--font-size-compact);
}
.revision-list {
border-top: 1px solid var(--line);
}
.revision-list li {
border-bottom: 1px solid var(--line);
}
.revision-list li.current {
border-left: 3px solid var(--accent);
}
.revision-list a {
display: grid;
gap: var(--space-1);
min-width: 0;
padding: var(--space-3) var(--space-4);
color: inherit;
text-decoration: none;
}
.revision-list p,
.revision-list small {
margin: 0;
overflow-wrap: anywhere;
}
.revision-list small {
color: var(--text-muted);
}
.memory-state {
padding: var(--space-4) 0;
color: var(--text-muted);
}
.memory-state strong {
color: var(--text-strong);
}
.memory-state p {
margin: var(--space-1) 0 0;
}
.memory-state.is-error,
.memory-state.is-error strong {
color: var(--danger);
}
@media (max-width: 900px) {
.memory-facts {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
@media (max-width: 600px) {
.memory-page-header,
.section-heading,
.candidate-record > header,
.continuation-note {
display: grid;
gap: var(--space-2);
}
.memory-facts,
.derived-list {
grid-template-columns: minmax(0, 1fr);
}
.memory-facts > div,
.evidence-list dl > div,
.source-list dl > div {
grid-template-columns: minmax(0, 1fr);
gap: var(--space-1);
}
.candidate-record {
padding-inline: var(--space-3);
}
}
</style>
@@ -0,0 +1,82 @@
import { loadJson, workspaceApiPath } from "$lib/workspace/api/http";
import {
MEMORY_API_LOAD_POLICY,
parseSubjektivMemoryListRevisionsResponse,
parseSubjektivMemoryReadResponse,
parseSubjektivSubjectResponse,
} from "$lib/workspace/memory/api";
import type { PageLoad } from "./$types";
const SAFE_INTEGER_TEXT = /^(0|[1-9][0-9]{0,15})$/;
function safeIntegerQuery(
source: URLSearchParams,
key: string,
positive = false,
): string | null {
const value = source.get(key);
if (value === null || !SAFE_INTEGER_TEXT.test(value)) return null;
const parsed = Number(value);
if (!Number.isSafeInteger(parsed) || (positive ? parsed < 1 : parsed < 0)) {
return null;
}
return String(parsed);
}
export const load: PageLoad = async ({ fetch, params, url }) => {
const subjectId = params.subjectId;
const memoryId = params.memoryId;
const subjectPath = `/subjektiv/subjects/${encodeURIComponent(subjectId)}`;
const memoryPath = `${subjectPath}/memories/${encodeURIComponent(memoryId)}`;
const detailQuery = new URLSearchParams({ limit: "1000" });
for (
const [key, positive] of [
["revision", true],
["offset", false],
["byte_offset", false],
] as const
) {
const value = safeIntegerQuery(url.searchParams, key, positive);
if (value !== null) detailQuery.set(key, value);
}
const evidenceCursor = url.searchParams.get("evidence_cursor");
if (
evidenceCursor !== null && evidenceCursor.length > 0 &&
new TextEncoder().encode(evidenceCursor).byteLength <= 512
) {
detailQuery.set("evidence_cursor", evidenceCursor);
}
const [subject, memory, revisions] = await Promise.all([
loadJson(
fetch,
workspaceApiPath(params.workspaceId, subjectPath),
undefined,
(value) => parseSubjektivSubjectResponse(value, subjectId),
MEMORY_API_LOAD_POLICY,
),
loadJson(
fetch,
`${workspaceApiPath(params.workspaceId, memoryPath)}?${detailQuery}`,
undefined,
(value) => parseSubjektivMemoryReadResponse(value, memoryId),
MEMORY_API_LOAD_POLICY,
),
loadJson(
fetch,
`${workspaceApiPath(params.workspaceId, `${memoryPath}/revisions`)}?limit=100`,
undefined,
(value) => parseSubjektivMemoryListRevisionsResponse(value, memoryId),
MEMORY_API_LOAD_POLICY,
),
]);
return {
workspaceId: params.workspaceId,
subjectId,
memoryId,
subject,
memory,
revisions,
};
};
@@ -2,75 +2,250 @@
import { cleanup, render, screen, waitFor } from "@testing-library/svelte";
import { afterEach, expect, test } from "vitest";
import MemoryPage from "./+page.svelte";
import type { PageProps } from "./$types";
function pageProps(memory: {
data: {
body_md: string;
created_at: string;
updated_at: string;
bytes: number;
record_source: string;
} | null;
error: string | null;
}): PageProps {
return {
data: { workspaceId: "workspace-1", memory },
params: { workspaceId: "workspace-1" },
} as unknown as PageProps;
}
import SubjectIndexPage from "./+page.svelte";
import SubjectPage from "./[subjectId]/+page.svelte";
import MemoryDetailPage from "./[subjectId]/[memoryId]/+page.svelte";
afterEach(cleanup);
test("makes the Markdown document primary and keeps technical metadata in disclosure", async () => {
const { container } = render(
MemoryPage,
pageProps({
data: {
body_md: "# Durable context\n\nRead the **current** document.",
created_at: "2026-01-01T00:00:00Z",
updated_at: "2026-01-02T03:04:05Z",
bytes: 48,
record_source: "sqlite_workspace_authority.memory_document",
},
error: null,
}),
);
const result = <T>(data: T | null, error: string | null = null) => ({
data,
error,
});
function subject() {
return {
id: "subject-1",
role: "Release coordinator",
state: "active" as const,
store_revision: 9,
created_at: "2026-01-01T00:00:00Z",
updated_at: "2026-01-02T03:04:05Z",
};
}
test("renders the subject index as the Memory product entry", () => {
render(SubjectIndexPage, {
data: {
workspaceId: "workspace-1",
subjects: result({ limit: 100, items: [subject()], has_more: false }),
},
} as never);
expect(screen.getByRole("heading", { name: "Subjects", level: 1 })).not
.toBeNull();
expect(screen.getByRole("link", { name: /Release coordinator/ })).not
.toBeNull();
expect(screen.getByText("subject-1")).not.toBeNull();
expect(screen.queryByText("Staging")).toBeNull();
});
test("renders a ready resident surface and committed Memory lifecycle states", async () => {
render(SubjectPage, {
data: {
workspaceId: "workspace-1",
subjectId: "subject-1",
subject: result(subject()),
surface: result({
subject_id: "subject-1",
availability: "ready" as const,
snapshot: {
snapshot_id: "snapshot-1",
body_md: "# Resident context\n\nUse the **current** committed record.",
memory_refs: [{ memory_id: "memory-1", revision: 3 }],
built_from_store_revision: 9,
created_at: "2026-01-02T03:04:05Z",
},
}),
memories: result({
items: [
{
id: "memory-1",
revision: 3,
kind: "decision" as const,
state: "active" as const,
claim: "Current decision",
excerpt: "Current excerpt",
updated_at: "2026-01-02T03:04:05Z",
},
{
id: "memory-2",
revision: 2,
kind: "lesson" as const,
state: "resolved" as const,
claim: "Resolved lesson",
excerpt: "Resolved excerpt",
updated_at: "2026-01-02T03:04:05Z",
},
{
id: "memory-3",
revision: 1,
kind: "constraint" as const,
state: "retracted" as const,
claim: "Retracted constraint",
excerpt: "Retracted excerpt",
updated_at: "2026-01-02T03:04:05Z",
},
],
has_more: false,
}),
},
} as never);
await waitFor(() => {
expect(screen.getByRole("heading", { name: "Durable context", level: 1 })).not.toBeNull();
expect(screen.getByRole("heading", { name: "Resident context", level: 1 }))
.not.toBeNull();
});
expect(screen.getByText("Read-only")).not.toBeNull();
expect(screen.getByText("Document details")).not.toBeNull();
expect(container.querySelector(".memory-document-page")?.classList.contains("card")).toBe(false);
expect(screen.queryByRole("heading", { name: "Memory Document" })).toBeNull();
expect(screen.getByRole("article", { name: "Memory document content" })).not.toBeNull();
expect(screen.getByRole("article", { name: "Resident surface" })).not
.toBeNull();
expect(screen.getAllByText("active", { exact: true }).length).toBeGreaterThan(
0,
);
expect(screen.getByText("resolved", { exact: true })).not.toBeNull();
expect(screen.getByText("retracted", { exact: true })).not.toBeNull();
});
test("distinguishes an empty document from request failure and unavailable data", async () => {
const view = render(
MemoryPage,
pageProps({
test("distinguishes ready-empty, stale, failed, unavailable, and request error surfaces", async () => {
const base = {
workspaceId: "workspace-1",
subjectId: "subject-1",
subject: result(subject()),
memories: result({ items: [], has_more: false }),
};
const view = render(SubjectPage, {
data: {
...base,
surface: result({
subject_id: "subject-1",
availability: "ready" as const,
snapshot: {
snapshot_id: "snapshot-empty",
body_md: "",
memory_refs: [],
built_from_store_revision: 9,
created_at: "2026-01-02T03:04:05Z",
},
}),
},
} as never);
expect(screen.getByText("Resident surface is ready and empty.")).not
.toBeNull();
await view.rerender(
{
data: {
body_md: " \n",
...base,
surface: result({
subject_id: "subject-1",
availability: "stale" as const,
}),
},
} as never,
);
expect(screen.getByText("Resident surface is stale.")).not.toBeNull();
await view.rerender(
{
data: {
...base,
surface: result({
subject_id: "subject-1",
availability: "failed" as const,
}),
},
} as never,
);
expect(screen.getByRole("alert").textContent).toContain("generation failed");
await view.rerender({ data: { ...base, surface: result(null) } } as never);
expect(screen.getByText("Resident surface data is unavailable.")).not
.toBeNull();
await view.rerender(
{
data: { ...base, surface: result(null, "Memory API request failed") },
} as never,
);
expect(screen.getByRole("alert").textContent).toContain(
"Resident surface unavailable",
);
});
test("renders committed Memory detail, provenance, derivation, and immutable revisions", async () => {
render(MemoryDetailPage, {
data: {
workspaceId: "workspace-1",
subjectId: "subject-1",
memoryId: "memory-1",
subject: result(subject()),
memory: result({
memory_id: "memory-1",
revision: 2,
current_revision: 3,
kind: "decision" as const,
state: "resolved" as const,
claim: "Keep provenance typed",
body_md: "# Committed detail\n\nRead-only body.",
why_useful: "Prevents origin loss.",
change_reason: "Resolved after implementation.",
created_at: "2026-01-01T00:00:00Z",
updated_at: "2026-01-02T03:04:05Z",
bytes: 3,
record_source: "fixture",
},
error: null,
}),
);
expect(screen.getByRole("status").textContent).toContain("Memory document is empty.");
expect(screen.queryByRole("alert")).toBeNull();
body_offset: 0,
body_byte_offset: 0,
body_truncated: false,
source_candidate_ids: ["candidate-1"],
source_candidates: [{
candidate_id: "candidate-1",
evidence: [{
id: "evidence-1",
kind: "message",
summary: "Explicit decision.",
}],
evidence_total: 1,
evidence_truncated: false,
source_refs: [{
session_id: "session-1",
segment_id: "segment-1",
evidence_id: "evidence-1",
label: "Decision discussion",
}],
source_refs_total: 1,
source_refs_truncated: false,
}],
derived_from: [{ memory_id: "memory-parent", revision: 4 }],
evidence_has_more: false,
}),
revisions: result({
memory_id: "memory-1",
current_revision: 3,
items: [{
revision: 3,
kind: "decision" as const,
state: "active" as const,
claim: "Current claim",
change_reason: "Clarified",
updated_at: "2026-01-03T00:00:00Z",
}, {
revision: 2,
kind: "decision" as const,
state: "resolved" as const,
claim: "Earlier claim",
change_reason: "Resolved",
updated_at: "2026-01-02T00:00:00Z",
}],
has_more: false,
}),
},
} as never);
await view.rerender(pageProps({ data: null, error: "Memory API request failed" }));
expect(screen.getByRole("alert").textContent).toContain("Memory document unavailable.");
expect(screen.getByRole("alert").textContent).toContain("Memory API request failed");
expect(screen.queryByText("Memory document is empty.")).toBeNull();
await view.rerender(pageProps({ data: null, error: null }));
expect(screen.getByRole("status").textContent).toBe("Memory document data is unavailable.");
expect(screen.queryByRole("alert")).toBeNull();
await waitFor(() => {
expect(screen.getByRole("heading", { name: "Committed detail", level: 1 }))
.not.toBeNull();
});
expect(screen.getByText("candidate-1")).not.toBeNull();
expect(screen.getByText("Decision discussion")).not.toBeNull();
expect(screen.getByText("memory-parent")).not.toBeNull();
expect(screen.getByRole("heading", { name: "Revision history" })).not
.toBeNull();
expect(screen.getByText("Historical revision")).not.toBeNull();
});
+410 -93
View File
@@ -3,12 +3,29 @@ declare const Deno: {
};
import {
parseMemoryDocumentResponse,
parseMemoryStagingListResponse,
parseSubjektivMemoryListRevisionsResponse,
parseSubjektivMemoryQueryResponse,
parseSubjektivMemoryReadResponse,
parseSubjektivResidentSurfaceResponse,
parseSubjektivSubjectListResponse,
parseSubjektivSubjectResponse,
} from "../src/lib/workspace/memory/api.ts";
function stable(value: unknown): unknown {
if (Array.isArray(value)) return value.map(stable);
if (typeof value === "object" && value !== null) {
return Object.fromEntries(
Object.entries(value as Record<string, unknown>)
.sort(([left], [right]) => left.localeCompare(right))
.map(([key, item]) => [key, stable(item)]),
);
}
return value;
}
function assertEquals(actual: unknown, expected: unknown): void {
if (JSON.stringify(actual) !== JSON.stringify(expected)) {
if (JSON.stringify(stable(actual)) !== JSON.stringify(stable(expected))) {
throw new Error(
`expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`,
);
@@ -27,8 +44,386 @@ function assertThrows(fn: () => void, expectedMessage: string): void {
throw new Error(`expected function to throw ${expectedMessage}`);
}
function fixture(origin: Record<string, unknown>) {
function subject(id = "subject-1") {
return {
id,
role: "Release coordinator",
state: "active",
store_revision: 12,
created_at: "2026-09-01T00:00:00Z",
updated_at: "2026-09-02T00:00:00Z",
};
}
function queryItem(id = "memory-1") {
return {
id,
revision: 3,
kind: "decision",
state: "active",
claim: "Keep provenance typed.",
excerpt: "Preserve source and derivation references.",
updated_at: "2026-09-02T03:04:05Z",
};
}
function detailFixture() {
const origin = {
kind: "flow_instruction",
workspace_id: "workspace-1",
runtime_id: "runtime-1",
worker_id: "worker-1",
flow_selector: "builtin:coder-review",
flow_definition_id: "flow-1",
flow_definition_revision: 7,
};
return {
memory_id: "memory-1",
revision: 2,
current_revision: 3,
kind: "decision",
state: "resolved",
claim: "Keep provenance typed.",
body_md: "# Committed Memory\n\nBounded body segment.",
why_useful: "Prevents origin loss.",
staleness: null,
change_reason: "Resolved after implementation.",
created_at: "2026-09-01T00:00:00Z",
updated_at: "2026-09-02T03:04:05Z",
body_offset: 0,
body_byte_offset: 0,
body_next_offset: 2,
body_next_byte_offset: 0,
body_truncated: true,
source_candidate_ids: ["candidate-1"],
source_candidates: [{
candidate_id: "candidate-1",
evidence: [{
id: "evidence-1",
kind: "message",
entry_range: [10, 12],
origin,
excerpt: "Use exact DTOs.",
summary: "The decision was explicit.",
}],
evidence_total: 2,
evidence_truncated: true,
source_refs: [{
session_id: "session-1",
segment_id: "segment-1",
entry_range: [10, 12],
evidence_id: "evidence-1",
origin,
evidence_kind: "message",
label: "Decision discussion",
summary: "Bounded host summary.",
}],
source_refs_total: 2,
source_refs_truncated: true,
}],
derived_from: [{ memory_id: "memory-parent", revision: 4 }],
evidence_next_cursor: "evidence-page-2",
evidence_has_more: true,
};
}
Deno.test("Subject parsers enforce identity, exact enums, safe revisions, and bounds", () => {
assertEquals(
parseSubjektivSubjectResponse(subject(), "subject-1"),
subject(),
);
assertThrows(
() => parseSubjektivSubjectResponse(subject(), "another-subject"),
"identity does not match",
);
assertThrows(
() => parseSubjektivSubjectResponse({ ...subject(), state: "paused" }),
"unknown Subject state",
);
assertThrows(
() =>
parseSubjektivSubjectResponse({
...subject(),
store_revision: Number.MAX_SAFE_INTEGER + 1,
}),
"safe integer",
);
assertThrows(
() => parseSubjektivSubjectResponse({ ...subject(), id: "x".repeat(513) }),
"bounded identifier",
);
assertThrows(
() => parseSubjektivSubjectResponse({ ...subject(), future: true }),
"unknown field",
);
assertThrows(
() =>
parseSubjektivSubjectListResponse({
limit: 100,
items: Array.from({ length: 101 }, (_, index) => subject(`subject-${index}`)),
has_more: false,
}),
"bounded array",
);
});
Deno.test("Subject list parser preserves the exact bounded response", () => {
const response = { limit: 100, items: [subject()], has_more: false };
assertEquals(parseSubjektivSubjectListResponse(response), response);
assertThrows(
() =>
parseSubjektivSubjectListResponse({
limit: 100,
items: [subject("duplicate"), subject("duplicate")],
has_more: false,
}),
"Subject ids must be unique",
);
});
Deno.test("Resident surface parser accepts ready-empty and enforces snapshot invariants", () => {
const readyEmpty = {
subject_id: "subject-1",
availability: "ready",
snapshot: {
snapshot_id: "snapshot-empty",
body_md: "",
memory_refs: [],
built_from_store_revision: 0,
created_at: "2026-09-02T00:00:00Z",
},
};
assertEquals(
parseSubjektivResidentSurfaceResponse(readyEmpty, "subject-1"),
readyEmpty,
);
assertEquals(
parseSubjektivResidentSurfaceResponse({
subject_id: "subject-1",
availability: "ungenerated",
}),
{ subject_id: "subject-1", availability: "ungenerated" },
);
assertThrows(
() =>
parseSubjektivResidentSurfaceResponse({
subject_id: "subject-1",
availability: "ready",
}),
"requires a snapshot",
);
for (const availability of ["ungenerated", "stale", "failed"]) {
assertThrows(
() =>
parseSubjektivResidentSurfaceResponse({
...readyEmpty,
availability,
}),
"must not include a snapshot",
);
}
assertThrows(
() =>
parseSubjektivResidentSurfaceResponse({
subject_id: "subject-1",
availability: "refreshing",
}),
"unknown resident surface availability",
);
assertThrows(
() => parseSubjektivResidentSurfaceResponse(readyEmpty, "subject-2"),
"identity does not match",
);
});
Deno.test("Current Memory list parser rejects unknown variants, unsafe integers, cursors, and oversized pages", () => {
const response = {
items: [queryItem()],
has_more: true,
next_cursor: "memory-page-2",
};
assertEquals(parseSubjektivMemoryQueryResponse(response), response);
assertThrows(
() =>
parseSubjektivMemoryQueryResponse({
...response,
items: [{ ...queryItem(), state: "pending" }],
}),
"unknown Memory state",
);
assertThrows(
() =>
parseSubjektivMemoryQueryResponse({
...response,
items: [{ ...queryItem(), kind: "future_kind" }],
}),
"unknown Memory candidate kind",
);
assertThrows(
() =>
parseSubjektivMemoryQueryResponse({
...response,
items: [{ ...queryItem(), revision: 0 }],
}),
"positive safe integer",
);
assertThrows(
() =>
parseSubjektivMemoryQueryResponse({
items: [queryItem()],
has_more: true,
}),
"cursor does not match",
);
assertThrows(
() =>
parseSubjektivMemoryQueryResponse({
items: Array.from({ length: 101 }, (_, index) => queryItem(`memory-${index}`)),
has_more: false,
}),
"bounded array",
);
});
Deno.test("Memory detail parser preserves body/evidence continuation, sources, origins, and derivation", () => {
const fixture = detailFixture();
const parsed = parseSubjektivMemoryReadResponse(fixture, "memory-1");
assertEquals(parsed, fixture);
assertEquals(parsed.body_next_offset, 2);
assertEquals(parsed.body_next_byte_offset, 0);
assertEquals(parsed.evidence_next_cursor, "evidence-page-2");
assertEquals(
parsed.source_candidates[0].evidence[0].origin,
fixture.source_candidates[0].evidence[0].origin,
);
assertEquals(
parsed.source_candidates[0].source_refs,
fixture.source_candidates[0].source_refs,
);
assertEquals(parsed.derived_from, [{
memory_id: "memory-parent",
revision: 4,
}]);
});
Deno.test("Memory detail parser fails closed on identity, variants, bounds, and continuation mismatch", () => {
const fixture = detailFixture();
assertThrows(
() => parseSubjektivMemoryReadResponse(fixture, "memory-2"),
"identity does not match",
);
assertThrows(
() =>
parseSubjektivMemoryReadResponse({
...fixture,
current_revision: Number.MAX_SAFE_INTEGER + 1,
}),
"safe integer",
);
const missingBodyContinuation = structuredClone(fixture) as Record<
string,
unknown
>;
delete missingBodyContinuation.body_next_byte_offset;
assertThrows(
() => parseSubjektivMemoryReadResponse(missingBodyContinuation),
"body continuation fields",
);
assertThrows(
() =>
parseSubjektivMemoryReadResponse({
...fixture,
source_candidate_ids: ["different-candidate"],
}),
"source candidate ids do not match",
);
assertThrows(
() =>
parseSubjektivMemoryReadResponse({
...fixture,
source_candidates: [{
...fixture.source_candidates[0],
evidence: [{
...fixture.source_candidates[0].evidence[0],
entry_range: [12, 10],
}],
}],
}),
"must be ordered",
);
const unknownOrigin = structuredClone(fixture);
unknownOrigin.source_candidates[0].evidence[0].origin.kind = "future_origin";
assertThrows(
() => parseSubjektivMemoryReadResponse(unknownOrigin),
"unknown Memory evidence origin kind",
);
assertThrows(
() =>
parseSubjektivMemoryReadResponse({
...fixture,
evidence_has_more: false,
}),
"cursor does not match",
);
});
Deno.test("Memory revision parser preserves immutable states and checks response identity", () => {
const response = {
memory_id: "memory-1",
current_revision: 3,
items: [{
revision: 3,
kind: "decision",
state: "active",
claim: "Current claim.",
change_reason: "Clarified wording.",
updated_at: "2026-09-03T00:00:00Z",
}, {
revision: 2,
kind: "decision",
state: "resolved",
claim: "Earlier claim.",
change_reason: "Resolved.",
updated_at: "2026-09-02T00:00:00Z",
}, {
revision: 1,
kind: "decision",
state: "retracted",
claim: "Original claim.",
change_reason: "Retracted.",
updated_at: "2026-09-01T00:00:00Z",
}],
has_more: false,
};
assertEquals(
parseSubjektivMemoryListRevisionsResponse(response, "memory-1"),
response,
);
assertThrows(
() => parseSubjektivMemoryListRevisionsResponse(response, "memory-2"),
"identity does not match",
);
assertThrows(
() =>
parseSubjektivMemoryListRevisionsResponse({
...response,
items: [{ ...response.items[0], state: "archived" }],
}),
"unknown Memory state",
);
assertThrows(
() =>
parseSubjektivMemoryListRevisionsResponse({
...response,
items: [{ ...response.items[0], revision: 4 }],
}),
"future revision",
);
});
Deno.test("Legacy staging parser remains strict for deprecated compatibility", () => {
const origin = { kind: "worker_input", worker_id: "worker-1" };
const response = {
limit: 100,
returned_count: 1,
total_valid_count: 1,
@@ -70,97 +465,19 @@ function fixture(origin: Record<string, unknown>) {
}],
diagnostics: [],
};
}
Deno.test("Memory document response requires the generated DTO fields", () => {
assertEquals(
parseMemoryDocumentResponse({
body_md: "# Memory\n",
created_at: "2026-09-01T00:00:00Z",
updated_at: "2026-09-01T00:00:00Z",
bytes: 9,
record_source: "sqlite_workspace_authority.memory_document",
}).bytes,
9,
);
assertThrows(
() => parseMemoryDocumentResponse({ body_md: "# Memory\n" }),
"missing a required field",
);
});
for (
const [kind, fields] of [
["human_input", { account_id: "account-1" }],
[
"worker_input",
{
workspace_id: "workspace-1",
runtime_id: "runtime-1",
worker_id: "worker-1",
},
],
["model_output", { runtime_id: "runtime-1", worker_id: "worker-1" }],
["tool_output", { runtime_id: "runtime-1", worker_id: "worker-1" }],
["legacy_unknown", {}],
] as const
) {
Deno.test(`Memory staging parser preserves ${kind} origin`, () => {
const parsed = parseMemoryStagingListResponse(fixture({ kind, ...fields }));
assertEquals(parsed.items[0].record.evidence[0].origin, {
kind,
...fields,
});
assertEquals(parsed.items[0].record.source_refs[0].origin, {
kind,
...fields,
});
});
}
Deno.test("Memory staging parser preserves Flow origin fields", () => {
const origin = {
kind: "flow_instruction" as const,
workspace_id: "workspace-1",
runtime_id: "runtime-1",
worker_id: "worker-1",
flow_selector: "builtin:coder-review",
flow_definition_id: "flow-1",
flow_definition_revision: 7,
};
const parsed = parseMemoryStagingListResponse(fixture(origin));
assertEquals(parsed.items[0].record.source_refs[0].origin, origin);
});
Deno.test("Memory staging parser rejects unknown or newer origin shapes", () => {
assertThrows(
() => parseMemoryStagingListResponse(fixture({ kind: "future_origin" })),
"unknown Memory evidence origin kind",
);
assertEquals(parseMemoryStagingListResponse(response), response);
assertThrows(
() =>
parseMemoryStagingListResponse(
fixture({ kind: "human_input", future_field: "must not be accepted" }),
),
"unknown field",
);
});
Deno.test("Memory staging parser rejects malformed records and unbounded origins", () => {
const malformed = fixture({ kind: "legacy_unknown" });
malformed.items[0].record.source_refs[0].entry_range = [1] as unknown as [
number,
number,
];
assertThrows(
() => parseMemoryStagingListResponse(malformed),
"two-item entry range",
);
assertThrows(
() =>
parseMemoryStagingListResponse(
fixture({ kind: "worker_input", worker_id: "x".repeat(513) }),
),
"exceeds the Memory origin limit",
parseMemoryStagingListResponse({
...response,
items: [{
...response.items[0],
record: {
...response.items[0].record,
source: { segment_id: "segment-1", range: [20, 10] },
},
}],
}),
"must be ordered",
);
});