interface EmbeddedSection { title: string; body: string; embedding: number[]; } /** * Write a document's :Section children (idempotent). Deletes any prior * (:KnowledgeDocument)-[:HAS_SECTION]->(:Section) so re-ingest does not * duplicate, then re-creates the section chain mirroring memory-ingest's * contract (HAS_SECTION + NEXT, embedding per section). Empty-embedding * sections are still written (property-searchable; reindex backfills the * vector). The `:Section` target label keeps this cleanup from touching other * HAS_SECTION uses (e.g. Notion db→row edges). */ export declare function writeKnowledgeDocSections(tx: { run: (q: string, p?: Record) => Promise; }, opts: { docId: string; accountId: string; scope: string; source: string; createdByAgent: string; sessionId: string | null; sourceDocumentKey: string; sections: EmbeddedSection[]; }): Promise; export interface LinkedInConnectionRow { givenName: string; familyName: string; linkedinUrl: string; email?: string | null; company?: string | null; title?: string | null; /** ISO 8601 date (YYYY-MM-DD), parsed by the caller from "23 Apr 2026". */ connectedOn: string; } /** * One Obsidian vault page, after parsing + wikilink resolution. The * obsidian-vault-import tool produces these from on-disk markdown and passes * them as the `rows` payload — the handler here writes them in fixed Cypher. */ export interface ObsidianPageRow { /** Vault-relative path with forward slashes. Natural key with accountId. */ obsidianPath: string; /** Filename without `.md`. */ title: string; /** Aliases declared in YAML frontmatter. */ aliases: string[]; /** Tag tokens (lowercased, no `#` prefix) — frontmatter ∪ inline body. */ tags: string[]; /** Body content with frontmatter stripped. */ body: string; /** SHA-256 of file bytes — idempotency check key. */ contentHash: string; /** ISO 8601 date when daily-note detected, else null. */ noteDate: string | null; /** Last-modified epoch-ms — written as `obsidianMtime`. */ mtimeMs: number; /** Wikilinks emitted by this page, with resolution already applied. */ wikilinks: ResolvedWikilink[]; /** Embedded attachments — copied to {accountDir}/archive/obsidian//. */ attachments: ResolvedAttachment[]; } export interface ResolvedWikilink { /** Display text the operator typed inside [[…]] (post-alias-strip). */ linkText: string; /** Alias text after `|`, or null when absent. */ alias: string | null; /** Anchor portion after `#`, or null. */ anchor: string | null; /** What the link resolves to. */ target: { kind: "page"; obsidianPath: string; } | { kind: "entity"; nodeId: string; nodeLabel: string; } | { kind: "stub"; name: string; }; } export interface ResolvedAttachment { /** Original path as written in the markdown. */ rawPath: string; /** Path under {accountDir}/archive/obsidian// after copy. */ archivePath: string; /** MIME-style encoding marker (image/png, application/pdf, …). */ encodingFormat: string; /** Display alt text or null. */ alt: string | null; } /** * One Notion page in a `notion-pages` archive call. The notion-import skill * parses page markdown and front-matter, then submits batches of these rows. * * Parent semantics: `parentNotionId === null` means "top-level page" — the * handler writes (:NotionWorkspace)-[:HAS_SECTION]->(:KnowledgeDocument) at * insert time. Non-null parents are NOT written here; the skill submits them * to `notion-relations-pass-2` with `edgeType: 'HAS_SECTION'` so cross-batch * forward references resolve correctly after every page is MERGEd. */ export interface NotionPageRow { /** 32-hex Notion UUID. Natural key with accountId. */ notionId: string; /** Human title from the basename. */ title: string; /** Full markdown body, front-matter already stripped. */ text: string; /** Null when this page is a workspace-root page. */ parentNotionId: string | null; /** ISO 8601 datetime from front-matter `Created`, or null. */ notionCreatedAt: string | null; /** ISO 8601 datetime from front-matter `Last edited`, or null. */ notionLastEditedAt: string | null; /** elementId of an in-account :Person resolved by the skill, or null. */ authorPersonNodeId: string | null; } /** * One row from a Notion database in a `notion-database-rows` archive call. * The per-call `label` (one of Person, Organization, Project, Task, Event, * KnowledgeDocument) and `databaseNotionId` live on the params, not the row. */ export interface NotionDatabaseRowRow { /** 32-hex Notion UUID. Natural key with accountId. */ notionId: string; /** Mapped properties (canonical camelCase where mapped, original col names otherwise). */ properties: Record; } /** * One pass-2 relation. Source and target must already exist (MERGEd by a * prior `notion-pages` or `notion-database-rows` call); rows whose endpoints * are missing are counted as `skippedUnknownTarget`, not errors. */ export interface NotionRelationRow { sourceNotionId: string; targetNotionId: string; /** One of NOTION_RELATION_EDGE_TYPES. Unknown types are counted, not thrown. */ edgeType: string; /** Edge properties (e.g. `mentionContext` for unmapped Notion relations). */ properties?: Record; } export type ArchiveRow = LinkedInConnectionRow | ObsidianPageRow | NotionPageRow | NotionDatabaseRowRow | NotionRelationRow; export interface ArchiveWriteParams { archiveType: string; ownerNodeId: string; accountId: string; rows: ArchiveRow[]; sessionId?: string; /** notion-pages: UUID per import run, stamped on every created node as `importId`. */ importId?: string; /** notion-database-rows: 32-hex Notion ID of the parent database (the `:KnowledgeDocument {kind:'notion-database'}`). */ databaseNotionId?: string; /** notion-database-rows: one of Person|Organization|Project|Task|Event|KnowledgeDocument. */ label?: string; /** * Task 1560 — per-embed-POST progress callback. Threaded into every * `embedBatch` call this tool makes (a large obsidian/notion/linkedin import * embeds many batches inside one CallTool handler). Each POST emits an MCP * `notifications/progress` that resets Claude Code's ~30-min stdio idle timer, * so a long import is not idle-aborted mid-run. Absent for every non-MCP caller. * * Progress restarts per batch by design: an import embeds an unknown number of * independent batches (per-page document embeds, tag/attachment maps, per-label * row batches) with no grand total known up front, so each batch reports its * own 0→n sub-sequence rather than one combined counter. The idle-timer reset * — the goal here — fires on every POST regardless; Claude Code resets the * timer on any notification (Task 1555), so per-batch sequences are sufficient. */ reportProgress?: (done: number, total: number) => void; } export interface ArchiveWriteResult { archiveType: string; processedRows: number; /** Per-handler counter map. Keys are documented in each archiveType's reference. */ counters: Record; errors: Array<{ rowIndex: number; reason: string; }>; /** Present only for archiveTypes that run a `finalize` step. */ nextChainStatus?: "ok" | "failed" | "partial" | "skipped"; nextChainReason?: string; } export interface ArchiveHandlerWriteContext { params: ArchiveWriteParams; batchRows: ArchiveRow[]; sessionId: string | null; } export interface ArchiveHandlerFinalizeContext { params: ArchiveWriteParams; sessionId: string | null; /** True when at least one writeBatch returned errors. The handler may * still run finalize; it reports `status='partial'` on success. */ hadBatchErrors: boolean; } export interface ArchiveHandlerFinalizeResult { counters: Record; /** Handlers report ok/partial/skipped. The outer entry-point sets `failed` * on the public result when finalize itself throws. */ status: "ok" | "partial" | "skipped"; reason?: string; } export declare function memoryArchiveWrite(params: ArchiveWriteParams): Promise; export declare const PERSON_AND_CONNECTED_EDGE_CYPHER = "\nMATCH (owner) WHERE elementId(owner) = $ownerNodeId\nWITH owner, $rows AS rows\nUNWIND rows AS row\nMERGE (p:Person {linkedinUrl: row.linkedinUrl})\n ON CREATE SET\n p.accountId = $accountId,\n p.source = 'linkedin',\n p.createdByAgent = 'linkedin-import',\n p.createdBySource = 'linkedin-import',\n p.createdBySession = $sessionId,\n p.createdAt = datetime(),\n p.scope = 'admin'\nSET\n p.givenName = row.givenName,\n p.familyName = row.familyName,\n p.currentTitle = row.title,\n p.embedding = row.personEmbedding\nFOREACH (_ IN CASE WHEN row.email IS NOT NULL AND row.email <> '' THEN [1] ELSE [] END |\n SET p.email = row.email\n)\nMERGE (owner)-[c:CONNECTED_ON_LINKEDIN]->(p)\n ON CREATE SET\n c.connectedOn = date(row.connectedOn),\n c.source = 'linkedin',\n c.createdAt = datetime()\nRETURN count(*) AS processed\n"; export declare const ORG_AND_WORKS_FOR_CYPHER = "\nUNWIND $rows AS row\nWITH row WHERE row.company IS NOT NULL AND row.company <> ''\nMATCH (p:Person {linkedinUrl: row.linkedinUrl})\nMERGE (o:Organization {accountId: $accountId, name: row.company})\n ON CREATE SET\n o.source = 'linkedin',\n o.createdByAgent = 'linkedin-import',\n o.createdBySource = 'linkedin-import',\n o.createdBySession = $sessionId,\n o.createdAt = datetime(),\n o.scope = 'admin',\n o.embedding = row.orgEmbedding\nMERGE (p)-[w:WORKS_FOR]->(o)\n ON CREATE SET\n w.title = row.title,\n w.source = 'linkedin',\n w.current = true,\n w.createdAt = datetime()\n ON MATCH SET\n w.title = coalesce(row.title, w.title)\nRETURN count(*) AS processed\n"; export declare const OBSIDIAN_PAGE_CYPHER = "\nMATCH (owner) WHERE elementId(owner) = $ownerNodeId\nWITH owner, $rows AS rows\nUNWIND rows AS row\nMERGE (k:KnowledgeDocument {accountId: $accountId, obsidianPath: row.obsidianPath})\n ON CREATE SET\n k.knowledgeDocId = randomUUID(),\n k.source = 'obsidian',\n k.createdByAgent = 'obsidian-import',\n k.createdBySource = 'obsidian-import',\n k.createdBySession = $sessionId,\n k.createdAt = datetime(),\n k.scope = 'admin'\nSET\n k.title = row.title,\n k.contentHash = row.contentHash,\n k.text = row.body,\n k.summary = row.summary,\n k.embedding = row.docEmbedding,\n k.aliases = row.aliases,\n k.tags = row.tags,\n k.obsidianMtime = datetime({ epochMillis: toInteger(row.mtimeMs) }),\n k.updatedAt = datetime()\nFOREACH (_ IN CASE WHEN row.noteDate IS NOT NULL THEN [1] ELSE [] END |\n SET k.noteDate = date(row.noteDate)\n)\nMERGE (owner)-[h:HAS_NOTE]->(k)\n ON CREATE SET\n h.source = 'obsidian',\n h.createdAt = datetime()\nRETURN collect({ key: row.obsidianPath, docId: elementId(k) }) AS docs\n"; export declare const OBSIDIAN_TAG_CYPHER = "\nUNWIND $rows AS row\nMATCH (k:KnowledgeDocument {accountId: $accountId, obsidianPath: row.obsidianPath})\nUNWIND row.tags AS tagName\nWITH k, tagName WHERE tagName IS NOT NULL AND tagName <> ''\nMERGE (t:DefinedTerm {accountId: $accountId, category: 'obsidian-tag', name: tagName})\n ON CREATE SET\n t.source = 'obsidian',\n t.createdByAgent = 'obsidian-import',\n t.createdBySource = 'obsidian-import',\n t.createdBySession = $sessionId,\n t.createdAt = datetime(),\n t.scope = 'admin',\n t.embedding = $tagEmbeddings[tagName]\nMERGE (k)-[r:TAGGED_WITH]->(t)\n ON CREATE SET\n r.source = 'obsidian',\n r.createdAt = datetime()\nRETURN count(*) AS processed\n"; export declare const OBSIDIAN_WIKILINK_PAGE_CYPHER = "\nUNWIND $pageLinks AS wl\nMATCH (src:KnowledgeDocument {accountId: $accountId, obsidianPath: wl.fromPath})\nMATCH (dst:KnowledgeDocument {accountId: $accountId, obsidianPath: wl.toPath})\nMERGE (src)-[r:WIKILINKS_TO]->(dst)\n ON CREATE SET\n r.linkText = wl.linkText,\n r.alias = wl.alias,\n r.anchor = wl.anchor,\n r.source = 'obsidian',\n r.createdAt = datetime()\nRETURN count(*) AS processed\n"; export declare const OBSIDIAN_WIKILINK_ENTITY_CYPHER = "\nUNWIND $entityLinks AS wl\nMATCH (src:KnowledgeDocument {accountId: $accountId, obsidianPath: wl.fromPath})\nMATCH (dst) WHERE elementId(dst) = wl.toNodeId AND dst.accountId = $accountId\nMERGE (src)-[r:MENTIONS]->(dst)\n ON CREATE SET\n r.linkText = wl.linkText,\n r.alias = wl.alias,\n r.source = 'obsidian',\n r.createdAt = datetime()\nRETURN count(*) AS processed\n"; export declare const OBSIDIAN_WIKILINK_STUB_CYPHER = "\nUNWIND $stubLinks AS wl\nMATCH (src:KnowledgeDocument {accountId: $accountId, obsidianPath: wl.fromPath})\nMERGE (c:Concept {accountId: $accountId, name: wl.stubName, source: 'obsidian-stub'})\n ON CREATE SET\n c.createdByAgent = 'obsidian-import',\n c.createdBySource = 'obsidian-import',\n c.createdBySession = $sessionId,\n c.createdAt = datetime(),\n c.scope = 'admin'\nMERGE (src)-[r:MENTIONS]->(c)\n ON CREATE SET\n r.linkText = wl.linkText,\n r.alias = wl.alias,\n r.source = 'obsidian',\n r.createdAt = datetime()\nRETURN count(*) AS processed\n"; export declare const OBSIDIAN_ATTACHMENT_CYPHER = "\nUNWIND $rows AS row\nWITH row WHERE size(row.attachments) > 0\nMATCH (k:KnowledgeDocument {accountId: $accountId, obsidianPath: row.obsidianPath})\nUNWIND row.attachments AS att\nMERGE (d:DigitalDocument {accountId: $accountId, attachmentPath: att.archivePath})\n ON CREATE SET\n d.digitalDocumentId = randomUUID(),\n d.source = 'obsidian',\n d.encodingFormat = att.encodingFormat,\n d.alt = att.alt,\n d.embedding = $attachmentEmbeddings[att.archivePath],\n d.createdByAgent = 'obsidian-import',\n d.createdBySource = 'obsidian-import',\n d.createdBySession = $sessionId,\n d.createdAt = datetime(),\n d.scope = 'admin'\nMERGE (d)-[e:EMBEDDED_IN]->(k)\n ON CREATE SET\n e.source = 'obsidian',\n e.createdAt = datetime()\nRETURN count(*) AS processed\n"; export declare const NOTION_PAGE_CYPHER = "\nMATCH (workspace:NotionWorkspace) WHERE elementId(workspace) = $ownerNodeId\nWITH workspace, $rows AS rows\nUNWIND rows AS row\nMERGE (k:KnowledgeDocument {accountId: $accountId, notionId: row.notionId})\n ON CREATE SET\n k.knowledgeDocId = randomUUID(),\n k.kind = 'notion-page',\n k.source = 'notion',\n k.createdByAgent = 'notion-import',\n k.createdBySource = 'notion-import',\n k.createdBySession = $sessionId,\n k.createdAt = datetime(),\n k.scope = 'admin',\n k.importId = $importId\nSET\n k.title = row.title,\n k.text = row.text,\n k.summary = row.summary,\n k.embedding = row.docEmbedding,\n k.updatedAt = datetime()\nFOREACH (_ IN CASE WHEN row.notionCreatedAt IS NOT NULL THEN [1] ELSE [] END |\n SET k.notionCreatedAt = datetime(row.notionCreatedAt)\n)\nFOREACH (_ IN CASE WHEN row.notionLastEditedAt IS NOT NULL THEN [1] ELSE [] END |\n SET k.notionLastEditedAt = datetime(row.notionLastEditedAt)\n)\nFOREACH (_ IN CASE WHEN row.parentNotionId IS NULL THEN [1] ELSE [] END |\n MERGE (workspace)-[r:HAS_SECTION]->(k)\n ON CREATE SET r.source = 'notion', r.createdAt = datetime()\n)\nRETURN collect({ key: row.notionId, docId: elementId(k) }) AS docs\n"; export declare const NOTION_PAGE_AUTHOR_CYPHER = "\nUNWIND $rows AS row\nWITH row WHERE row.authorPersonNodeId IS NOT NULL\nMATCH (page:KnowledgeDocument {accountId: $accountId, notionId: row.notionId})\nMATCH (author) WHERE elementId(author) = row.authorPersonNodeId AND author.accountId = $accountId\nMERGE (author)-[r:AUTHORED]->(page)\n ON CREATE SET r.source = 'notion', r.createdAt = datetime()\nRETURN count(*) AS processed\n"; export {}; //# sourceMappingURL=memory-archive-write.d.ts.map