@handy_wote/pi-agent-core 0.81.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (102) hide show
  1. package/README.md +507 -0
  2. package/dist/agent-loop.d.ts +24 -0
  3. package/dist/agent-loop.d.ts.map +1 -0
  4. package/dist/agent-loop.js +549 -0
  5. package/dist/agent-loop.js.map +1 -0
  6. package/dist/agent.d.ts +120 -0
  7. package/dist/agent.d.ts.map +1 -0
  8. package/dist/agent.js +413 -0
  9. package/dist/agent.js.map +1 -0
  10. package/dist/harness/agent-harness.d.ts +92 -0
  11. package/dist/harness/agent-harness.d.ts.map +1 -0
  12. package/dist/harness/agent-harness.js +952 -0
  13. package/dist/harness/agent-harness.js.map +1 -0
  14. package/dist/harness/compaction/branch-summarization.d.ts +55 -0
  15. package/dist/harness/compaction/branch-summarization.d.ts.map +1 -0
  16. package/dist/harness/compaction/branch-summarization.js +173 -0
  17. package/dist/harness/compaction/branch-summarization.js.map +1 -0
  18. package/dist/harness/compaction/compaction.d.ts +107 -0
  19. package/dist/harness/compaction/compaction.d.ts.map +1 -0
  20. package/dist/harness/compaction/compaction.js +577 -0
  21. package/dist/harness/compaction/compaction.js.map +1 -0
  22. package/dist/harness/compaction/utils.d.ts +25 -0
  23. package/dist/harness/compaction/utils.d.ts.map +1 -0
  24. package/dist/harness/compaction/utils.js +120 -0
  25. package/dist/harness/compaction/utils.js.map +1 -0
  26. package/dist/harness/env/nodejs.d.ts +51 -0
  27. package/dist/harness/env/nodejs.d.ts.map +1 -0
  28. package/dist/harness/env/nodejs.js +516 -0
  29. package/dist/harness/env/nodejs.js.map +1 -0
  30. package/dist/harness/messages.d.ts +51 -0
  31. package/dist/harness/messages.d.ts.map +1 -0
  32. package/dist/harness/messages.js +102 -0
  33. package/dist/harness/messages.js.map +1 -0
  34. package/dist/harness/prompt-templates.d.ts +48 -0
  35. package/dist/harness/prompt-templates.d.ts.map +1 -0
  36. package/dist/harness/prompt-templates.js +230 -0
  37. package/dist/harness/prompt-templates.js.map +1 -0
  38. package/dist/harness/session/jsonl-repo.d.ts +26 -0
  39. package/dist/harness/session/jsonl-repo.d.ts.map +1 -0
  40. package/dist/harness/session/jsonl-repo.js +103 -0
  41. package/dist/harness/session/jsonl-repo.js.map +1 -0
  42. package/dist/harness/session/jsonl-storage.d.ts +42 -0
  43. package/dist/harness/session/jsonl-storage.d.ts.map +1 -0
  44. package/dist/harness/session/jsonl-storage.js +293 -0
  45. package/dist/harness/session/jsonl-storage.js.map +1 -0
  46. package/dist/harness/session/memory-repo.d.ts +18 -0
  47. package/dist/harness/session/memory-repo.d.ts.map +1 -0
  48. package/dist/harness/session/memory-repo.js +42 -0
  49. package/dist/harness/session/memory-repo.js.map +1 -0
  50. package/dist/harness/session/memory-storage.d.ts +33 -0
  51. package/dist/harness/session/memory-storage.d.ts.map +1 -0
  52. package/dist/harness/session/memory-storage.js +168 -0
  53. package/dist/harness/session/memory-storage.js.map +1 -0
  54. package/dist/harness/session/repo-utils.d.ts +11 -0
  55. package/dist/harness/session/repo-utils.d.ts.map +1 -0
  56. package/dist/harness/session/repo-utils.js +39 -0
  57. package/dist/harness/session/repo-utils.js.map +1 -0
  58. package/dist/harness/session/session.d.ts +49 -0
  59. package/dist/harness/session/session.d.ts.map +1 -0
  60. package/dist/harness/session/session.js +263 -0
  61. package/dist/harness/session/session.js.map +1 -0
  62. package/dist/harness/skills.d.ts +44 -0
  63. package/dist/harness/skills.d.ts.map +1 -0
  64. package/dist/harness/skills.js +311 -0
  65. package/dist/harness/skills.js.map +1 -0
  66. package/dist/harness/system-prompt.d.ts +3 -0
  67. package/dist/harness/system-prompt.d.ts.map +1 -0
  68. package/dist/harness/system-prompt.js +30 -0
  69. package/dist/harness/system-prompt.js.map +1 -0
  70. package/dist/harness/types.d.ts +665 -0
  71. package/dist/harness/types.d.ts.map +1 -0
  72. package/dist/harness/types.js +94 -0
  73. package/dist/harness/types.js.map +1 -0
  74. package/dist/harness/utils/shell-output.d.ts +14 -0
  75. package/dist/harness/utils/shell-output.d.ts.map +1 -0
  76. package/dist/harness/utils/shell-output.js +126 -0
  77. package/dist/harness/utils/shell-output.js.map +1 -0
  78. package/dist/harness/utils/truncate.d.ts +70 -0
  79. package/dist/harness/utils/truncate.d.ts.map +1 -0
  80. package/dist/harness/utils/truncate.js +290 -0
  81. package/dist/harness/utils/truncate.js.map +1 -0
  82. package/dist/index.d.ts +23 -0
  83. package/dist/index.d.ts.map +1 -0
  84. package/dist/index.js +29 -0
  85. package/dist/index.js.map +1 -0
  86. package/dist/node.d.ts +3 -0
  87. package/dist/node.d.ts.map +1 -0
  88. package/dist/node.js +3 -0
  89. package/dist/node.js.map +1 -0
  90. package/dist/proxy.d.ts +69 -0
  91. package/dist/proxy.d.ts.map +1 -0
  92. package/dist/proxy.js +278 -0
  93. package/dist/proxy.js.map +1 -0
  94. package/dist/stream-fn.d.ts +10 -0
  95. package/dist/stream-fn.d.ts.map +1 -0
  96. package/dist/stream-fn.js +17 -0
  97. package/dist/stream-fn.js.map +1 -0
  98. package/dist/types.d.ts +407 -0
  99. package/dist/types.d.ts.map +1 -0
  100. package/dist/types.js +2 -0
  101. package/dist/types.js.map +1 -0
  102. package/package.json +60 -0
