@averyyy/pi-server 0.87.1-piclient.1 → 0.99.1-piclient.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.
package/CHANGELOG.md CHANGED
@@ -38,6 +38,7 @@
38
38
  - Updated pi-server runtime dependencies to the current lockstep fork packages.
39
39
  - Used `--legacy-peer-deps` for npm-global fork updates so existing upstream Pi installs do not trigger peer override warnings for forked prerelease aliases.
40
40
  - Increased the pending chunk budget to 1 GiB so large session-tree appends do not fail at the previous 64 MiB Base64 limit.
41
+ - Reused authoritative prompt prefixes for eligible server-side compaction summaries while retaining chunked handling for mismatched or oversized contexts.
41
42
 
42
43
  ### Changed
43
44
 
package/dist/cli.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"","sourcesContent":["#!/usr/bin/env node\nimport { createRequire } from \"node:module\";\nimport { startServer } from \"./server.ts\";\n\ninterface PackageMetadata {\n\tversion: string;\n}\n\nconst packageMetadata = createRequire(import.meta.url)(\"../package.json\") as PackageMetadata;\nconst args = process.argv.slice(2);\n\nconst unsupportedArgument = args.find((arg) => arg !== \"--help\" && arg !== \"-h\" && arg !== \"--version\" && arg !== \"-v\");\nif (unsupportedArgument !== undefined) {\n\tconsole.error(`Unknown option \"${unsupportedArgument}\". Use \"pi-server --help\" for usage.`);\n\tprocess.exitCode = 1;\n} else if (args.includes(\"--help\") || args.includes(\"-h\")) {\n\tprintHelp();\n} else if (args.includes(\"--version\") || args.includes(\"-v\")) {\n\tconsole.log(packageMetadata.version);\n} else {\n\tstartServer();\n}\n\nfunction printHelp(): void {\n\tconsole.log(`pi-server - HTTP proxy for pi-client\n\nUsage:\n pi-server\n\nOptions:\n --help, -h Show this help\n --version, -v Show the installed version`);\n}\n"]}
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":""}
package/dist/cli.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAC5C,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAM1C,MAAM,eAAe,GAAG,aAAa,CAAC,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,iBAAiB,CAAoB,CAAC;AAC7F,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;AAEnC,MAAM,mBAAmB,GAAG,IAAI,CAAC,IAAI,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,KAAK,QAAQ,IAAI,GAAG,KAAK,IAAI,IAAI,GAAG,KAAK,WAAW,IAAI,GAAG,KAAK,IAAI,CAAC,CAAC;AACxH,IAAI,mBAAmB,KAAK,SAAS,EAAE,CAAC;IACvC,OAAO,CAAC,KAAK,CAAC,mBAAmB,mBAAmB,sCAAsC,CAAC,CAAC;IAC5F,OAAO,CAAC,QAAQ,GAAG,CAAC,CAAC;AACtB,CAAC;KAAM,IAAI,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC,IAAI,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC;IAC3D,SAAS,EAAE,CAAC;AACb,CAAC;KAAM,IAAI,IAAI,CAAC,QAAQ,CAAC,WAAW,CAAC,IAAI,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC;IAC9D,OAAO,CAAC,GAAG,CAAC,eAAe,CAAC,OAAO,CAAC,CAAC;AACtC,CAAC;KAAM,CAAC;IACP,WAAW,EAAE,CAAC;AACf,CAAC;AAED,SAAS,SAAS,GAAS;IAC1B,OAAO,CAAC,GAAG,CAAC;;;;;;;8CAOiC,CAAC,CAAC;AAAA,CAC/C","sourcesContent":["#!/usr/bin/env node\nimport { createRequire } from \"node:module\";\nimport { startServer } from \"./server.ts\";\n\ninterface PackageMetadata {\n\tversion: string;\n}\n\nconst packageMetadata = createRequire(import.meta.url)(\"../package.json\") as PackageMetadata;\nconst args = process.argv.slice(2);\n\nconst unsupportedArgument = args.find((arg) => arg !== \"--help\" && arg !== \"-h\" && arg !== \"--version\" && arg !== \"-v\");\nif (unsupportedArgument !== undefined) {\n\tconsole.error(`Unknown option \"${unsupportedArgument}\". Use \"pi-server --help\" for usage.`);\n\tprocess.exitCode = 1;\n} else if (args.includes(\"--help\") || args.includes(\"-h\")) {\n\tprintHelp();\n} else if (args.includes(\"--version\") || args.includes(\"-v\")) {\n\tconsole.log(packageMetadata.version);\n} else {\n\tstartServer();\n}\n\nfunction printHelp(): void {\n\tconsole.log(`pi-server - HTTP proxy for pi-client\n\nUsage:\n pi-server\n\nOptions:\n --help, -h Show this help\n --version, -v Show the installed version`);\n}\n"]}
1
+ {"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAC5C,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAM1C,MAAM,eAAe,GAAG,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,iBAAiB,CAAoB,CAAC;AAC7F,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;AAEnC,MAAM,mBAAmB,GAAG,IAAI,CAAC,IAAI,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,KAAK,QAAQ,IAAI,GAAG,KAAK,IAAI,IAAI,GAAG,KAAK,WAAW,IAAI,GAAG,KAAK,IAAI,CAAC,CAAC;AACxH,IAAI,mBAAmB,KAAK,SAAS,EAAE,CAAC;IACvC,OAAO,CAAC,KAAK,CAAC,mBAAmB,mBAAmB,sCAAsC,CAAC,CAAC;IAC5F,OAAO,CAAC,QAAQ,GAAG,CAAC,CAAC;AACtB,CAAC;KAAM,IAAI,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC,IAAI,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC;IAC3D,SAAS,EAAE,CAAC;AACb,CAAC;KAAM,IAAI,IAAI,CAAC,QAAQ,CAAC,WAAW,CAAC,IAAI,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC;IAC9D,OAAO,CAAC,GAAG,CAAC,eAAe,CAAC,OAAO,CAAC,CAAC;AACtC,CAAC;KAAM,CAAC;IACP,WAAW,EAAE,CAAC;AACf,CAAC;AAED,SAAS,SAAS;IACjB,OAAO,CAAC,GAAG,CAAC;;;;;;;8CAOiC,CAAC,CAAC;AAChD,CAAC","sourcesContent":["#!/usr/bin/env node\nimport { createRequire } from \"node:module\";\nimport { startServer } from \"./server.ts\";\n\ninterface PackageMetadata {\n\tversion: string;\n}\n\nconst packageMetadata = createRequire(import.meta.url)(\"../package.json\") as PackageMetadata;\nconst args = process.argv.slice(2);\n\nconst unsupportedArgument = args.find((arg) => arg !== \"--help\" && arg !== \"-h\" && arg !== \"--version\" && arg !== \"-v\");\nif (unsupportedArgument !== undefined) {\n\tconsole.error(`Unknown option \"${unsupportedArgument}\". Use \"pi-server --help\" for usage.`);\n\tprocess.exitCode = 1;\n} else if (args.includes(\"--help\") || args.includes(\"-h\")) {\n\tprintHelp();\n} else if (args.includes(\"--version\") || args.includes(\"-v\")) {\n\tconsole.log(packageMetadata.version);\n} else {\n\tstartServer();\n}\n\nfunction printHelp(): void {\n\tconsole.log(`pi-server - HTTP proxy for pi-client\n\nUsage:\n pi-server\n\nOptions:\n --help, -h Show this help\n --version, -v Show the installed version`);\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAIA,MAAM,WAAW,YAAY;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,GAAG,SAAS,CAAC;IAC9B,eAAe,EAAE,MAAM,CAAC;IACxB,SAAS,EAAE,MAAM,CAAC;CAClB;AAyBD,wBAAgB,UAAU,CAAC,SAAS,CAAC,EAAE,OAAO,CAAC,YAAY,CAAC,GAAG,YAAY,CAsB1E","sourcesContent":["import { existsSync, readFileSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { join, resolve } from \"node:path\";\n\nexport interface ServerConfig {\n\thost: string;\n\tport: number;\n\tauthToken: string | undefined;\n\tsessionStoreDir: string;\n\tuploadDir: string;\n}\n\nconst DEFAULT_CONFIG: ServerConfig = {\n\thost: \"127.0.0.1\",\n\tport: 4217,\n\tauthToken: undefined,\n\tsessionStoreDir: resolve(\".pi\", \"pi-server\", \"sessions\"),\n\tuploadDir: join(homedir(), \".pi\", \"upload_files\"),\n};\n\nfunction loadConfigFile(): Partial<ServerConfig> {\n\tconst configPath = process.env.PI_SERVER_CONFIG;\n\tif (!configPath) return {};\n\tconst resolved = resolve(configPath);\n\tif (!existsSync(resolved)) {\n\t\tthrow new Error(`PI_SERVER_CONFIG file not found: ${resolved}`);\n\t}\n\ttry {\n\t\tconst content = readFileSync(resolved, \"utf-8\");\n\t\treturn JSON.parse(content) as Partial<ServerConfig>;\n\t} catch (err) {\n\t\tthrow new Error(`PI_SERVER_CONFIG file contains invalid JSON: ${resolved}`, { cause: err });\n\t}\n}\n\nexport function loadConfig(overrides?: Partial<ServerConfig>): ServerConfig {\n\tconst fileConfig = loadConfigFile();\n\n\treturn {\n\t\thost: overrides?.host ?? process.env.PI_SERVER_HOST ?? fileConfig.host ?? DEFAULT_CONFIG.host,\n\t\tport:\n\t\t\toverrides?.port ??\n\t\t\t(process.env.PI_SERVER_PORT ? parseInt(process.env.PI_SERVER_PORT, 10) : undefined) ??\n\t\t\tfileConfig.port ??\n\t\t\tDEFAULT_CONFIG.port,\n\t\tauthToken:\n\t\t\toverrides?.authToken ?? process.env.PI_SERVER_AUTH_TOKEN ?? fileConfig.authToken ?? DEFAULT_CONFIG.authToken,\n\t\tsessionStoreDir: resolve(\n\t\t\toverrides?.sessionStoreDir ??\n\t\t\t\tprocess.env.PI_SERVER_SESSION_STORE_DIR ??\n\t\t\t\tfileConfig.sessionStoreDir ??\n\t\t\t\tDEFAULT_CONFIG.sessionStoreDir,\n\t\t),\n\t\tuploadDir: resolve(\n\t\t\toverrides?.uploadDir ?? process.env.PI_SERVER_UPLOAD_DIR ?? fileConfig.uploadDir ?? DEFAULT_CONFIG.uploadDir,\n\t\t),\n\t};\n}\n"]}
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAIA,MAAM,WAAW,YAAY;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,GAAG,SAAS,CAAC;IAC9B,eAAe,EAAE,MAAM,CAAC;IACxB,SAAS,EAAE,MAAM,CAAC;CAClB;AAyBD,wBAAgB,UAAU,CAAC,SAAS,CAAC,EAAE,OAAO,CAAC,YAAY,CAAC,GAAG,YAAY,CAsB1E"}
@@ -1 +1 @@
1
- {"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACnD,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAClC,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAU1C,MAAM,cAAc,GAAiB;IACpC,IAAI,EAAE,WAAW;IACjB,IAAI,EAAE,IAAI;IACV,SAAS,EAAE,SAAS;IACpB,eAAe,EAAE,OAAO,CAAC,KAAK,EAAE,WAAW,EAAE,UAAU,CAAC;IACxD,SAAS,EAAE,IAAI,CAAC,OAAO,EAAE,EAAE,KAAK,EAAE,cAAc,CAAC;CACjD,CAAC;AAEF,SAAS,cAAc,GAA0B;IAChD,MAAM,UAAU,GAAG,OAAO,CAAC,GAAG,CAAC,gBAAgB,CAAC;IAChD,IAAI,CAAC,UAAU;QAAE,OAAO,EAAE,CAAC;IAC3B,MAAM,QAAQ,GAAG,OAAO,CAAC,UAAU,CAAC,CAAC;IACrC,IAAI,CAAC,UAAU,CAAC,QAAQ,CAAC,EAAE,CAAC;QAC3B,MAAM,IAAI,KAAK,CAAC,oCAAoC,QAAQ,EAAE,CAAC,CAAC;IACjE,CAAC;IACD,IAAI,CAAC;QACJ,MAAM,OAAO,GAAG,YAAY,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;QAChD,OAAO,IAAI,CAAC,KAAK,CAAC,OAAO,CAA0B,CAAC;IACrD,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACd,MAAM,IAAI,KAAK,CAAC,gDAAgD,QAAQ,EAAE,EAAE,EAAE,KAAK,EAAE,GAAG,EAAE,CAAC,CAAC;IAC7F,CAAC;AAAA,CACD;AAED,MAAM,UAAU,UAAU,CAAC,SAAiC,EAAgB;IAC3E,MAAM,UAAU,GAAG,cAAc,EAAE,CAAC;IAEpC,OAAO;QACN,IAAI,EAAE,SAAS,EAAE,IAAI,IAAI,OAAO,CAAC,GAAG,CAAC,cAAc,IAAI,UAAU,CAAC,IAAI,IAAI,cAAc,CAAC,IAAI;QAC7F,IAAI,EACH,SAAS,EAAE,IAAI;YACf,CAAC,OAAO,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC,CAAC,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,cAAc,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;YACnF,UAAU,CAAC,IAAI;YACf,cAAc,CAAC,IAAI;QACpB,SAAS,EACR,SAAS,EAAE,SAAS,IAAI,OAAO,CAAC,GAAG,CAAC,oBAAoB,IAAI,UAAU,CAAC,SAAS,IAAI,cAAc,CAAC,SAAS;QAC7G,eAAe,EAAE,OAAO,CACvB,SAAS,EAAE,eAAe;YACzB,OAAO,CAAC,GAAG,CAAC,2BAA2B;YACvC,UAAU,CAAC,eAAe;YAC1B,cAAc,CAAC,eAAe,CAC/B;QACD,SAAS,EAAE,OAAO,CACjB,SAAS,EAAE,SAAS,IAAI,OAAO,CAAC,GAAG,CAAC,oBAAoB,IAAI,UAAU,CAAC,SAAS,IAAI,cAAc,CAAC,SAAS,CAC5G;KACD,CAAC;AAAA,CACF","sourcesContent":["import { existsSync, readFileSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { join, resolve } from \"node:path\";\n\nexport interface ServerConfig {\n\thost: string;\n\tport: number;\n\tauthToken: string | undefined;\n\tsessionStoreDir: string;\n\tuploadDir: string;\n}\n\nconst DEFAULT_CONFIG: ServerConfig = {\n\thost: \"127.0.0.1\",\n\tport: 4217,\n\tauthToken: undefined,\n\tsessionStoreDir: resolve(\".pi\", \"pi-server\", \"sessions\"),\n\tuploadDir: join(homedir(), \".pi\", \"upload_files\"),\n};\n\nfunction loadConfigFile(): Partial<ServerConfig> {\n\tconst configPath = process.env.PI_SERVER_CONFIG;\n\tif (!configPath) return {};\n\tconst resolved = resolve(configPath);\n\tif (!existsSync(resolved)) {\n\t\tthrow new Error(`PI_SERVER_CONFIG file not found: ${resolved}`);\n\t}\n\ttry {\n\t\tconst content = readFileSync(resolved, \"utf-8\");\n\t\treturn JSON.parse(content) as Partial<ServerConfig>;\n\t} catch (err) {\n\t\tthrow new Error(`PI_SERVER_CONFIG file contains invalid JSON: ${resolved}`, { cause: err });\n\t}\n}\n\nexport function loadConfig(overrides?: Partial<ServerConfig>): ServerConfig {\n\tconst fileConfig = loadConfigFile();\n\n\treturn {\n\t\thost: overrides?.host ?? process.env.PI_SERVER_HOST ?? fileConfig.host ?? DEFAULT_CONFIG.host,\n\t\tport:\n\t\t\toverrides?.port ??\n\t\t\t(process.env.PI_SERVER_PORT ? parseInt(process.env.PI_SERVER_PORT, 10) : undefined) ??\n\t\t\tfileConfig.port ??\n\t\t\tDEFAULT_CONFIG.port,\n\t\tauthToken:\n\t\t\toverrides?.authToken ?? process.env.PI_SERVER_AUTH_TOKEN ?? fileConfig.authToken ?? DEFAULT_CONFIG.authToken,\n\t\tsessionStoreDir: resolve(\n\t\t\toverrides?.sessionStoreDir ??\n\t\t\t\tprocess.env.PI_SERVER_SESSION_STORE_DIR ??\n\t\t\t\tfileConfig.sessionStoreDir ??\n\t\t\t\tDEFAULT_CONFIG.sessionStoreDir,\n\t\t),\n\t\tuploadDir: resolve(\n\t\t\toverrides?.uploadDir ?? process.env.PI_SERVER_UPLOAD_DIR ?? fileConfig.uploadDir ?? DEFAULT_CONFIG.uploadDir,\n\t\t),\n\t};\n}\n"]}
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACnD,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAClC,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAU1C,MAAM,cAAc,GAAiB;IACpC,IAAI,EAAE,WAAW;IACjB,IAAI,EAAE,IAAI;IACV,SAAS,EAAE,SAAS;IACpB,eAAe,EAAE,OAAO,CAAC,KAAK,EAAE,WAAW,EAAE,UAAU,CAAC;IACxD,SAAS,EAAE,IAAI,CAAC,OAAO,EAAE,EAAE,KAAK,EAAE,cAAc,CAAC;CACjD,CAAC;AAEF,SAAS,cAAc;IACtB,MAAM,UAAU,GAAG,OAAO,CAAC,GAAG,CAAC,gBAAgB,CAAC;IAChD,IAAI,CAAC,UAAU;QAAE,OAAO,EAAE,CAAC;IAC3B,MAAM,QAAQ,GAAG,OAAO,CAAC,UAAU,CAAC,CAAC;IACrC,IAAI,CAAC,UAAU,CAAC,QAAQ,CAAC,EAAE,CAAC;QAC3B,MAAM,IAAI,KAAK,CAAC,oCAAoC,QAAQ,EAAE,CAAC,CAAC;IACjE,CAAC;IACD,IAAI,CAAC;QACJ,MAAM,OAAO,GAAG,YAAY,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;QAChD,OAAO,IAAI,CAAC,KAAK,CAAC,OAAO,CAA0B,CAAC;IACrD,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACd,MAAM,IAAI,KAAK,CAAC,gDAAgD,QAAQ,EAAE,EAAE,EAAE,KAAK,EAAE,GAAG,EAAE,CAAC,CAAC;IAC7F,CAAC;AACF,CAAC;AAED,MAAM,UAAU,UAAU,CAAC,SAAiC;IAC3D,MAAM,UAAU,GAAG,cAAc,EAAE,CAAC;IAEpC,OAAO;QACN,IAAI,EAAE,SAAS,EAAE,IAAI,IAAI,OAAO,CAAC,GAAG,CAAC,cAAc,IAAI,UAAU,CAAC,IAAI,IAAI,cAAc,CAAC,IAAI;QAC7F,IAAI,EACH,SAAS,EAAE,IAAI;YACf,CAAC,OAAO,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC,CAAC,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,cAAc,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;YACnF,UAAU,CAAC,IAAI;YACf,cAAc,CAAC,IAAI;QACpB,SAAS,EACR,SAAS,EAAE,SAAS,IAAI,OAAO,CAAC,GAAG,CAAC,oBAAoB,IAAI,UAAU,CAAC,SAAS,IAAI,cAAc,CAAC,SAAS;QAC7G,eAAe,EAAE,OAAO,CACvB,SAAS,EAAE,eAAe;YACzB,OAAO,CAAC,GAAG,CAAC,2BAA2B;YACvC,UAAU,CAAC,eAAe;YAC1B,cAAc,CAAC,eAAe,CAC/B;QACD,SAAS,EAAE,OAAO,CACjB,SAAS,EAAE,SAAS,IAAI,OAAO,CAAC,GAAG,CAAC,oBAAoB,IAAI,UAAU,CAAC,SAAS,IAAI,cAAc,CAAC,SAAS,CAC5G;KACD,CAAC;AACH,CAAC","sourcesContent":["import { existsSync, readFileSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { join, resolve } from \"node:path\";\n\nexport interface ServerConfig {\n\thost: string;\n\tport: number;\n\tauthToken: string | undefined;\n\tsessionStoreDir: string;\n\tuploadDir: string;\n}\n\nconst DEFAULT_CONFIG: ServerConfig = {\n\thost: \"127.0.0.1\",\n\tport: 4217,\n\tauthToken: undefined,\n\tsessionStoreDir: resolve(\".pi\", \"pi-server\", \"sessions\"),\n\tuploadDir: join(homedir(), \".pi\", \"upload_files\"),\n};\n\nfunction loadConfigFile(): Partial<ServerConfig> {\n\tconst configPath = process.env.PI_SERVER_CONFIG;\n\tif (!configPath) return {};\n\tconst resolved = resolve(configPath);\n\tif (!existsSync(resolved)) {\n\t\tthrow new Error(`PI_SERVER_CONFIG file not found: ${resolved}`);\n\t}\n\ttry {\n\t\tconst content = readFileSync(resolved, \"utf-8\");\n\t\treturn JSON.parse(content) as Partial<ServerConfig>;\n\t} catch (err) {\n\t\tthrow new Error(`PI_SERVER_CONFIG file contains invalid JSON: ${resolved}`, { cause: err });\n\t}\n}\n\nexport function loadConfig(overrides?: Partial<ServerConfig>): ServerConfig {\n\tconst fileConfig = loadConfigFile();\n\n\treturn {\n\t\thost: overrides?.host ?? process.env.PI_SERVER_HOST ?? fileConfig.host ?? DEFAULT_CONFIG.host,\n\t\tport:\n\t\t\toverrides?.port ??\n\t\t\t(process.env.PI_SERVER_PORT ? parseInt(process.env.PI_SERVER_PORT, 10) : undefined) ??\n\t\t\tfileConfig.port ??\n\t\t\tDEFAULT_CONFIG.port,\n\t\tauthToken:\n\t\t\toverrides?.authToken ?? process.env.PI_SERVER_AUTH_TOKEN ?? fileConfig.authToken ?? DEFAULT_CONFIG.authToken,\n\t\tsessionStoreDir: resolve(\n\t\t\toverrides?.sessionStoreDir ??\n\t\t\t\tprocess.env.PI_SERVER_SESSION_STORE_DIR ??\n\t\t\t\tfileConfig.sessionStoreDir ??\n\t\t\t\tDEFAULT_CONFIG.sessionStoreDir,\n\t\t),\n\t\tuploadDir: resolve(\n\t\t\toverrides?.uploadDir ?? process.env.PI_SERVER_UPLOAD_DIR ?? fileConfig.uploadDir ?? DEFAULT_CONFIG.uploadDir,\n\t\t),\n\t};\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"error-codes.d.ts","sourceRoot":"","sources":["../src/error-codes.ts"],"names":[],"mappings":"AAAA,8DAA8D;AAE9D,eAAO,MAAM,iBAAiB;;;;;;;;;;;;CAYpB,CAAC;AAEX,MAAM,MAAM,iBAAiB,GAAG,CAAC,OAAO,iBAAiB,CAAC,CAAC,MAAM,OAAO,iBAAiB,CAAC,CAAC;AAE3F,MAAM,WAAW,qBAAqB;IACrC,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,iBAAiB,CAAC;IACzB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAClC;AAED,qBAAa,aAAc,SAAQ,KAAK;IACvC,SAAgB,IAAI,EAAE,iBAAiB,CAAC;IACxC,SAAgB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAElD,YAAY,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,iBAAiB,EAAE,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAKtF;CACD;AAED,wBAAgB,+BAA+B,CAAC,IAAI,EAAE,iBAAiB,GAAG,SAAS,GAAG,OAAO,CAM5F;AAED,wBAAgB,mCAAmC,CAAC,IAAI,EAAE,iBAAiB,GAAG,SAAS,GAAG,OAAO,CAQhG","sourcesContent":["/** Structured error codes for the pi-server HTTP protocol. */\n\nexport const PiServerErrorCode = {\n\tSESSION_NOT_FOUND: \"SESSION_NOT_FOUND\",\n\tSESSION_NO_STATIC_CONTEXT: \"SESSION_NO_STATIC_CONTEXT\",\n\tPARENT_ENTRY_NOT_FOUND: \"PARENT_ENTRY_NOT_FOUND\",\n\tLEAF_ID_NOT_FOUND: \"LEAF_ID_NOT_FOUND\",\n\tENTRY_ALREADY_EXISTS: \"ENTRY_ALREADY_EXISTS\",\n\tINVALID_REQUEST: \"INVALID_REQUEST\",\n\tREQUIRED_FIELD_MISSING: \"REQUIRED_FIELD_MISSING\",\n\tRUN_NOT_FOUND: \"RUN_NOT_FOUND\",\n\tRUN_IN_PROGRESS: \"RUN_IN_PROGRESS\",\n\tSESSION_STATE_CONFLICT: \"SESSION_STATE_CONFLICT\",\n\tINTERNAL_ERROR: \"INTERNAL_ERROR\",\n} as const;\n\nexport type PiServerErrorCode = (typeof PiServerErrorCode)[keyof typeof PiServerErrorCode];\n\nexport interface PiServerErrorResponse {\n\terror: string;\n\tcode?: PiServerErrorCode;\n\tdetails?: Record<string, unknown>;\n}\n\nexport class PiServerError extends Error {\n\tpublic readonly code: PiServerErrorCode;\n\tpublic readonly details?: Record<string, unknown>;\n\n\tconstructor(message: string, code: PiServerErrorCode, details?: Record<string, unknown>) {\n\t\tsuper(message);\n\t\tthis.name = \"PiServerError\";\n\t\tthis.code = code;\n\t\tthis.details = details;\n\t}\n}\n\nexport function isRecoverableTreeDivergenceCode(code: PiServerErrorCode | undefined): boolean {\n\treturn (\n\t\tcode === PiServerErrorCode.PARENT_ENTRY_NOT_FOUND ||\n\t\tcode === PiServerErrorCode.LEAF_ID_NOT_FOUND ||\n\t\tcode === PiServerErrorCode.ENTRY_ALREADY_EXISTS\n\t);\n}\n\nexport function isRecoverableMissingServerStateCode(code: PiServerErrorCode | undefined): boolean {\n\treturn (\n\t\tcode === PiServerErrorCode.SESSION_NOT_FOUND ||\n\t\tcode === PiServerErrorCode.SESSION_NO_STATIC_CONTEXT ||\n\t\tcode === PiServerErrorCode.PARENT_ENTRY_NOT_FOUND ||\n\t\tcode === PiServerErrorCode.LEAF_ID_NOT_FOUND ||\n\t\tcode === PiServerErrorCode.ENTRY_ALREADY_EXISTS\n\t);\n}\n"]}
1
+ {"version":3,"file":"error-codes.d.ts","sourceRoot":"","sources":["../src/error-codes.ts"],"names":[],"mappings":"AAAA,8DAA8D;AAE9D,eAAO,MAAM,iBAAiB;;;;;;;;;;;;CAYpB,CAAC;AAEX,MAAM,MAAM,iBAAiB,GAAG,CAAC,OAAO,iBAAiB,CAAC,CAAC,MAAM,OAAO,iBAAiB,CAAC,CAAC;AAE3F,MAAM,WAAW,qBAAqB;IACrC,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,iBAAiB,CAAC;IACzB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAClC;AAED,qBAAa,aAAc,SAAQ,KAAK;IACvC,SAAgB,IAAI,EAAE,iBAAiB,CAAC;IACxC,SAAgB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;gBAEtC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,iBAAiB,EAAE,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;CAMvF;AAED,wBAAgB,+BAA+B,CAAC,IAAI,EAAE,iBAAiB,GAAG,SAAS,GAAG,OAAO,CAM5F;AAED,wBAAgB,mCAAmC,CAAC,IAAI,EAAE,iBAAiB,GAAG,SAAS,GAAG,OAAO,CAQhG"}
@@ -1 +1 @@
1
- {"version":3,"file":"error-codes.js","sourceRoot":"","sources":["../src/error-codes.ts"],"names":[],"mappings":"AAAA,8DAA8D;AAE9D,MAAM,CAAC,MAAM,iBAAiB,GAAG;IAChC,iBAAiB,EAAE,mBAAmB;IACtC,yBAAyB,EAAE,2BAA2B;IACtD,sBAAsB,EAAE,wBAAwB;IAChD,iBAAiB,EAAE,mBAAmB;IACtC,oBAAoB,EAAE,sBAAsB;IAC5C,eAAe,EAAE,iBAAiB;IAClC,sBAAsB,EAAE,wBAAwB;IAChD,aAAa,EAAE,eAAe;IAC9B,eAAe,EAAE,iBAAiB;IAClC,sBAAsB,EAAE,wBAAwB;IAChD,cAAc,EAAE,gBAAgB;CACvB,CAAC;AAUX,MAAM,OAAO,aAAc,SAAQ,KAAK;IACvB,IAAI,CAAoB;IACxB,OAAO,CAA2B;IAElD,YAAY,OAAe,EAAE,IAAuB,EAAE,OAAiC,EAAE;QACxF,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,eAAe,CAAC;QAC5B,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;QACjB,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;IAAA,CACvB;CACD;AAED,MAAM,UAAU,+BAA+B,CAAC,IAAmC,EAAW;IAC7F,OAAO,CACN,IAAI,KAAK,iBAAiB,CAAC,sBAAsB;QACjD,IAAI,KAAK,iBAAiB,CAAC,iBAAiB;QAC5C,IAAI,KAAK,iBAAiB,CAAC,oBAAoB,CAC/C,CAAC;AAAA,CACF;AAED,MAAM,UAAU,mCAAmC,CAAC,IAAmC,EAAW;IACjG,OAAO,CACN,IAAI,KAAK,iBAAiB,CAAC,iBAAiB;QAC5C,IAAI,KAAK,iBAAiB,CAAC,yBAAyB;QACpD,IAAI,KAAK,iBAAiB,CAAC,sBAAsB;QACjD,IAAI,KAAK,iBAAiB,CAAC,iBAAiB;QAC5C,IAAI,KAAK,iBAAiB,CAAC,oBAAoB,CAC/C,CAAC;AAAA,CACF","sourcesContent":["/** Structured error codes for the pi-server HTTP protocol. */\n\nexport const PiServerErrorCode = {\n\tSESSION_NOT_FOUND: \"SESSION_NOT_FOUND\",\n\tSESSION_NO_STATIC_CONTEXT: \"SESSION_NO_STATIC_CONTEXT\",\n\tPARENT_ENTRY_NOT_FOUND: \"PARENT_ENTRY_NOT_FOUND\",\n\tLEAF_ID_NOT_FOUND: \"LEAF_ID_NOT_FOUND\",\n\tENTRY_ALREADY_EXISTS: \"ENTRY_ALREADY_EXISTS\",\n\tINVALID_REQUEST: \"INVALID_REQUEST\",\n\tREQUIRED_FIELD_MISSING: \"REQUIRED_FIELD_MISSING\",\n\tRUN_NOT_FOUND: \"RUN_NOT_FOUND\",\n\tRUN_IN_PROGRESS: \"RUN_IN_PROGRESS\",\n\tSESSION_STATE_CONFLICT: \"SESSION_STATE_CONFLICT\",\n\tINTERNAL_ERROR: \"INTERNAL_ERROR\",\n} as const;\n\nexport type PiServerErrorCode = (typeof PiServerErrorCode)[keyof typeof PiServerErrorCode];\n\nexport interface PiServerErrorResponse {\n\terror: string;\n\tcode?: PiServerErrorCode;\n\tdetails?: Record<string, unknown>;\n}\n\nexport class PiServerError extends Error {\n\tpublic readonly code: PiServerErrorCode;\n\tpublic readonly details?: Record<string, unknown>;\n\n\tconstructor(message: string, code: PiServerErrorCode, details?: Record<string, unknown>) {\n\t\tsuper(message);\n\t\tthis.name = \"PiServerError\";\n\t\tthis.code = code;\n\t\tthis.details = details;\n\t}\n}\n\nexport function isRecoverableTreeDivergenceCode(code: PiServerErrorCode | undefined): boolean {\n\treturn (\n\t\tcode === PiServerErrorCode.PARENT_ENTRY_NOT_FOUND ||\n\t\tcode === PiServerErrorCode.LEAF_ID_NOT_FOUND ||\n\t\tcode === PiServerErrorCode.ENTRY_ALREADY_EXISTS\n\t);\n}\n\nexport function isRecoverableMissingServerStateCode(code: PiServerErrorCode | undefined): boolean {\n\treturn (\n\t\tcode === PiServerErrorCode.SESSION_NOT_FOUND ||\n\t\tcode === PiServerErrorCode.SESSION_NO_STATIC_CONTEXT ||\n\t\tcode === PiServerErrorCode.PARENT_ENTRY_NOT_FOUND ||\n\t\tcode === PiServerErrorCode.LEAF_ID_NOT_FOUND ||\n\t\tcode === PiServerErrorCode.ENTRY_ALREADY_EXISTS\n\t);\n}\n"]}
1
+ {"version":3,"file":"error-codes.js","sourceRoot":"","sources":["../src/error-codes.ts"],"names":[],"mappings":"AAAA,8DAA8D;AAE9D,MAAM,CAAC,MAAM,iBAAiB,GAAG;IAChC,iBAAiB,EAAE,mBAAmB;IACtC,yBAAyB,EAAE,2BAA2B;IACtD,sBAAsB,EAAE,wBAAwB;IAChD,iBAAiB,EAAE,mBAAmB;IACtC,oBAAoB,EAAE,sBAAsB;IAC5C,eAAe,EAAE,iBAAiB;IAClC,sBAAsB,EAAE,wBAAwB;IAChD,aAAa,EAAE,eAAe;IAC9B,eAAe,EAAE,iBAAiB;IAClC,sBAAsB,EAAE,wBAAwB;IAChD,cAAc,EAAE,gBAAgB;CACvB,CAAC;AAUX,MAAM,OAAO,aAAc,SAAQ,KAAK;IACvB,IAAI,CAAoB;IACxB,OAAO,CAA2B;IAElD,YAAY,OAAe,EAAE,IAAuB,EAAE,OAAiC;QACtF,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,eAAe,CAAC;QAC5B,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;QACjB,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;IACxB,CAAC;CACD;AAED,MAAM,UAAU,+BAA+B,CAAC,IAAmC;IAClF,OAAO,CACN,IAAI,KAAK,iBAAiB,CAAC,sBAAsB;QACjD,IAAI,KAAK,iBAAiB,CAAC,iBAAiB;QAC5C,IAAI,KAAK,iBAAiB,CAAC,oBAAoB,CAC/C,CAAC;AACH,CAAC;AAED,MAAM,UAAU,mCAAmC,CAAC,IAAmC;IACtF,OAAO,CACN,IAAI,KAAK,iBAAiB,CAAC,iBAAiB;QAC5C,IAAI,KAAK,iBAAiB,CAAC,yBAAyB;QACpD,IAAI,KAAK,iBAAiB,CAAC,sBAAsB;QACjD,IAAI,KAAK,iBAAiB,CAAC,iBAAiB;QAC5C,IAAI,KAAK,iBAAiB,CAAC,oBAAoB,CAC/C,CAAC;AACH,CAAC","sourcesContent":["/** Structured error codes for the pi-server HTTP protocol. */\n\nexport const PiServerErrorCode = {\n\tSESSION_NOT_FOUND: \"SESSION_NOT_FOUND\",\n\tSESSION_NO_STATIC_CONTEXT: \"SESSION_NO_STATIC_CONTEXT\",\n\tPARENT_ENTRY_NOT_FOUND: \"PARENT_ENTRY_NOT_FOUND\",\n\tLEAF_ID_NOT_FOUND: \"LEAF_ID_NOT_FOUND\",\n\tENTRY_ALREADY_EXISTS: \"ENTRY_ALREADY_EXISTS\",\n\tINVALID_REQUEST: \"INVALID_REQUEST\",\n\tREQUIRED_FIELD_MISSING: \"REQUIRED_FIELD_MISSING\",\n\tRUN_NOT_FOUND: \"RUN_NOT_FOUND\",\n\tRUN_IN_PROGRESS: \"RUN_IN_PROGRESS\",\n\tSESSION_STATE_CONFLICT: \"SESSION_STATE_CONFLICT\",\n\tINTERNAL_ERROR: \"INTERNAL_ERROR\",\n} as const;\n\nexport type PiServerErrorCode = (typeof PiServerErrorCode)[keyof typeof PiServerErrorCode];\n\nexport interface PiServerErrorResponse {\n\terror: string;\n\tcode?: PiServerErrorCode;\n\tdetails?: Record<string, unknown>;\n}\n\nexport class PiServerError extends Error {\n\tpublic readonly code: PiServerErrorCode;\n\tpublic readonly details?: Record<string, unknown>;\n\n\tconstructor(message: string, code: PiServerErrorCode, details?: Record<string, unknown>) {\n\t\tsuper(message);\n\t\tthis.name = \"PiServerError\";\n\t\tthis.code = code;\n\t\tthis.details = details;\n\t}\n}\n\nexport function isRecoverableTreeDivergenceCode(code: PiServerErrorCode | undefined): boolean {\n\treturn (\n\t\tcode === PiServerErrorCode.PARENT_ENTRY_NOT_FOUND ||\n\t\tcode === PiServerErrorCode.LEAF_ID_NOT_FOUND ||\n\t\tcode === PiServerErrorCode.ENTRY_ALREADY_EXISTS\n\t);\n}\n\nexport function isRecoverableMissingServerStateCode(code: PiServerErrorCode | undefined): boolean {\n\treturn (\n\t\tcode === PiServerErrorCode.SESSION_NOT_FOUND ||\n\t\tcode === PiServerErrorCode.SESSION_NO_STATIC_CONTEXT ||\n\t\tcode === PiServerErrorCode.PARENT_ENTRY_NOT_FOUND ||\n\t\tcode === PiServerErrorCode.LEAF_ID_NOT_FOUND ||\n\t\tcode === PiServerErrorCode.ENTRY_ALREADY_EXISTS\n\t);\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"event-encoding.d.ts","sourceRoot":"","sources":["../src/event-encoding.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,+BAA+B,CAAC;AAEhF,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,0BAA0B,GAAG,MAAM,CAE1E;AAED,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAExD;AAED,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,0BAA0B,GAAG,SAAS,CAKpF","sourcesContent":["import type { ProxyAssistantMessageEvent } from \"@earendil-works/pi-agent-core\";\n\nexport function encodeProxyEvent(event: ProxyAssistantMessageEvent): string {\n\treturn `data: ${JSON.stringify(event)}\\n\\n`;\n}\n\nexport function encodeErrorEvent(message: string): string {\n\treturn `data: ${JSON.stringify({ type: \"error\", reason: \"error\", errorMessage: message, usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, totalTokens: 0, cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 } } })}\\n\\n`;\n}\n\nexport function parseProxyEvent(line: string): ProxyAssistantMessageEvent | undefined {\n\tif (!line.startsWith(\"data: \")) return undefined;\n\tconst data = line.slice(6).trim();\n\tif (!data) return undefined;\n\treturn JSON.parse(data) as ProxyAssistantMessageEvent;\n}\n"]}
1
+ {"version":3,"file":"event-encoding.d.ts","sourceRoot":"","sources":["../src/event-encoding.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,+BAA+B,CAAC;AAEhF,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,0BAA0B,GAAG,MAAM,CAE1E;AAED,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAExD;AAED,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,0BAA0B,GAAG,SAAS,CAKpF"}
@@ -1 +1 @@
1
- {"version":3,"file":"event-encoding.js","sourceRoot":"","sources":["../src/event-encoding.ts"],"names":[],"mappings":"AAEA,MAAM,UAAU,gBAAgB,CAAC,KAAiC,EAAU;IAC3E,OAAO,SAAS,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,MAAM,CAAC;AAAA,CAC5C;AAED,MAAM,UAAU,gBAAgB,CAAC,OAAe,EAAU;IACzD,OAAO,SAAS,IAAI,CAAC,SAAS,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,YAAY,EAAE,OAAO,EAAE,KAAK,EAAE,EAAE,KAAK,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,SAAS,EAAE,CAAC,EAAE,UAAU,EAAE,CAAC,EAAE,WAAW,EAAE,CAAC,EAAE,IAAI,EAAE,EAAE,KAAK,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,SAAS,EAAE,CAAC,EAAE,UAAU,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,EAAE,EAAE,CAAC,MAAM,CAAC;AAAA,CACnP;AAED,MAAM,UAAU,eAAe,CAAC,IAAY,EAA0C;IACrF,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,QAAQ,CAAC;QAAE,OAAO,SAAS,CAAC;IACjD,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;IAClC,IAAI,CAAC,IAAI;QAAE,OAAO,SAAS,CAAC;IAC5B,OAAO,IAAI,CAAC,KAAK,CAAC,IAAI,CAA+B,CAAC;AAAA,CACtD","sourcesContent":["import type { ProxyAssistantMessageEvent } from \"@earendil-works/pi-agent-core\";\n\nexport function encodeProxyEvent(event: ProxyAssistantMessageEvent): string {\n\treturn `data: ${JSON.stringify(event)}\\n\\n`;\n}\n\nexport function encodeErrorEvent(message: string): string {\n\treturn `data: ${JSON.stringify({ type: \"error\", reason: \"error\", errorMessage: message, usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, totalTokens: 0, cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 } } })}\\n\\n`;\n}\n\nexport function parseProxyEvent(line: string): ProxyAssistantMessageEvent | undefined {\n\tif (!line.startsWith(\"data: \")) return undefined;\n\tconst data = line.slice(6).trim();\n\tif (!data) return undefined;\n\treturn JSON.parse(data) as ProxyAssistantMessageEvent;\n}\n"]}
1
+ {"version":3,"file":"event-encoding.js","sourceRoot":"","sources":["../src/event-encoding.ts"],"names":[],"mappings":"AAEA,MAAM,UAAU,gBAAgB,CAAC,KAAiC;IACjE,OAAO,SAAS,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,MAAM,CAAC;AAC7C,CAAC;AAED,MAAM,UAAU,gBAAgB,CAAC,OAAe;IAC/C,OAAO,SAAS,IAAI,CAAC,SAAS,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,YAAY,EAAE,OAAO,EAAE,KAAK,EAAE,EAAE,KAAK,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,SAAS,EAAE,CAAC,EAAE,UAAU,EAAE,CAAC,EAAE,WAAW,EAAE,CAAC,EAAE,IAAI,EAAE,EAAE,KAAK,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,SAAS,EAAE,CAAC,EAAE,UAAU,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,EAAE,EAAE,CAAC,MAAM,CAAC;AACpP,CAAC;AAED,MAAM,UAAU,eAAe,CAAC,IAAY;IAC3C,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,QAAQ,CAAC;QAAE,OAAO,SAAS,CAAC;IACjD,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;IAClC,IAAI,CAAC,IAAI;QAAE,OAAO,SAAS,CAAC;IAC5B,OAAO,IAAI,CAAC,KAAK,CAAC,IAAI,CAA+B,CAAC;AACvD,CAAC","sourcesContent":["import type { ProxyAssistantMessageEvent } from \"@earendil-works/pi-agent-core\";\n\nexport function encodeProxyEvent(event: ProxyAssistantMessageEvent): string {\n\treturn `data: ${JSON.stringify(event)}\\n\\n`;\n}\n\nexport function encodeErrorEvent(message: string): string {\n\treturn `data: ${JSON.stringify({ type: \"error\", reason: \"error\", errorMessage: message, usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, totalTokens: 0, cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 } } })}\\n\\n`;\n}\n\nexport function parseProxyEvent(line: string): ProxyAssistantMessageEvent | undefined {\n\tif (!line.startsWith(\"data: \")) return undefined;\n\tconst data = line.slice(6).trim();\n\tif (!data) return undefined;\n\treturn JSON.parse(data) as ProxyAssistantMessageEvent;\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,KAAK,YAAY,EAAE,MAAM,aAAa,CAAC;AAC5D,OAAO,EACN,mCAAmC,EACnC,+BAA+B,EAC/B,aAAa,EACb,iBAAiB,EACjB,KAAK,qBAAqB,GAC1B,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AAC1F,OAAO,EAAE,cAAc,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC1D,OAAO,EACN,cAAc,EACd,gBAAgB,EAChB,aAAa,EACb,kBAAkB,EAClB,kBAAkB,EAClB,UAAU,EACV,kBAAkB,EAClB,YAAY,EACZ,KAAK,qBAAqB,EAC1B,mBAAmB,EACnB,KAAK,YAAY,EACjB,KAAK,oBAAoB,EACzB,KAAK,cAAc,EACnB,gBAAgB,GAChB,MAAM,oBAAoB,CAAC","sourcesContent":["export { loadConfig, type ServerConfig } from \"./config.ts\";\nexport {\n\tisRecoverableMissingServerStateCode,\n\tisRecoverableTreeDivergenceCode,\n\tPiServerError,\n\tPiServerErrorCode,\n\ttype PiServerErrorResponse,\n} from \"./error-codes.ts\";\nexport { encodeErrorEvent, encodeProxyEvent, parseProxyEvent } from \"./event-encoding.ts\";\nexport { createPiServer, startServer } from \"./server.ts\";\nexport {\n\tappendMessages,\n\tclearAllSessions,\n\tdeleteSession,\n\texportSessionState,\n\tgetOrCreateSession,\n\tgetSession,\n\thashSessionEntries,\n\tlistSessions,\n\ttype PersistedSessionState,\n\trestoreSessionState,\n\ttype SessionState,\n\ttype SessionStaticContext,\n\ttype SessionSummary,\n\tsetStaticContext,\n} from \"./session-store.ts\";\n"]}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,KAAK,YAAY,EAAE,MAAM,aAAa,CAAC;AAC5D,OAAO,EACN,mCAAmC,EACnC,+BAA+B,EAC/B,aAAa,EACb,iBAAiB,EACjB,KAAK,qBAAqB,GAC1B,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AAC1F,OAAO,EAAE,cAAc,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC1D,OAAO,EACN,cAAc,EACd,gBAAgB,EAChB,aAAa,EACb,kBAAkB,EAClB,kBAAkB,EAClB,UAAU,EACV,kBAAkB,EAClB,YAAY,EACZ,KAAK,qBAAqB,EAC1B,mBAAmB,EACnB,KAAK,YAAY,EACjB,KAAK,oBAAoB,EACzB,KAAK,cAAc,EACnB,gBAAgB,GAChB,MAAM,oBAAoB,CAAC"}
@@ -1,10 +1,11 @@
1
1
  import type { SessionTreeEntry } from "@earendil-works/pi-agent-core";
2
- import type { Tool } from "@earendil-works/pi-ai";
2
+ import type { Message, Tool } from "@earendil-works/pi-ai";
3
3
  export interface PiServerStaticContext {
4
4
  systemPrompt?: string;
5
5
  tools?: Tool[];
6
6
  }
7
7
  export declare function hashPiServerStaticContext(context: PiServerStaticContext | undefined): string;
8
+ export declare function hashPiServerProviderMessages(messages: readonly Message[]): string;
8
9
  export declare const PI_SERVER_EMPTY_TREE_HASH: string;
9
10
  export declare function hashPiServerTreeEntry(entry: SessionTreeEntry): string;
10
11
  export declare function appendPiServerTreeHash(previousHash: string, entryHash: string): string;
@@ -1 +1 @@
1
- {"version":3,"file":"pi-server-protocol.d.ts","sourceRoot":"","sources":["../src/pi-server-protocol.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,+BAA+B,CAAC;AACtE,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,uBAAuB,CAAC;AAElD,MAAM,WAAW,qBAAqB;IACrC,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,KAAK,CAAC,EAAE,IAAI,EAAE,CAAC;CACf;AAED,wBAAgB,yBAAyB,CAAC,OAAO,EAAE,qBAAqB,GAAG,SAAS,GAAG,MAAM,CAW5F;AAED,eAAO,MAAM,yBAAyB,QAA0D,CAAC;AAEjG,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,gBAAgB,GAAG,MAAM,CAErE;AAED,wBAAgB,sBAAsB,CAAC,YAAY,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,CAEtF;AAED,wBAAgB,6BAA6B,CAAC,OAAO,EAAE,gBAAgB,EAAE,GAAG,MAAM,EAAE,CAMnF;AAED,wBAAgB,0BAA0B,CAAC,OAAO,EAAE,gBAAgB,EAAE,GAAG,MAAM,CAE9E","sourcesContent":["import { createHash } from \"node:crypto\";\nimport type { SessionTreeEntry } from \"@earendil-works/pi-agent-core\";\nimport type { Tool } from \"@earendil-works/pi-ai\";\n\nexport interface PiServerStaticContext {\n\tsystemPrompt?: string;\n\ttools?: Tool[];\n}\n\nexport function hashPiServerStaticContext(context: PiServerStaticContext | undefined): string {\n\tif (!context) return \"\";\n\tconst canonical = {\n\t\tsystemPrompt: context.systemPrompt,\n\t\ttools: context.tools?.map((tool) => ({\n\t\t\tname: tool.name,\n\t\t\tdescription: tool.description,\n\t\t\tparameters: tool.parameters,\n\t\t})),\n\t};\n\treturn createHash(\"sha256\").update(JSON.stringify(canonical)).digest(\"hex\");\n}\n\nexport const PI_SERVER_EMPTY_TREE_HASH = createHash(\"sha256\").update(\"pi-tree-v1\").digest(\"hex\");\n\nexport function hashPiServerTreeEntry(entry: SessionTreeEntry): string {\n\treturn createHash(\"sha256\").update(JSON.stringify(entry)).digest(\"hex\");\n}\n\nexport function appendPiServerTreeHash(previousHash: string, entryHash: string): string {\n\treturn createHash(\"sha256\").update(`${previousHash}:${entryHash}`).digest(\"hex\");\n}\n\nexport function buildPiServerTreePrefixHashes(entries: SessionTreeEntry[]): string[] {\n\tconst hashes = [PI_SERVER_EMPTY_TREE_HASH];\n\tfor (const entry of entries) {\n\t\thashes.push(appendPiServerTreeHash(hashes[hashes.length - 1], hashPiServerTreeEntry(entry)));\n\t}\n\treturn hashes;\n}\n\nexport function hashPiServerSessionEntries(entries: SessionTreeEntry[]): string {\n\treturn buildPiServerTreePrefixHashes(entries)[entries.length];\n}\n"]}
1
+ {"version":3,"file":"pi-server-protocol.d.ts","sourceRoot":"","sources":["../src/pi-server-protocol.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,+BAA+B,CAAC;AACtE,OAAO,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,uBAAuB,CAAC;AAE3D,MAAM,WAAW,qBAAqB;IACrC,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,KAAK,CAAC,EAAE,IAAI,EAAE,CAAC;CACf;AAED,wBAAgB,yBAAyB,CAAC,OAAO,EAAE,qBAAqB,GAAG,SAAS,GAAG,MAAM,CAW5F;AAeD,wBAAgB,4BAA4B,CAAC,QAAQ,EAAE,SAAS,OAAO,EAAE,GAAG,MAAM,CAIjF;AAED,eAAO,MAAM,yBAAyB,QAA0D,CAAC;AAEjG,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,gBAAgB,GAAG,MAAM,CAErE;AAED,wBAAgB,sBAAsB,CAAC,YAAY,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,CAEtF;AAED,wBAAgB,6BAA6B,CAAC,OAAO,EAAE,gBAAgB,EAAE,GAAG,MAAM,EAAE,CAMnF;AAED,wBAAgB,0BAA0B,CAAC,OAAO,EAAE,gBAAgB,EAAE,GAAG,MAAM,CAE9E"}
@@ -12,6 +12,22 @@ export function hashPiServerStaticContext(context) {
12
12
  };
13
13
  return createHash("sha256").update(JSON.stringify(canonical)).digest("hex");
14
14
  }
15
+ function canonicalizeJson(value) {
16
+ if (Array.isArray(value))
17
+ return value.map(canonicalizeJson);
18
+ if (value && typeof value === "object") {
19
+ const record = value;
20
+ return Object.fromEntries(Object.entries(record)
21
+ .sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0))
22
+ .map(([key, item]) => [key, canonicalizeJson(item)]));
23
+ }
24
+ return value;
25
+ }
26
+ export function hashPiServerProviderMessages(messages) {
27
+ return createHash("sha256")
28
+ .update(JSON.stringify(canonicalizeJson(messages)))
29
+ .digest("hex");
30
+ }
15
31
  export const PI_SERVER_EMPTY_TREE_HASH = createHash("sha256").update("pi-tree-v1").digest("hex");
16
32
  export function hashPiServerTreeEntry(entry) {
17
33
  return createHash("sha256").update(JSON.stringify(entry)).digest("hex");
@@ -1 +1 @@
1
- {"version":3,"file":"pi-server-protocol.js","sourceRoot":"","sources":["../src/pi-server-protocol.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AASzC,MAAM,UAAU,yBAAyB,CAAC,OAA0C,EAAU;IAC7F,IAAI,CAAC,OAAO;QAAE,OAAO,EAAE,CAAC;IACxB,MAAM,SAAS,GAAG;QACjB,YAAY,EAAE,OAAO,CAAC,YAAY;QAClC,KAAK,EAAE,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;YACpC,IAAI,EAAE,IAAI,CAAC,IAAI;YACf,WAAW,EAAE,IAAI,CAAC,WAAW;YAC7B,UAAU,EAAE,IAAI,CAAC,UAAU;SAC3B,CAAC,CAAC;KACH,CAAC;IACF,OAAO,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,SAAS,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;AAAA,CAC5E;AAED,MAAM,CAAC,MAAM,yBAAyB,GAAG,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,YAAY,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;AAEjG,MAAM,UAAU,qBAAqB,CAAC,KAAuB,EAAU;IACtE,OAAO,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;AAAA,CACxE;AAED,MAAM,UAAU,sBAAsB,CAAC,YAAoB,EAAE,SAAiB,EAAU;IACvF,OAAO,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,GAAG,YAAY,IAAI,SAAS,EAAE,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;AAAA,CACjF;AAED,MAAM,UAAU,6BAA6B,CAAC,OAA2B,EAAY;IACpF,MAAM,MAAM,GAAG,CAAC,yBAAyB,CAAC,CAAC;IAC3C,KAAK,MAAM,KAAK,IAAI,OAAO,EAAE,CAAC;QAC7B,MAAM,CAAC,IAAI,CAAC,sBAAsB,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,EAAE,qBAAqB,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;IAC9F,CAAC;IACD,OAAO,MAAM,CAAC;AAAA,CACd;AAED,MAAM,UAAU,0BAA0B,CAAC,OAA2B,EAAU;IAC/E,OAAO,6BAA6B,CAAC,OAAO,CAAC,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;AAAA,CAC9D","sourcesContent":["import { createHash } from \"node:crypto\";\nimport type { SessionTreeEntry } from \"@earendil-works/pi-agent-core\";\nimport type { Tool } from \"@earendil-works/pi-ai\";\n\nexport interface PiServerStaticContext {\n\tsystemPrompt?: string;\n\ttools?: Tool[];\n}\n\nexport function hashPiServerStaticContext(context: PiServerStaticContext | undefined): string {\n\tif (!context) return \"\";\n\tconst canonical = {\n\t\tsystemPrompt: context.systemPrompt,\n\t\ttools: context.tools?.map((tool) => ({\n\t\t\tname: tool.name,\n\t\t\tdescription: tool.description,\n\t\t\tparameters: tool.parameters,\n\t\t})),\n\t};\n\treturn createHash(\"sha256\").update(JSON.stringify(canonical)).digest(\"hex\");\n}\n\nexport const PI_SERVER_EMPTY_TREE_HASH = createHash(\"sha256\").update(\"pi-tree-v1\").digest(\"hex\");\n\nexport function hashPiServerTreeEntry(entry: SessionTreeEntry): string {\n\treturn createHash(\"sha256\").update(JSON.stringify(entry)).digest(\"hex\");\n}\n\nexport function appendPiServerTreeHash(previousHash: string, entryHash: string): string {\n\treturn createHash(\"sha256\").update(`${previousHash}:${entryHash}`).digest(\"hex\");\n}\n\nexport function buildPiServerTreePrefixHashes(entries: SessionTreeEntry[]): string[] {\n\tconst hashes = [PI_SERVER_EMPTY_TREE_HASH];\n\tfor (const entry of entries) {\n\t\thashes.push(appendPiServerTreeHash(hashes[hashes.length - 1], hashPiServerTreeEntry(entry)));\n\t}\n\treturn hashes;\n}\n\nexport function hashPiServerSessionEntries(entries: SessionTreeEntry[]): string {\n\treturn buildPiServerTreePrefixHashes(entries)[entries.length];\n}\n"]}
1
+ {"version":3,"file":"pi-server-protocol.js","sourceRoot":"","sources":["../src/pi-server-protocol.ts"],"names":[],"mappings":"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","sourcesContent":["import { createHash } from \"node:crypto\";\nimport type { SessionTreeEntry } from \"@earendil-works/pi-agent-core\";\nimport type { Message, Tool } from \"@earendil-works/pi-ai\";\n\nexport interface PiServerStaticContext {\n\tsystemPrompt?: string;\n\ttools?: Tool[];\n}\n\nexport function hashPiServerStaticContext(context: PiServerStaticContext | undefined): string {\n\tif (!context) return \"\";\n\tconst canonical = {\n\t\tsystemPrompt: context.systemPrompt,\n\t\ttools: context.tools?.map((tool) => ({\n\t\t\tname: tool.name,\n\t\t\tdescription: tool.description,\n\t\t\tparameters: tool.parameters,\n\t\t})),\n\t};\n\treturn createHash(\"sha256\").update(JSON.stringify(canonical)).digest(\"hex\");\n}\n\nfunction canonicalizeJson(value: unknown): unknown {\n\tif (Array.isArray(value)) return value.map(canonicalizeJson);\n\tif (value && typeof value === \"object\") {\n\t\tconst record = value as Record<string, unknown>;\n\t\treturn Object.fromEntries(\n\t\t\tObject.entries(record)\n\t\t\t\t.sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0))\n\t\t\t\t.map(([key, item]) => [key, canonicalizeJson(item)]),\n\t\t);\n\t}\n\treturn value;\n}\n\nexport function hashPiServerProviderMessages(messages: readonly Message[]): string {\n\treturn createHash(\"sha256\")\n\t\t.update(JSON.stringify(canonicalizeJson(messages)))\n\t\t.digest(\"hex\");\n}\n\nexport const PI_SERVER_EMPTY_TREE_HASH = createHash(\"sha256\").update(\"pi-tree-v1\").digest(\"hex\");\n\nexport function hashPiServerTreeEntry(entry: SessionTreeEntry): string {\n\treturn createHash(\"sha256\").update(JSON.stringify(entry)).digest(\"hex\");\n}\n\nexport function appendPiServerTreeHash(previousHash: string, entryHash: string): string {\n\treturn createHash(\"sha256\").update(`${previousHash}:${entryHash}`).digest(\"hex\");\n}\n\nexport function buildPiServerTreePrefixHashes(entries: SessionTreeEntry[]): string[] {\n\tconst hashes = [PI_SERVER_EMPTY_TREE_HASH];\n\tfor (const entry of entries) {\n\t\thashes.push(appendPiServerTreeHash(hashes[hashes.length - 1], hashPiServerTreeEntry(entry)));\n\t}\n\treturn hashes;\n}\n\nexport function hashPiServerSessionEntries(entries: SessionTreeEntry[]): string {\n\treturn buildPiServerTreePrefixHashes(entries)[entries.length];\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"receive-upload.d.ts","sourceRoot":"","sources":["../src/receive-upload.ts"],"names":[],"mappings":"AASA,qBAAa,kBAAmB,SAAQ,KAAK;IAC5C,MAAM,EAAE,MAAM,CAAC;IAEf,YAAY,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAG1C;CACD;AA8DD,wBAAgB,aAAa,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAqB/F","sourcesContent":["import { existsSync, mkdirSync, mkdtempSync, renameSync, rmSync, writeFileSync } from \"node:fs\";\nimport { dirname, join } from \"node:path\";\n\ninterface UploadEntry {\n\tpath: string;\n\ttype: \"file\" | \"directory\";\n\tdata?: Buffer;\n}\n\nexport class ReceiveUploadError extends Error {\n\tstatus: number;\n\n\tconstructor(status: number, message: string) {\n\t\tsuper(message);\n\t\tthis.status = status;\n\t}\n}\n\nfunction invalid(message: string): never {\n\tthrow new ReceiveUploadError(400, message);\n}\n\nfunction validatePath(path: unknown): string {\n\tif (typeof path !== \"string\") invalid(\"entry path must be a string\");\n\tif (path.includes(\"\\\\\") || path.includes(\"\\0\")) invalid(`invalid entry path: ${path}`);\n\tif (path !== \"\" && path.split(\"/\").some((part) => part === \"\" || part === \".\" || part === \"..\")) {\n\t\tinvalid(`invalid entry path: ${path}`);\n\t}\n\treturn path;\n}\n\nfunction validateBody(body: unknown): { name: string; entries: UploadEntry[] } {\n\tif (typeof body !== \"object\" || body === null) invalid(\"upload body must be an object\");\n\tconst upload = body as { name?: unknown; entries?: unknown };\n\tif (\n\t\ttypeof upload.name !== \"string\" ||\n\t\tupload.name === \"\" ||\n\t\tupload.name === \".\" ||\n\t\tupload.name === \"..\" ||\n\t\tupload.name.includes(\"/\") ||\n\t\tupload.name.includes(\"\\\\\") ||\n\t\tupload.name.includes(\"\\0\")\n\t) {\n\t\tinvalid(\"invalid upload name\");\n\t}\n\tif (!Array.isArray(upload.entries) || upload.entries.length === 0) invalid(\"entries must be a non-empty array\");\n\n\tconst entries: UploadEntry[] = [];\n\tconst entryTypes = new Map<string, \"file\" | \"directory\">();\n\tfor (const value of upload.entries) {\n\t\tif (typeof value !== \"object\" || value === null) invalid(\"each entry must be an object\");\n\t\tconst entry = value as { path?: unknown; type?: unknown; data?: unknown };\n\t\tconst path = validatePath(entry.path);\n\t\tif (entry.type !== \"file\" && entry.type !== \"directory\") invalid(`invalid entry type: ${String(entry.type)}`);\n\t\tif (entryTypes.has(path)) invalid(`duplicate entry path: ${path}`);\n\t\tentryTypes.set(path, entry.type);\n\t\tif (entry.type === \"directory\") {\n\t\t\tif (entry.data !== undefined) invalid(`directory entry must not contain data: ${path}`);\n\t\t\tentries.push({ path, type: \"directory\" });\n\t\t\tcontinue;\n\t\t}\n\t\tif (typeof entry.data !== \"string\") invalid(`file entry data must be base64: ${path}`);\n\t\tconst data = Buffer.from(entry.data, \"base64\");\n\t\tif (data.toString(\"base64\") !== entry.data) invalid(`file entry data must be valid base64: ${path}`);\n\t\tentries.push({ path, type: \"file\", data });\n\t}\n\n\tconst rootType = entryTypes.get(\"\");\n\tif (!rootType) invalid(\"entries must include the root path\");\n\tif (rootType === \"file\" && entries.length !== 1) invalid(\"a file upload cannot contain child entries\");\n\tfor (const entry of entries) {\n\t\tif (entry.path === \"\") continue;\n\t\tconst parent = entry.path.includes(\"/\") ? entry.path.slice(0, entry.path.lastIndexOf(\"/\")) : \"\";\n\t\tif (entryTypes.get(parent) !== \"directory\") invalid(`missing directory entry: ${parent}`);\n\t}\n\treturn { name: upload.name, entries };\n}\n\nexport function receiveUpload(uploadDir: string, body: unknown): { path: string; files: number } {\n\tconst upload = validateBody(body);\n\tmkdirSync(uploadDir, { recursive: true });\n\tconst destination = join(uploadDir, upload.name);\n\tif (existsSync(destination)) throw new ReceiveUploadError(409, `destination already exists: ${destination}`);\n\tconst temporaryDir = mkdtempSync(join(uploadDir, \".upload-\"));\n\tconst temporaryRoot = join(temporaryDir, upload.name);\n\ttry {\n\t\tfor (const entry of upload.entries.filter((entry) => entry.type === \"directory\")) {\n\t\t\tmkdirSync(join(temporaryRoot, entry.path), { recursive: true });\n\t\t}\n\t\tfor (const entry of upload.entries.filter((entry) => entry.type === \"file\")) {\n\t\t\tconst path = join(temporaryRoot, entry.path);\n\t\t\tmkdirSync(dirname(path), { recursive: true });\n\t\t\twriteFileSync(path, entry.data!);\n\t\t}\n\t\trenameSync(temporaryRoot, destination);\n\t} finally {\n\t\trmSync(temporaryDir, { recursive: true, force: true });\n\t}\n\treturn { path: destination, files: upload.entries.filter((entry) => entry.type === \"file\").length };\n}\n"]}
1
+ {"version":3,"file":"receive-upload.d.ts","sourceRoot":"","sources":["../src/receive-upload.ts"],"names":[],"mappings":"AASA,qBAAa,kBAAmB,SAAQ,KAAK;IAC5C,MAAM,EAAE,MAAM,CAAC;gBAEH,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM;CAI3C;AA8DD,wBAAgB,aAAa,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAqB/F"}
@@ -1 +1 @@
1
- {"version":3,"file":"receive-upload.js","sourceRoot":"","sources":["../src/receive-upload.ts"],"names":[],"mappings":"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","sourcesContent":["import { existsSync, mkdirSync, mkdtempSync, renameSync, rmSync, writeFileSync } from \"node:fs\";\nimport { dirname, join } from \"node:path\";\n\ninterface UploadEntry {\n\tpath: string;\n\ttype: \"file\" | \"directory\";\n\tdata?: Buffer;\n}\n\nexport class ReceiveUploadError extends Error {\n\tstatus: number;\n\n\tconstructor(status: number, message: string) {\n\t\tsuper(message);\n\t\tthis.status = status;\n\t}\n}\n\nfunction invalid(message: string): never {\n\tthrow new ReceiveUploadError(400, message);\n}\n\nfunction validatePath(path: unknown): string {\n\tif (typeof path !== \"string\") invalid(\"entry path must be a string\");\n\tif (path.includes(\"\\\\\") || path.includes(\"\\0\")) invalid(`invalid entry path: ${path}`);\n\tif (path !== \"\" && path.split(\"/\").some((part) => part === \"\" || part === \".\" || part === \"..\")) {\n\t\tinvalid(`invalid entry path: ${path}`);\n\t}\n\treturn path;\n}\n\nfunction validateBody(body: unknown): { name: string; entries: UploadEntry[] } {\n\tif (typeof body !== \"object\" || body === null) invalid(\"upload body must be an object\");\n\tconst upload = body as { name?: unknown; entries?: unknown };\n\tif (\n\t\ttypeof upload.name !== \"string\" ||\n\t\tupload.name === \"\" ||\n\t\tupload.name === \".\" ||\n\t\tupload.name === \"..\" ||\n\t\tupload.name.includes(\"/\") ||\n\t\tupload.name.includes(\"\\\\\") ||\n\t\tupload.name.includes(\"\\0\")\n\t) {\n\t\tinvalid(\"invalid upload name\");\n\t}\n\tif (!Array.isArray(upload.entries) || upload.entries.length === 0) invalid(\"entries must be a non-empty array\");\n\n\tconst entries: UploadEntry[] = [];\n\tconst entryTypes = new Map<string, \"file\" | \"directory\">();\n\tfor (const value of upload.entries) {\n\t\tif (typeof value !== \"object\" || value === null) invalid(\"each entry must be an object\");\n\t\tconst entry = value as { path?: unknown; type?: unknown; data?: unknown };\n\t\tconst path = validatePath(entry.path);\n\t\tif (entry.type !== \"file\" && entry.type !== \"directory\") invalid(`invalid entry type: ${String(entry.type)}`);\n\t\tif (entryTypes.has(path)) invalid(`duplicate entry path: ${path}`);\n\t\tentryTypes.set(path, entry.type);\n\t\tif (entry.type === \"directory\") {\n\t\t\tif (entry.data !== undefined) invalid(`directory entry must not contain data: ${path}`);\n\t\t\tentries.push({ path, type: \"directory\" });\n\t\t\tcontinue;\n\t\t}\n\t\tif (typeof entry.data !== \"string\") invalid(`file entry data must be base64: ${path}`);\n\t\tconst data = Buffer.from(entry.data, \"base64\");\n\t\tif (data.toString(\"base64\") !== entry.data) invalid(`file entry data must be valid base64: ${path}`);\n\t\tentries.push({ path, type: \"file\", data });\n\t}\n\n\tconst rootType = entryTypes.get(\"\");\n\tif (!rootType) invalid(\"entries must include the root path\");\n\tif (rootType === \"file\" && entries.length !== 1) invalid(\"a file upload cannot contain child entries\");\n\tfor (const entry of entries) {\n\t\tif (entry.path === \"\") continue;\n\t\tconst parent = entry.path.includes(\"/\") ? entry.path.slice(0, entry.path.lastIndexOf(\"/\")) : \"\";\n\t\tif (entryTypes.get(parent) !== \"directory\") invalid(`missing directory entry: ${parent}`);\n\t}\n\treturn { name: upload.name, entries };\n}\n\nexport function receiveUpload(uploadDir: string, body: unknown): { path: string; files: number } {\n\tconst upload = validateBody(body);\n\tmkdirSync(uploadDir, { recursive: true });\n\tconst destination = join(uploadDir, upload.name);\n\tif (existsSync(destination)) throw new ReceiveUploadError(409, `destination already exists: ${destination}`);\n\tconst temporaryDir = mkdtempSync(join(uploadDir, \".upload-\"));\n\tconst temporaryRoot = join(temporaryDir, upload.name);\n\ttry {\n\t\tfor (const entry of upload.entries.filter((entry) => entry.type === \"directory\")) {\n\t\t\tmkdirSync(join(temporaryRoot, entry.path), { recursive: true });\n\t\t}\n\t\tfor (const entry of upload.entries.filter((entry) => entry.type === \"file\")) {\n\t\t\tconst path = join(temporaryRoot, entry.path);\n\t\t\tmkdirSync(dirname(path), { recursive: true });\n\t\t\twriteFileSync(path, entry.data!);\n\t\t}\n\t\trenameSync(temporaryRoot, destination);\n\t} finally {\n\t\trmSync(temporaryDir, { recursive: true, force: true });\n\t}\n\treturn { path: destination, files: upload.entries.filter((entry) => entry.type === \"file\").length };\n}\n"]}
1
+ {"version":3,"file":"receive-upload.js","sourceRoot":"","sources":["../src/receive-upload.ts"],"names":[],"mappings":"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","sourcesContent":["import { existsSync, mkdirSync, mkdtempSync, renameSync, rmSync, writeFileSync } from \"node:fs\";\nimport { dirname, join } from \"node:path\";\n\ninterface UploadEntry {\n\tpath: string;\n\ttype: \"file\" | \"directory\";\n\tdata?: Buffer;\n}\n\nexport class ReceiveUploadError extends Error {\n\tstatus: number;\n\n\tconstructor(status: number, message: string) {\n\t\tsuper(message);\n\t\tthis.status = status;\n\t}\n}\n\nfunction invalid(message: string): never {\n\tthrow new ReceiveUploadError(400, message);\n}\n\nfunction validatePath(path: unknown): string {\n\tif (typeof path !== \"string\") invalid(\"entry path must be a string\");\n\tif (path.includes(\"\\\\\") || path.includes(\"\\0\")) invalid(`invalid entry path: ${path}`);\n\tif (path !== \"\" && path.split(\"/\").some((part) => part === \"\" || part === \".\" || part === \"..\")) {\n\t\tinvalid(`invalid entry path: ${path}`);\n\t}\n\treturn path;\n}\n\nfunction validateBody(body: unknown): { name: string; entries: UploadEntry[] } {\n\tif (typeof body !== \"object\" || body === null) invalid(\"upload body must be an object\");\n\tconst upload = body as { name?: unknown; entries?: unknown };\n\tif (\n\t\ttypeof upload.name !== \"string\" ||\n\t\tupload.name === \"\" ||\n\t\tupload.name === \".\" ||\n\t\tupload.name === \"..\" ||\n\t\tupload.name.includes(\"/\") ||\n\t\tupload.name.includes(\"\\\\\") ||\n\t\tupload.name.includes(\"\\0\")\n\t) {\n\t\tinvalid(\"invalid upload name\");\n\t}\n\tif (!Array.isArray(upload.entries) || upload.entries.length === 0) invalid(\"entries must be a non-empty array\");\n\n\tconst entries: UploadEntry[] = [];\n\tconst entryTypes = new Map<string, \"file\" | \"directory\">();\n\tfor (const value of upload.entries) {\n\t\tif (typeof value !== \"object\" || value === null) invalid(\"each entry must be an object\");\n\t\tconst entry = value as { path?: unknown; type?: unknown; data?: unknown };\n\t\tconst path = validatePath(entry.path);\n\t\tif (entry.type !== \"file\" && entry.type !== \"directory\") invalid(`invalid entry type: ${String(entry.type)}`);\n\t\tif (entryTypes.has(path)) invalid(`duplicate entry path: ${path}`);\n\t\tentryTypes.set(path, entry.type);\n\t\tif (entry.type === \"directory\") {\n\t\t\tif (entry.data !== undefined) invalid(`directory entry must not contain data: ${path}`);\n\t\t\tentries.push({ path, type: \"directory\" });\n\t\t\tcontinue;\n\t\t}\n\t\tif (typeof entry.data !== \"string\") invalid(`file entry data must be base64: ${path}`);\n\t\tconst data = Buffer.from(entry.data, \"base64\");\n\t\tif (data.toString(\"base64\") !== entry.data) invalid(`file entry data must be valid base64: ${path}`);\n\t\tentries.push({ path, type: \"file\", data });\n\t}\n\n\tconst rootType = entryTypes.get(\"\");\n\tif (!rootType) invalid(\"entries must include the root path\");\n\tif (rootType === \"file\" && entries.length !== 1) invalid(\"a file upload cannot contain child entries\");\n\tfor (const entry of entries) {\n\t\tif (entry.path === \"\") continue;\n\t\tconst parent = entry.path.includes(\"/\") ? entry.path.slice(0, entry.path.lastIndexOf(\"/\")) : \"\";\n\t\tif (entryTypes.get(parent) !== \"directory\") invalid(`missing directory entry: ${parent}`);\n\t}\n\treturn { name: upload.name, entries };\n}\n\nexport function receiveUpload(uploadDir: string, body: unknown): { path: string; files: number } {\n\tconst upload = validateBody(body);\n\tmkdirSync(uploadDir, { recursive: true });\n\tconst destination = join(uploadDir, upload.name);\n\tif (existsSync(destination)) throw new ReceiveUploadError(409, `destination already exists: ${destination}`);\n\tconst temporaryDir = mkdtempSync(join(uploadDir, \".upload-\"));\n\tconst temporaryRoot = join(temporaryDir, upload.name);\n\ttry {\n\t\tfor (const entry of upload.entries.filter((entry) => entry.type === \"directory\")) {\n\t\t\tmkdirSync(join(temporaryRoot, entry.path), { recursive: true });\n\t\t}\n\t\tfor (const entry of upload.entries.filter((entry) => entry.type === \"file\")) {\n\t\t\tconst path = join(temporaryRoot, entry.path);\n\t\t\tmkdirSync(dirname(path), { recursive: true });\n\t\t\twriteFileSync(path, entry.data!);\n\t\t}\n\t\trenameSync(temporaryRoot, destination);\n\t} finally {\n\t\trmSync(temporaryDir, { recursive: true, force: true });\n\t}\n\treturn { path: destination, files: upload.entries.filter((entry) => entry.type === \"file\").length };\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"request-chunks.d.ts","sourceRoot":"","sources":["../src/request-chunks.ts"],"names":[],"mappings":"AAGA,eAAO,MAAM,cAAc,uBAAuB,CAAC;AAenD,MAAM,WAAW,gBAAgB;IAChC,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;CACd;AAwBD,UAAU,QAAQ;IACjB,QAAQ,EAAE,IAAI,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;CACpB;AAED,UAAU,kBAAkB;IAC3B,QAAQ,EAAE,KAAK,CAAC;IAChB,GAAG,EAAE,QAAQ,CAAC;CACd;AAED,UAAU,mBAAmB;IAC5B,QAAQ,EAAE,IAAI,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;CACjB;AAMD,eAAO,MAAM,4BAA4B,QAAgB,CAAC;AAC1D,eAAO,MAAM,+BAA+B,QAAqB,CAAC;AAClE,eAAO,MAAM,8BAA8B,QAAY,CAAC;AAExD,UAAU,0BAA0B;IACnC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,cAAc,CAAC,EAAE,MAAM,CAAC;CACxB;AAkED,wBAAgB,mBAAmB,CAClC,IAAI,EAAE,gBAAgB,EACtB,OAAO,GAAE,0BAA+B,GACtC,kBAAkB,GAAG,mBAAmB,CAuF1C;AAED,wBAAgB,qBAAqB,IAAI,IAAI,CAI5C","sourcesContent":["import { Buffer } from \"node:buffer\";\nimport { createHash } from \"node:crypto\";\n\nexport const CHUNK_ENDPOINT = \"/api/request/chunk\";\n\nconst ALLOWED_TARGETS = new Set([\n\t\"/api/session/init\",\n\t\"/api/session/update\",\n\t\"/api/session/sync\",\n\t\"/api/session/append\",\n\t\"/api/session/tree/sync\",\n\t\"/api/session/tree/append\",\n\t\"/api/session/tree/switch\",\n\t\"/api/session/compact\",\n\t\"/api/stream\",\n\t\"/api/receive\",\n]);\n\nexport interface RequestChunkBody {\n\trequestId: string;\n\ttarget: string;\n\tchunkIndex: number;\n\ttotalChunks: number;\n\tsha256: string;\n\tchunk: string;\n}\n\ninterface RequestChunk {\n\tchunk: string;\n\tsha256: string;\n}\n\ninterface PendingRequest {\n\ttarget: string;\n\ttotalChunks: number;\n\tchunks: Map<number, RequestChunk>;\n\treceivedBytes: number;\n\tupdatedAtMs: number;\n}\n\ninterface CompletedRequest {\n\ttarget: string;\n\ttotalChunks: number;\n\tbodyJson: string;\n\tcompletedChunkIndex: number;\n\tcompletedAtMs: number;\n\tchunks: Map<number, RequestChunk>;\n}\n\ninterface ChunkAck {\n\treceived: true;\n\trequestId: string;\n\tchunkIndex: number;\n\ttotalChunks: number;\n}\n\ninterface PendingChunkResult {\n\tcomplete: false;\n\tack: ChunkAck;\n}\n\ninterface CompleteChunkResult {\n\tcomplete: true;\n\ttarget: string;\n\tbodyJson: string;\n}\n\nconst pendingRequests = new Map<string, PendingRequest>();\nconst completedRequests = new Map<string, CompletedRequest>();\nlet pendingRequestBytes = 0;\n\nexport const REQUEST_CHUNK_PENDING_TTL_MS = 5 * 60 * 1000;\nexport const REQUEST_CHUNK_MAX_PENDING_BYTES = 1024 * 1024 * 1024;\nexport const REQUEST_CHUNK_COMPLETED_TTL_MS = 60 * 1000;\n\ninterface ReceiveRequestChunkOptions {\n\tnowMs?: number;\n\tpendingTtlMs?: number;\n\tmaxPendingBytes?: number;\n\tcompletedTtlMs?: number;\n}\n\nfunction sha256(value: string): string {\n\treturn createHash(\"sha256\").update(value).digest(\"hex\");\n}\n\nfunction assertValidChunk(body: RequestChunkBody): void {\n\tif (!body.requestId) throw new Error(\"requestId is required\");\n\tif (!ALLOWED_TARGETS.has(body.target)) throw new Error(`Unsupported chunk target: ${body.target}`);\n\tif (!Number.isInteger(body.totalChunks) || body.totalChunks <= 0) {\n\t\tthrow new Error(\"totalChunks must be a positive integer\");\n\t}\n\tif (!Number.isInteger(body.chunkIndex) || body.chunkIndex < 0 || body.chunkIndex >= body.totalChunks) {\n\t\tthrow new Error(\"chunkIndex must be an integer within the chunk range\");\n\t}\n\tif (typeof body.chunk !== \"string\") throw new Error(\"chunk must be a string\");\n\tif (typeof body.sha256 !== \"string\" || !/^[a-f0-9]{64}$/i.test(body.sha256)) {\n\t\tthrow new Error(\"sha256 must be a 64-character hex string\");\n\t}\n\tif (sha256(body.chunk) !== body.sha256) {\n\t\tthrow new Error(`Chunk checksum mismatch: ${body.chunkIndex}`);\n\t}\n}\n\nfunction makeAck(body: RequestChunkBody): PendingChunkResult {\n\treturn {\n\t\tcomplete: false,\n\t\tack: {\n\t\t\treceived: true,\n\t\t\trequestId: body.requestId,\n\t\t\tchunkIndex: body.chunkIndex,\n\t\t\ttotalChunks: body.totalChunks,\n\t\t},\n\t};\n}\n\nfunction deletePendingRequest(requestId: string, pending: PendingRequest): void {\n\tpendingRequests.delete(requestId);\n\tpendingRequestBytes -= pending.receivedBytes;\n}\n\nfunction cleanupExpiredRequests(nowMs: number, pendingTtlMs: number, completedTtlMs: number): void {\n\tfor (const [requestId, pending] of pendingRequests) {\n\t\tif (nowMs - pending.updatedAtMs > pendingTtlMs) {\n\t\t\tdeletePendingRequest(requestId, pending);\n\t\t}\n\t}\n\tfor (const [requestId, completed] of completedRequests) {\n\t\tif (nowMs - completed.completedAtMs > completedTtlMs) {\n\t\t\tcompletedRequests.delete(requestId);\n\t\t}\n\t}\n}\n\nfunction cleanupForPendingBytes(extraBytes: number, maxPendingBytes: number, protectedRequestId: string): void {\n\tfor (const [requestId, pending] of pendingRequests) {\n\t\tif (pendingRequestBytes + extraBytes <= maxPendingBytes) return;\n\t\tif (requestId !== protectedRequestId) {\n\t\t\tdeletePendingRequest(requestId, pending);\n\t\t}\n\t}\n\tif (pendingRequestBytes + extraBytes > maxPendingBytes) {\n\t\tthrow new Error(\"Request chunk pending bytes limit exceeded\");\n\t}\n}\n\nexport function receiveRequestChunk(\n\tbody: RequestChunkBody,\n\toptions: ReceiveRequestChunkOptions = {},\n): PendingChunkResult | CompleteChunkResult {\n\tassertValidChunk(body);\n\n\tconst nowMs = options.nowMs ?? Date.now();\n\tcleanupExpiredRequests(\n\t\tnowMs,\n\t\toptions.pendingTtlMs ?? REQUEST_CHUNK_PENDING_TTL_MS,\n\t\toptions.completedTtlMs ?? REQUEST_CHUNK_COMPLETED_TTL_MS,\n\t);\n\n\tconst completed = completedRequests.get(body.requestId);\n\tif (completed) {\n\t\tif (completed.target !== body.target || completed.totalChunks !== body.totalChunks) {\n\t\t\tthrow new Error(\"Chunk metadata does not match the completed request\");\n\t\t}\n\t\tconst existing = completed.chunks.get(body.chunkIndex);\n\t\tif (!existing || existing.chunk !== body.chunk || existing.sha256 !== body.sha256) {\n\t\t\tthrow new Error(`Duplicate chunk index does not match: ${body.chunkIndex}`);\n\t\t}\n\t\tif (body.chunkIndex === completed.completedChunkIndex) {\n\t\t\treturn {\n\t\t\t\tcomplete: true,\n\t\t\t\ttarget: completed.target,\n\t\t\t\tbodyJson: completed.bodyJson,\n\t\t\t};\n\t\t}\n\t\treturn makeAck(body);\n\t}\n\n\tlet pending = pendingRequests.get(body.requestId);\n\tif (pending) {\n\t\tif (pending.target !== body.target || pending.totalChunks !== body.totalChunks) {\n\t\t\tthrow new Error(\"Chunk metadata does not match the pending request\");\n\t\t}\n\t\tconst existing = pending.chunks.get(body.chunkIndex);\n\t\tif (existing) {\n\t\t\tif (existing.chunk !== body.chunk || existing.sha256 !== body.sha256) {\n\t\t\t\tthrow new Error(`Duplicate chunk index does not match: ${body.chunkIndex}`);\n\t\t\t}\n\t\t\tpending.updatedAtMs = nowMs;\n\t\t\treturn makeAck(body);\n\t\t}\n\t}\n\n\tconst chunkBytes = Buffer.byteLength(body.chunk, \"utf-8\");\n\tcleanupForPendingBytes(chunkBytes, options.maxPendingBytes ?? REQUEST_CHUNK_MAX_PENDING_BYTES, body.requestId);\n\tif (!pending) {\n\t\tpending = {\n\t\t\ttarget: body.target,\n\t\t\ttotalChunks: body.totalChunks,\n\t\t\tchunks: new Map(),\n\t\t\treceivedBytes: 0,\n\t\t\tupdatedAtMs: nowMs,\n\t\t};\n\t\tpendingRequests.set(body.requestId, pending);\n\t}\n\tpending.chunks.set(body.chunkIndex, { chunk: body.chunk, sha256: body.sha256 });\n\tpending.receivedBytes += chunkBytes;\n\tpending.updatedAtMs = nowMs;\n\tpendingRequestBytes += chunkBytes;\n\n\tif (pending.chunks.size !== pending.totalChunks) {\n\t\treturn makeAck(body);\n\t}\n\n\tconst encodedChunks: string[] = [];\n\tfor (let index = 0; index < pending.totalChunks; index++) {\n\t\tconst chunk = pending.chunks.get(index);\n\t\tif (chunk === undefined) throw new Error(`Missing chunk index: ${index}`);\n\t\tencodedChunks.push(chunk.chunk);\n\t}\n\n\tdeletePendingRequest(body.requestId, pending);\n\tconst bodyJson = Buffer.from(encodedChunks.join(\"\"), \"base64\").toString(\"utf-8\");\n\tcompletedRequests.set(body.requestId, {\n\t\ttarget: pending.target,\n\t\ttotalChunks: pending.totalChunks,\n\t\tbodyJson,\n\t\tcompletedChunkIndex: body.chunkIndex,\n\t\tcompletedAtMs: nowMs,\n\t\tchunks: pending.chunks,\n\t});\n\treturn {\n\t\tcomplete: true,\n\t\ttarget: pending.target,\n\t\tbodyJson,\n\t};\n}\n\nexport function clearAllRequestChunks(): void {\n\tpendingRequests.clear();\n\tcompletedRequests.clear();\n\tpendingRequestBytes = 0;\n}\n"]}
1
+ {"version":3,"file":"request-chunks.d.ts","sourceRoot":"","sources":["../src/request-chunks.ts"],"names":[],"mappings":"AAGA,eAAO,MAAM,cAAc,uBAAuB,CAAC;AAenD,MAAM,WAAW,gBAAgB;IAChC,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;CACd;AAwBD,UAAU,QAAQ;IACjB,QAAQ,EAAE,IAAI,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;CACpB;AAED,UAAU,kBAAkB;IAC3B,QAAQ,EAAE,KAAK,CAAC;IAChB,GAAG,EAAE,QAAQ,CAAC;CACd;AAED,UAAU,mBAAmB;IAC5B,QAAQ,EAAE,IAAI,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;CACjB;AAMD,eAAO,MAAM,4BAA4B,QAAgB,CAAC;AAC1D,eAAO,MAAM,+BAA+B,QAAqB,CAAC;AAClE,eAAO,MAAM,8BAA8B,QAAY,CAAC;AAExD,UAAU,0BAA0B;IACnC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,cAAc,CAAC,EAAE,MAAM,CAAC;CACxB;AAkED,wBAAgB,mBAAmB,CAClC,IAAI,EAAE,gBAAgB,EACtB,OAAO,GAAE,0BAA+B,GACtC,kBAAkB,GAAG,mBAAmB,CAuF1C;AAED,wBAAgB,qBAAqB,IAAI,IAAI,CAI5C"}
@@ -1 +1 @@
1
- {"version":3,"file":"request-chunks.js","sourceRoot":"","sources":["../src/request-chunks.ts"],"names":[],"mappings":"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","sourcesContent":["import { Buffer } from \"node:buffer\";\nimport { createHash } from \"node:crypto\";\n\nexport const CHUNK_ENDPOINT = \"/api/request/chunk\";\n\nconst ALLOWED_TARGETS = new Set([\n\t\"/api/session/init\",\n\t\"/api/session/update\",\n\t\"/api/session/sync\",\n\t\"/api/session/append\",\n\t\"/api/session/tree/sync\",\n\t\"/api/session/tree/append\",\n\t\"/api/session/tree/switch\",\n\t\"/api/session/compact\",\n\t\"/api/stream\",\n\t\"/api/receive\",\n]);\n\nexport interface RequestChunkBody {\n\trequestId: string;\n\ttarget: string;\n\tchunkIndex: number;\n\ttotalChunks: number;\n\tsha256: string;\n\tchunk: string;\n}\n\ninterface RequestChunk {\n\tchunk: string;\n\tsha256: string;\n}\n\ninterface PendingRequest {\n\ttarget: string;\n\ttotalChunks: number;\n\tchunks: Map<number, RequestChunk>;\n\treceivedBytes: number;\n\tupdatedAtMs: number;\n}\n\ninterface CompletedRequest {\n\ttarget: string;\n\ttotalChunks: number;\n\tbodyJson: string;\n\tcompletedChunkIndex: number;\n\tcompletedAtMs: number;\n\tchunks: Map<number, RequestChunk>;\n}\n\ninterface ChunkAck {\n\treceived: true;\n\trequestId: string;\n\tchunkIndex: number;\n\ttotalChunks: number;\n}\n\ninterface PendingChunkResult {\n\tcomplete: false;\n\tack: ChunkAck;\n}\n\ninterface CompleteChunkResult {\n\tcomplete: true;\n\ttarget: string;\n\tbodyJson: string;\n}\n\nconst pendingRequests = new Map<string, PendingRequest>();\nconst completedRequests = new Map<string, CompletedRequest>();\nlet pendingRequestBytes = 0;\n\nexport const REQUEST_CHUNK_PENDING_TTL_MS = 5 * 60 * 1000;\nexport const REQUEST_CHUNK_MAX_PENDING_BYTES = 1024 * 1024 * 1024;\nexport const REQUEST_CHUNK_COMPLETED_TTL_MS = 60 * 1000;\n\ninterface ReceiveRequestChunkOptions {\n\tnowMs?: number;\n\tpendingTtlMs?: number;\n\tmaxPendingBytes?: number;\n\tcompletedTtlMs?: number;\n}\n\nfunction sha256(value: string): string {\n\treturn createHash(\"sha256\").update(value).digest(\"hex\");\n}\n\nfunction assertValidChunk(body: RequestChunkBody): void {\n\tif (!body.requestId) throw new Error(\"requestId is required\");\n\tif (!ALLOWED_TARGETS.has(body.target)) throw new Error(`Unsupported chunk target: ${body.target}`);\n\tif (!Number.isInteger(body.totalChunks) || body.totalChunks <= 0) {\n\t\tthrow new Error(\"totalChunks must be a positive integer\");\n\t}\n\tif (!Number.isInteger(body.chunkIndex) || body.chunkIndex < 0 || body.chunkIndex >= body.totalChunks) {\n\t\tthrow new Error(\"chunkIndex must be an integer within the chunk range\");\n\t}\n\tif (typeof body.chunk !== \"string\") throw new Error(\"chunk must be a string\");\n\tif (typeof body.sha256 !== \"string\" || !/^[a-f0-9]{64}$/i.test(body.sha256)) {\n\t\tthrow new Error(\"sha256 must be a 64-character hex string\");\n\t}\n\tif (sha256(body.chunk) !== body.sha256) {\n\t\tthrow new Error(`Chunk checksum mismatch: ${body.chunkIndex}`);\n\t}\n}\n\nfunction makeAck(body: RequestChunkBody): PendingChunkResult {\n\treturn {\n\t\tcomplete: false,\n\t\tack: {\n\t\t\treceived: true,\n\t\t\trequestId: body.requestId,\n\t\t\tchunkIndex: body.chunkIndex,\n\t\t\ttotalChunks: body.totalChunks,\n\t\t},\n\t};\n}\n\nfunction deletePendingRequest(requestId: string, pending: PendingRequest): void {\n\tpendingRequests.delete(requestId);\n\tpendingRequestBytes -= pending.receivedBytes;\n}\n\nfunction cleanupExpiredRequests(nowMs: number, pendingTtlMs: number, completedTtlMs: number): void {\n\tfor (const [requestId, pending] of pendingRequests) {\n\t\tif (nowMs - pending.updatedAtMs > pendingTtlMs) {\n\t\t\tdeletePendingRequest(requestId, pending);\n\t\t}\n\t}\n\tfor (const [requestId, completed] of completedRequests) {\n\t\tif (nowMs - completed.completedAtMs > completedTtlMs) {\n\t\t\tcompletedRequests.delete(requestId);\n\t\t}\n\t}\n}\n\nfunction cleanupForPendingBytes(extraBytes: number, maxPendingBytes: number, protectedRequestId: string): void {\n\tfor (const [requestId, pending] of pendingRequests) {\n\t\tif (pendingRequestBytes + extraBytes <= maxPendingBytes) return;\n\t\tif (requestId !== protectedRequestId) {\n\t\t\tdeletePendingRequest(requestId, pending);\n\t\t}\n\t}\n\tif (pendingRequestBytes + extraBytes > maxPendingBytes) {\n\t\tthrow new Error(\"Request chunk pending bytes limit exceeded\");\n\t}\n}\n\nexport function receiveRequestChunk(\n\tbody: RequestChunkBody,\n\toptions: ReceiveRequestChunkOptions = {},\n): PendingChunkResult | CompleteChunkResult {\n\tassertValidChunk(body);\n\n\tconst nowMs = options.nowMs ?? Date.now();\n\tcleanupExpiredRequests(\n\t\tnowMs,\n\t\toptions.pendingTtlMs ?? REQUEST_CHUNK_PENDING_TTL_MS,\n\t\toptions.completedTtlMs ?? REQUEST_CHUNK_COMPLETED_TTL_MS,\n\t);\n\n\tconst completed = completedRequests.get(body.requestId);\n\tif (completed) {\n\t\tif (completed.target !== body.target || completed.totalChunks !== body.totalChunks) {\n\t\t\tthrow new Error(\"Chunk metadata does not match the completed request\");\n\t\t}\n\t\tconst existing = completed.chunks.get(body.chunkIndex);\n\t\tif (!existing || existing.chunk !== body.chunk || existing.sha256 !== body.sha256) {\n\t\t\tthrow new Error(`Duplicate chunk index does not match: ${body.chunkIndex}`);\n\t\t}\n\t\tif (body.chunkIndex === completed.completedChunkIndex) {\n\t\t\treturn {\n\t\t\t\tcomplete: true,\n\t\t\t\ttarget: completed.target,\n\t\t\t\tbodyJson: completed.bodyJson,\n\t\t\t};\n\t\t}\n\t\treturn makeAck(body);\n\t}\n\n\tlet pending = pendingRequests.get(body.requestId);\n\tif (pending) {\n\t\tif (pending.target !== body.target || pending.totalChunks !== body.totalChunks) {\n\t\t\tthrow new Error(\"Chunk metadata does not match the pending request\");\n\t\t}\n\t\tconst existing = pending.chunks.get(body.chunkIndex);\n\t\tif (existing) {\n\t\t\tif (existing.chunk !== body.chunk || existing.sha256 !== body.sha256) {\n\t\t\t\tthrow new Error(`Duplicate chunk index does not match: ${body.chunkIndex}`);\n\t\t\t}\n\t\t\tpending.updatedAtMs = nowMs;\n\t\t\treturn makeAck(body);\n\t\t}\n\t}\n\n\tconst chunkBytes = Buffer.byteLength(body.chunk, \"utf-8\");\n\tcleanupForPendingBytes(chunkBytes, options.maxPendingBytes ?? REQUEST_CHUNK_MAX_PENDING_BYTES, body.requestId);\n\tif (!pending) {\n\t\tpending = {\n\t\t\ttarget: body.target,\n\t\t\ttotalChunks: body.totalChunks,\n\t\t\tchunks: new Map(),\n\t\t\treceivedBytes: 0,\n\t\t\tupdatedAtMs: nowMs,\n\t\t};\n\t\tpendingRequests.set(body.requestId, pending);\n\t}\n\tpending.chunks.set(body.chunkIndex, { chunk: body.chunk, sha256: body.sha256 });\n\tpending.receivedBytes += chunkBytes;\n\tpending.updatedAtMs = nowMs;\n\tpendingRequestBytes += chunkBytes;\n\n\tif (pending.chunks.size !== pending.totalChunks) {\n\t\treturn makeAck(body);\n\t}\n\n\tconst encodedChunks: string[] = [];\n\tfor (let index = 0; index < pending.totalChunks; index++) {\n\t\tconst chunk = pending.chunks.get(index);\n\t\tif (chunk === undefined) throw new Error(`Missing chunk index: ${index}`);\n\t\tencodedChunks.push(chunk.chunk);\n\t}\n\n\tdeletePendingRequest(body.requestId, pending);\n\tconst bodyJson = Buffer.from(encodedChunks.join(\"\"), \"base64\").toString(\"utf-8\");\n\tcompletedRequests.set(body.requestId, {\n\t\ttarget: pending.target,\n\t\ttotalChunks: pending.totalChunks,\n\t\tbodyJson,\n\t\tcompletedChunkIndex: body.chunkIndex,\n\t\tcompletedAtMs: nowMs,\n\t\tchunks: pending.chunks,\n\t});\n\treturn {\n\t\tcomplete: true,\n\t\ttarget: pending.target,\n\t\tbodyJson,\n\t};\n}\n\nexport function clearAllRequestChunks(): void {\n\tpendingRequests.clear();\n\tcompletedRequests.clear();\n\tpendingRequestBytes = 0;\n}\n"]}
1
+ {"version":3,"file":"request-chunks.js","sourceRoot":"","sources":["../src/request-chunks.ts"],"names":[],"mappings":"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","sourcesContent":["import { Buffer } from \"node:buffer\";\nimport { createHash } from \"node:crypto\";\n\nexport const CHUNK_ENDPOINT = \"/api/request/chunk\";\n\nconst ALLOWED_TARGETS = new Set([\n\t\"/api/session/init\",\n\t\"/api/session/update\",\n\t\"/api/session/sync\",\n\t\"/api/session/append\",\n\t\"/api/session/tree/sync\",\n\t\"/api/session/tree/append\",\n\t\"/api/session/tree/switch\",\n\t\"/api/session/compact\",\n\t\"/api/stream\",\n\t\"/api/receive\",\n]);\n\nexport interface RequestChunkBody {\n\trequestId: string;\n\ttarget: string;\n\tchunkIndex: number;\n\ttotalChunks: number;\n\tsha256: string;\n\tchunk: string;\n}\n\ninterface RequestChunk {\n\tchunk: string;\n\tsha256: string;\n}\n\ninterface PendingRequest {\n\ttarget: string;\n\ttotalChunks: number;\n\tchunks: Map<number, RequestChunk>;\n\treceivedBytes: number;\n\tupdatedAtMs: number;\n}\n\ninterface CompletedRequest {\n\ttarget: string;\n\ttotalChunks: number;\n\tbodyJson: string;\n\tcompletedChunkIndex: number;\n\tcompletedAtMs: number;\n\tchunks: Map<number, RequestChunk>;\n}\n\ninterface ChunkAck {\n\treceived: true;\n\trequestId: string;\n\tchunkIndex: number;\n\ttotalChunks: number;\n}\n\ninterface PendingChunkResult {\n\tcomplete: false;\n\tack: ChunkAck;\n}\n\ninterface CompleteChunkResult {\n\tcomplete: true;\n\ttarget: string;\n\tbodyJson: string;\n}\n\nconst pendingRequests = new Map<string, PendingRequest>();\nconst completedRequests = new Map<string, CompletedRequest>();\nlet pendingRequestBytes = 0;\n\nexport const REQUEST_CHUNK_PENDING_TTL_MS = 5 * 60 * 1000;\nexport const REQUEST_CHUNK_MAX_PENDING_BYTES = 1024 * 1024 * 1024;\nexport const REQUEST_CHUNK_COMPLETED_TTL_MS = 60 * 1000;\n\ninterface ReceiveRequestChunkOptions {\n\tnowMs?: number;\n\tpendingTtlMs?: number;\n\tmaxPendingBytes?: number;\n\tcompletedTtlMs?: number;\n}\n\nfunction sha256(value: string): string {\n\treturn createHash(\"sha256\").update(value).digest(\"hex\");\n}\n\nfunction assertValidChunk(body: RequestChunkBody): void {\n\tif (!body.requestId) throw new Error(\"requestId is required\");\n\tif (!ALLOWED_TARGETS.has(body.target)) throw new Error(`Unsupported chunk target: ${body.target}`);\n\tif (!Number.isInteger(body.totalChunks) || body.totalChunks <= 0) {\n\t\tthrow new Error(\"totalChunks must be a positive integer\");\n\t}\n\tif (!Number.isInteger(body.chunkIndex) || body.chunkIndex < 0 || body.chunkIndex >= body.totalChunks) {\n\t\tthrow new Error(\"chunkIndex must be an integer within the chunk range\");\n\t}\n\tif (typeof body.chunk !== \"string\") throw new Error(\"chunk must be a string\");\n\tif (typeof body.sha256 !== \"string\" || !/^[a-f0-9]{64}$/i.test(body.sha256)) {\n\t\tthrow new Error(\"sha256 must be a 64-character hex string\");\n\t}\n\tif (sha256(body.chunk) !== body.sha256) {\n\t\tthrow new Error(`Chunk checksum mismatch: ${body.chunkIndex}`);\n\t}\n}\n\nfunction makeAck(body: RequestChunkBody): PendingChunkResult {\n\treturn {\n\t\tcomplete: false,\n\t\tack: {\n\t\t\treceived: true,\n\t\t\trequestId: body.requestId,\n\t\t\tchunkIndex: body.chunkIndex,\n\t\t\ttotalChunks: body.totalChunks,\n\t\t},\n\t};\n}\n\nfunction deletePendingRequest(requestId: string, pending: PendingRequest): void {\n\tpendingRequests.delete(requestId);\n\tpendingRequestBytes -= pending.receivedBytes;\n}\n\nfunction cleanupExpiredRequests(nowMs: number, pendingTtlMs: number, completedTtlMs: number): void {\n\tfor (const [requestId, pending] of pendingRequests) {\n\t\tif (nowMs - pending.updatedAtMs > pendingTtlMs) {\n\t\t\tdeletePendingRequest(requestId, pending);\n\t\t}\n\t}\n\tfor (const [requestId, completed] of completedRequests) {\n\t\tif (nowMs - completed.completedAtMs > completedTtlMs) {\n\t\t\tcompletedRequests.delete(requestId);\n\t\t}\n\t}\n}\n\nfunction cleanupForPendingBytes(extraBytes: number, maxPendingBytes: number, protectedRequestId: string): void {\n\tfor (const [requestId, pending] of pendingRequests) {\n\t\tif (pendingRequestBytes + extraBytes <= maxPendingBytes) return;\n\t\tif (requestId !== protectedRequestId) {\n\t\t\tdeletePendingRequest(requestId, pending);\n\t\t}\n\t}\n\tif (pendingRequestBytes + extraBytes > maxPendingBytes) {\n\t\tthrow new Error(\"Request chunk pending bytes limit exceeded\");\n\t}\n}\n\nexport function receiveRequestChunk(\n\tbody: RequestChunkBody,\n\toptions: ReceiveRequestChunkOptions = {},\n): PendingChunkResult | CompleteChunkResult {\n\tassertValidChunk(body);\n\n\tconst nowMs = options.nowMs ?? Date.now();\n\tcleanupExpiredRequests(\n\t\tnowMs,\n\t\toptions.pendingTtlMs ?? REQUEST_CHUNK_PENDING_TTL_MS,\n\t\toptions.completedTtlMs ?? REQUEST_CHUNK_COMPLETED_TTL_MS,\n\t);\n\n\tconst completed = completedRequests.get(body.requestId);\n\tif (completed) {\n\t\tif (completed.target !== body.target || completed.totalChunks !== body.totalChunks) {\n\t\t\tthrow new Error(\"Chunk metadata does not match the completed request\");\n\t\t}\n\t\tconst existing = completed.chunks.get(body.chunkIndex);\n\t\tif (!existing || existing.chunk !== body.chunk || existing.sha256 !== body.sha256) {\n\t\t\tthrow new Error(`Duplicate chunk index does not match: ${body.chunkIndex}`);\n\t\t}\n\t\tif (body.chunkIndex === completed.completedChunkIndex) {\n\t\t\treturn {\n\t\t\t\tcomplete: true,\n\t\t\t\ttarget: completed.target,\n\t\t\t\tbodyJson: completed.bodyJson,\n\t\t\t};\n\t\t}\n\t\treturn makeAck(body);\n\t}\n\n\tlet pending = pendingRequests.get(body.requestId);\n\tif (pending) {\n\t\tif (pending.target !== body.target || pending.totalChunks !== body.totalChunks) {\n\t\t\tthrow new Error(\"Chunk metadata does not match the pending request\");\n\t\t}\n\t\tconst existing = pending.chunks.get(body.chunkIndex);\n\t\tif (existing) {\n\t\t\tif (existing.chunk !== body.chunk || existing.sha256 !== body.sha256) {\n\t\t\t\tthrow new Error(`Duplicate chunk index does not match: ${body.chunkIndex}`);\n\t\t\t}\n\t\t\tpending.updatedAtMs = nowMs;\n\t\t\treturn makeAck(body);\n\t\t}\n\t}\n\n\tconst chunkBytes = Buffer.byteLength(body.chunk, \"utf-8\");\n\tcleanupForPendingBytes(chunkBytes, options.maxPendingBytes ?? REQUEST_CHUNK_MAX_PENDING_BYTES, body.requestId);\n\tif (!pending) {\n\t\tpending = {\n\t\t\ttarget: body.target,\n\t\t\ttotalChunks: body.totalChunks,\n\t\t\tchunks: new Map(),\n\t\t\treceivedBytes: 0,\n\t\t\tupdatedAtMs: nowMs,\n\t\t};\n\t\tpendingRequests.set(body.requestId, pending);\n\t}\n\tpending.chunks.set(body.chunkIndex, { chunk: body.chunk, sha256: body.sha256 });\n\tpending.receivedBytes += chunkBytes;\n\tpending.updatedAtMs = nowMs;\n\tpendingRequestBytes += chunkBytes;\n\n\tif (pending.chunks.size !== pending.totalChunks) {\n\t\treturn makeAck(body);\n\t}\n\n\tconst encodedChunks: string[] = [];\n\tfor (let index = 0; index < pending.totalChunks; index++) {\n\t\tconst chunk = pending.chunks.get(index);\n\t\tif (chunk === undefined) throw new Error(`Missing chunk index: ${index}`);\n\t\tencodedChunks.push(chunk.chunk);\n\t}\n\n\tdeletePendingRequest(body.requestId, pending);\n\tconst bodyJson = Buffer.from(encodedChunks.join(\"\"), \"base64\").toString(\"utf-8\");\n\tcompletedRequests.set(body.requestId, {\n\t\ttarget: pending.target,\n\t\ttotalChunks: pending.totalChunks,\n\t\tbodyJson,\n\t\tcompletedChunkIndex: body.chunkIndex,\n\t\tcompletedAtMs: nowMs,\n\t\tchunks: pending.chunks,\n\t});\n\treturn {\n\t\tcomplete: true,\n\t\ttarget: pending.target,\n\t\tbodyJson,\n\t};\n}\n\nexport function clearAllRequestChunks(): void {\n\tpendingRequests.clear();\n\tcompletedRequests.clear();\n\tpendingRequestBytes = 0;\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA,OAAO,EAAgB,KAAK,MAAM,IAAI,UAAU,EAA6C,MAAM,WAAW,CAAC;AAY/G,OAAO,EAGN,KAAK,OAAO,EAGZ,KAAK,OAAO,EACZ,KAAK,KAAK,EAEV,KAAK,mBAAmB,EACxB,MAAM,uBAAuB,CAAC;AAE/B,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAOhD,OAAO,EAWN,KAAK,YAAY,EACjB,KAAK,oBAAoB,EAGzB,MAAM,oBAAoB,CAAC;AAE5B,OAAO,EAAE,UAAU,EAAE,KAAK,YAAY,EAAE,MAAM,aAAa,CAAC;AAc5D,UAAU,iBAAiB;IAC1B,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC;IAClB,OAAO,CAAC,EAAE,mBAAmB,CAAC;IAC9B,aAAa,CAAC,EAAE,oBAAoB,CAAC;IACrC,iBAAiB,CAAC,EAAE,OAAO,EAAE,CAAC;IAC9B,cAAc,CAAC,EAAE,OAAO,EAAE,CAAC;CAC3B;AA+4BD,wBAAgB,kBAAkB,CACjC,OAAO,EAAE,YAAY,EACrB,IAAI,EAAE,IAAI,CAAC,iBAAiB,EAAE,gBAAgB,GAAG,mBAAmB,CAAC,GACnE,OAAO,CAOT;AAoMD,wBAAgB,cAAc,CAAC,cAAc,CAAC,EAAE,OAAO,CAAC,YAAY,CAAC,GAAG,UAAU,CAoIjF;AAqED,wBAAgB,WAAW,CAAC,cAAc,CAAC,EAAE,OAAO,CAAC,YAAY,CAAC,GAAG,UAAU,CAU9E","sourcesContent":["import { createServer, type Server as HttpServer, type IncomingMessage, type ServerResponse } from \"node:http\";\nimport { createRequire } from \"node:module\";\nimport {\n\ttype CompactionPreparationOptions,\n\ttype CompactionSettings,\n\tcompactLegacy,\n\tDEFAULT_COMPACTION_SETTINGS,\n\ttype LegacyCompactResult,\n\ttype ProxyAssistantMessageEvent,\n\tprepareLegacyCompaction,\n\ttype SessionTreeEntry,\n} from \"@earendil-works/pi-agent-core\";\nimport {\n\ttype AssistantMessage,\n\ttype AssistantMessageEvent,\n\ttype Context,\n\tcreateModels,\n\tcreateProvider,\n\ttype Message,\n\ttype Model,\n\ttype ProviderStreams,\n\ttype SimpleStreamOptions,\n} from \"@earendil-works/pi-ai\";\nimport { streamSimple } from \"@earendil-works/pi-ai/compat\";\nimport type { ServerConfig } from \"./config.ts\";\nimport { loadConfig } from \"./config.ts\";\nimport { PiServerError, PiServerErrorCode, type PiServerErrorResponse } from \"./error-codes.ts\";\nimport { encodeErrorEvent, encodeProxyEvent } from \"./event-encoding.ts\";\nimport { ReceiveUploadError, receiveUpload } from \"./receive-upload.ts\";\nimport { CHUNK_ENDPOINT, type RequestChunkBody, receiveRequestChunk } from \"./request-chunks.ts\";\nimport { deletePersistedSession, loadPersistedSessions, savePersistedSession } from \"./session-persistence.ts\";\nimport {\n\tappendCompactionEntry,\n\tappendMessages,\n\tappendSessionEntries,\n\tdeleteSession as deleteSessionFromStore,\n\tgetOrCreateSession,\n\tgetSession,\n\tgetSessionBranch,\n\tlistSessions,\n\treplaceMessages,\n\treplaceSessionTree,\n\ttype SessionState,\n\ttype SessionStaticContext,\n\tsetStaticContext,\n\tswitchSessionLeaf,\n} from \"./session-store.ts\";\n\nexport { loadConfig, type ServerConfig } from \"./config.ts\";\n\ninterface PackageMetadata {\n\tversion: string;\n}\n\nconst packageMetadata = createRequire(import.meta.url)(\"../package.json\") as PackageMetadata;\nconst PI_SERVER_VERSION = packageMetadata.version;\n\ninterface SessionInitBody {\n\tsessionId: string;\n\tstaticContext?: SessionStaticContext;\n}\n\ninterface StreamRequestBody {\n\tsessionId: string;\n\trunId?: string;\n\tmodel: Model<any>;\n\toptions?: SimpleStreamOptions;\n\tstaticContext?: SessionStaticContext;\n\tephemeralMessages?: Message[];\n\tcontextOverlay?: Message[];\n}\n\ninterface SessionSyncBody {\n\tsessionId: string;\n\tmessages: Message[];\n\tstaticContext?: SessionStaticContext;\n}\n\ninterface SessionAppendBody {\n\tsessionId: string;\n\tmessages: Message[];\n\tstaticContext?: SessionStaticContext;\n}\n\ninterface SessionTreeSyncBody {\n\tsessionId: string;\n\tentries: SessionTreeEntry[];\n\tleafId: string | null;\n\tstaticContext?: SessionStaticContext;\n}\n\ninterface SessionTreeSwitchBody {\n\tsessionId: string;\n\tleafId: string | null;\n}\n\ninterface SessionCompactBody {\n\tsessionId: string;\n\trunId?: string;\n\tmodel: Model<any>;\n\toptions?: SimpleStreamOptions;\n\tsettings?: CompactionSettings;\n\tpreparation?: CompactionPreparationOptions;\n\tcustomInstructions?: string;\n\tbaseTreeHash?: string;\n\tfullResponse?: boolean;\n\tstreamResponse?: boolean;\n}\n\nfunction createRequestModels(model: Model<any>, options: SimpleStreamOptions) {\n\tconst models = createModels();\n\tconst requestStream: ProviderStreams[\"streamSimple\"] = (requestModel, context, streamOptions) =>\n\t\tstreamSimple(requestModel, context, {\n\t\t\t...streamOptions,\n\t\t\t...(options.timeoutMs !== undefined ? { timeoutMs: options.timeoutMs } : {}),\n\t\t});\n\tmodels.setProvider(\n\t\tcreateProvider({\n\t\t\tid: model.provider,\n\t\t\tname: model.provider,\n\t\t\tmodels: [model],\n\t\t\tauth: {\n\t\t\t\tapiKey: {\n\t\t\t\t\tname: \"pi-server request auth\",\n\t\t\t\t\tresolve: async () => ({ auth: { apiKey: options.apiKey, headers: options.headers } }),\n\t\t\t\t},\n\t\t\t},\n\t\t\tapi: {\n\t\t\t\tstream: requestStream,\n\t\t\t\tstreamSimple: requestStream,\n\t\t\t},\n\t\t}),\n\t);\n\treturn models;\n}\n\ninterface StreamRunRecord {\n\tsessionId: string;\n\trunId: string;\n\tkind: \"stream\" | \"compact\";\n\tstatus: \"running\" | \"completed\" | \"failed\" | \"aborted\";\n\tmessage?: AssistantMessage;\n\tcompactResult?: SessionCompactSuccessBody;\n\terrorMessage?: string;\n\tcreatedAt: number;\n\tupdatedAt: number;\n\tcancelRequested: boolean;\n\tcontroller: AbortController;\n\tsettled: Promise<void>;\n\tresolveSettled: (() => void) | undefined;\n}\n\nconst STREAM_HEARTBEAT = \": keep-alive\\n\\n\";\nconst JSON_HEARTBEAT = \" \\n\";\nconst STREAM_HEARTBEAT_INTERVAL_MS = 25_000;\nconst STREAM_RUN_TTL_MS = 60 * 60 * 1000; // 1 hour\nconst streamRuns = new Map<string, StreamRunRecord>();\n\nfunction readBody(req: IncomingMessage): Promise<string> {\n\treturn new Promise((resolve, reject) => {\n\t\tconst chunks: Buffer[] = [];\n\t\treq.on(\"data\", (chunk: Buffer) => chunks.push(chunk));\n\t\treq.on(\"end\", () => resolve(Buffer.concat(chunks).toString(\"utf-8\")));\n\t\treq.on(\"error\", reject);\n\t});\n}\n\nfunction sendJson(res: ServerResponse, status: number, body: unknown): void {\n\tconst data = JSON.stringify(body);\n\tres.writeHead(status, { \"Content-Type\": \"application/json\", \"Content-Length\": Buffer.byteLength(data) });\n\tres.end(data);\n}\n\nfunction sendError(\n\tres: ServerResponse,\n\tstatus: number,\n\tmessage: string,\n\tcode?: PiServerErrorCode,\n\tdetails?: Record<string, unknown>,\n): void {\n\tconst body: PiServerErrorResponse = { error: message };\n\tif (code) body.code = code;\n\tif (details) body.details = details;\n\tsendJson(res, status, body);\n}\n\nfunction logRequestError(req: IncomingMessage, error: unknown): void {\n\tconst message = error instanceof Error ? error.stack || error.message : String(error);\n\tconsole.error(`${req.method ?? \"UNKNOWN\"} ${req.url ?? \"/\"} failed: ${message}`);\n}\n\nfunction authenticate(config: ServerConfig, req: IncomingMessage): boolean {\n\tif (!config.authToken) return true;\n\tconst header = req.headers.authorization;\n\tif (!header) return false;\n\tconst token = header.startsWith(\"Bearer \") ? header.slice(7) : header;\n\treturn token === config.authToken;\n}\n\nfunction sessionResponseBody(session: SessionState) {\n\treturn {\n\t\tsessionId: session.sessionId,\n\t\tstaticContextHash: session.staticContextHash,\n\t\ttreeHash: session.treeHash,\n\t\tmessageCount: session.messages.length,\n\t\tentryCount: session.entries.length,\n\t\tleafId: session.leafId,\n\t\trevision: session.revision,\n\t};\n}\n\nfunction persistSession(config: ServerConfig, session: SessionState): void {\n\tsavePersistedSession(config.sessionStoreDir, session);\n}\n\nfunction runKey(sessionId: string, runId: string): string {\n\treturn `${sessionId}\\0${runId}`;\n}\n\nfunction getStreamRun(sessionId: string, runId: string): StreamRunRecord | undefined {\n\treturn streamRuns.get(runKey(sessionId, runId));\n}\n\nfunction createRunRecord(sessionId: string, runId: string, kind: StreamRunRecord[\"kind\"]): StreamRunRecord {\n\tlet resolveSettled: (() => void) | undefined;\n\tconst settled = new Promise<void>((resolve) => {\n\t\tresolveSettled = resolve;\n\t});\n\treturn {\n\t\tsessionId,\n\t\trunId,\n\t\tkind,\n\t\tstatus: \"running\",\n\t\tcreatedAt: Date.now(),\n\t\tupdatedAt: Date.now(),\n\t\tcancelRequested: false,\n\t\tcontroller: new AbortController(),\n\t\tsettled,\n\t\tresolveSettled,\n\t};\n}\n\nfunction settleRun(run: StreamRunRecord | undefined): void {\n\tif (!run) return;\n\tconst resolve = run.resolveSettled;\n\trun.resolveSettled = undefined;\n\tresolve?.();\n}\n\nfunction startStreamRun(sessionId: string, runId: string, kind: StreamRunRecord[\"kind\"] = \"stream\"): StreamRunRecord {\n\tconst existing = getStreamRun(sessionId, runId);\n\tif (existing?.status === \"completed\" || existing?.status === \"aborted\") return existing;\n\tconst run = existing ?? createRunRecord(sessionId, runId, kind);\n\tif (existing) {\n\t\trun.message = undefined;\n\t\trun.errorMessage = undefined;\n\t\trun.cancelRequested = false;\n\t\trun.controller = new AbortController();\n\t\tlet resolveSettled: (() => void) | undefined;\n\t\trun.settled = new Promise<void>((resolve) => {\n\t\t\tresolveSettled = resolve;\n\t\t});\n\t\trun.resolveSettled = resolveSettled;\n\t}\n\trun.kind = kind;\n\trun.status = \"running\";\n\trun.updatedAt = Date.now();\n\tstreamRuns.set(runKey(sessionId, runId), run);\n\treturn run;\n}\n\nfunction streamRunResponseBody(run: StreamRunRecord) {\n\treturn {\n\t\tsessionId: run.sessionId,\n\t\trunId: run.runId,\n\t\tstatus: run.status,\n\t\tcreatedAt: run.createdAt,\n\t\tupdatedAt: run.updatedAt,\n\t\t...(run.message ? { message: run.message } : {}),\n\t\t...(run.compactResult ? { compactResult: run.compactResult } : {}),\n\t\t...(run.errorMessage ? { errorMessage: run.errorMessage } : {}),\n\t};\n}\n\nfunction replayStreamRun(run: StreamRunRecord): ProxyAssistantMessageEvent[] {\n\tif (!run.message) return [];\n\tif (\n\t\trun.message.stopReason !== \"stop\" &&\n\t\trun.message.stopReason !== \"length\" &&\n\t\trun.message.stopReason !== \"toolUse\" &&\n\t\trun.message.stopReason !== \"deferred\"\n\t) {\n\t\treturn [];\n\t}\n\tconst events: ProxyAssistantMessageEvent[] = [{ type: \"start\" }];\n\tfor (const [contentIndex, content] of run.message.content.entries()) {\n\t\tswitch (content.type) {\n\t\t\tcase \"text\":\n\t\t\t\tevents.push(\n\t\t\t\t\t{ type: \"text_start\", contentIndex },\n\t\t\t\t\t{ type: \"text_delta\", contentIndex, delta: content.text },\n\t\t\t\t\t{ type: \"text_end\", contentIndex, contentSignature: content.textSignature },\n\t\t\t\t);\n\t\t\t\tbreak;\n\t\t\tcase \"thinking\":\n\t\t\t\tevents.push(\n\t\t\t\t\t{ type: \"thinking_start\", contentIndex },\n\t\t\t\t\t{ type: \"thinking_delta\", contentIndex, delta: content.thinking },\n\t\t\t\t\t{ type: \"thinking_end\", contentIndex, contentSignature: content.thinkingSignature },\n\t\t\t\t);\n\t\t\t\tbreak;\n\t\t\tcase \"toolCall\":\n\t\t\t\tevents.push(\n\t\t\t\t\t{ type: \"toolcall_start\", contentIndex, id: content.id, toolName: content.name },\n\t\t\t\t\t{ type: \"toolcall_delta\", contentIndex, delta: JSON.stringify(content.arguments) },\n\t\t\t\t\t{ type: \"toolcall_end\", contentIndex, toolCall: content },\n\t\t\t\t);\n\t\t\t\tbreak;\n\t\t\tdefault:\n\t\t\t\tbreak;\n\t\t}\n\t}\n\tevents.push({\n\t\ttype: \"done\",\n\t\treason: run.message.stopReason,\n\t\tusage: run.message.usage,\n\t\tdeferred: run.message.deferred,\n\t\tmessage: run.message,\n\t});\n\treturn events;\n}\n\nfunction completeStreamRun(run: StreamRunRecord | undefined, message: AssistantMessage): void {\n\tif (!run) return;\n\tif (run.cancelRequested) {\n\t\trun.status = \"aborted\";\n\t\trun.message = undefined;\n\t\trun.errorMessage = \"Stream run aborted\";\n\t\trun.updatedAt = Date.now();\n\t\treturn;\n\t}\n\trun.status = \"completed\";\n\trun.message = message;\n\trun.errorMessage = undefined;\n\trun.updatedAt = Date.now();\n}\n\nfunction failStreamRun(run: StreamRunRecord | undefined, errorMessage: string): void {\n\tif (!run) return;\n\tif (run.cancelRequested) {\n\t\trun.status = \"aborted\";\n\t\trun.errorMessage = \"Stream run aborted\";\n\t\trun.updatedAt = Date.now();\n\t\treturn;\n\t}\n\trun.status = \"failed\";\n\trun.errorMessage = errorMessage;\n\trun.updatedAt = Date.now();\n}\n\nfunction touchStreamRun(run: StreamRunRecord | undefined): void {\n\tif (run) run.updatedAt = Date.now();\n}\n\nfunction writeStreamEvent(res: ServerResponse, event: ProxyAssistantMessageEvent): void {\n\tif (!res.writableEnded && !res.destroyed) {\n\t\tres.write(encodeProxyEvent(event));\n\t}\n}\n\nfunction writeStreamError(res: ServerResponse, message: string): void {\n\tif (!res.writableEnded && !res.destroyed) {\n\t\tres.write(encodeErrorEvent(message));\n\t}\n}\n\nfunction endStreamResponse(res: ServerResponse): void {\n\tif (!res.writableEnded && !res.destroyed) {\n\t\tres.end();\n\t}\n}\n\nfunction cleanupExpiredStreamRuns(nowMs: number): void {\n\tfor (const [key, run] of streamRuns) {\n\t\tif (nowMs - run.updatedAt > STREAM_RUN_TTL_MS) {\n\t\t\tstreamRuns.delete(key);\n\t\t}\n\t}\n}\n\nfunction deleteStreamRunsForSession(sessionId: string): void {\n\tfor (const [key, run] of streamRuns) {\n\t\tif (run.sessionId === sessionId) streamRuns.delete(key);\n\t}\n}\n\nconst streamRunCleanupTimer = setInterval(() => cleanupExpiredStreamRuns(Date.now()), STREAM_RUN_TTL_MS / 4);\nstreamRunCleanupTimer.unref();\n\nfunction sessionHistoryFullResponseBody(session: SessionState, baseMessageCount: number) {\n\treturn {\n\t\tsessionId: session.sessionId,\n\t\tstaticContext: session.staticContext,\n\t\tstaticContextHash: session.staticContextHash,\n\t\ttreeHash: session.treeHash,\n\t\tmessageCount: session.messages.length,\n\t\tentryCount: session.entries.length,\n\t\tleafId: session.leafId,\n\t\trevision: session.revision,\n\t\tentries: session.entries,\n\t\tbaseMessageCount,\n\t\tmessages: session.messages.slice(baseMessageCount),\n\t};\n}\n\nfunction sessionTreePatchResponseBody(\n\tsession: SessionState,\n\tbaseMessageCount: number,\n\tentriesFrom: number,\n\tbaseRevision: number | undefined,\n) {\n\treturn {\n\t\tsessionId: session.sessionId,\n\t\tstaticContext: session.staticContext,\n\t\tstaticContextHash: session.staticContextHash,\n\t\ttreeHash: session.treeHash,\n\t\tmessageCount: session.messages.length,\n\t\tentryCount: session.entries.length,\n\t\tleafId: session.leafId,\n\t\trevision: session.revision,\n\t\tbaseMessageCount,\n\t\tmessages: session.messages.slice(baseMessageCount),\n\t\ttreePatch: {\n\t\t\tentriesFrom,\n\t\t\tbaseRevision,\n\t\t\tentries: session.entries.slice(entriesFrom),\n\t\t\tleafId: session.leafId,\n\t\t\trevision: session.revision,\n\t\t},\n\t};\n}\n\ntype PreparedCompaction = Parameters<typeof compactLegacy>[0];\n\ninterface PreparedSessionCompact {\n\tsession: SessionState;\n\tsessionId: string;\n\trevision: number;\n\ttreeHash: string;\n\tleafId: string | null;\n\tentryCount: number;\n\tpreparation: PreparedCompaction;\n\toptions: SimpleStreamOptions;\n}\n\ninterface SessionCompactSuccessBody {\n\tsuccess: true;\n\tcompaction: Omit<LegacyCompactResult, \"retainedTail\">;\n\tcompactionEntry: SessionTreeEntry;\n\tsessionId: string;\n\tstaticContextHash: string;\n\ttreeHash: string;\n\tmessageCount: number;\n\tentryCount: number;\n\tleafId: string | null;\n\trevision: number;\n\tstaticContext?: SessionStaticContext;\n\ttreePatch?: {\n\t\tbaseTreeHash: string;\n\t\tentriesFrom: number;\n\t\tentries: SessionTreeEntry[];\n\t\tleafId: string | null;\n\t\trevision: number;\n\t};\n\tentries?: SessionTreeEntry[];\n\tmessages?: Message[];\n}\n\ninterface SessionCompactHttpResponse {\n\tstatus: number;\n\tbody: PiServerErrorResponse | SessionCompactSuccessBody;\n}\n\nfunction handleSessionInit(config: ServerConfig, body: SessionInitBody, res: ServerResponse): void {\n\tif (!body.sessionId) {\n\t\tsendError(res, 400, \"sessionId is required\", PiServerErrorCode.REQUIRED_FIELD_MISSING);\n\t\treturn;\n\t}\n\tif (body.staticContext) {\n\t\tsetStaticContext(body.sessionId, body.staticContext);\n\t} else {\n\t\tgetOrCreateSession(body.sessionId);\n\t}\n\tconst session = getSession(body.sessionId)!;\n\tpersistSession(config, session);\n\tsendJson(res, 200, sessionResponseBody(session));\n}\n\nfunction handleSessionUpdate(\n\tconfig: ServerConfig,\n\tbody: SessionInitBody & { staticContext: SessionStaticContext },\n\tres: ServerResponse,\n): void {\n\tif (!body.sessionId) {\n\t\tsendError(res, 400, \"sessionId is required\", PiServerErrorCode.REQUIRED_FIELD_MISSING);\n\t\treturn;\n\t}\n\tif (!body.staticContext) {\n\t\tsendError(res, 400, \"staticContext is required for update\", PiServerErrorCode.REQUIRED_FIELD_MISSING);\n\t\treturn;\n\t}\n\tsetStaticContext(body.sessionId, body.staticContext);\n\tconst session = getSession(body.sessionId)!;\n\tpersistSession(config, session);\n\tsendJson(res, 200, sessionResponseBody(session));\n}\n\nfunction handleSessionSync(config: ServerConfig, body: SessionSyncBody, res: ServerResponse): void {\n\tif (!body.sessionId) {\n\t\tsendError(res, 400, \"sessionId is required\", PiServerErrorCode.REQUIRED_FIELD_MISSING);\n\t\treturn;\n\t}\n\tif (!Array.isArray(body.messages)) {\n\t\tsendError(res, 400, \"messages is required\", PiServerErrorCode.REQUIRED_FIELD_MISSING);\n\t\treturn;\n\t}\n\tif (body.staticContext) {\n\t\tsetStaticContext(body.sessionId, body.staticContext);\n\t}\n\tconst session = replaceMessages(body.sessionId, body.messages);\n\tpersistSession(config, session);\n\tsendJson(res, 200, sessionResponseBody(session));\n}\n\nfunction handleSessionAppend(config: ServerConfig, body: SessionAppendBody, res: ServerResponse): void {\n\tif (!body.sessionId) {\n\t\tsendError(res, 400, \"sessionId is required\", PiServerErrorCode.REQUIRED_FIELD_MISSING);\n\t\treturn;\n\t}\n\tif (!Array.isArray(body.messages)) {\n\t\tsendError(res, 400, \"messages is required\", PiServerErrorCode.REQUIRED_FIELD_MISSING);\n\t\treturn;\n\t}\n\tif (body.staticContext) {\n\t\tsetStaticContext(body.sessionId, body.staticContext);\n\t}\n\tconst session = appendMessages(body.sessionId, body.messages);\n\tpersistSession(config, session);\n\tsendJson(res, 200, sessionResponseBody(session));\n}\n\nfunction handleSessionTreeSync(config: ServerConfig, body: SessionTreeSyncBody, res: ServerResponse): void {\n\tif (!body.sessionId) {\n\t\tsendError(res, 400, \"sessionId is required\", PiServerErrorCode.REQUIRED_FIELD_MISSING);\n\t\treturn;\n\t}\n\tif (!Array.isArray(body.entries)) {\n\t\tsendError(res, 400, \"entries is required\", PiServerErrorCode.REQUIRED_FIELD_MISSING);\n\t\treturn;\n\t}\n\tif (body.staticContext) {\n\t\tsetStaticContext(body.sessionId, body.staticContext);\n\t}\n\tconst session = replaceSessionTree(body.sessionId, body.entries, body.leafId ?? null);\n\tpersistSession(config, session);\n\tsendJson(res, 200, sessionResponseBody(session));\n}\n\nfunction handleSessionTreeAppend(config: ServerConfig, body: SessionTreeSyncBody, res: ServerResponse): void {\n\tif (!body.sessionId) {\n\t\tsendError(res, 400, \"sessionId is required\", PiServerErrorCode.REQUIRED_FIELD_MISSING);\n\t\treturn;\n\t}\n\tif (!Array.isArray(body.entries)) {\n\t\tsendError(res, 400, \"entries is required\", PiServerErrorCode.REQUIRED_FIELD_MISSING);\n\t\treturn;\n\t}\n\ttry {\n\t\tif (body.staticContext) {\n\t\t\tsetStaticContext(body.sessionId, body.staticContext);\n\t\t}\n\t\tconst session = appendSessionEntries(body.sessionId, body.entries, body.leafId ?? null);\n\t\tpersistSession(config, session);\n\t\tsendJson(res, 200, sessionResponseBody(session));\n\t} catch (error) {\n\t\tif (error instanceof PiServerError) {\n\t\t\tsendError(res, 400, error.message, error.code, error.details);\n\t\t} else {\n\t\t\tconst message = error instanceof Error ? error.message : String(error);\n\t\t\tsendError(res, 500, message, PiServerErrorCode.INTERNAL_ERROR);\n\t\t}\n\t}\n}\n\nfunction handleSessionTreeSwitch(config: ServerConfig, body: SessionTreeSwitchBody, res: ServerResponse): void {\n\tif (!body.sessionId) {\n\t\tsendError(res, 400, \"sessionId is required\", PiServerErrorCode.REQUIRED_FIELD_MISSING);\n\t\treturn;\n\t}\n\ttry {\n\t\tconst session = switchSessionLeaf(body.sessionId, body.leafId ?? null);\n\t\tpersistSession(config, session);\n\t\tsendJson(res, 200, sessionResponseBody(session));\n\t} catch (error) {\n\t\tif (error instanceof PiServerError) {\n\t\t\tsendError(res, 400, error.message, error.code, error.details);\n\t\t} else {\n\t\t\tconst message = error instanceof Error ? error.message : String(error);\n\t\t\tsendError(res, 500, message, PiServerErrorCode.INTERNAL_ERROR);\n\t\t}\n\t}\n}\n\nfunction prepareSessionCompact(body: SessionCompactBody): PreparedSessionCompact | SessionCompactHttpResponse {\n\tif (!body.sessionId) {\n\t\treturn { status: 400, body: { error: \"sessionId is required\" } };\n\t}\n\tif (!body.model) {\n\t\treturn { status: 400, body: { error: \"model is required\" } };\n\t}\n\n\tconst session = getSession(body.sessionId);\n\tif (!session) {\n\t\treturn { status: 404, body: { error: \"session not found\", code: PiServerErrorCode.SESSION_NOT_FOUND } };\n\t}\n\n\tconst entries = getSessionBranch(session);\n\tconst leaf = entries.at(-1);\n\tif (leaf?.type === \"compaction\") {\n\t\tif (!leaf.firstKeptEntryId) {\n\t\t\treturn { status: 400, body: { error: \"Stored compaction is missing firstKeptEntryId\" } };\n\t\t}\n\t\treturn {\n\t\t\tstatus: 200,\n\t\t\tbody: {\n\t\t\t\tsuccess: true,\n\t\t\t\tcompaction: {\n\t\t\t\t\tsummary: leaf.summary,\n\t\t\t\t\tfirstKeptEntryId: leaf.firstKeptEntryId,\n\t\t\t\t\ttokensBefore: leaf.tokensBefore,\n\t\t\t\t\tdetails: leaf.details,\n\t\t\t\t},\n\t\t\t\tcompactionEntry: leaf,\n\t\t\t\t...sessionResponseBody(session),\n\t\t\t\tentries: session.entries,\n\t\t\t\tmessages: session.messages,\n\t\t\t},\n\t\t};\n\t}\n\tconst preparationResult = prepareLegacyCompaction(\n\t\tentries,\n\t\tbody.settings ?? DEFAULT_COMPACTION_SETTINGS,\n\t\tbody.preparation,\n\t);\n\tif (!preparationResult.ok) {\n\t\treturn { status: 400, body: { error: preparationResult.error.message } };\n\t}\n\tif (!preparationResult.value) {\n\t\treturn { status: 400, body: { error: \"Nothing to compact\" } };\n\t}\n\n\tconst options = body.options ?? {};\n\treturn {\n\t\tsession,\n\t\tsessionId: session.sessionId,\n\t\trevision: session.revision,\n\t\ttreeHash: session.treeHash,\n\t\tleafId: session.leafId,\n\t\tentryCount: session.entries.length,\n\t\tpreparation: preparationResult.value,\n\t\toptions,\n\t};\n}\n\nfunction sessionCompactConflict(\n\tbody: SessionCompactBody,\n\tprepared: PreparedSessionCompact,\n\tcurrentSession: SessionState | undefined,\n): SessionCompactHttpResponse {\n\treturn {\n\t\tstatus: 409,\n\t\tbody: {\n\t\t\terror: \"Session changed while compaction was running\",\n\t\t\tcode: PiServerErrorCode.SESSION_STATE_CONFLICT,\n\t\t\tdetails: {\n\t\t\t\tsessionId: body.sessionId,\n\t\t\t\texpectedRevision: prepared.revision,\n\t\t\t\texpectedTreeHash: prepared.treeHash,\n\t\t\t\texpectedLeafId: prepared.leafId,\n\t\t\t\tactualRevision: currentSession?.revision ?? null,\n\t\t\t\tactualTreeHash: currentSession?.treeHash ?? null,\n\t\t\t\tactualLeafId: currentSession?.leafId ?? null,\n\t\t\t},\n\t\t},\n\t};\n}\n\nasync function completeSessionCompact(\n\tconfig: ServerConfig,\n\tbody: SessionCompactBody,\n\tprepared: PreparedSessionCompact,\n\trun?: StreamRunRecord,\n): Promise<SessionCompactHttpResponse> {\n\tconst result = await compactLegacy(\n\t\tprepared.preparation,\n\t\tcreateRequestModels(body.model, prepared.options),\n\t\tbody.model,\n\t\tbody.customInstructions,\n\t\trun?.controller.signal,\n\t\tprepared.options.reasoning,\n\t);\n\tif (!result.ok) {\n\t\treturn { status: 500, body: { error: result.error.message, code: PiServerErrorCode.INTERNAL_ERROR } };\n\t}\n\tif (run?.cancelRequested || run?.controller.signal.aborted) {\n\t\treturn { status: 409, body: { error: \"Compaction aborted\", code: PiServerErrorCode.INVALID_REQUEST } };\n\t}\n\n\tconst currentSession = getSession(prepared.sessionId);\n\tif (\n\t\t!currentSession ||\n\t\tcurrentSession !== prepared.session ||\n\t\tcurrentSession.revision !== prepared.revision ||\n\t\tcurrentSession.treeHash !== prepared.treeHash ||\n\t\tcurrentSession.leafId !== prepared.leafId\n\t) {\n\t\treturn sessionCompactConflict(body, prepared, currentSession);\n\t}\n\n\tconst baseTreeHash = prepared.treeHash;\n\tconst baseEntryCount = prepared.entryCount;\n\tconst compaction = result.value;\n\tconst { session: updatedSession, entry: compactionEntry } = appendCompactionEntry(body.sessionId, compaction);\n\tpersistSession(config, updatedSession);\n\tif (!body.fullResponse && body.baseTreeHash === baseTreeHash) {\n\t\treturn {\n\t\t\tstatus: 200,\n\t\t\tbody: {\n\t\t\t\tsuccess: true,\n\t\t\t\tcompaction: compaction satisfies LegacyCompactResult,\n\t\t\t\tcompactionEntry,\n\t\t\t\t...sessionResponseBody(updatedSession),\n\t\t\t\tstaticContext: updatedSession.staticContext,\n\t\t\t\ttreePatch: {\n\t\t\t\t\tbaseTreeHash,\n\t\t\t\t\tentriesFrom: baseEntryCount,\n\t\t\t\t\tentries: [compactionEntry],\n\t\t\t\t\tleafId: updatedSession.leafId,\n\t\t\t\t\trevision: updatedSession.revision,\n\t\t\t\t},\n\t\t\t},\n\t\t};\n\t}\n\treturn {\n\t\tstatus: 200,\n\t\tbody: {\n\t\t\tsuccess: true,\n\t\t\tcompaction: compaction satisfies LegacyCompactResult,\n\t\t\tcompactionEntry,\n\t\t\t...sessionResponseBody(updatedSession),\n\t\t\tstaticContext: updatedSession.staticContext,\n\t\t\tentries: updatedSession.entries,\n\t\t\tmessages: updatedSession.messages,\n\t\t},\n\t};\n}\n\nfunction writeServerSentEvent(res: ServerResponse, event: string, body: unknown): void {\n\tres.write(`event: ${event}\\n`);\n\tres.write(`data: ${JSON.stringify(body)}\\n\\n`);\n}\n\nasync function handleSessionCompactStream(\n\tconfig: ServerConfig,\n\tbody: SessionCompactBody,\n\tprepared: PreparedSessionCompact,\n\tres: ServerResponse,\n\trun?: StreamRunRecord,\n): Promise<void> {\n\tres.writeHead(200, {\n\t\t\"Content-Type\": \"text/event-stream\",\n\t\t\"Cache-Control\": \"no-cache\",\n\t\tConnection: \"keep-alive\",\n\t});\n\tres.flushHeaders();\n\tres.write(STREAM_HEARTBEAT);\n\n\tconst heartbeat = setInterval(() => {\n\t\ttouchStreamRun(run);\n\t\tif (!res.writableEnded) {\n\t\t\tres.write(STREAM_HEARTBEAT);\n\t\t}\n\t}, STREAM_HEARTBEAT_INTERVAL_MS);\n\theartbeat.unref();\n\n\ttry {\n\t\tconst result = await completeSessionCompact(config, body, prepared, run);\n\t\tfinishCompactRun(run, result);\n\t\twriteServerSentEvent(res, result.status >= 400 ? \"error\" : \"result\", result.body);\n\t} catch (err) {\n\t\tconst message = err instanceof Error ? err.message : String(err);\n\t\tif (run?.status === \"running\") {\n\t\t\trun.status = run.cancelRequested ? \"aborted\" : \"failed\";\n\t\t\trun.errorMessage = message;\n\t\t\trun.updatedAt = Date.now();\n\t\t}\n\t\twriteServerSentEvent(res, \"error\", { error: message });\n\t} finally {\n\t\tclearInterval(heartbeat);\n\t\tsettleRun(run);\n\t\tres.end();\n\t}\n}\n\nasync function handleSessionCompactJsonStream(\n\tconfig: ServerConfig,\n\tbody: SessionCompactBody,\n\tprepared: PreparedSessionCompact,\n\tres: ServerResponse,\n\trun?: StreamRunRecord,\n): Promise<void> {\n\tres.writeHead(200, {\n\t\t\"Content-Type\": \"application/json\",\n\t\t\"Cache-Control\": \"no-cache\",\n\t\tConnection: \"keep-alive\",\n\t});\n\tres.flushHeaders();\n\tres.write(JSON_HEARTBEAT);\n\n\tconst heartbeat = setInterval(() => {\n\t\ttouchStreamRun(run);\n\t\tif (!res.writableEnded) {\n\t\t\tres.write(JSON_HEARTBEAT);\n\t\t}\n\t}, STREAM_HEARTBEAT_INTERVAL_MS);\n\theartbeat.unref();\n\n\ttry {\n\t\tconst result = await completeSessionCompact(config, body, prepared, run);\n\t\tfinishCompactRun(run, result);\n\t\tres.write(JSON.stringify(result.body));\n\t} catch (err) {\n\t\tconst message = err instanceof Error ? err.message : String(err);\n\t\tif (run?.status === \"running\") {\n\t\t\trun.status = run.cancelRequested ? \"aborted\" : \"failed\";\n\t\t\trun.errorMessage = message;\n\t\t\trun.updatedAt = Date.now();\n\t\t}\n\t\tres.write(JSON.stringify({ error: message }));\n\t} finally {\n\t\tclearInterval(heartbeat);\n\t\tsettleRun(run);\n\t\tres.end();\n\t}\n}\n\nfunction finishCompactRun(run: StreamRunRecord | undefined, result: SessionCompactHttpResponse): void {\n\tif (!run) return;\n\tif (run.cancelRequested || run.controller.signal.aborted) {\n\t\trun.status = \"aborted\";\n\t\trun.errorMessage = \"Compaction aborted\";\n\t} else {\n\t\trun.status = result.status >= 400 ? \"failed\" : \"completed\";\n\t\tif (result.status >= 400 && \"error\" in result.body) {\n\t\t\trun.errorMessage = result.body.error;\n\t\t} else if (\"success\" in result.body) {\n\t\t\trun.compactResult = { ...result.body, entries: result.body.entries?.slice() };\n\t\t}\n\t}\n\trun.updatedAt = Date.now();\n}\n\nasync function handleSessionCompact(\n\tconfig: ServerConfig,\n\tbody: SessionCompactBody,\n\tres: ServerResponse,\n): Promise<void> {\n\tconst existingRun = body.runId ? getStreamRun(body.sessionId, body.runId) : undefined;\n\tif (existingRun?.status === \"aborted\") {\n\t\tsendJson(res, 409, { error: \"Compaction run was aborted\", code: PiServerErrorCode.INVALID_REQUEST });\n\t\treturn;\n\t}\n\tif (existingRun?.status === \"completed\" && existingRun.compactResult) {\n\t\tsendJson(res, 200, existingRun.compactResult);\n\t\treturn;\n\t}\n\tconst prepared = prepareSessionCompact(body);\n\tif (\"status\" in prepared) {\n\t\tsendJson(res, prepared.status, prepared.body);\n\t\treturn;\n\t}\n\n\tlet run: StreamRunRecord | undefined;\n\tif (body.runId) {\n\t\tconst existingRun = getStreamRun(body.sessionId, body.runId);\n\t\tif (existingRun?.status === \"aborted\") {\n\t\t\tsendJson(res, 409, { error: \"Compaction run was aborted\", code: PiServerErrorCode.INVALID_REQUEST });\n\t\t\treturn;\n\t\t}\n\t\tif (existingRun?.status === \"running\") {\n\t\t\tsendError(res, 409, \"A run with this runId is already in progress\", PiServerErrorCode.RUN_IN_PROGRESS);\n\t\t\treturn;\n\t\t}\n\t\trun = startStreamRun(body.sessionId, body.runId, \"compact\");\n\t}\n\n\tif (body.streamResponse) {\n\t\tawait handleSessionCompactStream(config, body, prepared, res, run);\n\t\treturn;\n\t}\n\n\tawait handleSessionCompactJsonStream(config, body, prepared, res, run);\n}\n\nfunction handleSessionHistory(\n\tsessionId: string,\n\tfrom: number | undefined,\n\tentriesFrom: number | undefined,\n\trevision: number | undefined,\n\tbaseTreeHash: string | undefined,\n\tres: ServerResponse,\n): void {\n\tconst session = getSession(sessionId);\n\tif (!session) {\n\t\tsendError(res, 404, \"session not found\", PiServerErrorCode.SESSION_NOT_FOUND);\n\t\treturn;\n\t}\n\tconst baseMessageCount = from ?? 0;\n\tif (\n\t\tentriesFrom !== undefined &&\n\t\tentriesFrom <= session.entries.length &&\n\t\t(revision === undefined || revision <= session.revision) &&\n\t\t(baseTreeHash === undefined || baseTreeHash === session.prefixHashes[entriesFrom])\n\t) {\n\t\tsendJson(res, 200, sessionTreePatchResponseBody(session, baseMessageCount, entriesFrom, revision));\n\t\treturn;\n\t}\n\tsendJson(res, 200, sessionHistoryFullResponseBody(session, baseMessageCount));\n}\n\nfunction handleSessionRun(sessionId: string, runId: string, res: ServerResponse): void {\n\tconst run = getStreamRun(sessionId, runId);\n\tif (!run) {\n\t\tsendError(res, 404, \"run not found\", PiServerErrorCode.RUN_NOT_FOUND);\n\t\treturn;\n\t}\n\tsendJson(res, 200, streamRunResponseBody(run));\n}\n\nasync function handleSessionRunAbort(sessionId: string, runId: string, res: ServerResponse): Promise<void> {\n\tif (!sessionId || !runId) {\n\t\tsendError(res, 400, \"sessionId and runId are required\", PiServerErrorCode.REQUIRED_FIELD_MISSING);\n\t\treturn;\n\t}\n\n\tlet run = getStreamRun(sessionId, runId);\n\tif (!run) {\n\t\trun = createRunRecord(sessionId, runId, \"stream\");\n\t\trun.status = \"aborted\";\n\t\trun.cancelRequested = true;\n\t\trun.errorMessage = \"Stream run aborted before it started\";\n\t\trun.updatedAt = Date.now();\n\t\tsettleRun(run);\n\t\tstreamRuns.set(runKey(sessionId, runId), run);\n\t\tsendJson(res, 200, streamRunResponseBody(run));\n\t\treturn;\n\t}\n\n\tif (run.status !== \"running\") {\n\t\tawait run.settled;\n\t\tsendJson(res, 200, streamRunResponseBody(run));\n\t\treturn;\n\t}\n\n\trun.cancelRequested = true;\n\trun.updatedAt = Date.now();\n\trun.controller.abort();\n\tawait run.settled;\n\tsendJson(res, 200, streamRunResponseBody(run));\n}\n\nexport function buildStreamContext(\n\tsession: SessionState,\n\tbody: Pick<StreamRequestBody, \"contextOverlay\" | \"ephemeralMessages\">,\n): Context {\n\tconst messages = body.contextOverlay ?? [...session.messages, ...(body.ephemeralMessages ?? [])];\n\treturn {\n\t\tsystemPrompt: session.staticContext?.systemPrompt,\n\t\tmessages,\n\t\ttools: session.staticContext?.tools,\n\t};\n}\n\nfunction handleStream(config: ServerConfig, body: StreamRequestBody, res: ServerResponse): void {\n\tif (!body.sessionId) {\n\t\tsendError(res, 400, \"sessionId is required\", PiServerErrorCode.REQUIRED_FIELD_MISSING);\n\t\treturn;\n\t}\n\tif (!body.model) {\n\t\tsendError(res, 400, \"model is required\", PiServerErrorCode.REQUIRED_FIELD_MISSING);\n\t\treturn;\n\t}\n\n\tcleanupExpiredStreamRuns(Date.now());\n\n\tconst session = getOrCreateSession(body.sessionId);\n\n\tif (body.staticContext) {\n\t\tsetStaticContext(body.sessionId, body.staticContext);\n\t\tpersistSession(config, session);\n\t}\n\n\tif (!session.staticContext && !body.staticContext) {\n\t\tsendError(\n\t\t\tres,\n\t\t\t400,\n\t\t\t\"Session has no static context. Initialize with /api/session/init first.\",\n\t\t\tPiServerErrorCode.SESSION_NO_STATIC_CONTEXT,\n\t\t);\n\t\treturn;\n\t}\n\n\tif (body.ephemeralMessages !== undefined && !Array.isArray(body.ephemeralMessages)) {\n\t\tsendError(res, 400, \"ephemeralMessages must be an array\", PiServerErrorCode.INVALID_REQUEST);\n\t\treturn;\n\t}\n\tif (body.contextOverlay !== undefined && !Array.isArray(body.contextOverlay)) {\n\t\tsendError(res, 400, \"contextOverlay must be an array\", PiServerErrorCode.INVALID_REQUEST);\n\t\treturn;\n\t}\n\n\tconst context = buildStreamContext(session, body);\n\tconst existingRun = body.runId ? getStreamRun(body.sessionId, body.runId) : undefined;\n\n\tconst resolvedModel = body.model;\n\n\tif (existingRun?.status === \"completed\") {\n\t\tres.writeHead(200, {\n\t\t\t\"Content-Type\": \"text/event-stream\",\n\t\t\t\"Cache-Control\": \"no-cache\",\n\t\t\tConnection: \"keep-alive\",\n\t\t});\n\t\tres.flushHeaders();\n\t\tres.write(STREAM_HEARTBEAT);\n\t\tfor (const event of replayStreamRun(existingRun)) {\n\t\t\twriteStreamEvent(res, event);\n\t\t}\n\t\tendStreamResponse(res);\n\t\treturn;\n\t}\n\tif (existingRun?.status === \"aborted\") {\n\t\tsendError(res, 409, \"The stream run was aborted\", PiServerErrorCode.INVALID_REQUEST);\n\t\treturn;\n\t}\n\tif (existingRun?.status === \"running\") {\n\t\tsendError(res, 409, \"A stream with this runId is already in progress\", PiServerErrorCode.RUN_IN_PROGRESS);\n\t\treturn;\n\t}\n\n\tconst run = body.runId ? startStreamRun(body.sessionId, body.runId) : undefined;\n\tconst streamOptions: SimpleStreamOptions = {\n\t\t...(body.options ?? {}),\n\t\t...(run ? { signal: run.controller.signal } : {}),\n\t};\n\n\tres.writeHead(200, {\n\t\t\"Content-Type\": \"text/event-stream\",\n\t\t\"Cache-Control\": \"no-cache\",\n\t\tConnection: \"keep-alive\",\n\t});\n\tres.flushHeaders();\n\tres.write(STREAM_HEARTBEAT);\n\n\tconst heartbeat = setInterval(() => {\n\t\ttouchStreamRun(run);\n\t\tif (!res.writableEnded && !res.destroyed) {\n\t\t\tres.write(STREAM_HEARTBEAT);\n\t\t}\n\t}, STREAM_HEARTBEAT_INTERVAL_MS);\n\theartbeat.unref();\n\n\tlet stream: AsyncIterable<AssistantMessageEvent>;\n\ttry {\n\t\tstream = streamSimple(resolvedModel, context, streamOptions);\n\t} catch (err) {\n\t\tclearInterval(heartbeat);\n\t\tconst message = err instanceof Error ? err.message : String(err);\n\t\tfailStreamRun(run, message);\n\t\tsettleRun(run);\n\t\twriteStreamError(res, message);\n\t\tendStreamResponse(res);\n\t\treturn;\n\t}\n\n\tvoid (async () => {\n\t\ttry {\n\t\t\tfor await (const event of stream) {\n\t\t\t\tconst proxyEvent = toProxyEvent(event);\n\t\t\t\tif (proxyEvent) {\n\t\t\t\t\ttouchStreamRun(run);\n\t\t\t\t\twriteStreamEvent(res, proxyEvent);\n\t\t\t\t}\n\t\t\t\tif (event.type === \"done\") {\n\t\t\t\t\tcompleteStreamRun(run, event.message);\n\t\t\t\t} else if (event.type === \"error\") {\n\t\t\t\t\tfailStreamRun(run, event.error.errorMessage ?? event.reason);\n\t\t\t\t}\n\t\t\t}\n\t\t} catch (err) {\n\t\t\tconst message = err instanceof Error ? err.message : String(err);\n\t\t\tfailStreamRun(run, message);\n\t\t\twriteStreamError(res, message);\n\t\t} finally {\n\t\t\tclearInterval(heartbeat);\n\t\t\tif (run?.status === \"running\") {\n\t\t\t\tfailStreamRun(run, run.cancelRequested ? \"Stream run aborted\" : \"Stream ended before a terminal event\");\n\t\t\t}\n\t\t\tsettleRun(run);\n\t\t\tendStreamResponse(res);\n\t\t}\n\t})();\n}\n\nasync function handlePostRequest(\n\tconfig: ServerConfig,\n\tpathname: string,\n\tbody: unknown,\n\tres: ServerResponse,\n): Promise<boolean> {\n\tif (pathname === \"/api/receive\") {\n\t\ttry {\n\t\t\tsendJson(res, 200, receiveUpload(config.uploadDir, body));\n\t\t} catch (error) {\n\t\t\tif (!(error instanceof ReceiveUploadError)) throw error;\n\t\t\tsendJson(res, error.status, { error: error.message });\n\t\t}\n\t\treturn true;\n\t}\n\n\tif (pathname === \"/api/session/init\") {\n\t\thandleSessionInit(config, body as SessionInitBody, res);\n\t\treturn true;\n\t}\n\n\tif (pathname === \"/api/session/update\") {\n\t\thandleSessionUpdate(config, body as SessionInitBody & { staticContext: SessionStaticContext }, res);\n\t\treturn true;\n\t}\n\n\tif (pathname === \"/api/session/sync\") {\n\t\thandleSessionSync(config, body as SessionSyncBody, res);\n\t\treturn true;\n\t}\n\n\tif (pathname === \"/api/session/append\") {\n\t\thandleSessionAppend(config, body as SessionAppendBody, res);\n\t\treturn true;\n\t}\n\n\tif (pathname === \"/api/session/tree/sync\") {\n\t\thandleSessionTreeSync(config, body as SessionTreeSyncBody, res);\n\t\treturn true;\n\t}\n\n\tif (pathname === \"/api/session/tree/append\") {\n\t\thandleSessionTreeAppend(config, body as SessionTreeSyncBody, res);\n\t\treturn true;\n\t}\n\n\tif (pathname === \"/api/session/tree/switch\") {\n\t\thandleSessionTreeSwitch(config, body as SessionTreeSwitchBody, res);\n\t\treturn true;\n\t}\n\n\tif (pathname === \"/api/session/compact\") {\n\t\tawait handleSessionCompact(config, body as SessionCompactBody, res);\n\t\treturn true;\n\t}\n\n\tif (pathname === \"/api/stream\") {\n\t\thandleStream(config, body as StreamRequestBody, res);\n\t\treturn true;\n\t}\n\n\treturn false;\n}\n\nexport function createPiServer(configOverride?: Partial<ServerConfig>): HttpServer {\n\tconst config = loadConfig(configOverride);\n\tloadPersistedSessions(config.sessionStoreDir);\n\n\tconst server = createServer(async (req, res) => {\n\t\tconst url = new URL(req.url ?? \"/\", \"http://localhost\");\n\n\t\tif (req.method === \"GET\" && url.pathname === \"/health\") {\n\t\t\tsendJson(res, 200, { status: \"ok\" });\n\t\t\treturn;\n\t\t}\n\n\t\tif (!authenticate(config, req)) {\n\t\t\tconst body =\n\t\t\t\treq.method === \"GET\" && url.pathname === \"/\"\n\t\t\t\t\t? { error: \"Unauthorized\", version: PI_SERVER_VERSION }\n\t\t\t\t\t: { error: \"Unauthorized\" };\n\t\t\tsendJson(res, 401, body);\n\t\t\treturn;\n\t\t}\n\n\t\tif (req.method === \"GET\" && url.pathname === \"/api/sessions\") {\n\t\t\tsendJson(res, 200, { sessions: listSessions() });\n\t\t\treturn;\n\t\t}\n\n\t\tconst runMatch = /^\\/api\\/session\\/([^/]+)\\/runs\\/([^/]+)$/.exec(url.pathname);\n\t\tif (req.method === \"GET\" && runMatch) {\n\t\t\thandleSessionRun(decodeURIComponent(runMatch[1]), decodeURIComponent(runMatch[2]), res);\n\t\t\treturn;\n\t\t}\n\n\t\tconst runAbortMatch = /^\\/api\\/session\\/([^/]+)\\/runs\\/([^/]+)\\/abort$/.exec(url.pathname);\n\t\tif (req.method === \"POST\" && runAbortMatch) {\n\t\t\tawait readBody(req);\n\t\t\tawait handleSessionRunAbort(decodeURIComponent(runAbortMatch[1]), decodeURIComponent(runAbortMatch[2]), res);\n\t\t\treturn;\n\t\t}\n\n\t\tif (req.method === \"GET\" && url.pathname.startsWith(\"/api/session/\") && url.pathname.endsWith(\"/history\")) {\n\t\t\tconst encodedSessionId = url.pathname.slice(\"/api/session/\".length, -\"/history\".length);\n\t\t\tconst fromParam = url.searchParams.get(\"from\");\n\t\t\tconst from = fromParam === null ? undefined : Number(fromParam);\n\t\t\tif (from !== undefined && (!Number.isInteger(from) || from < 0)) {\n\t\t\t\tsendError(res, 400, \"from must be a non-negative integer\", PiServerErrorCode.INVALID_REQUEST);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst entriesFromParam = url.searchParams.get(\"entriesFrom\");\n\t\t\tconst entriesFrom = entriesFromParam === null ? undefined : Number(entriesFromParam);\n\t\t\tif (entriesFrom !== undefined && (!Number.isInteger(entriesFrom) || entriesFrom < 0)) {\n\t\t\t\tsendError(res, 400, \"entriesFrom must be a non-negative integer\", PiServerErrorCode.INVALID_REQUEST);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst revisionParam = url.searchParams.get(\"revision\");\n\t\t\tconst revision = revisionParam === null ? undefined : Number(revisionParam);\n\t\t\tif (revision !== undefined && (!Number.isInteger(revision) || revision < 0)) {\n\t\t\t\tsendError(res, 400, \"revision must be a non-negative integer\", PiServerErrorCode.INVALID_REQUEST);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\thandleSessionHistory(\n\t\t\t\tdecodeURIComponent(encodedSessionId),\n\t\t\t\tfrom,\n\t\t\t\tentriesFrom,\n\t\t\t\trevision,\n\t\t\t\turl.searchParams.get(\"baseTreeHash\") ?? undefined,\n\t\t\t\tres,\n\t\t\t);\n\t\t\treturn;\n\t\t}\n\n\t\tif (req.method === \"POST\" && url.pathname === CHUNK_ENDPOINT) {\n\t\t\ttry {\n\t\t\t\tconst body = JSON.parse(await readBody(req)) as RequestChunkBody;\n\t\t\t\tconst chunkResult = receiveRequestChunk(body);\n\t\t\t\tif (!chunkResult.complete) {\n\t\t\t\t\tsendJson(res, 200, chunkResult.ack);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tconst handled = await handlePostRequest(\n\t\t\t\t\tconfig,\n\t\t\t\t\tchunkResult.target,\n\t\t\t\t\tJSON.parse(chunkResult.bodyJson) as unknown,\n\t\t\t\t\tres,\n\t\t\t\t);\n\t\t\t\tif (!handled && !res.headersSent) {\n\t\t\t\t\tsendError(res, 404, \"Not found\", PiServerErrorCode.INVALID_REQUEST);\n\t\t\t\t}\n\t\t\t} catch (err) {\n\t\t\t\tlogRequestError(req, err);\n\t\t\t\tif (!res.headersSent) {\n\t\t\t\t\tsendError(res, 400, err instanceof Error ? err.message : String(err), PiServerErrorCode.INVALID_REQUEST);\n\t\t\t\t} else {\n\t\t\t\t\tres.write(encodeErrorEvent(err instanceof Error ? err.message : String(err)));\n\t\t\t\t\tres.end();\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\n\t\tif (req.method === \"POST\") {\n\t\t\ttry {\n\t\t\t\tconst body = JSON.parse(await readBody(req)) as unknown;\n\t\t\t\tif (await handlePostRequest(config, url.pathname, body, res)) return;\n\t\t\t\tif (!res.headersSent) {\n\t\t\t\t\tsendError(res, 404, \"Not found\", PiServerErrorCode.INVALID_REQUEST);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t} catch (err) {\n\t\t\t\tlogRequestError(req, err);\n\t\t\t\tif (!res.headersSent) {\n\t\t\t\t\tsendError(res, 500, err instanceof Error ? err.message : String(err), PiServerErrorCode.INTERNAL_ERROR);\n\t\t\t\t} else {\n\t\t\t\t\tres.write(encodeErrorEvent(err instanceof Error ? err.message : String(err)));\n\t\t\t\t\tres.end();\n\t\t\t\t}\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\n\t\tif (req.method === \"DELETE\" && url.pathname.startsWith(\"/api/session/\")) {\n\t\t\tconst sessionId = decodeURIComponent(url.pathname.slice(\"/api/session/\".length));\n\t\t\tdeleteSessionFromStore(sessionId);\n\t\t\tdeleteStreamRunsForSession(sessionId);\n\t\t\tdeletePersistedSession(config.sessionStoreDir, sessionId);\n\t\t\tsendJson(res, 200, { deleted: sessionId });\n\t\t\treturn;\n\t\t}\n\n\t\tsendError(res, 404, \"Not found\", PiServerErrorCode.INVALID_REQUEST);\n\t});\n\n\treturn server;\n}\n\nfunction toProxyEvent(event: AssistantMessageEvent): ProxyAssistantMessageEvent | undefined {\n\tswitch (event.type) {\n\t\tcase \"start\":\n\t\t\treturn { type: \"start\" };\n\t\tcase \"text_start\":\n\t\t\treturn { type: \"text_start\", contentIndex: event.contentIndex };\n\t\tcase \"text_delta\":\n\t\t\treturn { type: \"text_delta\", contentIndex: event.contentIndex, delta: event.delta };\n\t\tcase \"text_end\":\n\t\t\treturn {\n\t\t\t\ttype: \"text_end\",\n\t\t\t\tcontentIndex: event.contentIndex,\n\t\t\t\tcontentSignature:\n\t\t\t\t\tevent.partial.content[event.contentIndex]?.type === \"text\"\n\t\t\t\t\t\t? (event.partial.content[event.contentIndex] as { textSignature?: string }).textSignature\n\t\t\t\t\t\t: undefined,\n\t\t\t};\n\t\tcase \"thinking_start\":\n\t\t\treturn { type: \"thinking_start\", contentIndex: event.contentIndex };\n\t\tcase \"thinking_delta\":\n\t\t\treturn { type: \"thinking_delta\", contentIndex: event.contentIndex, delta: event.delta };\n\t\tcase \"thinking_end\":\n\t\t\treturn {\n\t\t\t\ttype: \"thinking_end\",\n\t\t\t\tcontentIndex: event.contentIndex,\n\t\t\t\tcontentSignature:\n\t\t\t\t\tevent.partial.content[event.contentIndex]?.type === \"thinking\"\n\t\t\t\t\t\t? (event.partial.content[event.contentIndex] as { thinkingSignature?: string }).thinkingSignature\n\t\t\t\t\t\t: undefined,\n\t\t\t};\n\t\tcase \"toolcall_start\":\n\t\t\treturn {\n\t\t\t\ttype: \"toolcall_start\",\n\t\t\t\tcontentIndex: event.contentIndex,\n\t\t\t\tid:\n\t\t\t\t\tevent.partial.content[event.contentIndex]?.type === \"toolCall\"\n\t\t\t\t\t\t? (event.partial.content[event.contentIndex] as { id: string }).id\n\t\t\t\t\t\t: \"\",\n\t\t\t\ttoolName:\n\t\t\t\t\tevent.partial.content[event.contentIndex]?.type === \"toolCall\"\n\t\t\t\t\t\t? (event.partial.content[event.contentIndex] as { name: string }).name\n\t\t\t\t\t\t: \"\",\n\t\t\t};\n\t\tcase \"toolcall_delta\":\n\t\t\treturn { type: \"toolcall_delta\", contentIndex: event.contentIndex, delta: event.delta };\n\t\tcase \"toolcall_end\":\n\t\t\treturn { type: \"toolcall_end\", contentIndex: event.contentIndex, toolCall: event.toolCall };\n\t\tcase \"done\":\n\t\t\treturn {\n\t\t\t\ttype: \"done\",\n\t\t\t\treason: event.reason,\n\t\t\t\tusage: event.message.usage,\n\t\t\t\tdeferred: event.message.deferred,\n\t\t\t\tmessage: event.message,\n\t\t\t};\n\t\tcase \"error\":\n\t\t\treturn {\n\t\t\t\ttype: \"error\",\n\t\t\t\treason: event.reason,\n\t\t\t\terrorMessage: event.error.errorMessage,\n\t\t\t\tusage: event.error.usage,\n\t\t\t};\n\t\tdefault:\n\t\t\treturn undefined;\n\t}\n}\n\nexport function startServer(configOverride?: Partial<ServerConfig>): HttpServer {\n\tconst config = loadConfig(configOverride);\n\tconst server = createPiServer(configOverride);\n\tserver.listen(config.port, config.host, () => {\n\t\tconst address = server.address();\n\t\tconst port = typeof address === \"object\" && address !== null ? address.port : config.port;\n\t\tconst host = config.host.includes(\":\") && !config.host.startsWith(\"[\") ? `[${config.host}]` : config.host;\n\t\tconsole.log(`pi-server listening on ${host}:${port}`);\n\t});\n\treturn server;\n}\n"]}
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA,OAAO,EAAgB,KAAK,MAAM,IAAI,UAAU,EAA6C,MAAM,WAAW,CAAC;AAc/G,OAAO,EAGN,KAAK,OAAO,EAGZ,KAAK,OAAO,EACZ,KAAK,KAAK,EAEV,KAAK,mBAAmB,EACxB,MAAM,uBAAuB,CAAC;AAE/B,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAQhD,OAAO,EAWN,KAAK,YAAY,EACjB,KAAK,oBAAoB,EAGzB,MAAM,oBAAoB,CAAC;AAE5B,OAAO,EAAE,UAAU,EAAE,KAAK,YAAY,EAAE,MAAM,aAAa,CAAC;AAc5D,UAAU,iBAAiB;IAC1B,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC;IAClB,OAAO,CAAC,EAAE,mBAAmB,CAAC;IAC9B,aAAa,CAAC,EAAE,oBAAoB,CAAC;IACrC,iBAAiB,CAAC,EAAE,OAAO,EAAE,CAAC;IAC9B,cAAc,CAAC,EAAE,OAAO,EAAE,CAAC;CAC3B;AA67BD,wBAAgB,kBAAkB,CACjC,OAAO,EAAE,YAAY,EACrB,IAAI,EAAE,IAAI,CAAC,iBAAiB,EAAE,gBAAgB,GAAG,mBAAmB,CAAC,GACnE,OAAO,CAOT;AAoMD,wBAAgB,cAAc,CAAC,cAAc,CAAC,EAAE,OAAO,CAAC,YAAY,CAAC,GAAG,UAAU,CAoIjF;AAqED,wBAAgB,WAAW,CAAC,cAAc,CAAC,EAAE,OAAO,CAAC,YAAY,CAAC,GAAG,UAAU,CAU9E"}
package/dist/server.js CHANGED
@@ -1,11 +1,12 @@
1
1
  import { createServer } from "node:http";
2
2
  import { createRequire } from "node:module";
3
- import { compactLegacy, DEFAULT_COMPACTION_SETTINGS, prepareLegacyCompaction, } from "@earendil-works/pi-agent-core";
3
+ import { compactLegacy, convertToLlm, DEFAULT_COMPACTION_SETTINGS, prepareLegacyCompaction, } from "@earendil-works/pi-agent-core";
4
4
  import { createModels, createProvider, } from "@earendil-works/pi-ai";
5
5
  import { streamSimple } from "@earendil-works/pi-ai/compat";
6
6
  import { loadConfig } from "./config.js";
7
7
  import { PiServerError, PiServerErrorCode } from "./error-codes.js";
8
8
  import { encodeErrorEvent, encodeProxyEvent } from "./event-encoding.js";
9
+ import { hashPiServerProviderMessages } from "./pi-server-protocol.js";
9
10
  import { ReceiveUploadError, receiveUpload } from "./receive-upload.js";
10
11
  import { CHUNK_ENDPOINT, receiveRequestChunk } from "./request-chunks.js";
11
12
  import { deletePersistedSession, loadPersistedSessions, savePersistedSession } from "./session-persistence.js";
@@ -417,6 +418,13 @@ function prepareSessionCompact(body) {
417
418
  if (!body.model) {
418
419
  return { status: 400, body: { error: "model is required" } };
419
420
  }
421
+ const validDigest = (value) => value === undefined || /^[a-f0-9]{64}$/.test(value);
422
+ if (!validDigest(body.cacheContextHash) || !validDigest(body.cacheStaticContextHash)) {
423
+ return { status: 400, body: { error: "cache context digests must be 64-character SHA-256 hex strings" } };
424
+ }
425
+ if ((body.cacheContextHash === undefined) !== (body.cacheStaticContextHash === undefined)) {
426
+ return { status: 400, body: { error: "cacheContextHash and cacheStaticContextHash must be supplied together" } };
427
+ }
420
428
  const session = getSession(body.sessionId);
421
429
  if (!session) {
422
430
  return { status: 404, body: { error: "session not found", code: PiServerErrorCode.SESSION_NOT_FOUND } };
@@ -452,6 +460,36 @@ function prepareSessionCompact(body) {
452
460
  return { status: 400, body: { error: "Nothing to compact" } };
453
461
  }
454
462
  const options = body.options ?? {};
463
+ let cacheContext;
464
+ let cacheFallbackReason = body.cacheFallbackReason ?? "cache_context_digest_missing";
465
+ if (body.cacheContextHash && body.cacheStaticContextHash) {
466
+ if (options.sessionId !== session.sessionId) {
467
+ cacheFallbackReason = "session_id_mismatch";
468
+ }
469
+ else if (options.cacheRetention === "none") {
470
+ cacheFallbackReason = "prompt_cache_disabled";
471
+ }
472
+ else if (body.cacheStaticContextHash !== session.staticContextHash) {
473
+ cacheFallbackReason = "static_context_hash_mismatch";
474
+ }
475
+ else {
476
+ const projectedMessages = convertToLlm(session.messages);
477
+ const priorAssistant = [...session.messages].reverse().find((message) => message.role === "assistant");
478
+ if (hashPiServerProviderMessages(projectedMessages) !== body.cacheContextHash) {
479
+ cacheFallbackReason = "request_context_digest_mismatch";
480
+ }
481
+ else if (!priorAssistant ||
482
+ priorAssistant.provider !== body.model.provider ||
483
+ priorAssistant.model !== body.model.id ||
484
+ priorAssistant.api !== body.model.api) {
485
+ cacheFallbackReason = "summary_model_mismatch";
486
+ }
487
+ else {
488
+ cacheContext = buildStreamContext(session, { contextOverlay: projectedMessages });
489
+ cacheFallbackReason = "exact_prefix_unavailable";
490
+ }
491
+ }
492
+ }
455
493
  return {
456
494
  session,
457
495
  sessionId: session.sessionId,
@@ -461,6 +499,8 @@ function prepareSessionCompact(body) {
461
499
  entryCount: session.entries.length,
462
500
  preparation: preparationResult.value,
463
501
  options,
502
+ cacheContext,
503
+ cacheFallbackReason,
464
504
  };
465
505
  }
466
506
  function sessionCompactConflict(body, prepared, currentSession) {
@@ -482,7 +522,7 @@ function sessionCompactConflict(body, prepared, currentSession) {
482
522
  };
483
523
  }
484
524
  async function completeSessionCompact(config, body, prepared, run) {
485
- const result = await compactLegacy(prepared.preparation, createRequestModels(body.model, prepared.options), body.model, body.customInstructions, run?.controller.signal, prepared.options.reasoning);
525
+ const result = await compactLegacy(prepared.preparation, createRequestModels(body.model, prepared.options), body.model, body.customInstructions, run?.controller.signal, prepared.options.reasoning, undefined, undefined, prepared.cacheContext, prepared.options, prepared.cacheFallbackReason);
486
526
  if (!result.ok) {
487
527
  return { status: 500, body: { error: result.error.message, code: PiServerErrorCode.INTERNAL_ERROR } };
488
528
  }