diff --git a/src/agent-harness/answer-context.test.ts b/src/agent-harness/answer-context.test.ts index 95ae672..572bde4 100644 --- a/src/agent-harness/answer-context.test.ts +++ b/src/agent-harness/answer-context.test.ts @@ -152,7 +152,6 @@ function makeLedger(): EvidenceLedgerSnapshot { data: Buffer.from(revenuePng).toString("base64"), }, ], - evidenceText: [], stopReasons: [], failureReasons: [], decisionTraces: [], diff --git a/src/agent-harness/ledger.ts b/src/agent-harness/ledger.ts index 59092ef..937fa70 100644 --- a/src/agent-harness/ledger.ts +++ b/src/agent-harness/ledger.ts @@ -20,7 +20,6 @@ type MutableLedger = { chunks: EvidenceChunk[] assets: EvidenceAsset[] evidence: EvidencePart[] - evidenceText: string[] stopReasons: string[] failureReasons: string[] decisionTraces: unknown[] @@ -57,7 +56,6 @@ export function createEvidenceLedger() { chunks: [], assets: [], evidence: [], - evidenceText: [], stopReasons: [], failureReasons: [], decisionTraces: [], @@ -70,9 +68,6 @@ export function createEvidenceLedger() { ledger.evidence.push(...readQueryEvidence(response)) - const evidenceText = response.evidenceText?.trim() - if (evidenceText) ledger.evidenceText.push(evidenceText) - const stopReason = response.stopReason?.trim() if (stopReason) ledger.stopReasons.push(stopReason) @@ -193,11 +188,7 @@ export function createEvidenceLedger() { }, hasEvidence(): boolean { - return ( - ledger.chunks.length > 0 || - ledger.evidence.length > 0 || - ledger.evidenceText.length > 0 - ) + return ledger.chunks.length > 0 || ledger.evidence.length > 0 }, snapshot(): EvidenceLedgerSnapshot { @@ -607,7 +598,6 @@ function snapshot(ledger: MutableLedger): EvidenceLedgerSnapshot { chunks: [...ledger.chunks], assets: [...ledger.assets], evidence: [...ledger.evidence], - evidenceText: [...ledger.evidenceText], stopReasons: [...ledger.stopReasons], failureReasons: [...ledger.failureReasons], decisionTraces: [...ledger.decisionTraces], diff --git a/src/agent-harness/types.ts b/src/agent-harness/types.ts index 4566c2e..907e43e 100644 --- a/src/agent-harness/types.ts +++ b/src/agent-harness/types.ts @@ -266,7 +266,6 @@ export type EvidenceLedgerSnapshot = { readonly chunks: readonly EvidenceChunk[] readonly assets: readonly EvidenceAsset[] readonly evidence: readonly EvidencePart[] - readonly evidenceText: readonly string[] readonly stopReasons: readonly string[] readonly failureReasons: readonly string[] readonly decisionTraces: readonly unknown[] diff --git a/src/app/api/memory-areas/[areaId]/route.ts b/src/app/api/memory-areas/[areaId]/route.ts new file mode 100644 index 0000000..ea1ad44 --- /dev/null +++ b/src/app/api/memory-areas/[areaId]/route.ts @@ -0,0 +1,17 @@ +import { NextResponse } from "next/server" + +import { notebookRequestContext } from "@/domains/workspace/request-context" +import { getMemoryAreaDocument } from "@/integrations/memento/client" +import { withApiErrorResponse } from "@/lib/api-error-response" + +export async function GET( + _request: Request, + context: { params: Promise<{ areaId: string }> }, +): Promise { + return withApiErrorResponse("memory-areas:read", async () => { + const { areaId } = await context.params + const { workspace } = await notebookRequestContext.getAuthenticatedWithClient() + const document = await getMemoryAreaDocument(workspace.id, areaId) + return NextResponse.json(document) + }) +} diff --git a/src/app/api/memory-areas/route.ts b/src/app/api/memory-areas/route.ts new file mode 100644 index 0000000..6071274 --- /dev/null +++ b/src/app/api/memory-areas/route.ts @@ -0,0 +1,13 @@ +import { NextResponse } from "next/server" + +import { listMemoryAreas } from "@/integrations/memento/client" +import { withApiErrorResponse } from "@/lib/api-error-response" +import { notebookRequestContext } from "@/domains/workspace/request-context" + +export async function GET(): Promise { + return withApiErrorResponse("memory-areas:list", async () => { + const { workspace } = await notebookRequestContext.getAuthenticatedWithClient() + const areas = await listMemoryAreas(workspace.id) + return NextResponse.json({ areas }) + }) +} diff --git a/src/app/api/memory-items/[itemId]/route.ts b/src/app/api/memory-items/[itemId]/route.ts new file mode 100644 index 0000000..54509de --- /dev/null +++ b/src/app/api/memory-items/[itemId]/route.ts @@ -0,0 +1,17 @@ +import { NextResponse } from "next/server" + +import { notebookRequestContext } from "@/domains/workspace/request-context" +import { deleteMemoryItem } from "@/integrations/memento/client" +import { withApiErrorResponse } from "@/lib/api-error-response" + +export async function DELETE( + _request: Request, + context: { params: Promise<{ itemId: string }> }, +): Promise { + return withApiErrorResponse("memory-items:delete", async () => { + const { itemId } = await context.params + const { workspace } = await notebookRequestContext.getAuthenticatedWithClient() + await deleteMemoryItem(workspace.id, itemId) + return NextResponse.json({ deleted: true }) + }) +} diff --git a/src/components/chat-composer.test.ts b/src/components/chat-composer.test.ts index a12264a..f34e985 100644 --- a/src/components/chat-composer.test.ts +++ b/src/components/chat-composer.test.ts @@ -48,7 +48,7 @@ describe("ChatComposer", () => { const tooltip = await screen.findByRole("tooltip"); expect(tooltip.textContent).toContain( - "Deep search plans document selection and navigation", + "Deep search turns on agent retrieval", ); }); diff --git a/src/components/chat-composer.tsx b/src/components/chat-composer.tsx index 4aab263..f3143cc 100644 --- a/src/components/chat-composer.tsx +++ b/src/components/chat-composer.tsx @@ -214,8 +214,8 @@ export function ChatComposer({ - Deep search plans document selection and navigation for - more thorough answers. Turn off for faster classic search. + Deep search turns on agent retrieval. Turn it off for + classic search without an agent. diff --git a/src/components/memory-documents.tsx b/src/components/memory-documents.tsx new file mode 100644 index 0000000..278702a --- /dev/null +++ b/src/components/memory-documents.tsx @@ -0,0 +1,94 @@ +"use client" + +import { useEffect, useState } from "react" +import type { ReactElement } from "react" + +type MemoryArea = { + readonly id: string + readonly title: string + readonly summaryStatus: "current" | "pending" +} + +type MemoryMember = { + readonly itemId: string + readonly kind: string + readonly status: string + readonly text: string +} + +type MemoryDocument = { + readonly id: string + readonly title: string + readonly summaryStatus: "current" | "pending" + readonly summary: string | null + readonly members: readonly MemoryMember[] +} + +export function MemoryDocuments(props: { + readonly onBack: () => void +}): ReactElement { + const [areas, setAreas] = useState([]) + const [document, setDocument] = useState(null) + + useEffect(() => { + void fetch("/api/memory-areas") + .then((response) => response.json()) + .then((body: { areas?: readonly MemoryArea[] }) => { + setAreas(body.areas ?? []) + }) + }, []) + + async function openArea(areaId: string): Promise { + const response = await fetch(`/api/memory-areas/${areaId}`) + setDocument((await response.json()) as MemoryDocument) + } + + async function deleteItem(itemId: string): Promise { + if (!document) return + await fetch(`/api/memory-items/${itemId}`, { method: "DELETE" }) + await openArea(document.id) + } + + return ( +
+
+ +

+ {document?.title ?? "MEMORY.MD"} +

+
+ {document ? ( +
+ {document.summaryStatus === "current" && document.summary ? ( +

{document.summary}

+ ) : null} +
    + {document.members.map((member) => ( +
  • +

    + {member.kind} {member.status} +

    +

    {member.text}

    + +
  • + ))} +
+
+ ) : ( +
    + {areas.map((area) => ( +
  • + +
  • + ))} +
+ )} +
+ ) +} diff --git a/src/components/sources-panel.tsx b/src/components/sources-panel.tsx index 70f200c..bc02397 100644 --- a/src/components/sources-panel.tsx +++ b/src/components/sources-panel.tsx @@ -55,6 +55,7 @@ export type SourcesPanelProps = { readonly isNarrow?: boolean; readonly addingLibrarySourceIds?: readonly string[]; readonly isLibraryOpen?: boolean; + readonly isMemoryOpen?: boolean; readonly officialLibrarySources?: readonly OfficialLibrarySourceView[]; sources: SourceView[]; onSourceUploaded?: (source: SourceView) => void; @@ -64,6 +65,7 @@ export type SourcesPanelProps = { onArchiveSource?: (sourceId: string) => void; onRetrySource?: (sourceId: string) => void; onLibraryOpen?: () => void; + onMemoryOpen?: () => void; folders?: readonly FolderView[]; currentFolderId?: string | null; creatingFolder?: boolean; @@ -94,6 +96,7 @@ type SourcePageState = { export function SourcesPanel({ isNarrow = false, isLibraryOpen = false, + isMemoryOpen = false, sources = [], onSourceUploaded, selectedSourceId = null, @@ -102,6 +105,7 @@ export function SourcesPanel({ onArchiveSource, onRetrySource, onLibraryOpen, + onMemoryOpen, folders = [], currentFolderId = null, creatingFolder = false, @@ -386,6 +390,19 @@ export function SourcesPanel({ {isNarrow ? null : "open library"} + {onOpenFolder ? ( @@ -141,6 +142,7 @@ export type WorkspaceShellLayoutProps = { readonly onLoginClick: () => void readonly onLibraryBack: () => void readonly onLibraryOpen: () => void + readonly onMemoryOpen: () => void readonly onMobilePanelChange: (panel: PanelId) => void readonly onOfficialLibrarySourceAdd: ( demoSourceId: string, @@ -238,8 +240,10 @@ export function WorkspaceShellLayout( sources={props.sources} selectedSourceId={props.selectedSourceId} isLibraryOpen={props.contentView === "library"} + isMemoryOpen={props.contentView === "memory"} onExpand={() => props.onDesktopPanelExpand("sources")} onLibraryOpen={props.onLibraryOpen} + onMemoryOpen={props.onMemoryOpen} onSourceSelected={props.onSourceSelected} /> ) : ( @@ -247,6 +251,7 @@ export function WorkspaceShellLayout( sources={[...props.sources]} officialLibrarySources={[...officialLibrarySources]} isLibraryOpen={props.contentView === "library"} + isMemoryOpen={props.contentView === "memory"} isNarrow={isSourcesPanelNarrow} analyticsContext={props.analyticsContext} sourceCountSnapshot={props.sources.length} @@ -268,6 +273,7 @@ export function WorkspaceShellLayout( props.isGuest ? undefined : props.onOfficialLibrarySourceAdd } onLibraryOpen={props.onLibraryOpen} + onMemoryOpen={props.onMemoryOpen} archivingSourceIds={[...props.archivingSourceIds]} retryingSourceIds={[...retryingSourceIds]} addingLibrarySourceIds={[...addingLibrarySourceIds]} @@ -309,7 +315,9 @@ export function WorkspaceShellLayout( width: `${props.desktopPanelWidths.chunks}px`, }} > - {props.contentView === "library" ? ( + {props.contentView === "memory" ? ( + + ) : props.contentView === "library" ? ( { @@ -442,6 +451,10 @@ export function WorkspaceShellLayout( props.onLibraryOpen() props.onMobilePanelChange("content") }} + onMemoryOpen={() => { + props.onMemoryOpen() + props.onMobilePanelChange("content") + }} archivingSourceIds={[...props.archivingSourceIds]} retryingSourceIds={[...retryingSourceIds]} addingLibrarySourceIds={[...addingLibrarySourceIds]} @@ -469,7 +482,9 @@ export function WorkspaceShellLayout( props.mobilePanel === "content" ? "flex flex-col" : "hidden" }`} > - {props.contentView === "library" ? ( + {props.contentView === "memory" ? ( + + ) : props.contentView === "library" ? ( void readonly onLibraryOpen?: () => void + readonly onMemoryOpen?: () => void readonly onSourceSelected: (sourceId: string | null) => void readonly selectedSourceId: string | null readonly sources: readonly SourceView[] @@ -655,6 +674,15 @@ function CompactSourcesSidebar({ > + onMemoryOpen?.()} + > + M +
{sources.length === 0 ? ( diff --git a/src/components/workspace-shell.tsx b/src/components/workspace-shell.tsx index 5254403..3c21802 100644 --- a/src/components/workspace-shell.tsx +++ b/src/components/workspace-shell.tsx @@ -178,6 +178,10 @@ function WorkspaceShellContent({ setContentView("library") } + function handleMemoryOpen(): void { + setContentView("memory") + } + function handleLibraryBack(): void { setContentView("chunks") } @@ -279,6 +283,7 @@ function WorkspaceShellContent({ onLoginClick={redirectToLogin} onLibraryBack={handleLibraryBack} onLibraryOpen={handleLibraryOpen} + onMemoryOpen={handleMemoryOpen} onMobilePanelChange={setMobilePanel} onSelectChatThread={chatWorkflow.handleSelectChatThread} onSourceSelected={handleSourceSelected} diff --git a/src/domains/chat/agent-trace.test.ts b/src/domains/chat/agent-trace.test.ts index 24f1b7b..434f4c5 100644 --- a/src/domains/chat/agent-trace.test.ts +++ b/src/domains/chat/agent-trace.test.ts @@ -116,7 +116,6 @@ function makeTrace(overrides: Partial = {}): HarnessTrace { ], assets: [], evidence: [], - evidenceText: [], stopReasons: [], failureReasons: [], decisionTraces: [ diff --git a/src/domains/chat/commit-turn.test.ts b/src/domains/chat/commit-turn.test.ts index e44fa6b..a59d851 100644 --- a/src/domains/chat/commit-turn.test.ts +++ b/src/domains/chat/commit-turn.test.ts @@ -110,7 +110,6 @@ describe("commitChatTurn", () => { ], assets: [], evidence: [], - evidenceText: [], stopReasons: [], failureReasons: [], decisionTraces: [], @@ -264,7 +263,6 @@ describe("commitChatTurn", () => { ], assets: [], evidence: [], - evidenceText: [], stopReasons: [], failureReasons: [], decisionTraces: [], @@ -343,7 +341,6 @@ describe("commitChatTurn", () => { chunks: [], assets: [], evidence: [], - evidenceText: [], stopReasons: [], failureReasons: [], decisionTraces: [], @@ -420,7 +417,6 @@ describe("commitChatTurn", () => { chunks: [], assets: [], evidence: [], - evidenceText: [], stopReasons: [], failureReasons: [], decisionTraces: [], diff --git a/src/domains/chat/index.test.ts b/src/domains/chat/index.test.ts index 849753e..c2041b5 100644 --- a/src/domains/chat/index.test.ts +++ b/src/domains/chat/index.test.ts @@ -499,14 +499,14 @@ describe("answerQuestionWithRetrieval", () => { }) .mockResolvedValueOnce({ results: [legacyResult], - evidenceText: "Legacy namespace evidence", - referencedChunks: [], - namespace: "notebook-legacy", - query: "legacy document answer", - routerUsed: "workflow_single_step", - answerText: null, - stopReason: "answer_done", - failureReason: null, + evidenceText: null, + referencedChunks: [], + namespace: "notebook-legacy", + query: "legacy document answer", + routerUsed: "workflow_single_step", + answerText: null, + stopReason: "answer_done", + failureReason: null, }), }; const generateAnswer = vi.fn(async ({ searchSources }) => { @@ -516,7 +516,7 @@ describe("answerQuestionWithRetrieval", () => { stopReason: "answer_done", failureReason: null, results: [legacyResult], - evidenceText: "Legacy namespace evidence", + evidenceText: null, }); return makeHarnessRunResult("The legacy answer is grounded."); }); @@ -871,9 +871,15 @@ describe("answerQuestionWithRetrieval", () => { }), ), ], - evidenceText: `Evidence https://blob.example/evidence.jpg ${"evidence ".repeat( - 80, - )}`, + evidence: [ + { + type: "text" as const, + text: `Evidence https://blob.example/evidence.jpg ${"evidence ".repeat( + 80, + )}`, + }, + ], + evidenceText: null, referencedChunks: [ { chunkId: "chunk_identity_1", @@ -1262,7 +1268,6 @@ describe("answerQuestionWithRetrieval", () => { ledger: { retrievalCount: 1, evidence: [], - evidenceText: ["Identity image evidence."], stopReasons: [], failureReasons: [], decisionTraces: [], @@ -2003,7 +2008,6 @@ describe("answerQuestionWithRetrieval", () => { ledger: { retrievalCount: 1, evidence: [], - evidenceText: ["Identity image candidates."], stopReasons: [], failureReasons: [], decisionTraces: [], @@ -2357,7 +2361,6 @@ describe("answerQuestionWithRetrieval", () => { ledger: { retrievalCount: 1, evidence: [], - evidenceText: ["Diagram candidate."], stopReasons: [], failureReasons: [], decisionTraces: [], @@ -2479,7 +2482,6 @@ describe("answerQuestionWithRetrieval", () => { ledger: { retrievalCount: 1, evidence: [], - evidenceText: ["Plan comparison evidence."], stopReasons: [], failureReasons: [], decisionTraces: [], @@ -3239,7 +3241,6 @@ function makeHarnessRunResult(text: string): HarnessRunResult { chunks: [], assets: [], evidence: [], - evidenceText: [], stopReasons: [], failureReasons: [], decisionTraces: [], @@ -3293,7 +3294,6 @@ function makeHarnessRunResultWithLedger( chunks, assets: input.assets ?? [], evidence: [], - evidenceText: [], stopReasons: [], failureReasons: [], decisionTraces: [], diff --git a/src/domains/chat/index.ts b/src/domains/chat/index.ts index 5aba30b..2cf563e 100644 --- a/src/domains/chat/index.ts +++ b/src/domains/chat/index.ts @@ -626,7 +626,7 @@ function formatKnowhereQueryResponseForLog( KNOWHERE_RESPONSE_TEXT_LOG_LIMIT, ), evidenceText: truncateLogText( - response.evidenceText ?? "", + evidencePartsText(response), KNOWHERE_RESPONSE_TEXT_LOG_LIMIT, ), results: response.results @@ -890,10 +890,6 @@ function mergeRetrievalResponses( .slice(0, evidenceLimits.referencedChunkCountPerResponse), ) .slice(0, evidenceLimits.referencedChunkCount) - const evidenceTexts = responses - .map((response) => response.evidenceText) - .filter((value): value is string => Boolean(value)) - .map(truncateAgenticModelText) const answerTexts = responses .map((response) => response.answerText) .filter((value): value is string => Boolean(value)) @@ -904,7 +900,7 @@ function mergeRetrievalResponses( namespace: responses.map((response) => response.namespace).join(","), routerUsed: joinResponseText(responses.map((response) => response.routerUsed)) ?? "", answerText: answerTexts.length > 0 ? answerTexts.join("\n\n") : null, - evidenceText: evidenceTexts.length > 0 ? evidenceTexts.join("\n\n") : null, + evidenceText: null, stopReason: joinResponseText( statusResponses.map((response) => response.stopReason), ), @@ -955,11 +951,25 @@ function hasRetrievalEvidence(response: RetrievalQueryResponse): boolean { return ( response.results.length > 0 || response.referencedChunks.some(hasReferencedChunkEvidence) || - Boolean(response.evidenceText?.trim()) || + hasEvidenceParts(response) || Boolean(response.answerText?.trim()) ) } +function hasEvidenceParts(response: RetrievalQueryResponse): boolean { + return (response.evidence ?? []).some((part) => + part.type === "text" ? Boolean(part.text.trim()) : true, + ) +} + +function evidencePartsText(response: RetrievalQueryResponse): string { + return (response.evidence ?? []) + .flatMap((part) => + part.type === "text" && part.text.trim() ? [part.text] : [], + ) + .join("") +} + function joinResponseText( values: readonly (string | null | undefined)[], ): string | null { diff --git a/src/domains/chat/service.test.ts b/src/domains/chat/service.test.ts index 88ed074..27fce97 100644 --- a/src/domains/chat/service.test.ts +++ b/src/domains/chat/service.test.ts @@ -465,7 +465,6 @@ function makeHarnessRunResult(text: string): HarnessRunResult { chunks: [], assets: [], evidence: [], - evidenceText: [], stopReasons: [], failureReasons: [], decisionTraces: [], diff --git a/src/integrations/memento/client.ts b/src/integrations/memento/client.ts index fb72d6b..4df7d7f 100644 --- a/src/integrations/memento/client.ts +++ b/src/integrations/memento/client.ts @@ -195,6 +195,118 @@ function parseActivations(body: unknown): readonly MementoActivation[] { }) } +export type MementoMemoryArea = { + readonly id: string + readonly title: string + readonly kind: string + readonly summaryStatus: "current" | "pending" +} + +export type MementoMemoryDocument = { + readonly id: string + readonly title: string + readonly summaryStatus: "current" | "pending" + readonly summary: string | null + readonly members: readonly { + readonly itemId: string + readonly kind: string + readonly status: string + readonly text: string + }[] +} + +export async function listMemoryAreas( + workspaceId: string, +): Promise { + const body = await getJson( + `/memory-areas?${new URLSearchParams({ workspaceId }).toString()}`, + ) + return parseMemoryAreas(body) +} + +export async function getMemoryAreaDocument( + workspaceId: string, + areaId: string, +): Promise { + const body = await getJson( + `/memory-areas/${areaId}?${new URLSearchParams({ workspaceId }).toString()}`, + ) + return parseMemoryAreaDocument(body) +} + +export async function deleteMemoryItem( + workspaceId: string, + itemId: string, +): Promise { + await deleteJson( + `/memory-items/${itemId}?${new URLSearchParams({ workspaceId }).toString()}`, + ) +} + +function parseMemoryAreas(body: unknown): readonly MementoMemoryArea[] { + if (typeof body !== "object" || body === null) { + throw new Error("memento memory areas: invalid response") + } + const areas = Reflect.get(body, "areas") + if (!Array.isArray(areas)) { + throw new Error("memento memory areas: invalid response") + } + return areas.flatMap((area) => { + if (typeof area !== "object" || area === null) return [] + const id = Reflect.get(area, "id") + const title = Reflect.get(area, "title") + const kind = Reflect.get(area, "kind") + const summaryStatus = Reflect.get(area, "summaryStatus") + if (typeof id !== "string" || typeof title !== "string" || typeof kind !== "string") { + return [] + } + if (summaryStatus !== "current" && summaryStatus !== "pending") return [] + return [{ id, title, kind, summaryStatus }] + }) +} + +function parseMemoryAreaDocument(body: unknown): MementoMemoryDocument { + if (typeof body !== "object" || body === null) { + throw new Error("memento memory area: invalid response") + } + const id = Reflect.get(body, "id") + const title = Reflect.get(body, "title") + const summaryStatus = Reflect.get(body, "summaryStatus") + const summary = Reflect.get(body, "summary") + const members = Reflect.get(body, "members") + if (typeof id !== "string" || typeof title !== "string") { + throw new Error("memento memory area: invalid response") + } + if (summaryStatus !== "current" && summaryStatus !== "pending") { + throw new Error("memento memory area: invalid response") + } + if (!Array.isArray(members)) { + throw new Error("memento memory area: invalid response") + } + return { + id, + title, + summaryStatus, + summary: typeof summary === "string" ? summary : null, + members: members.flatMap((member) => { + if (typeof member !== "object" || member === null) return [] + const itemId = Reflect.get(member, "itemId") + const kind = Reflect.get(member, "kind") + const status = Reflect.get(member, "status") + const text = Reflect.get(member, "text") + if ( + typeof itemId !== "string" || + typeof kind !== "string" || + typeof status !== "string" || + typeof text !== "string" + ) { + return [] + } + return [{ itemId, kind, status, text }] + }), + } +} + async function getJson(path: string): Promise { const { status, body } = await Effect.runPromise( Effect.gen(function* () { @@ -218,6 +330,22 @@ async function getJson(path: string): Promise { return body } +async function deleteJson(path: string): Promise { + const status = await Effect.runPromise( + Effect.gen(function* () { + const { baseUrl, serviceKey } = readMementoConfig() + const request = HttpClientRequest.del(`${baseUrl}${path}`).pipe( + HttpClientRequest.setHeader("Authorization", `Bearer ${serviceKey}`), + ) + const response = yield* HttpClient.execute(request) + return response.status + }).pipe(Effect.provide(FetchHttpClient.layer)), + ) + if (status < 200 || status >= 300) { + throw new Error(`memento ${path}: HTTP ${status}`) + } +} + async function postJson(path: string, body: unknown): Promise { const status = await Effect.runPromise( Effect.gen(function* () {