@@ -117,34 +117,39 @@ export type SessionAccessScope = {
117117 storePath ?: string ;
118118} ;
119119
120- export type SessionTranscriptReadScope = Omit < SessionAccessScope , "sessionKey" > & {
120+ export type SessionTranscriptAccessScope = Omit < SessionAccessScope , "sessionKey" > & {
121121 /** Explicit transcript file path; bypasses store lookup when already known. */
122122 sessionFile ?: string ;
123123 /** Runtime session id used to derive a transcript file when no explicit file is provided. */
124124 sessionId : string ;
125- /** Optional key for read callers that can resolve via the session entry . */
125+ /** Required when resolving through session metadata; optional for explicit transcript artifacts . */
126126 sessionKey ?: string ;
127127 /** Channel thread suffix used when deriving topic transcript paths. */
128128 threadId ?: string | number ;
129129} ;
130130
131- export type SessionTranscriptAccessScope = SessionTranscriptReadScope & {
132- /**
133- * Identifies the owning entry when the transcript target must be resolved
134- * (and possibly persisted) through the session store. May be omitted only
135- * when an explicit sessionFile binds the operation to a concrete artifact;
136- * such writes never read or update entry metadata.
137- */
138- sessionKey ?: string ;
139- } ;
140-
141131export type SessionTranscriptRuntimeScope = SessionAccessScope & {
142132 /** Resolved file-backed artifact for the current runtime target. */
143133 sessionFile ?: string ;
144134 sessionId : string ;
145135 threadId ?: string | number ;
146136} ;
147137
138+ export type SessionTranscriptReadScope = Omit < SessionTranscriptRuntimeScope , "sessionKey" > & {
139+ /** Canonical key when the caller has a session-store identity for this read. */
140+ sessionKey ?: string ;
141+ /** Entry already loaded by hot callers; avoids rereading the session store. */
142+ sessionEntry ?: Pick < SessionEntry , "sessionFile" > & Partial < Pick < SessionEntry , "sessionId" > > ;
143+ } ;
144+
145+ export type SessionTranscriptReadTarget = Omit <
146+ SessionTranscriptRuntimeTarget ,
147+ "agentId" | "sessionKey"
148+ > & {
149+ agentId ?: string ;
150+ sessionKey ?: string ;
151+ } ;
152+
148153export type SessionTranscriptWriteScope = Omit < SessionTranscriptAccessScope , "sessionId" > & {
149154 /** Optional for appenders that can operate on an existing explicit transcript target. */
150155 sessionId ?: string ;
@@ -846,7 +851,7 @@ export async function persistSessionRolloverLifecycle(params: {
846851
847852/** Reads parsed transcript records from an explicit or derived transcript target. */
848853export async function loadTranscriptEvents (
849- scope : SessionTranscriptReadScope ,
854+ scope : SessionTranscriptAccessScope ,
850855) : Promise < TranscriptEvent [ ] > {
851856 const transcript = await resolveTranscriptReadAccess ( scope ) ;
852857 const events : TranscriptEvent [ ] = [ ] ;
@@ -1479,6 +1484,82 @@ export async function resolveSessionTranscriptRuntimeReadTarget(
14791484 } ;
14801485}
14811486
1487+ /**
1488+ * Resolves the current file-backed target for read-only transcript callers.
1489+ * Unlike writer/runtime resolution, this does not persist missing sessionFile
1490+ * metadata; reader projections must not mutate session metadata.
1491+ */
1492+ export function resolveSessionTranscriptReadTarget (
1493+ scope : SessionTranscriptReadScope ,
1494+ ) : SessionTranscriptReadTarget {
1495+ const explicitSessionFile = scope . sessionFile ?. trim ( ) ;
1496+ if ( explicitSessionFile ) {
1497+ return {
1498+ sessionFile : explicitSessionFile ,
1499+ sessionId : scope . sessionId ,
1500+ ...( scope . agentId ? { agentId : scope . agentId } : { } ) ,
1501+ ...( scope . sessionKey ? { sessionKey : scope . sessionKey } : { } ) ,
1502+ } ;
1503+ }
1504+ const agentId = scope . agentId ?? resolveAgentIdFromSessionKey ( scope . sessionKey ) ;
1505+ if ( ! agentId ) {
1506+ throw new Error ( `Cannot resolve transcript scope without an agent id: ${ scope . sessionKey } ` ) ;
1507+ }
1508+ const storePath = resolveConcreteReadStorePath ( scope . storePath ) ;
1509+ const resolvedStoreEntry =
1510+ scope . sessionEntry || ! scope . sessionKey
1511+ ? undefined
1512+ : storePath
1513+ ? resolveSessionStoreEntry ( {
1514+ store : loadSessionStore ( storePath , { skipCache : true } ) ,
1515+ sessionKey : scope . sessionKey ,
1516+ } )
1517+ : undefined ;
1518+ const sessionEntry =
1519+ scope . sessionEntry ??
1520+ resolvedStoreEntry ?. existing ??
1521+ ( scope . sessionKey ? loadSessionEntry ( { ...scope , sessionKey : scope . sessionKey } ) : undefined ) ;
1522+ const sessionKey = resolvedStoreEntry ?. normalizedKey ?? scope . sessionKey ;
1523+ const matchingSessionEntry =
1524+ sessionEntry ?. sessionId === undefined || sessionEntry . sessionId === scope . sessionId
1525+ ? sessionEntry
1526+ : undefined ;
1527+ const threadId =
1528+ scope . threadId ?? ( sessionKey ? parseSessionThreadInfo ( sessionKey ) . threadId : undefined ) ;
1529+ const sessionFile = matchingSessionEntry ?. sessionFile
1530+ ? resolveSessionFilePath (
1531+ scope . sessionId ,
1532+ matchingSessionEntry ,
1533+ resolveSessionFilePathOptions ( {
1534+ agentId,
1535+ ...( storePath ? { storePath } : { } ) ,
1536+ } ) ,
1537+ )
1538+ : storePath
1539+ ? resolveSessionTranscriptPathInDir (
1540+ // File-backed readers derive beside sessions.json only for the JSON-store
1541+ // deprecation window; the SQLite flip resolves from canonical metadata.
1542+ scope . sessionId ,
1543+ path . dirname ( path . resolve ( storePath ) ) ,
1544+ threadId ,
1545+ )
1546+ : resolveSessionTranscriptPath ( scope . sessionId , agentId , threadId ) ;
1547+ return {
1548+ agentId,
1549+ sessionFile,
1550+ sessionId : scope . sessionId ,
1551+ ...( sessionKey ? { sessionKey } : { } ) ,
1552+ } ;
1553+ }
1554+
1555+ function resolveConcreteReadStorePath ( storePath : string | undefined ) : string | undefined {
1556+ const trimmed = storePath ?. trim ( ) ;
1557+ if ( ! trimmed || trimmed === "(multiple)" || trimmed . includes ( "{agentId}" ) ) {
1558+ return undefined ;
1559+ }
1560+ return trimmed ;
1561+ }
1562+
14821563function createFallbackSessionEntry ( patch : Partial < SessionEntry > ) : SessionEntry {
14831564 const now = Date . now ( ) ;
14841565 return {
@@ -1555,7 +1636,7 @@ function resolveAccessStorePath(scope: SessionAccessScope): string {
15551636 } ) ;
15561637}
15571638
1558- async function resolveTranscriptReadAccess ( scope : SessionTranscriptReadScope ) : Promise < {
1639+ async function resolveTranscriptReadAccess ( scope : SessionTranscriptAccessScope ) : Promise < {
15591640 sessionFile : string ;
15601641} > {
15611642 if ( scope . sessionFile ?. trim ( ) ) {
0 commit comments