{"version":3,"file":"session-store.d.ts","sourceRoot":"","sources":["../src/session-store.ts"],"names":[],"mappings":"AAEA,OAAO,EAA2C,KAAK,gBAAgB,EAAE,MAAM,+BAA+B,CAAC;AAC/G,OAAO,EAA2B,KAAK,OAAO,EAAE,KAAK,IAAI,EAAE,MAAM,uBAAuB,CAAC;AAUzF,MAAM,WAAW,oBAAoB;IACpC,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,KAAK,CAAC,EAAE,IAAI,EAAE,CAAC;CACf;AAED,MAAM,WAAW,YAAY;IAC5B,SAAS,EAAE,MAAM,CAAC;IAClB,aAAa,EAAE,oBAAoB,GAAG,SAAS,CAAC;IAChD,iBAAiB,EAAE,MAAM,CAAC;IAC1B,OAAO,EAAE,gBAAgB,EAAE,CAAC;IAC5B,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,QAAQ,EAAE,OAAO,EAAE,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,iBAAiB,EAAE,wBAAwB,GAAG,SAAS,CAAC;CACxD;AAED,MAAM,WAAW,cAAc;IAC9B,SAAS,EAAE,MAAM,CAAC;IAClB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,qBAAqB;IACrC,SAAS,EAAE,MAAM,CAAC;IAClB,aAAa,EAAE,oBAAoB,GAAG,SAAS,CAAC;IAChD,OAAO,EAAE,gBAAgB,EAAE,CAAC;IAC5B,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,MAAM,wBAAwB,GAAG;IAAE,IAAI,EAAE,UAAU,CAAA;CAAE,GAAG;IAAE,IAAI,EAAE,KAAK,CAAC;IAAC,OAAO,EAAE,gBAAgB,EAAE,CAAA;CAAE,CAAC;AAQ3G,wBAAgB,kBAAkB,CAAC,SAAS,EAAE,MAAM,GAAG,YAAY,CAqBlE;AAED,wBAAgB,UAAU,CAAC,SAAS,EAAE,MAAM,GAAG,YAAY,GAAG,SAAS,CAEtE;AAED,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,gBAAgB,EAAE,GAAG,MAAM,CAEtE;AAED,wBAAgB,YAAY,IAAI,cAAc,EAAE,CAc/C;AAED,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,YAAY,GAAG,qBAAqB,CAU/E;AAED,wBAAgB,mBAAmB,CAAC,SAAS,EAAE,qBAAqB,GAAG,YAAY,CAmBlF;AAED,wBAAgB,gBAAgB,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,oBAAoB,GAAG,YAAY,CAU/F;AAYD,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,YAAY,GAAG,gBAAgB,EAAE,CAe1E;AAgCD,wBAAgB,kBAAkB,CACjC,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,gBAAgB,EAAE,EAC3B,MAAM,EAAE,MAAM,GAAG,IAAI,GACnB,YAAY,CAWd;AAED,wBAAgB,oBAAoB,CACnC,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,gBAAgB,EAAE,EAC3B,MAAM,EAAE,MAAM,GAAG,IAAI,GACnB,YAAY,CA0Cd;AAED,wBAAgB,iBAAiB,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI,GAAG,YAAY,CASxF;AAED,wBAAgB,qBAAqB,CACpC,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,gBAAgB,EAAE,MAAM,CAAC;IAAC,YAAY,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,OAAO,CAAA;CAAE,GAChG;IAAE,OAAO,EAAE,YAAY,CAAC;IAAC,KAAK,EAAE,gBAAgB,CAAA;CAAE,CAuBpD;AAED,wBAAgB,cAAc,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,EAAE,GAAG,YAAY,CAchF;AAED,wBAAgB,eAAe,CAAC,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,OAAO,EAAE,GAAG,YAAY,CAepF;AAED,wBAAgB,aAAa,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAExD;AAED,wBAAgB,gBAAgB,IAAI,IAAI,CAEvC;AAUD,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,YAAY,GAAG,IAAI,CAEhE","sourcesContent":["import { randomUUID } from \"node:crypto\";\nimport { isDeepStrictEqual } from \"node:util\";\nimport { buildLegacySessionContext, convertToLlm, type SessionTreeEntry } from \"@earendil-works/pi-agent-core\";\nimport { getCurrentSystemMessage, type Message, type Tool } from \"@earendil-works/pi-ai\";\nimport { PiServerError, PiServerErrorCode } from \"./error-codes.ts\";\nimport {\n\tappendPiServerTreeHash,\n\tbuildPiServerTreePrefixHashes,\n\thashPiServerSessionEntries,\n\thashPiServerStaticContext,\n\thashPiServerTreeEntry,\n} from \"./pi-server-protocol.ts\";\n\nexport interface SessionStaticContext {\n\tsystemPrompt?: string;\n\ttools?: Tool[];\n}\n\nexport interface SessionState {\n\tsessionId: string;\n\tstaticContext: SessionStaticContext | undefined;\n\tstaticContextHash: string;\n\tentries: SessionTreeEntry[];\n\tleafId: string | null;\n\ttreeHash: string;\n\tprefixHashes: string[];\n\tmessages: Message[];\n\trevision: number;\n\tcreatedAt: number;\n\tupdatedAt: number;\n\tpersistenceChange: SessionPersistenceChange | undefined;\n}\n\nexport interface SessionSummary {\n\tsessionId: string;\n\tstaticContextHash: string;\n\ttreeHash: string;\n\tmessageCount: number;\n\tentryCount: number;\n\tleafId: string | null;\n\trevision: number;\n\tcreatedAt: number;\n\tupdatedAt: number;\n}\n\nexport interface PersistedSessionState {\n\tsessionId: string;\n\tstaticContext: SessionStaticContext | undefined;\n\tentries: SessionTreeEntry[];\n\tleafId: string | null;\n\trevision: number;\n\tcreatedAt: number;\n\tupdatedAt: number;\n}\n\nexport type SessionPersistenceChange = { kind: \"snapshot\" } | { kind: \"wal\"; entries: SessionTreeEntry[] };\n\nconst sessions = new Map<string, SessionState>();\n\nfunction hashStaticContext(ctx: SessionStaticContext | undefined): string {\n\treturn hashPiServerStaticContext(ctx);\n}\n\nexport function getOrCreateSession(sessionId: string): SessionState {\n\tlet session = sessions.get(sessionId);\n\tif (!session) {\n\t\tconst now = Date.now();\n\t\tsession = {\n\t\t\tsessionId,\n\t\t\tstaticContext: undefined,\n\t\t\tstaticContextHash: \"\",\n\t\t\tentries: [],\n\t\t\tleafId: null,\n\t\t\ttreeHash: hashPiServerSessionEntries([]),\n\t\t\tprefixHashes: buildPiServerTreePrefixHashes([]),\n\t\t\tmessages: [],\n\t\t\trevision: 0,\n\t\t\tcreatedAt: now,\n\t\t\tupdatedAt: now,\n\t\t\tpersistenceChange: { kind: \"snapshot\" },\n\t\t};\n\t\tsessions.set(sessionId, session);\n\t}\n\treturn session;\n}\n\nexport function getSession(sessionId: string): SessionState | undefined {\n\treturn sessions.get(sessionId);\n}\n\nexport function hashSessionEntries(entries: SessionTreeEntry[]): string {\n\treturn hashPiServerSessionEntries(entries);\n}\n\nexport function listSessions(): SessionSummary[] {\n\treturn Array.from(sessions.values())\n\t\t.map((session) => ({\n\t\t\tsessionId: session.sessionId,\n\t\t\tstaticContextHash: session.staticContextHash,\n\t\t\ttreeHash: session.treeHash,\n\t\t\tmessageCount: session.messages.length,\n\t\t\tentryCount: session.entries.length,\n\t\t\tleafId: session.leafId,\n\t\t\trevision: session.revision,\n\t\t\tcreatedAt: session.createdAt,\n\t\t\tupdatedAt: session.updatedAt,\n\t\t}))\n\t\t.sort((a, b) => b.updatedAt - a.updatedAt || a.sessionId.localeCompare(b.sessionId));\n}\n\nexport function exportSessionState(session: SessionState): PersistedSessionState {\n\treturn {\n\t\tsessionId: session.sessionId,\n\t\tstaticContext: session.staticContext,\n\t\tentries: session.entries.map((entry) => ({ ...entry })),\n\t\tleafId: session.leafId,\n\t\trevision: session.revision,\n\t\tcreatedAt: session.createdAt,\n\t\tupdatedAt: session.updatedAt,\n\t};\n}\n\nexport function restoreSessionState(persisted: PersistedSessionState): SessionState {\n\tassertValidLeaf(persisted.entries, persisted.leafId);\n\tconst session: SessionState = {\n\t\tsessionId: persisted.sessionId,\n\t\tstaticContext: persisted.staticContext,\n\t\tstaticContextHash: hashStaticContext(persisted.staticContext),\n\t\tentries: persisted.entries.map((entry) => ({ ...entry })),\n\t\tleafId: persisted.leafId,\n\t\ttreeHash: hashSessionEntries(persisted.entries),\n\t\tprefixHashes: buildPiServerTreePrefixHashes(persisted.entries),\n\t\tmessages: [],\n\t\trevision: persisted.revision,\n\t\tcreatedAt: persisted.createdAt,\n\t\tupdatedAt: persisted.updatedAt,\n\t\tpersistenceChange: undefined,\n\t};\n\trefreshActiveMessages(session);\n\tsessions.set(session.sessionId, session);\n\treturn session;\n}\n\nexport function setStaticContext(sessionId: string, context: SessionStaticContext): SessionState {\n\tconst session = getOrCreateSession(sessionId);\n\tconst newHash = hashStaticContext(context);\n\tif (session.staticContextHash === newHash) return session;\n\tsession.staticContext = context;\n\tsession.staticContextHash = newHash;\n\tsession.revision++;\n\tsession.updatedAt = Date.now();\n\tmarkWalPersistenceChange(session, []);\n\treturn session;\n}\n\nfunction entryToMessageEntry(message: Message, parentId: string | null): SessionTreeEntry {\n\treturn {\n\t\ttype: \"message\",\n\t\tid: randomUUID(),\n\t\tparentId,\n\t\ttimestamp: new Date(message.timestamp).toISOString(),\n\t\tmessage,\n\t};\n}\n\nexport function getSessionBranch(session: SessionState): SessionTreeEntry[] {\n\tconst byId = new Map(session.entries.map((entry) => [entry.id, entry]));\n\tconst branch: SessionTreeEntry[] = [];\n\tlet current = session.leafId ? byId.get(session.leafId) : undefined;\n\tconst seen = new Set<string>();\n\twhile (current) {\n\t\tif (seen.has(current.id)) {\n\t\t\tthrow new Error(`session tree contains a parent cycle at entry ${current.id}`);\n\t\t}\n\t\tseen.add(current.id);\n\t\tbranch.push(current);\n\t\tcurrent = current.parentId ? byId.get(current.parentId) : undefined;\n\t}\n\tbranch.reverse();\n\treturn branch;\n}\n\nfunction deriveActiveMessages(session: SessionState): Message[] {\n\tconst branch = getSessionBranch(session);\n\treturn convertToLlm(buildLegacySessionContext(branch).messages);\n}\n\nfunction refreshActiveMessages(session: SessionState): void {\n\tsession.messages = deriveActiveMessages(session);\n}\n\nfunction refreshTreeHashes(session: SessionState): void {\n\tsession.prefixHashes = buildPiServerTreePrefixHashes(session.entries);\n\tsession.treeHash = session.prefixHashes[session.prefixHashes.length - 1];\n}\n\nfunction appendTreeHashes(session: SessionState, entries: SessionTreeEntry[]): void {\n\tfor (const entry of entries) {\n\t\tsession.treeHash = appendPiServerTreeHash(session.treeHash, hashPiServerTreeEntry(entry));\n\t\tsession.prefixHashes.push(session.treeHash);\n\t}\n}\n\nfunction assertValidLeaf(entries: SessionTreeEntry[], leafId: string | null): void {\n\tif (leafId === null) return;\n\tif (!entries.some((entry) => entry.id === leafId)) {\n\t\tthrow new PiServerError(`leafId ${leafId} does not exist in session tree`, PiServerErrorCode.LEAF_ID_NOT_FOUND, {\n\t\t\tleafId,\n\t\t});\n\t}\n}\n\nexport function replaceSessionTree(\n\tsessionId: string,\n\tentries: SessionTreeEntry[],\n\tleafId: string | null,\n): SessionState {\n\tassertValidLeaf(entries, leafId);\n\tconst session = getOrCreateSession(sessionId);\n\tsession.entries = entries.map((entry) => ({ ...entry }));\n\tsession.leafId = leafId;\n\trefreshTreeHashes(session);\n\tsession.revision++;\n\tsession.updatedAt = Date.now();\n\trefreshActiveMessages(session);\n\tsession.persistenceChange = { kind: \"snapshot\" };\n\treturn session;\n}\n\nexport function appendSessionEntries(\n\tsessionId: string,\n\tentries: SessionTreeEntry[],\n\tleafId: string | null,\n): SessionState {\n\tconst session = getOrCreateSession(sessionId);\n\tconst knownEntries = new Map(session.entries.map((entry) => [entry.id, entry]));\n\tconst entriesToAppend: SessionTreeEntry[] = [];\n\tfor (const entry of entries) {\n\t\tconst knownEntry = knownEntries.get(entry.id);\n\t\tif (knownEntry) {\n\t\t\tif (!isDeepStrictEqual(knownEntry, entry)) {\n\t\t\t\tthrow new PiServerError(`entry ${entry.id} already exists`, PiServerErrorCode.ENTRY_ALREADY_EXISTS, {\n\t\t\t\t\tentryId: entry.id,\n\t\t\t\t});\n\t\t\t}\n\t\t\tcontinue;\n\t\t}\n\t\tif (entry.parentId !== null && !knownEntries.has(entry.parentId)) {\n\t\t\tthrow new PiServerError(\n\t\t\t\t`parent entry ${entry.parentId} does not exist`,\n\t\t\t\tPiServerErrorCode.PARENT_ENTRY_NOT_FOUND,\n\t\t\t\t{ parentId: entry.parentId },\n\t\t\t);\n\t\t}\n\t\tconst entryToAppend = { ...entry };\n\t\tentriesToAppend.push(entryToAppend);\n\t\tknownEntries.set(entryToAppend.id, entryToAppend);\n\t}\n\tif (leafId !== null && !knownEntries.has(leafId)) {\n\t\tthrow new PiServerError(`leafId ${leafId} does not exist in session tree`, PiServerErrorCode.LEAF_ID_NOT_FOUND, {\n\t\t\tleafId,\n\t\t});\n\t}\n\tif (entriesToAppend.length === 0 && session.leafId === leafId) {\n\t\treturn session;\n\t}\n\tsession.entries.push(...entriesToAppend);\n\tassertValidLeaf(session.entries, leafId);\n\tsession.leafId = leafId;\n\tappendTreeHashes(session, entriesToAppend);\n\tsession.revision++;\n\tsession.updatedAt = Date.now();\n\trefreshActiveMessages(session);\n\tmarkWalPersistenceChange(session, entriesToAppend);\n\treturn session;\n}\n\nexport function switchSessionLeaf(sessionId: string, leafId: string | null): SessionState {\n\tconst session = getOrCreateSession(sessionId);\n\tassertValidLeaf(session.entries, leafId);\n\tsession.leafId = leafId;\n\tsession.revision++;\n\tsession.updatedAt = Date.now();\n\trefreshActiveMessages(session);\n\tmarkWalPersistenceChange(session, []);\n\treturn session;\n}\n\nexport function appendCompactionEntry(\n\tsessionId: string,\n\tcompaction: { summary: string; firstKeptEntryId: string; tokensBefore: number; details?: unknown },\n): { session: SessionState; entry: SessionTreeEntry } {\n\tconst session = getOrCreateSession(sessionId);\n\tconst timestamp = new Date().toISOString();\n\tconst systemMessage = getCurrentSystemMessage(buildLegacySessionContext(getSessionBranch(session)).messages);\n\tconst entry: SessionTreeEntry = {\n\t\ttype: \"compaction\",\n\t\tid: randomUUID(),\n\t\tparentId: session.leafId,\n\t\ttimestamp,\n\t\tsummary: compaction.summary,\n\t\tfirstKeptEntryId: compaction.firstKeptEntryId,\n\t\ttokensBefore: compaction.tokensBefore,\n\t\tdetails: compaction.details,\n\t\t...(systemMessage ? { systemMessage: { ...systemMessage, timestamp: new Date(timestamp).getTime() } } : {}),\n\t};\n\tsession.entries.push(entry);\n\tsession.leafId = entry.id;\n\tappendTreeHashes(session, [entry]);\n\tsession.revision++;\n\tsession.updatedAt = Date.now();\n\trefreshActiveMessages(session);\n\tmarkWalPersistenceChange(session, [entry]);\n\treturn { session, entry };\n}\n\nexport function appendMessages(sessionId: string, delta: Message[]): SessionState {\n\tconst session = getOrCreateSession(sessionId);\n\tconst entries = delta.map((message) => {\n\t\tconst entry = entryToMessageEntry(message, session.leafId);\n\t\tsession.leafId = entry.id;\n\t\treturn entry;\n\t});\n\tsession.entries.push(...entries);\n\tappendTreeHashes(session, entries);\n\tsession.revision++;\n\tsession.updatedAt = Date.now();\n\trefreshActiveMessages(session);\n\tmarkWalPersistenceChange(session, entries);\n\treturn session;\n}\n\nexport function replaceMessages(sessionId: string, messages: Message[]): SessionState {\n\tconst session = getOrCreateSession(sessionId);\n\tlet parentId: string | null = null;\n\tsession.entries = messages.map((message) => {\n\t\tconst entry = entryToMessageEntry(message, parentId);\n\t\tparentId = entry.id;\n\t\treturn entry;\n\t});\n\tsession.leafId = parentId;\n\trefreshTreeHashes(session);\n\tsession.revision++;\n\tsession.updatedAt = Date.now();\n\trefreshActiveMessages(session);\n\tsession.persistenceChange = { kind: \"snapshot\" };\n\treturn session;\n}\n\nexport function deleteSession(sessionId: string): boolean {\n\treturn sessions.delete(sessionId);\n}\n\nexport function clearAllSessions(): void {\n\tsessions.clear();\n}\n\nfunction markWalPersistenceChange(session: SessionState, entries: SessionTreeEntry[]): void {\n\tif (session.persistenceChange?.kind === \"snapshot\") return;\n\tsession.persistenceChange = {\n\t\tkind: \"wal\",\n\t\tentries: [...(session.persistenceChange?.entries ?? []), ...entries.map((entry) => ({ ...entry }))],\n\t};\n}\n\nexport function markSessionPersisted(session: SessionState): void {\n\tsession.persistenceChange = undefined;\n}\n"]}