@@ -0,0 +1 @@
1
+ {"version":3,"file":"jsonl-repo.js","sourceRoot":"","sources":["../../../src/harness/session/jsonl-repo.ts"],"names":[],"mappings":"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","sourcesContent":["import type {\n\tFileSystem,\n\tJsonlSessionCreateOptions,\n\tJsonlSessionListOptions,\n\tJsonlSessionMetadata,\n\tJsonlSessionRepoApi,\n\tSession,\n} from \"../types.ts\";\nimport { SessionError, toError } from \"../types.ts\";\nimport { JsonlSessionStorage, loadJsonlSessionMetadata } from \"./jsonl-storage.ts\";\nimport {\n\tcreateSessionId,\n\tcreateTimestamp,\n\tgetEntriesToFork,\n\tgetFileSystemResultOrThrow,\n\ttoSession,\n} from \"./repo-utils.ts\";\n\ntype JsonlSessionRepoFileSystem = Pick<\n\tFileSystem,\n\t| \"cwd\"\n\t| \"absolutePath\"\n\t| \"joinPath\"\n\t| \"readTextFile\"\n\t| \"readTextLines\"\n\t| \"writeFile\"\n\t| \"appendFile\"\n\t| \"listDir\"\n\t| \"exists\"\n\t| \"createDir\"\n\t| \"remove\"\n>;\n\nfunction encodeCwd(cwd: string): string {\n\treturn `--${cwd.replace(/^[/\\\\]/, \"\").replace(/[/\\\\:]/g, \"-\")}--`;\n}\n\nexport class JsonlSessionRepo implements JsonlSessionRepoApi {\n\tprivate readonly fs: JsonlSessionRepoFileSystem;\n\tprivate readonly sessionsRootInput: string;\n\tprivate sessionsRoot: string | undefined;\n\n\tconstructor(options: { fs: JsonlSessionRepoFileSystem; sessionsRoot: string }) {\n\t\tthis.fs = options.fs;\n\t\tthis.sessionsRootInput = options.sessionsRoot;\n\t}\n\n\tprivate async getSessionsRoot(): Promise<string> {\n\t\tif (!this.sessionsRoot) {\n\t\t\tthis.sessionsRoot = getFileSystemResultOrThrow(\n\t\t\t\tawait this.fs.absolutePath(this.sessionsRootInput),\n\t\t\t\t`Failed to resolve sessions root ${this.sessionsRootInput}`,\n\t\t\t);\n\t\t}\n\t\treturn this.sessionsRoot;\n\t}\n\n\tprivate async getSessionDir(cwd: string): Promise<string> {\n\t\treturn getFileSystemResultOrThrow(\n\t\t\tawait this.fs.joinPath([await this.getSessionsRoot(), encodeCwd(cwd)]),\n\t\t\t`Failed to resolve session directory for ${cwd}`,\n\t\t);\n\t}\n\n\tprivate async createSessionFilePath(cwd: string, sessionId: string, timestamp: string): Promise<string> {\n\t\treturn getFileSystemResultOrThrow(\n\t\t\tawait this.fs.joinPath([\n\t\t\t\tawait this.getSessionDir(cwd),\n\t\t\t\t`${timestamp.replace(/[:.]/g, \"-\")}_${sessionId}.jsonl`,\n\t\t\t]),\n\t\t\t`Failed to resolve session file path for ${sessionId}`,\n\t\t);\n\t}\n\n\tasync create(options: JsonlSessionCreateOptions): Promise<Session<JsonlSessionMetadata>> {\n\t\tconst id = options.id ?? createSessionId();\n\t\tconst createdAt = createTimestamp();\n\t\tconst sessionDir = await this.getSessionDir(options.cwd);\n\t\tgetFileSystemResultOrThrow(\n\t\t\tawait this.fs.createDir(sessionDir, { recursive: true }),\n\t\t\t`Failed to create session directory ${sessionDir}`,\n\t\t);\n\t\tconst filePath = await this.createSessionFilePath(options.cwd, id, createdAt);\n\t\tconst storage = await JsonlSessionStorage.create(this.fs, filePath, {\n\t\t\tcwd: options.cwd,\n\t\t\tsessionId: id,\n\t\t\tparentSessionPath: options.parentSessionPath,\n\t\t\tmetadata: options.metadata,\n\t\t});\n\t\treturn toSession(storage);\n\t}\n\n\tasync open(metadata: JsonlSessionMetadata): Promise<Session<JsonlSessionMetadata>> {\n\t\tif (\n\t\t\t!getFileSystemResultOrThrow(await this.fs.exists(metadata.path), `Failed to check session ${metadata.path}`)\n\t\t) {\n\t\t\tthrow new SessionError(\"not_found\", `Session not found: ${metadata.path}`);\n\t\t}\n\t\tconst storage = await JsonlSessionStorage.open(this.fs, metadata.path);\n\t\treturn toSession(storage);\n\t}\n\n\tasync list(options: JsonlSessionListOptions = {}): Promise<JsonlSessionMetadata[]> {\n\t\tconst dirs = options.cwd ? [await this.getSessionDir(options.cwd)] : await this.listSessionDirs();\n\t\tconst sessions: JsonlSessionMetadata[] = [];\n\t\tfor (const dir of dirs) {\n\t\t\tif (!getFileSystemResultOrThrow(await this.fs.exists(dir), `Failed to check session directory ${dir}`)) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tconst files = getFileSystemResultOrThrow(\n\t\t\t\tawait this.fs.listDir(dir),\n\t\t\t\t`Failed to list sessions in ${dir}`,\n\t\t\t).filter((file) => file.kind !== \"directory\" && file.name.endsWith(\".jsonl\"));\n\t\t\tfor (const file of files) {\n\t\t\t\ttry {\n\t\t\t\t\tsessions.push(await loadJsonlSessionMetadata(this.fs, file.path));\n\t\t\t\t} catch (error) {\n\t\t\t\t\tconst cause = toError(error);\n\t\t\t\t\tif (!(cause instanceof SessionError) || cause.code !== \"invalid_session\") throw cause;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tsessions.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());\n\t\treturn sessions;\n\t}\n\n\tasync delete(metadata: JsonlSessionMetadata): Promise<void> {\n\t\tgetFileSystemResultOrThrow(\n\t\t\tawait this.fs.remove(metadata.path, { force: true }),\n\t\t\t`Failed to delete session ${metadata.path}`,\n\t\t);\n\t}\n\n\tasync fork(\n\t\tsourceMetadata: JsonlSessionMetadata,\n\t\toptions: JsonlSessionCreateOptions & { entryId?: string; position?: \"before\" | \"at\"; id?: string },\n\t): Promise<Session<JsonlSessionMetadata>> {\n\t\tconst source = await this.open(sourceMetadata);\n\t\tconst forkedEntries = await getEntriesToFork(source.getStorage(), options);\n\t\tconst id = options.id ?? createSessionId();\n\t\tconst createdAt = createTimestamp();\n\t\tconst sessionDir = await this.getSessionDir(options.cwd);\n\t\tgetFileSystemResultOrThrow(\n\t\t\tawait this.fs.createDir(sessionDir, { recursive: true }),\n\t\t\t`Failed to create session directory ${sessionDir}`,\n\t\t);\n\t\tconst storage = await JsonlSessionStorage.create(\n\t\t\tthis.fs,\n\t\t\tawait this.createSessionFilePath(options.cwd, id, createdAt),\n\t\t\t{\n\t\t\t\tcwd: options.cwd,\n\t\t\t\tsessionId: id,\n\t\t\t\tparentSessionPath: options.parentSessionPath ?? sourceMetadata.path,\n\t\t\t\tmetadata: options.metadata ?? sourceMetadata.metadata,\n\t\t\t},\n\t\t);\n\t\tfor (const entry of forkedEntries) {\n\t\t\tawait storage.appendEntry(entry);\n\t\t}\n\t\treturn toSession(storage);\n\t}\n\n\tprivate async listSessionDirs(): Promise<string[]> {\n\t\tconst sessionsRoot = await this.getSessionsRoot();\n\t\tif (\n\t\t\t!getFileSystemResultOrThrow(\n\t\t\t\tawait this.fs.exists(sessionsRoot),\n\t\t\t\t`Failed to check sessions root ${sessionsRoot}`,\n\t\t\t)\n\t\t) {\n\t\t\treturn [];\n\t\t}\n\t\tconst entries = getFileSystemResultOrThrow(\n\t\t\tawait this.fs.listDir(sessionsRoot),\n\t\t\t`Failed to list sessions root ${sessionsRoot}`,\n\t\t);\n\t\treturn entries.filter((entry) => entry.kind === \"directory\").map((entry) => entry.path);\n\t}\n}\n"]}
@@ -0,0 +1,42 @@
1
+ import type { FileSystem, JsonlSessionMetadata, SessionEntryCursorOptions, SessionStorage, SessionTreeEntry } from "../types.ts";
2
+ type JsonlSessionStorageFileSystem = Pick<FileSystem, "readTextFile" | "readTextLines" | "writeFile" | "appendFile">;
3
+ export declare function loadJsonlSessionMetadata(fs: JsonlSessionStorageFileSystem, filePath: string): Promise<JsonlSessionMetadata>;
4
+ export declare class JsonlSessionStorage implements SessionStorage<JsonlSessionMetadata> {
5
+ private readonly fs;
6
+ private readonly filePath;
7
+ private readonly metadata;
8
+ private entries;
9
+ private byId;
10
+ private labelsById;
11
+ private currentLeafId;
12
+ private constructor();
13
+ static open(fs: JsonlSessionStorageFileSystem, filePath: string): Promise<JsonlSessionStorage>;
14
+ static create(fs: JsonlSessionStorageFileSystem, filePath: string, options: {
15
+ cwd: string;
16
+ sessionId: string;
17
+ parentSessionPath?: string;
18
+ metadata?: Record<string, unknown>;
19
+ }): Promise<JsonlSessionStorage>;
20
+ getMetadata(): Promise<JsonlSessionMetadata>;
21
+ getLeafId(): Promise<string | null>;
22
+ setLeafId(leafId: string | null): Promise<void>;
23
+ createEntryId(): Promise<string>;
24
+ appendEntry(entry: SessionTreeEntry): Promise<void>;
25
+ getEntry(id: string): Promise<SessionTreeEntry | undefined>;
26
+ findEntries<TType extends SessionTreeEntry["type"]>(type: TType): Promise<Array<Extract<SessionTreeEntry, {
27
+ type: TType;
28
+ }>>>;
29
+ getLabel(id: string): Promise<string | undefined>;
30
+ getSessionName(): Promise<string | undefined>;
31
+ getSessionStats(): Promise<{
32
+ messageCount: number;
33
+ cachedTokens: number;
34
+ uncachedTokens: number;
35
+ totalTokens: number;
36
+ costTotal: number;
37
+ }>;
38
+ getPathToRootOrCompaction(leafId: string | null): Promise<SessionTreeEntry[]>;
39
+ getEntries(options?: SessionEntryCursorOptions): Promise<SessionTreeEntry[]>;
40
+ }
41
+ export {};
42
+ //# sourceMappingURL=jsonl-storage.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"jsonl-storage.d.ts","sourceRoot":"","sources":["../../../src/harness/session/jsonl-storage.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACX,UAAU,EACV,oBAAoB,EAEpB,yBAAyB,EACzB,cAAc,EACd,gBAAgB,EAChB,MAAM,aAAa,CAAC;AAIrB,KAAK,6BAA6B,GAAG,IAAI,CAAC,UAAU,EAAE,cAAc,GAAG,eAAe,GAAG,WAAW,GAAG,YAAY,CAAC,CAAC;AAwIrH,wBAAsB,wBAAwB,CAC7C,EAAE,EAAE,6BAA6B,EACjC,QAAQ,EAAE,MAAM,GACd,OAAO,CAAC,oBAAoB,CAAC,CAQ/B;AA2BD,qBAAa,mBAAoB,YAAW,cAAc,CAAC,oBAAoB,CAAC;IAC/E,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAgC;IACnD,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;IAClC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAuB;IAChD,OAAO,CAAC,OAAO,CAAqB;IACpC,OAAO,CAAC,IAAI,CAAgC;IAC5C,OAAO,CAAC,UAAU,CAAsB;IACxC,OAAO,CAAC,aAAa,CAAgB;IAErC,OAAO,eAcN;IAED,OAAa,IAAI,CAAC,EAAE,EAAE,6BAA6B,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,mBAAmB,CAAC,CAGnG;IAED,OAAa,MAAM,CAClB,EAAE,EAAE,6BAA6B,EACjC,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE;QACR,GAAG,EAAE,MAAM,CAAC;QACZ,SAAS,EAAE,MAAM,CAAC;QAClB,iBAAiB,CAAC,EAAE,MAAM,CAAC;QAC3B,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;KACnC,GACC,OAAO,CAAC,mBAAmB,CAAC,CAe9B;IAEK,WAAW,IAAI,OAAO,CAAC,oBAAoB,CAAC,CAEjD;IAEK,SAAS,IAAI,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAKxC;IAEK,SAAS,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAkBpD;IAEK,aAAa,IAAI,OAAO,CAAC,MAAM,CAAC,CAErC;IAEK,WAAW,CAAC,KAAK,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC,CASxD;IAEK,QAAQ,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,GAAG,SAAS,CAAC,CAEhE;IAEK,WAAW,CAAC,KAAK,SAAS,gBAAgB,CAAC,MAAM,CAAC,EACvD,IAAI,EAAE,KAAK,GACT,OAAO,CAAC,KAAK,CAAC,OAAO,CAAC,gBAAgB,EAAE;QAAE,IAAI,EAAE,KAAK,CAAA;KAAE,CAAC,CAAC,CAAC,CAE5D;IAEK,QAAQ,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAEtD;IAEK,cAAc,IAAI,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAGlD;IAEK,eAAe;;;;;;OAwCpB;IAEK,yBAAyB,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO,CAAC,gBAAgB,EAAE,CAAC,CAmBlF;IAEK,UAAU,CAAC,OAAO,CAAC,EAAE,yBAAyB,GAAG,OAAO,CAAC,gBAAgB,EAAE,CAAC,CAIjF;CACD","sourcesContent":["import { uuidv7 } from \"@handy_wote/pi-ai\";\nimport type {\n\tFileSystem,\n\tJsonlSessionMetadata,\n\tLeafEntry,\n\tSessionEntryCursorOptions,\n\tSessionStorage,\n\tSessionTreeEntry,\n} from \"../types.ts\";\nimport { SessionError, toError } from \"../types.ts\";\nimport { getFileSystemResultOrThrow } from \"./repo-utils.ts\";\n\ntype JsonlSessionStorageFileSystem = Pick<FileSystem, \"readTextFile\" | \"readTextLines\" | \"writeFile\" | \"appendFile\">;\n\ninterface SessionHeader {\n\ttype: \"session\";\n\tversion: 3;\n\tid: string;\n\ttimestamp: string;\n\tcwd: string;\n\tparentSession?: string;\n\tmetadata?: Record<string, unknown>;\n}\n\nfunction updateLabelCache(labelsById: Map<string, string>, entry: SessionTreeEntry): void {\n\tif (entry.type !== \"label\") return;\n\tconst label = entry.label?.trim();\n\tif (label) {\n\t\tlabelsById.set(entry.targetId, label);\n\t} else {\n\t\tlabelsById.delete(entry.targetId);\n\t}\n}\n\nfunction buildLabelsById(entries: SessionTreeEntry[]): Map<string, string> {\n\tconst labelsById = new Map<string, string>();\n\tfor (const entry of entries) {\n\t\tupdateLabelCache(labelsById, entry);\n\t}\n\treturn labelsById;\n}\n\nfunction generateEntryId(byId: { has(id: string): boolean }): string {\n\tfor (let i = 0; i < 100; i++) {\n\t\t// The uuidv7 prefix is timestamp-derived and nearly constant between calls,\n\t\t// so short ids must come from the random tail.\n\t\tconst id = uuidv7().slice(-8);\n\t\tif (!byId.has(id)) return id;\n\t}\n\treturn uuidv7();\n}\n\nfunction invalidSession(filePath: string, message: string, cause?: Error): SessionError {\n\treturn new SessionError(\"invalid_session\", `Invalid JSONL session file ${filePath}: ${message}`, cause);\n}\n\nfunction invalidEntry(filePath: string, lineNumber: number, message: string, cause?: Error): SessionError {\n\treturn new SessionError(\n\t\t\"invalid_entry\",\n\t\t`Invalid JSONL session file ${filePath}: line ${lineNumber} ${message}`,\n\t\tcause,\n\t);\n}\n\nfunction parseHeaderLine(line: string, filePath: string): SessionHeader {\n\tlet parsed: unknown;\n\ttry {\n\t\tparsed = JSON.parse(line);\n\t} catch (error) {\n\t\tthrow invalidSession(filePath, \"first line is not a valid session header\", toError(error));\n\t}\n\tif (typeof parsed !== \"object\" || parsed === null) {\n\t\tthrow invalidSession(filePath, \"first line is not a valid session header\");\n\t}\n\tconst header = parsed as Partial<SessionHeader>;\n\tif (header.type !== \"session\") throw invalidSession(filePath, \"first line is not a valid session header\");\n\tif (header.version !== 3) throw invalidSession(filePath, \"unsupported session version\");\n\tif (typeof header.id !== \"string\" || !header.id) throw invalidSession(filePath, \"session header is missing id\");\n\tif (typeof header.timestamp !== \"string\" || !header.timestamp) {\n\t\tthrow invalidSession(filePath, \"session header is missing timestamp\");\n\t}\n\tif (typeof header.cwd !== \"string\" || !header.cwd) throw invalidSession(filePath, \"session header is missing cwd\");\n\tif (header.parentSession !== undefined && typeof header.parentSession !== \"string\") {\n\t\tthrow invalidSession(filePath, \"session header parentSession must be a string\");\n\t}\n\tif (\n\t\theader.metadata !== undefined &&\n\t\t(typeof header.metadata !== \"object\" || header.metadata === null || Array.isArray(header.metadata))\n\t) {\n\t\tthrow invalidSession(filePath, \"session header metadata must be an object\");\n\t}\n\treturn {\n\t\ttype: \"session\",\n\t\tversion: 3,\n\t\tid: header.id,\n\t\ttimestamp: header.timestamp,\n\t\tcwd: header.cwd,\n\t\tparentSession: header.parentSession,\n\t\tmetadata: header.metadata,\n\t};\n}\n\nfunction parseEntryLine(line: string, filePath: string, lineNumber: number): SessionTreeEntry {\n\tlet parsed: unknown;\n\ttry {\n\t\tparsed = JSON.parse(line);\n\t} catch (error) {\n\t\tthrow invalidEntry(filePath, lineNumber, \"is not valid JSON\", toError(error));\n\t}\n\tif (typeof parsed !== \"object\" || parsed === null) {\n\t\tthrow invalidEntry(filePath, lineNumber, \"is not a valid session entry\");\n\t}\n\tconst entry = parsed as {\n\t\ttype?: unknown;\n\t\tid?: unknown;\n\t\tparentId?: unknown;\n\t\ttimestamp?: unknown;\n\t\ttargetId?: unknown;\n\t};\n\tif (typeof entry.type !== \"string\") throw invalidEntry(filePath, lineNumber, \"is missing entry type\");\n\tif (typeof entry.id !== \"string\" || !entry.id) throw invalidEntry(filePath, lineNumber, \"is missing entry id\");\n\tif (entry.parentId !== null && typeof entry.parentId !== \"string\") {\n\t\tthrow invalidEntry(filePath, lineNumber, \"has invalid parentId\");\n\t}\n\tif (typeof entry.timestamp !== \"string\" || !entry.timestamp) {\n\t\tthrow invalidEntry(filePath, lineNumber, \"is missing timestamp\");\n\t}\n\tif (entry.type === \"leaf\" && entry.targetId !== null && typeof entry.targetId !== \"string\") {\n\t\tthrow invalidEntry(filePath, lineNumber, \"has invalid targetId\");\n\t}\n\treturn entry as SessionTreeEntry;\n}\n\nfunction leafIdAfterEntry(entry: SessionTreeEntry): string | null {\n\treturn entry.type === \"leaf\" ? entry.targetId : entry.id;\n}\n\nfunction headerToSessionMetadata(header: SessionHeader, path: string): JsonlSessionMetadata {\n\treturn {\n\t\tid: header.id,\n\t\tcreatedAt: header.timestamp,\n\t\tcwd: header.cwd,\n\t\tpath,\n\t\tparentSessionPath: header.parentSession,\n\t\tmetadata: header.metadata,\n\t};\n}\n\nexport async function loadJsonlSessionMetadata(\n\tfs: JsonlSessionStorageFileSystem,\n\tfilePath: string,\n): Promise<JsonlSessionMetadata> {\n\tconst lines = getFileSystemResultOrThrow(\n\t\tawait fs.readTextLines(filePath, { maxLines: 1 }),\n\t\t`Failed to read session header ${filePath}`,\n\t);\n\tconst line = lines[0];\n\tif (line?.trim()) return headerToSessionMetadata(parseHeaderLine(line, filePath), filePath);\n\tthrow invalidSession(filePath, \"missing session header\");\n}\n\nasync function loadJsonlStorage(\n\tfs: JsonlSessionStorageFileSystem,\n\tfilePath: string,\n): Promise<{\n\theader: SessionHeader;\n\tentries: SessionTreeEntry[];\n\tleafId: string | null;\n}> {\n\tconst content = getFileSystemResultOrThrow(await fs.readTextFile(filePath), `Failed to read session ${filePath}`);\n\tconst lines = content.split(\"\\n\").filter((line) => line.trim());\n\tif (lines.length === 0) {\n\t\tthrow invalidSession(filePath, \"missing session header\");\n\t}\n\n\tconst header = parseHeaderLine(lines[0]!, filePath);\n\tconst entries: SessionTreeEntry[] = [];\n\tlet leafId: string | null = null;\n\tfor (let i = 1; i < lines.length; i++) {\n\t\tconst entry = parseEntryLine(lines[i]!, filePath, i + 1);\n\t\tentries.push(entry);\n\t\tleafId = leafIdAfterEntry(entry);\n\t}\n\treturn { header, entries, leafId };\n}\n\nexport class JsonlSessionStorage implements SessionStorage<JsonlSessionMetadata> {\n\tprivate readonly fs: JsonlSessionStorageFileSystem;\n\tprivate readonly filePath: string;\n\tprivate readonly metadata: JsonlSessionMetadata;\n\tprivate entries: SessionTreeEntry[];\n\tprivate byId: Map<string, SessionTreeEntry>;\n\tprivate labelsById: Map<string, string>;\n\tprivate currentLeafId: string | null;\n\n\tprivate constructor(\n\t\tfs: JsonlSessionStorageFileSystem,\n\t\tfilePath: string,\n\t\theader: SessionHeader,\n\t\tentries: SessionTreeEntry[],\n\t\tleafId: string | null,\n\t) {\n\t\tthis.fs = fs;\n\t\tthis.filePath = filePath;\n\t\tthis.metadata = headerToSessionMetadata(header, this.filePath);\n\t\tthis.entries = entries;\n\t\tthis.byId = new Map(entries.map((entry) => [entry.id, entry]));\n\t\tthis.labelsById = buildLabelsById(entries);\n\t\tthis.currentLeafId = leafId;\n\t}\n\n\tstatic async open(fs: JsonlSessionStorageFileSystem, filePath: string): Promise<JsonlSessionStorage> {\n\t\tconst loaded = await loadJsonlStorage(fs, filePath);\n\t\treturn new JsonlSessionStorage(fs, filePath, loaded.header, loaded.entries, loaded.leafId);\n\t}\n\n\tstatic async create(\n\t\tfs: JsonlSessionStorageFileSystem,\n\t\tfilePath: string,\n\t\toptions: {\n\t\t\tcwd: string;\n\t\t\tsessionId: string;\n\t\t\tparentSessionPath?: string;\n\t\t\tmetadata?: Record<string, unknown>;\n\t\t},\n\t): Promise<JsonlSessionStorage> {\n\t\tconst header: SessionHeader = {\n\t\t\ttype: \"session\",\n\t\t\tversion: 3,\n\t\t\tid: options.sessionId,\n\t\t\ttimestamp: new Date().toISOString(),\n\t\t\tcwd: options.cwd,\n\t\t\tparentSession: options.parentSessionPath,\n\t\t\tmetadata: options.metadata,\n\t\t};\n\t\tgetFileSystemResultOrThrow(\n\t\t\tawait fs.writeFile(filePath, `${JSON.stringify(header)}\\n`),\n\t\t\t`Failed to create session ${filePath}`,\n\t\t);\n\t\treturn new JsonlSessionStorage(fs, filePath, header, [], null);\n\t}\n\n\tasync getMetadata(): Promise<JsonlSessionMetadata> {\n\t\treturn this.metadata;\n\t}\n\n\tasync getLeafId(): Promise<string | null> {\n\t\tif (this.currentLeafId !== null && !this.byId.has(this.currentLeafId)) {\n\t\t\tthrow new SessionError(\"invalid_session\", `Entry ${this.currentLeafId} not found`);\n\t\t}\n\t\treturn this.currentLeafId;\n\t}\n\n\tasync setLeafId(leafId: string | null): Promise<void> {\n\t\tif (leafId !== null && !this.byId.has(leafId)) {\n\t\t\tthrow new SessionError(\"not_found\", `Entry ${leafId} not found`);\n\t\t}\n\t\tconst entry: LeafEntry = {\n\t\t\ttype: \"leaf\",\n\t\t\tid: generateEntryId(this.byId),\n\t\t\tparentId: this.currentLeafId,\n\t\t\ttimestamp: new Date().toISOString(),\n\t\t\ttargetId: leafId,\n\t\t};\n\t\tgetFileSystemResultOrThrow(\n\t\t\tawait this.fs.appendFile(this.filePath, `${JSON.stringify(entry)}\\n`),\n\t\t\t`Failed to append session leaf ${entry.id}`,\n\t\t);\n\t\tthis.entries.push(entry);\n\t\tthis.byId.set(entry.id, entry);\n\t\tthis.currentLeafId = leafId;\n\t}\n\n\tasync createEntryId(): Promise<string> {\n\t\treturn generateEntryId(this.byId);\n\t}\n\n\tasync appendEntry(entry: SessionTreeEntry): Promise<void> {\n\t\tgetFileSystemResultOrThrow(\n\t\t\tawait this.fs.appendFile(this.filePath, `${JSON.stringify(entry)}\\n`),\n\t\t\t`Failed to append session entry ${entry.id}`,\n\t\t);\n\t\tthis.entries.push(entry);\n\t\tthis.byId.set(entry.id, entry);\n\t\tupdateLabelCache(this.labelsById, entry);\n\t\tthis.currentLeafId = leafIdAfterEntry(entry);\n\t}\n\n\tasync getEntry(id: string): Promise<SessionTreeEntry | undefined> {\n\t\treturn this.byId.get(id);\n\t}\n\n\tasync findEntries<TType extends SessionTreeEntry[\"type\"]>(\n\t\ttype: TType,\n\t): Promise<Array<Extract<SessionTreeEntry, { type: TType }>>> {\n\t\treturn this.entries.filter((entry): entry is Extract<SessionTreeEntry, { type: TType }> => entry.type === type);\n\t}\n\n\tasync getLabel(id: string): Promise<string | undefined> {\n\t\treturn this.labelsById.get(id);\n\t}\n\n\tasync getSessionName(): Promise<string | undefined> {\n\t\tconst entries = await this.findEntries(\"session_info\");\n\t\treturn entries[entries.length - 1]?.name?.trim() || undefined;\n\t}\n\n\tasync getSessionStats() {\n\t\tlet messageCount = 0;\n\t\tlet cachedTokens = 0;\n\t\tlet uncachedTokens = 0;\n\t\tlet totalTokens = 0;\n\t\tlet costTotal = 0;\n\t\tfor (const entry of this.entries) {\n\t\t\tif (entry.type === \"message\") {\n\t\t\t\tmessageCount += 1;\n\t\t\t}\n\t\t\tconst usage =\n\t\t\t\tentry.type === \"message\"\n\t\t\t\t\t? entry.message.role === \"assistant\"\n\t\t\t\t\t\t? entry.message.usage\n\t\t\t\t\t\t: undefined\n\t\t\t\t\t: entry.type === \"compaction\" || entry.type === \"branch_summary\"\n\t\t\t\t\t\t? entry.usage\n\t\t\t\t\t\t: undefined;\n\t\t\tif (\n\t\t\t\t!usage ||\n\t\t\t\ttypeof usage.input !== \"number\" ||\n\t\t\t\ttypeof usage.output !== \"number\" ||\n\t\t\t\ttypeof usage.cacheRead !== \"number\" ||\n\t\t\t\ttypeof usage.cacheWrite !== \"number\" ||\n\t\t\t\ttypeof usage.cost?.total !== \"number\"\n\t\t\t) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tcachedTokens += usage.cacheRead;\n\t\t\tuncachedTokens += usage.input + usage.cacheWrite;\n\t\t\ttotalTokens += usage.input + usage.output + usage.cacheRead + usage.cacheWrite;\n\t\t\tcostTotal += usage.cost.total;\n\t\t}\n\t\treturn {\n\t\t\tmessageCount,\n\t\t\tcachedTokens,\n\t\t\tuncachedTokens,\n\t\t\ttotalTokens,\n\t\t\tcostTotal,\n\t\t};\n\t}\n\n\tasync getPathToRootOrCompaction(leafId: string | null): Promise<SessionTreeEntry[]> {\n\t\tif (leafId === null) return [];\n\t\tconst path: SessionTreeEntry[] = [];\n\t\tlet stopAtEntryId: string | null = null;\n\t\tlet current = this.byId.get(leafId);\n\t\tif (!current) throw new SessionError(\"not_found\", `Entry ${leafId} not found`);\n\t\twhile (current) {\n\t\t\tpath.unshift(current);\n\t\t\tif (stopAtEntryId !== null && current.id === stopAtEntryId) break;\n\t\t\tif (current.type === \"compaction\") {\n\t\t\t\tif (current.retainedTail) break;\n\t\t\t\tstopAtEntryId = current.firstKeptEntryId ?? null;\n\t\t\t}\n\t\t\tif (!current.parentId) break;\n\t\t\tconst parent = this.byId.get(current.parentId);\n\t\t\tif (!parent) throw new SessionError(\"invalid_session\", `Entry ${current.parentId} not found`);\n\t\t\tcurrent = parent;\n\t\t}\n\t\treturn path;\n\t}\n\n\tasync getEntries(options?: SessionEntryCursorOptions): Promise<SessionTreeEntry[]> {\n\t\tconst start = options?.afterEntrySeq ?? 0;\n\t\tconst end = options?.limit === undefined ? undefined : start + options.limit;\n\t\treturn this.entries.slice(start, end);\n\t}\n}\n"]}
@@ -0,0 +1,293 @@
1
+ import { uuidv7 } from "@handy_wote/pi-ai";
2
+ import { SessionError, toError } from "../types.js";
3
+ import { getFileSystemResultOrThrow } from "./repo-utils.js";
4
+ function updateLabelCache(labelsById, entry) {
5
+ if (entry.type !== "label")
6
+ return;
7
+ const label = entry.label?.trim();
8
+ if (label) {
9
+ labelsById.set(entry.targetId, label);
10
+ }
11
+ else {
12
+ labelsById.delete(entry.targetId);
13
+ }
14
+ }
15
+ function buildLabelsById(entries) {
16
+ const labelsById = new Map();
17
+ for (const entry of entries) {
18
+ updateLabelCache(labelsById, entry);
19
+ }
20
+ return labelsById;
21
+ }
22
+ function generateEntryId(byId) {
23
+ for (let i = 0; i < 100; i++) {
24
+ // The uuidv7 prefix is timestamp-derived and nearly constant between calls,
25
+ // so short ids must come from the random tail.
26
+ const id = uuidv7().slice(-8);
27
+ if (!byId.has(id))
28
+ return id;
29
+ }
30
+ return uuidv7();
31
+ }
32
+ function invalidSession(filePath, message, cause) {
33
+ return new SessionError("invalid_session", `Invalid JSONL session file ${filePath}: ${message}`, cause);
34
+ }
35
+ function invalidEntry(filePath, lineNumber, message, cause) {
36
+ return new SessionError("invalid_entry", `Invalid JSONL session file ${filePath}: line ${lineNumber} ${message}`, cause);
37
+ }
38
+ function parseHeaderLine(line, filePath) {
39
+ let parsed;
40
+ try {
41
+ parsed = JSON.parse(line);
42
+ }
43
+ catch (error) {
44
+ throw invalidSession(filePath, "first line is not a valid session header", toError(error));
45
+ }
46
+ if (typeof parsed !== "object" || parsed === null) {
47
+ throw invalidSession(filePath, "first line is not a valid session header");
48
+ }
49
+ const header = parsed;
50
+ if (header.type !== "session")
51
+ throw invalidSession(filePath, "first line is not a valid session header");
52
+ if (header.version !== 3)
53
+ throw invalidSession(filePath, "unsupported session version");
54
+ if (typeof header.id !== "string" || !header.id)
55
+ throw invalidSession(filePath, "session header is missing id");
56
+ if (typeof header.timestamp !== "string" || !header.timestamp) {
57
+ throw invalidSession(filePath, "session header is missing timestamp");
58
+ }
59
+ if (typeof header.cwd !== "string" || !header.cwd)
60
+ throw invalidSession(filePath, "session header is missing cwd");
61
+ if (header.parentSession !== undefined && typeof header.parentSession !== "string") {
62
+ throw invalidSession(filePath, "session header parentSession must be a string");
63
+ }
64
+ if (header.metadata !== undefined &&
65
+ (typeof header.metadata !== "object" || header.metadata === null || Array.isArray(header.metadata))) {
66
+ throw invalidSession(filePath, "session header metadata must be an object");
67
+ }
68
+ return {
69
+ type: "session",
70
+ version: 3,
71
+ id: header.id,
72
+ timestamp: header.timestamp,
73
+ cwd: header.cwd,
74
+ parentSession: header.parentSession,
75
+ metadata: header.metadata,
76
+ };
77
+ }
78
+ function parseEntryLine(line, filePath, lineNumber) {
79
+ let parsed;
80
+ try {
81
+ parsed = JSON.parse(line);
82
+ }
83
+ catch (error) {
84
+ throw invalidEntry(filePath, lineNumber, "is not valid JSON", toError(error));
85
+ }
86
+ if (typeof parsed !== "object" || parsed === null) {
87
+ throw invalidEntry(filePath, lineNumber, "is not a valid session entry");
88
+ }
89
+ const entry = parsed;
90
+ if (typeof entry.type !== "string")
91
+ throw invalidEntry(filePath, lineNumber, "is missing entry type");
92
+ if (typeof entry.id !== "string" || !entry.id)
93
+ throw invalidEntry(filePath, lineNumber, "is missing entry id");
94
+ if (entry.parentId !== null && typeof entry.parentId !== "string") {
95
+ throw invalidEntry(filePath, lineNumber, "has invalid parentId");
96
+ }
97
+ if (typeof entry.timestamp !== "string" || !entry.timestamp) {
98
+ throw invalidEntry(filePath, lineNumber, "is missing timestamp");
99
+ }
100
+ if (entry.type === "leaf" && entry.targetId !== null && typeof entry.targetId !== "string") {
101
+ throw invalidEntry(filePath, lineNumber, "has invalid targetId");
102
+ }
103
+ return entry;
104
+ }
105
+ function leafIdAfterEntry(entry) {
106
+ return entry.type === "leaf" ? entry.targetId : entry.id;
107
+ }
108
+ function headerToSessionMetadata(header, path) {
109
+ return {
110
+ id: header.id,
111
+ createdAt: header.timestamp,
112
+ cwd: header.cwd,
113
+ path,
114
+ parentSessionPath: header.parentSession,
115
+ metadata: header.metadata,
116
+ };
117
+ }
118
+ export async function loadJsonlSessionMetadata(fs, filePath) {
119
+ const lines = getFileSystemResultOrThrow(await fs.readTextLines(filePath, { maxLines: 1 }), `Failed to read session header ${filePath}`);
120
+ const line = lines[0];
121
+ if (line?.trim())
122
+ return headerToSessionMetadata(parseHeaderLine(line, filePath), filePath);
123
+ throw invalidSession(filePath, "missing session header");
124
+ }
125
+ async function loadJsonlStorage(fs, filePath) {
126
+ const content = getFileSystemResultOrThrow(await fs.readTextFile(filePath), `Failed to read session ${filePath}`);
127
+ const lines = content.split("\n").filter((line) => line.trim());
128
+ if (lines.length === 0) {
129
+ throw invalidSession(filePath, "missing session header");
130
+ }
131
+ const header = parseHeaderLine(lines[0], filePath);
132
+ const entries = [];
133
+ let leafId = null;
134
+ for (let i = 1; i < lines.length; i++) {
135
+ const entry = parseEntryLine(lines[i], filePath, i + 1);
136
+ entries.push(entry);
137
+ leafId = leafIdAfterEntry(entry);
138
+ }
139
+ return { header, entries, leafId };
140
+ }
141
+ export class JsonlSessionStorage {
142
+ fs;
143
+ filePath;
144
+ metadata;
145
+ entries;
146
+ byId;
147
+ labelsById;
148
+ currentLeafId;
149
+ constructor(fs, filePath, header, entries, leafId) {
150
+ this.fs = fs;
151
+ this.filePath = filePath;
152
+ this.metadata = headerToSessionMetadata(header, this.filePath);
153
+ this.entries = entries;
154
+ this.byId = new Map(entries.map((entry) => [entry.id, entry]));
155
+ this.labelsById = buildLabelsById(entries);
156
+ this.currentLeafId = leafId;
157
+ }
158
+ static async open(fs, filePath) {
159
+ const loaded = await loadJsonlStorage(fs, filePath);
160
+ return new JsonlSessionStorage(fs, filePath, loaded.header, loaded.entries, loaded.leafId);
161
+ }
162
+ static async create(fs, filePath, options) {
163
+ const header = {
164
+ type: "session",
165
+ version: 3,
166
+ id: options.sessionId,
167
+ timestamp: new Date().toISOString(),
168
+ cwd: options.cwd,
169
+ parentSession: options.parentSessionPath,
170
+ metadata: options.metadata,
171
+ };
172
+ getFileSystemResultOrThrow(await fs.writeFile(filePath, `${JSON.stringify(header)}\n`), `Failed to create session ${filePath}`);
173
+ return new JsonlSessionStorage(fs, filePath, header, [], null);
174
+ }
175
+ async getMetadata() {
176
+ return this.metadata;
177
+ }
178
+ async getLeafId() {
179
+ if (this.currentLeafId !== null && !this.byId.has(this.currentLeafId)) {
180
+ throw new SessionError("invalid_session", `Entry ${this.currentLeafId} not found`);
181
+ }
182
+ return this.currentLeafId;
183
+ }
184
+ async setLeafId(leafId) {
185
+ if (leafId !== null && !this.byId.has(leafId)) {
186
+ throw new SessionError("not_found", `Entry ${leafId} not found`);
187
+ }
188
+ const entry = {
189
+ type: "leaf",
190
+ id: generateEntryId(this.byId),
191
+ parentId: this.currentLeafId,
192
+ timestamp: new Date().toISOString(),
193
+ targetId: leafId,
194
+ };
195
+ getFileSystemResultOrThrow(await this.fs.appendFile(this.filePath, `${JSON.stringify(entry)}\n`), `Failed to append session leaf ${entry.id}`);
196
+ this.entries.push(entry);
197
+ this.byId.set(entry.id, entry);
198
+ this.currentLeafId = leafId;
199
+ }
200
+ async createEntryId() {
201
+ return generateEntryId(this.byId);
202
+ }
203
+ async appendEntry(entry) {
204
+ getFileSystemResultOrThrow(await this.fs.appendFile(this.filePath, `${JSON.stringify(entry)}\n`), `Failed to append session entry ${entry.id}`);
205
+ this.entries.push(entry);
206
+ this.byId.set(entry.id, entry);
207
+ updateLabelCache(this.labelsById, entry);
208
+ this.currentLeafId = leafIdAfterEntry(entry);
209
+ }
210
+ async getEntry(id) {
211
+ return this.byId.get(id);
212
+ }
213
+ async findEntries(type) {
214
+ return this.entries.filter((entry) => entry.type === type);
215
+ }
216
+ async getLabel(id) {
217
+ return this.labelsById.get(id);
218
+ }
219
+ async getSessionName() {
220
+ const entries = await this.findEntries("session_info");
221
+ return entries[entries.length - 1]?.name?.trim() || undefined;
222
+ }
223
+ async getSessionStats() {
224
+ let messageCount = 0;
225
+ let cachedTokens = 0;
226
+ let uncachedTokens = 0;
227
+ let totalTokens = 0;
228
+ let costTotal = 0;
229
+ for (const entry of this.entries) {
230
+ if (entry.type === "message") {
231
+ messageCount += 1;
232
+ }
233
+ const usage = entry.type === "message"
234
+ ? entry.message.role === "assistant"
235
+ ? entry.message.usage
236
+ : undefined
237
+ : entry.type === "compaction" || entry.type === "branch_summary"
238
+ ? entry.usage
239
+ : undefined;
240
+ if (!usage ||
241
+ typeof usage.input !== "number" ||
242
+ typeof usage.output !== "number" ||
243
+ typeof usage.cacheRead !== "number" ||
244
+ typeof usage.cacheWrite !== "number" ||
245
+ typeof usage.cost?.total !== "number") {
246
+ continue;
247
+ }
248
+ cachedTokens += usage.cacheRead;
249
+ uncachedTokens += usage.input + usage.cacheWrite;
250
+ totalTokens += usage.input + usage.output + usage.cacheRead + usage.cacheWrite;
251
+ costTotal += usage.cost.total;
252
+ }
253
+ return {
254
+ messageCount,
255
+ cachedTokens,
256
+ uncachedTokens,
257
+ totalTokens,
258
+ costTotal,
259
+ };
260
+ }
261
+ async getPathToRootOrCompaction(leafId) {
262
+ if (leafId === null)
263
+ return [];
264
+ const path = [];
265
+ let stopAtEntryId = null;
266
+ let current = this.byId.get(leafId);
267
+ if (!current)
268
+ throw new SessionError("not_found", `Entry ${leafId} not found`);
269
+ while (current) {
270
+ path.unshift(current);
271
+ if (stopAtEntryId !== null && current.id === stopAtEntryId)
272
+ break;
273
+ if (current.type === "compaction") {
274
+ if (current.retainedTail)
275
+ break;
276
+ stopAtEntryId = current.firstKeptEntryId ?? null;
277
+ }
278
+ if (!current.parentId)
279
+ break;
280
+ const parent = this.byId.get(current.parentId);
281
+ if (!parent)
282
+ throw new SessionError("invalid_session", `Entry ${current.parentId} not found`);
283
+ current = parent;
284
+ }
285
+ return path;
286
+ }
287
+ async getEntries(options) {
288
+ const start = options?.afterEntrySeq ?? 0;
289
+ const end = options?.limit === undefined ? undefined : start + options.limit;
290
+ return this.entries.slice(start, end);
291
+ }
292
+ }
293
+ //# sourceMappingURL=jsonl-storage.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"jsonl-storage.js","sourceRoot":"","sources":["../../../src/harness/session/jsonl-storage.ts"],"names":[],"mappings":"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","sourcesContent":["import { uuidv7 } from \"@handy_wote/pi-ai\";\nimport type {\n\tFileSystem,\n\tJsonlSessionMetadata,\n\tLeafEntry,\n\tSessionEntryCursorOptions,\n\tSessionStorage,\n\tSessionTreeEntry,\n} from \"../types.ts\";\nimport { SessionError, toError } from \"../types.ts\";\nimport { getFileSystemResultOrThrow } from \"./repo-utils.ts\";\n\ntype JsonlSessionStorageFileSystem = Pick<FileSystem, \"readTextFile\" | \"readTextLines\" | \"writeFile\" | \"appendFile\">;\n\ninterface SessionHeader {\n\ttype: \"session\";\n\tversion: 3;\n\tid: string;\n\ttimestamp: string;\n\tcwd: string;\n\tparentSession?: string;\n\tmetadata?: Record<string, unknown>;\n}\n\nfunction updateLabelCache(labelsById: Map<string, string>, entry: SessionTreeEntry): void {\n\tif (entry.type !== \"label\") return;\n\tconst label = entry.label?.trim();\n\tif (label) {\n\t\tlabelsById.set(entry.targetId, label);\n\t} else {\n\t\tlabelsById.delete(entry.targetId);\n\t}\n}\n\nfunction buildLabelsById(entries: SessionTreeEntry[]): Map<string, string> {\n\tconst labelsById = new Map<string, string>();\n\tfor (const entry of entries) {\n\t\tupdateLabelCache(labelsById, entry);\n\t}\n\treturn labelsById;\n}\n\nfunction generateEntryId(byId: { has(id: string): boolean }): string {\n\tfor (let i = 0; i < 100; i++) {\n\t\t// The uuidv7 prefix is timestamp-derived and nearly constant between calls,\n\t\t// so short ids must come from the random tail.\n\t\tconst id = uuidv7().slice(-8);\n\t\tif (!byId.has(id)) return id;\n\t}\n\treturn uuidv7();\n}\n\nfunction invalidSession(filePath: string, message: string, cause?: Error): SessionError {\n\treturn new SessionError(\"invalid_session\", `Invalid JSONL session file ${filePath}: ${message}`, cause);\n}\n\nfunction invalidEntry(filePath: string, lineNumber: number, message: string, cause?: Error): SessionError {\n\treturn new SessionError(\n\t\t\"invalid_entry\",\n\t\t`Invalid JSONL session file ${filePath}: line ${lineNumber} ${message}`,\n\t\tcause,\n\t);\n}\n\nfunction parseHeaderLine(line: string, filePath: string): SessionHeader {\n\tlet parsed: unknown;\n\ttry {\n\t\tparsed = JSON.parse(line);\n\t} catch (error) {\n\t\tthrow invalidSession(filePath, \"first line is not a valid session header\", toError(error));\n\t}\n\tif (typeof parsed !== \"object\" || parsed === null) {\n\t\tthrow invalidSession(filePath, \"first line is not a valid session header\");\n\t}\n\tconst header = parsed as Partial<SessionHeader>;\n\tif (header.type !== \"session\") throw invalidSession(filePath, \"first line is not a valid session header\");\n\tif (header.version !== 3) throw invalidSession(filePath, \"unsupported session version\");\n\tif (typeof header.id !== \"string\" || !header.id) throw invalidSession(filePath, \"session header is missing id\");\n\tif (typeof header.timestamp !== \"string\" || !header.timestamp) {\n\t\tthrow invalidSession(filePath, \"session header is missing timestamp\");\n\t}\n\tif (typeof header.cwd !== \"string\" || !header.cwd) throw invalidSession(filePath, \"session header is missing cwd\");\n\tif (header.parentSession !== undefined && typeof header.parentSession !== \"string\") {\n\t\tthrow invalidSession(filePath, \"session header parentSession must be a string\");\n\t}\n\tif (\n\t\theader.metadata !== undefined &&\n\t\t(typeof header.metadata !== \"object\" || header.metadata === null || Array.isArray(header.metadata))\n\t) {\n\t\tthrow invalidSession(filePath, \"session header metadata must be an object\");\n\t}\n\treturn {\n\t\ttype: \"session\",\n\t\tversion: 3,\n\t\tid: header.id,\n\t\ttimestamp: header.timestamp,\n\t\tcwd: header.cwd,\n\t\tparentSession: header.parentSession,\n\t\tmetadata: header.metadata,\n\t};\n}\n\nfunction parseEntryLine(line: string, filePath: string, lineNumber: number): SessionTreeEntry {\n\tlet parsed: unknown;\n\ttry {\n\t\tparsed = JSON.parse(line);\n\t} catch (error) {\n\t\tthrow invalidEntry(filePath, lineNumber, \"is not valid JSON\", toError(error));\n\t}\n\tif (typeof parsed !== \"object\" || parsed === null) {\n\t\tthrow invalidEntry(filePath, lineNumber, \"is not a valid session entry\");\n\t}\n\tconst entry = parsed as {\n\t\ttype?: unknown;\n\t\tid?: unknown;\n\t\tparentId?: unknown;\n\t\ttimestamp?: unknown;\n\t\ttargetId?: unknown;\n\t};\n\tif (typeof entry.type !== \"string\") throw invalidEntry(filePath, lineNumber, \"is missing entry type\");\n\tif (typeof entry.id !== \"string\" || !entry.id) throw invalidEntry(filePath, lineNumber, \"is missing entry id\");\n\tif (entry.parentId !== null && typeof entry.parentId !== \"string\") {\n\t\tthrow invalidEntry(filePath, lineNumber, \"has invalid parentId\");\n\t}\n\tif (typeof entry.timestamp !== \"string\" || !entry.timestamp) {\n\t\tthrow invalidEntry(filePath, lineNumber, \"is missing timestamp\");\n\t}\n\tif (entry.type === \"leaf\" && entry.targetId !== null && typeof entry.targetId !== \"string\") {\n\t\tthrow invalidEntry(filePath, lineNumber, \"has invalid targetId\");\n\t}\n\treturn entry as SessionTreeEntry;\n}\n\nfunction leafIdAfterEntry(entry: SessionTreeEntry): string | null {\n\treturn entry.type === \"leaf\" ? entry.targetId : entry.id;\n}\n\nfunction headerToSessionMetadata(header: SessionHeader, path: string): JsonlSessionMetadata {\n\treturn {\n\t\tid: header.id,\n\t\tcreatedAt: header.timestamp,\n\t\tcwd: header.cwd,\n\t\tpath,\n\t\tparentSessionPath: header.parentSession,\n\t\tmetadata: header.metadata,\n\t};\n}\n\nexport async function loadJsonlSessionMetadata(\n\tfs: JsonlSessionStorageFileSystem,\n\tfilePath: string,\n): Promise<JsonlSessionMetadata> {\n\tconst lines = getFileSystemResultOrThrow(\n\t\tawait fs.readTextLines(filePath, { maxLines: 1 }),\n\t\t`Failed to read session header ${filePath}`,\n\t);\n\tconst line = lines[0];\n\tif (line?.trim()) return headerToSessionMetadata(parseHeaderLine(line, filePath), filePath);\n\tthrow invalidSession(filePath, \"missing session header\");\n}\n\nasync function loadJsonlStorage(\n\tfs: JsonlSessionStorageFileSystem,\n\tfilePath: string,\n): Promise<{\n\theader: SessionHeader;\n\tentries: SessionTreeEntry[];\n\tleafId: string | null;\n}> {\n\tconst content = getFileSystemResultOrThrow(await fs.readTextFile(filePath), `Failed to read session ${filePath}`);\n\tconst lines = content.split(\"\\n\").filter((line) => line.trim());\n\tif (lines.length === 0) {\n\t\tthrow invalidSession(filePath, \"missing session header\");\n\t}\n\n\tconst header = parseHeaderLine(lines[0]!, filePath);\n\tconst entries: SessionTreeEntry[] = [];\n\tlet leafId: string | null = null;\n\tfor (let i = 1; i < lines.length; i++) {\n\t\tconst entry = parseEntryLine(lines[i]!, filePath, i + 1);\n\t\tentries.push(entry);\n\t\tleafId = leafIdAfterEntry(entry);\n\t}\n\treturn { header, entries, leafId };\n}\n\nexport class JsonlSessionStorage implements SessionStorage<JsonlSessionMetadata> {\n\tprivate readonly fs: JsonlSessionStorageFileSystem;\n\tprivate readonly filePath: string;\n\tprivate readonly metadata: JsonlSessionMetadata;\n\tprivate entries: SessionTreeEntry[];\n\tprivate byId: Map<string, SessionTreeEntry>;\n\tprivate labelsById: Map<string, string>;\n\tprivate currentLeafId: string | null;\n\n\tprivate constructor(\n\t\tfs: JsonlSessionStorageFileSystem,\n\t\tfilePath: string,\n\t\theader: SessionHeader,\n\t\tentries: SessionTreeEntry[],\n\t\tleafId: string | null,\n\t) {\n\t\tthis.fs = fs;\n\t\tthis.filePath = filePath;\n\t\tthis.metadata = headerToSessionMetadata(header, this.filePath);\n\t\tthis.entries = entries;\n\t\tthis.byId = new Map(entries.map((entry) => [entry.id, entry]));\n\t\tthis.labelsById = buildLabelsById(entries);\n\t\tthis.currentLeafId = leafId;\n\t}\n\n\tstatic async open(fs: JsonlSessionStorageFileSystem, filePath: string): Promise<JsonlSessionStorage> {\n\t\tconst loaded = await loadJsonlStorage(fs, filePath);\n\t\treturn new JsonlSessionStorage(fs, filePath, loaded.header, loaded.entries, loaded.leafId);\n\t}\n\n\tstatic async create(\n\t\tfs: JsonlSessionStorageFileSystem,\n\t\tfilePath: string,\n\t\toptions: {\n\t\t\tcwd: string;\n\t\t\tsessionId: string;\n\t\t\tparentSessionPath?: string;\n\t\t\tmetadata?: Record<string, unknown>;\n\t\t},\n\t): Promise<JsonlSessionStorage> {\n\t\tconst header: SessionHeader = {\n\t\t\ttype: \"session\",\n\t\t\tversion: 3,\n\t\t\tid: options.sessionId,\n\t\t\ttimestamp: new Date().toISOString(),\n\t\t\tcwd: options.cwd,\n\t\t\tparentSession: options.parentSessionPath,\n\t\t\tmetadata: options.metadata,\n\t\t};\n\t\tgetFileSystemResultOrThrow(\n\t\t\tawait fs.writeFile(filePath, `${JSON.stringify(header)}\\n`),\n\t\t\t`Failed to create session ${filePath}`,\n\t\t);\n\t\treturn new JsonlSessionStorage(fs, filePath, header, [], null);\n\t}\n\n\tasync getMetadata(): Promise<JsonlSessionMetadata> {\n\t\treturn this.metadata;\n\t}\n\n\tasync getLeafId(): Promise<string | null> {\n\t\tif (this.currentLeafId !== null && !this.byId.has(this.currentLeafId)) {\n\t\t\tthrow new SessionError(\"invalid_session\", `Entry ${this.currentLeafId} not found`);\n\t\t}\n\t\treturn this.currentLeafId;\n\t}\n\n\tasync setLeafId(leafId: string | null): Promise<void> {\n\t\tif (leafId !== null && !this.byId.has(leafId)) {\n\t\t\tthrow new SessionError(\"not_found\", `Entry ${leafId} not found`);\n\t\t}\n\t\tconst entry: LeafEntry = {\n\t\t\ttype: \"leaf\",\n\t\t\tid: generateEntryId(this.byId),\n\t\t\tparentId: this.currentLeafId,\n\t\t\ttimestamp: new Date().toISOString(),\n\t\t\ttargetId: leafId,\n\t\t};\n\t\tgetFileSystemResultOrThrow(\n\t\t\tawait this.fs.appendFile(this.filePath, `${JSON.stringify(entry)}\\n`),\n\t\t\t`Failed to append session leaf ${entry.id}`,\n\t\t);\n\t\tthis.entries.push(entry);\n\t\tthis.byId.set(entry.id, entry);\n\t\tthis.currentLeafId = leafId;\n\t}\n\n\tasync createEntryId(): Promise<string> {\n\t\treturn generateEntryId(this.byId);\n\t}\n\n\tasync appendEntry(entry: SessionTreeEntry): Promise<void> {\n\t\tgetFileSystemResultOrThrow(\n\t\t\tawait this.fs.appendFile(this.filePath, `${JSON.stringify(entry)}\\n`),\n\t\t\t`Failed to append session entry ${entry.id}`,\n\t\t);\n\t\tthis.entries.push(entry);\n\t\tthis.byId.set(entry.id, entry);\n\t\tupdateLabelCache(this.labelsById, entry);\n\t\tthis.currentLeafId = leafIdAfterEntry(entry);\n\t}\n\n\tasync getEntry(id: string): Promise<SessionTreeEntry | undefined> {\n\t\treturn this.byId.get(id);\n\t}\n\n\tasync findEntries<TType extends SessionTreeEntry[\"type\"]>(\n\t\ttype: TType,\n\t): Promise<Array<Extract<SessionTreeEntry, { type: TType }>>> {\n\t\treturn this.entries.filter((entry): entry is Extract<SessionTreeEntry, { type: TType }> => entry.type === type);\n\t}\n\n\tasync getLabel(id: string): Promise<string | undefined> {\n\t\treturn this.labelsById.get(id);\n\t}\n\n\tasync getSessionName(): Promise<string | undefined> {\n\t\tconst entries = await this.findEntries(\"session_info\");\n\t\treturn entries[entries.length - 1]?.name?.trim() || undefined;\n\t}\n\n\tasync getSessionStats() {\n\t\tlet messageCount = 0;\n\t\tlet cachedTokens = 0;\n\t\tlet uncachedTokens = 0;\n\t\tlet totalTokens = 0;\n\t\tlet costTotal = 0;\n\t\tfor (const entry of this.entries) {\n\t\t\tif (entry.type === \"message\") {\n\t\t\t\tmessageCount += 1;\n\t\t\t}\n\t\t\tconst usage =\n\t\t\t\tentry.type === \"message\"\n\t\t\t\t\t? entry.message.role === \"assistant\"\n\t\t\t\t\t\t? entry.message.usage\n\t\t\t\t\t\t: undefined\n\t\t\t\t\t: entry.type === \"compaction\" || entry.type === \"branch_summary\"\n\t\t\t\t\t\t? entry.usage\n\t\t\t\t\t\t: undefined;\n\t\t\tif (\n\t\t\t\t!usage ||\n\t\t\t\ttypeof usage.input !== \"number\" ||\n\t\t\t\ttypeof usage.output !== \"number\" ||\n\t\t\t\ttypeof usage.cacheRead !== \"number\" ||\n\t\t\t\ttypeof usage.cacheWrite !== \"number\" ||\n\t\t\t\ttypeof usage.cost?.total !== \"number\"\n\t\t\t) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tcachedTokens += usage.cacheRead;\n\t\t\tuncachedTokens += usage.input + usage.cacheWrite;\n\t\t\ttotalTokens += usage.input + usage.output + usage.cacheRead + usage.cacheWrite;\n\t\t\tcostTotal += usage.cost.total;\n\t\t}\n\t\treturn {\n\t\t\tmessageCount,\n\t\t\tcachedTokens,\n\t\t\tuncachedTokens,\n\t\t\ttotalTokens,\n\t\t\tcostTotal,\n\t\t};\n\t}\n\n\tasync getPathToRootOrCompaction(leafId: string | null): Promise<SessionTreeEntry[]> {\n\t\tif (leafId === null) return [];\n\t\tconst path: SessionTreeEntry[] = [];\n\t\tlet stopAtEntryId: string | null = null;\n\t\tlet current = this.byId.get(leafId);\n\t\tif (!current) throw new SessionError(\"not_found\", `Entry ${leafId} not found`);\n\t\twhile (current) {\n\t\t\tpath.unshift(current);\n\t\t\tif (stopAtEntryId !== null && current.id === stopAtEntryId) break;\n\t\t\tif (current.type === \"compaction\") {\n\t\t\t\tif (current.retainedTail) break;\n\t\t\t\tstopAtEntryId = current.firstKeptEntryId ?? null;\n\t\t\t}\n\t\t\tif (!current.parentId) break;\n\t\t\tconst parent = this.byId.get(current.parentId);\n\t\t\tif (!parent) throw new SessionError(\"invalid_session\", `Entry ${current.parentId} not found`);\n\t\t\tcurrent = parent;\n\t\t}\n\t\treturn path;\n\t}\n\n\tasync getEntries(options?: SessionEntryCursorOptions): Promise<SessionTreeEntry[]> {\n\t\tconst start = options?.afterEntrySeq ?? 0;\n\t\tconst end = options?.limit === undefined ? undefined : start + options.limit;\n\t\treturn this.entries.slice(start, end);\n\t}\n}\n"]}
@@ -0,0 +1,18 @@
1
+ import { type Session, type SessionMetadata, type SessionRepo } from "../types.ts";
2
+ export declare class InMemorySessionRepo implements SessionRepo<SessionMetadata, {
3
+ id?: string;
4
+ }, void> {
5
+ private sessions;
6
+ create(options?: {
7
+ id?: string;
8
+ }): Promise<Session<SessionMetadata>>;
9
+ open(metadata: SessionMetadata): Promise<Session<SessionMetadata>>;
10
+ list(): Promise<SessionMetadata[]>;
11
+ delete(metadata: SessionMetadata): Promise<void>;
12
+ fork(sourceMetadata: SessionMetadata, options: {
13
+ entryId?: string;
14
+ position?: "before" | "at";
15
+ id?: string;
16
+ }): Promise<Session<SessionMetadata>>;
17
+ }
18
+ //# sourceMappingURL=memory-repo.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"memory-repo.d.ts","sourceRoot":"","sources":["../../../src/harness/session/memory-repo.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,OAAO,EAAgB,KAAK,eAAe,EAAE,KAAK,WAAW,EAAE,MAAM,aAAa,CAAC;AAIjG,qBAAa,mBAAoB,YAAW,WAAW,CAAC,eAAe,EAAE;IAAE,EAAE,CAAC,EAAE,MAAM,CAAA;CAAE,EAAE,IAAI,CAAC;IAC9F,OAAO,CAAC,QAAQ,CAA+C;IAEzD,MAAM,CAAC,OAAO,GAAE;QAAE,EAAE,CAAC,EAAE,MAAM,CAAA;KAAO,GAAG,OAAO,CAAC,OAAO,CAAC,eAAe,CAAC,CAAC,CAS7E;IAEK,IAAI,CAAC,QAAQ,EAAE,eAAe,GAAG,OAAO,CAAC,OAAO,CAAC,eAAe,CAAC,CAAC,CAMvE;IAEK,IAAI,IAAI,OAAO,CAAC,eAAe,EAAE,CAAC,CAEvC;IAEK,MAAM,CAAC,QAAQ,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC,CAErD;IAEK,IAAI,CACT,cAAc,EAAE,eAAe,EAC/B,OAAO,EAAE;QAAE,OAAO,CAAC,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,EAAE,QAAQ,GAAG,IAAI,CAAC;QAAC,EAAE,CAAC,EAAE,MAAM,CAAA;KAAE,GACpE,OAAO,CAAC,OAAO,CAAC,eAAe,CAAC,CAAC,CAWnC;CACD","sourcesContent":["import { type Session, SessionError, type SessionMetadata, type SessionRepo } from \"../types.ts\";\nimport { InMemorySessionStorage } from \"./memory-storage.ts\";\nimport { createSessionId, createTimestamp, getEntriesToFork, toSession } from \"./repo-utils.ts\";\n\nexport class InMemorySessionRepo implements SessionRepo<SessionMetadata, { id?: string }, void> {\n\tprivate sessions = new Map<string, Session<SessionMetadata>>();\n\n\tasync create(options: { id?: string } = {}): Promise<Session<SessionMetadata>> {\n\t\tconst metadata: SessionMetadata = {\n\t\t\tid: options.id ?? createSessionId(),\n\t\t\tcreatedAt: createTimestamp(),\n\t\t};\n\t\tconst storage = new InMemorySessionStorage({ metadata });\n\t\tconst session = toSession(storage);\n\t\tthis.sessions.set(metadata.id, session);\n\t\treturn session;\n\t}\n\n\tasync open(metadata: SessionMetadata): Promise<Session<SessionMetadata>> {\n\t\tconst session = this.sessions.get(metadata.id);\n\t\tif (!session) {\n\t\t\tthrow new SessionError(\"not_found\", `Session not found: ${metadata.id}`);\n\t\t}\n\t\treturn session;\n\t}\n\n\tasync list(): Promise<SessionMetadata[]> {\n\t\treturn Promise.all([...this.sessions.values()].map((session) => session.getMetadata()));\n\t}\n\n\tasync delete(metadata: SessionMetadata): Promise<void> {\n\t\tthis.sessions.delete(metadata.id);\n\t}\n\n\tasync fork(\n\t\tsourceMetadata: SessionMetadata,\n\t\toptions: { entryId?: string; position?: \"before\" | \"at\"; id?: string },\n\t): Promise<Session<SessionMetadata>> {\n\t\tconst source = await this.open(sourceMetadata);\n\t\tconst forkedEntries = await getEntriesToFork(source.getStorage(), options);\n\t\tconst metadata: SessionMetadata = {\n\t\t\tid: options.id ?? createSessionId(),\n\t\t\tcreatedAt: createTimestamp(),\n\t\t};\n\t\tconst storage = new InMemorySessionStorage({ metadata, entries: forkedEntries });\n\t\tconst session = toSession(storage);\n\t\tthis.sessions.set(metadata.id, session);\n\t\treturn session;\n\t}\n}\n"]}
@@ -0,0 +1,42 @@
1
+ import { SessionError } from "../types.js";
2
+ import { InMemorySessionStorage } from "./memory-storage.js";
3
+ import { createSessionId, createTimestamp, getEntriesToFork, toSession } from "./repo-utils.js";
4
+ export class InMemorySessionRepo {
5
+ sessions = new Map();
6
+ async create(options = {}) {
7
+ const metadata = {
8
+ id: options.id ?? createSessionId(),
9
+ createdAt: createTimestamp(),
10
+ };
11
+ const storage = new InMemorySessionStorage({ metadata });
12
+ const session = toSession(storage);
13
+ this.sessions.set(metadata.id, session);
14
+ return session;
15
+ }
16
+ async open(metadata) {
17
+ const session = this.sessions.get(metadata.id);
18
+ if (!session) {
19
+ throw new SessionError("not_found", `Session not found: ${metadata.id}`);
20
+ }
21
+ return session;
22
+ }
23
+ async list() {
24
+ return Promise.all([...this.sessions.values()].map((session) => session.getMetadata()));
25
+ }
26
+ async delete(metadata) {
27
+ this.sessions.delete(metadata.id);
28
+ }
29
+ async fork(sourceMetadata, options) {
30
+ const source = await this.open(sourceMetadata);
31
+ const forkedEntries = await getEntriesToFork(source.getStorage(), options);
32
+ const metadata = {
33
+ id: options.id ?? createSessionId(),
34
+ createdAt: createTimestamp(),
35
+ };
36
+ const storage = new InMemorySessionStorage({ metadata, entries: forkedEntries });
37
+ const session = toSession(storage);
38
+ this.sessions.set(metadata.id, session);
39
+ return session;
40
+ }
41
+ }
42
+ //# sourceMappingURL=memory-repo.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"memory-repo.js","sourceRoot":"","sources":["../../../src/harness/session/memory-repo.ts"],"names":[],"mappings":"AAAA,OAAO,EAAgB,YAAY,EAA0C,MAAM,aAAa,CAAC;AACjG,OAAO,EAAE,sBAAsB,EAAE,MAAM,qBAAqB,CAAC;AAC7D,OAAO,EAAE,eAAe,EAAE,eAAe,EAAE,gBAAgB,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAEhG,MAAM,OAAO,mBAAmB;IACvB,QAAQ,GAAG,IAAI,GAAG,EAAoC,CAAC;IAE/D,KAAK,CAAC,MAAM,CAAC,OAAO,GAAoB,EAAE,EAAqC;QAC9E,MAAM,QAAQ,GAAoB;YACjC,EAAE,EAAE,OAAO,CAAC,EAAE,IAAI,eAAe,EAAE;YACnC,SAAS,EAAE,eAAe,EAAE;SAC5B,CAAC;QACF,MAAM,OAAO,GAAG,IAAI,sBAAsB,CAAC,EAAE,QAAQ,EAAE,CAAC,CAAC;QACzD,MAAM,OAAO,GAAG,SAAS,CAAC,OAAO,CAAC,CAAC;QACnC,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,QAAQ,CAAC,EAAE,EAAE,OAAO,CAAC,CAAC;QACxC,OAAO,OAAO,CAAC;IAAA,CACf;IAED,KAAK,CAAC,IAAI,CAAC,QAAyB,EAAqC;QACxE,MAAM,OAAO,GAAG,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC;QAC/C,IAAI,CAAC,OAAO,EAAE,CAAC;YACd,MAAM,IAAI,YAAY,CAAC,WAAW,EAAE,sBAAsB,QAAQ,CAAC,EAAE,EAAE,CAAC,CAAC;QAC1E,CAAC;QACD,OAAO,OAAO,CAAC;IAAA,CACf;IAED,KAAK,CAAC,IAAI,GAA+B;QACxC,OAAO,OAAO,CAAC,GAAG,CAAC,CAAC,GAAG,IAAI,CAAC,QAAQ,CAAC,MAAM,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC;IAAA,CACxF;IAED,KAAK,CAAC,MAAM,CAAC,QAAyB,EAAiB;QACtD,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC;IAAA,CAClC;IAED,KAAK,CAAC,IAAI,CACT,cAA+B,EAC/B,OAAsE,EAClC;QACpC,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,IAAI,CAAC,cAAc,CAAC,CAAC;QAC/C,MAAM,aAAa,GAAG,MAAM,gBAAgB,CAAC,MAAM,CAAC,UAAU,EAAE,EAAE,OAAO,CAAC,CAAC;QAC3E,MAAM,QAAQ,GAAoB;YACjC,EAAE,EAAE,OAAO,CAAC,EAAE,IAAI,eAAe,EAAE;YACnC,SAAS,EAAE,eAAe,EAAE;SAC5B,CAAC;QACF,MAAM,OAAO,GAAG,IAAI,sBAAsB,CAAC,EAAE,QAAQ,EAAE,OAAO,EAAE,aAAa,EAAE,CAAC,CAAC;QACjF,MAAM,OAAO,GAAG,SAAS,CAAC,OAAO,CAAC,CAAC;QACnC,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,QAAQ,CAAC,EAAE,EAAE,OAAO,CAAC,CAAC;QACxC,OAAO,OAAO,CAAC;IAAA,CACf;CACD","sourcesContent":["import { type Session, SessionError, type SessionMetadata, type SessionRepo } from \"../types.ts\";\nimport { InMemorySessionStorage } from \"./memory-storage.ts\";\nimport { createSessionId, createTimestamp, getEntriesToFork, toSession } from \"./repo-utils.ts\";\n\nexport class InMemorySessionRepo implements SessionRepo<SessionMetadata, { id?: string }, void> {\n\tprivate sessions = new Map<string, Session<SessionMetadata>>();\n\n\tasync create(options: { id?: string } = {}): Promise<Session<SessionMetadata>> {\n\t\tconst metadata: SessionMetadata = {\n\t\t\tid: options.id ?? createSessionId(),\n\t\t\tcreatedAt: createTimestamp(),\n\t\t};\n\t\tconst storage = new InMemorySessionStorage({ metadata });\n\t\tconst session = toSession(storage);\n\t\tthis.sessions.set(metadata.id, session);\n\t\treturn session;\n\t}\n\n\tasync open(metadata: SessionMetadata): Promise<Session<SessionMetadata>> {\n\t\tconst session = this.sessions.get(metadata.id);\n\t\tif (!session) {\n\t\t\tthrow new SessionError(\"not_found\", `Session not found: ${metadata.id}`);\n\t\t}\n\t\treturn session;\n\t}\n\n\tasync list(): Promise<SessionMetadata[]> {\n\t\treturn Promise.all([...this.sessions.values()].map((session) => session.getMetadata()));\n\t}\n\n\tasync delete(metadata: SessionMetadata): Promise<void> {\n\t\tthis.sessions.delete(metadata.id);\n\t}\n\n\tasync fork(\n\t\tsourceMetadata: SessionMetadata,\n\t\toptions: { entryId?: string; position?: \"before\" | \"at\"; id?: string },\n\t): Promise<Session<SessionMetadata>> {\n\t\tconst source = await this.open(sourceMetadata);\n\t\tconst forkedEntries = await getEntriesToFork(source.getStorage(), options);\n\t\tconst metadata: SessionMetadata = {\n\t\t\tid: options.id ?? createSessionId(),\n\t\t\tcreatedAt: createTimestamp(),\n\t\t};\n\t\tconst storage = new InMemorySessionStorage({ metadata, entries: forkedEntries });\n\t\tconst session = toSession(storage);\n\t\tthis.sessions.set(metadata.id, session);\n\t\treturn session;\n\t}\n}\n"]}
@@ -0,0 +1,33 @@
1
+ import { type SessionEntryCursorOptions, type SessionMetadata, type SessionStorage, type SessionTreeEntry } from "../types.ts";
2
+ export declare class InMemorySessionStorage<TMetadata extends SessionMetadata = SessionMetadata> implements SessionStorage<TMetadata> {
3
+ private readonly metadata;
4
+ private entries;
5
+ private byId;
6
+ private labelsById;
7
+ private leafId;
8
+ constructor(options?: {
9
+ entries?: SessionTreeEntry[];
10
+ metadata?: TMetadata;
11
+ });
12
+ getMetadata(): Promise<TMetadata>;
13
+ getLeafId(): Promise<string | null>;
14
+ setLeafId(leafId: string | null): Promise<void>;
15
+ createEntryId(): Promise<string>;
16
+ appendEntry(entry: SessionTreeEntry): Promise<void>;
17
+ getEntry(id: string): Promise<SessionTreeEntry | undefined>;
18
+ findEntries<TType extends SessionTreeEntry["type"]>(type: TType): Promise<Array<Extract<SessionTreeEntry, {
19
+ type: TType;
20
+ }>>>;
21
+ getLabel(id: string): Promise<string | undefined>;
22
+ getSessionName(): Promise<string | undefined>;
23
+ getSessionStats(): Promise<{
24
+ messageCount: number;
25
+ cachedTokens: number;
26
+ uncachedTokens: number;
27
+ totalTokens: number;
28
+ costTotal: number;
29
+ }>;
30
+ getPathToRootOrCompaction(leafId: string | null): Promise<SessionTreeEntry[]>;
31
+ getEntries(options?: SessionEntryCursorOptions): Promise<SessionTreeEntry[]>;
32
+ }
33
+ //# sourceMappingURL=memory-storage.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"memory-storage.d.ts","sourceRoot":"","sources":["../../../src/harness/session/memory-storage.ts"],"names":[],"mappings":"AACA,OAAO,EAEN,KAAK,yBAAyB,EAE9B,KAAK,eAAe,EACpB,KAAK,cAAc,EACnB,KAAK,gBAAgB,EACrB,MAAM,aAAa,CAAC;AAkCrB,qBAAa,sBAAsB,CAAC,SAAS,SAAS,eAAe,GAAG,eAAe,CACtF,YAAW,cAAc,CAAC,SAAS,CAAC;IAEpC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAY;IACrC,OAAO,CAAC,OAAO,CAAqB;IACpC,OAAO,CAAC,IAAI,CAAgC;IAC5C,OAAO,CAAC,UAAU,CAAsB;IACxC,OAAO,CAAC,MAAM,CAAgB;IAE9B,YAAY,OAAO,CAAC,EAAE;QAAE,OAAO,CAAC,EAAE,gBAAgB,EAAE,CAAC;QAAC,QAAQ,CAAC,EAAE,SAAS,CAAA;KAAE,EAU3E;IAEK,WAAW,IAAI,OAAO,CAAC,SAAS,CAAC,CAEtC;IAEK,SAAS,IAAI,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAKxC;IAEK,SAAS,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAcpD;IAEK,aAAa,IAAI,OAAO,CAAC,MAAM,CAAC,CAErC;IAEK,WAAW,CAAC,KAAK,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC,CAKxD;IAEK,QAAQ,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,GAAG,SAAS,CAAC,CAEhE;IAEK,WAAW,CAAC,KAAK,SAAS,gBAAgB,CAAC,MAAM,CAAC,EACvD,IAAI,EAAE,KAAK,GACT,OAAO,CAAC,KAAK,CAAC,OAAO,CAAC,gBAAgB,EAAE;QAAE,IAAI,EAAE,KAAK,CAAA;KAAE,CAAC,CAAC,CAAC,CAE5D;IAEK,QAAQ,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAEtD;IAEK,cAAc,IAAI,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAGlD;IAEK,eAAe;;;;;;OAwCpB;IAEK,yBAAyB,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO,CAAC,gBAAgB,EAAE,CAAC,CAmBlF;IAEK,UAAU,CAAC,OAAO,CAAC,EAAE,yBAAyB,GAAG,OAAO,CAAC,gBAAgB,EAAE,CAAC,CAIjF;CACD","sourcesContent":["import { uuidv7 } from \"@handy_wote/pi-ai\";\nimport {\n\ttype LeafEntry,\n\ttype SessionEntryCursorOptions,\n\tSessionError,\n\ttype SessionMetadata,\n\ttype SessionStorage,\n\ttype SessionTreeEntry,\n} from \"../types.ts\";\n\nfunction updateLabelCache(labelsById: Map<string, string>, entry: SessionTreeEntry): void {\n\tif (entry.type !== \"label\") return;\n\tconst label = entry.label?.trim();\n\tif (label) {\n\t\tlabelsById.set(entry.targetId, label);\n\t} else {\n\t\tlabelsById.delete(entry.targetId);\n\t}\n}\n\nfunction buildLabelsById(entries: SessionTreeEntry[]): Map<string, string> {\n\tconst labelsById = new Map<string, string>();\n\tfor (const entry of entries) {\n\t\tupdateLabelCache(labelsById, entry);\n\t}\n\treturn labelsById;\n}\n\nfunction generateEntryId(byId: { has(id: string): boolean }): string {\n\tfor (let i = 0; i < 100; i++) {\n\t\t// The uuidv7 prefix is timestamp-derived and nearly constant between calls,\n\t\t// so short ids must come from the random tail.\n\t\tconst id = uuidv7().slice(-8);\n\t\tif (!byId.has(id)) return id;\n\t}\n\treturn uuidv7();\n}\n\nfunction leafIdAfterEntry(entry: SessionTreeEntry): string | null {\n\treturn entry.type === \"leaf\" ? entry.targetId : entry.id;\n}\n\nexport class InMemorySessionStorage<TMetadata extends SessionMetadata = SessionMetadata>\n\timplements SessionStorage<TMetadata>\n{\n\tprivate readonly metadata: TMetadata;\n\tprivate entries: SessionTreeEntry[];\n\tprivate byId: Map<string, SessionTreeEntry>;\n\tprivate labelsById: Map<string, string>;\n\tprivate leafId: string | null;\n\n\tconstructor(options?: { entries?: SessionTreeEntry[]; metadata?: TMetadata }) {\n\t\tthis.entries = options?.entries ? [...options.entries] : [];\n\t\tthis.byId = new Map(this.entries.map((entry) => [entry.id, entry]));\n\t\tthis.labelsById = buildLabelsById(this.entries);\n\t\tthis.leafId = null;\n\t\tfor (const entry of this.entries) this.leafId = leafIdAfterEntry(entry);\n\t\tif (this.leafId !== null && !this.byId.has(this.leafId)) {\n\t\t\tthrow new SessionError(\"invalid_session\", `Entry ${this.leafId} not found`);\n\t\t}\n\t\tthis.metadata = options?.metadata ?? ({ id: uuidv7(), createdAt: new Date().toISOString() } as TMetadata);\n\t}\n\n\tasync getMetadata(): Promise<TMetadata> {\n\t\treturn this.metadata;\n\t}\n\n\tasync getLeafId(): Promise<string | null> {\n\t\tif (this.leafId !== null && !this.byId.has(this.leafId)) {\n\t\t\tthrow new SessionError(\"invalid_session\", `Entry ${this.leafId} not found`);\n\t\t}\n\t\treturn this.leafId;\n\t}\n\n\tasync setLeafId(leafId: string | null): Promise<void> {\n\t\tif (leafId !== null && !this.byId.has(leafId)) {\n\t\t\tthrow new SessionError(\"not_found\", `Entry ${leafId} not found`);\n\t\t}\n\t\tconst entry: LeafEntry = {\n\t\t\ttype: \"leaf\",\n\t\t\tid: generateEntryId(this.byId),\n\t\t\tparentId: this.leafId,\n\t\t\ttimestamp: new Date().toISOString(),\n\t\t\ttargetId: leafId,\n\t\t};\n\t\tthis.entries.push(entry);\n\t\tthis.byId.set(entry.id, entry);\n\t\tthis.leafId = leafId;\n\t}\n\n\tasync createEntryId(): Promise<string> {\n\t\treturn generateEntryId(this.byId);\n\t}\n\n\tasync appendEntry(entry: SessionTreeEntry): Promise<void> {\n\t\tthis.entries.push(entry);\n\t\tthis.byId.set(entry.id, entry);\n\t\tupdateLabelCache(this.labelsById, entry);\n\t\tthis.leafId = leafIdAfterEntry(entry);\n\t}\n\n\tasync getEntry(id: string): Promise<SessionTreeEntry | undefined> {\n\t\treturn this.byId.get(id);\n\t}\n\n\tasync findEntries<TType extends SessionTreeEntry[\"type\"]>(\n\t\ttype: TType,\n\t): Promise<Array<Extract<SessionTreeEntry, { type: TType }>>> {\n\t\treturn this.entries.filter((entry): entry is Extract<SessionTreeEntry, { type: TType }> => entry.type === type);\n\t}\n\n\tasync getLabel(id: string): Promise<string | undefined> {\n\t\treturn this.labelsById.get(id);\n\t}\n\n\tasync getSessionName(): Promise<string | undefined> {\n\t\tconst entries = await this.findEntries(\"session_info\");\n\t\treturn entries[entries.length - 1]?.name?.trim() || undefined;\n\t}\n\n\tasync getSessionStats() {\n\t\tlet messageCount = 0;\n\t\tlet cachedTokens = 0;\n\t\tlet uncachedTokens = 0;\n\t\tlet totalTokens = 0;\n\t\tlet costTotal = 0;\n\t\tfor (const entry of this.entries) {\n\t\t\tif (entry.type === \"message\") {\n\t\t\t\tmessageCount += 1;\n\t\t\t}\n\t\t\tconst usage =\n\t\t\t\tentry.type === \"message\"\n\t\t\t\t\t? entry.message.role === \"assistant\"\n\t\t\t\t\t\t? entry.message.usage\n\t\t\t\t\t\t: undefined\n\t\t\t\t\t: entry.type === \"compaction\" || entry.type === \"branch_summary\"\n\t\t\t\t\t\t? entry.usage\n\t\t\t\t\t\t: undefined;\n\t\t\tif (\n\t\t\t\t!usage ||\n\t\t\t\ttypeof usage.input !== \"number\" ||\n\t\t\t\ttypeof usage.output !== \"number\" ||\n\t\t\t\ttypeof usage.cacheRead !== \"number\" ||\n\t\t\t\ttypeof usage.cacheWrite !== \"number\" ||\n\t\t\t\ttypeof usage.cost?.total !== \"number\"\n\t\t\t) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tcachedTokens += usage.cacheRead;\n\t\t\tuncachedTokens += usage.input + usage.cacheWrite;\n\t\t\ttotalTokens += usage.input + usage.output + usage.cacheRead + usage.cacheWrite;\n\t\t\tcostTotal += usage.cost.total;\n\t\t}\n\t\treturn {\n\t\t\tmessageCount,\n\t\t\tcachedTokens,\n\t\t\tuncachedTokens,\n\t\t\ttotalTokens,\n\t\t\tcostTotal,\n\t\t};\n\t}\n\n\tasync getPathToRootOrCompaction(leafId: string | null): Promise<SessionTreeEntry[]> {\n\t\tif (leafId === null) return [];\n\t\tconst path: SessionTreeEntry[] = [];\n\t\tlet stopAtEntryId: string | null = null;\n\t\tlet current = this.byId.get(leafId);\n\t\tif (!current) throw new SessionError(\"not_found\", `Entry ${leafId} not found`);\n\t\twhile (current) {\n\t\t\tpath.unshift(current);\n\t\t\tif (stopAtEntryId !== null && current.id === stopAtEntryId) break;\n\t\t\tif (current.type === \"compaction\") {\n\t\t\t\tif (current.retainedTail) break;\n\t\t\t\tstopAtEntryId = current.firstKeptEntryId ?? null;\n\t\t\t}\n\t\t\tif (!current.parentId) break;\n\t\t\tconst parent = this.byId.get(current.parentId);\n\t\t\tif (!parent) throw new SessionError(\"invalid_session\", `Entry ${current.parentId} not found`);\n\t\t\tcurrent = parent;\n\t\t}\n\t\treturn path;\n\t}\n\n\tasync getEntries(options?: SessionEntryCursorOptions): Promise<SessionTreeEntry[]> {\n\t\tconst start = options?.afterEntrySeq ?? 0;\n\t\tconst end = options?.limit === undefined ? undefined : start + options.limit;\n\t\treturn this.entries.slice(start, end);\n\t}\n}\n"]}