@averyyy/pi-server 0.80.3-piclient.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/README.md +46 -0
  3. package/bin/pi-server.js +26 -0
  4. package/bin/update.js +86 -0
  5. package/dist/cli.d.ts +3 -0
  6. package/dist/cli.d.ts.map +1 -0
  7. package/dist/cli.js +4 -0
  8. package/dist/cli.js.map +1 -0
  9. package/dist/config.d.ts +8 -0
  10. package/dist/config.d.ts.map +1 -0
  11. package/dist/config.js +40 -0
  12. package/dist/config.js.map +1 -0
  13. package/dist/event-encoding.d.ts +5 -0
  14. package/dist/event-encoding.d.ts.map +1 -0
  15. package/dist/event-encoding.js +15 -0
  16. package/dist/event-encoding.js.map +1 -0
  17. package/dist/index.d.ts +5 -0
  18. package/dist/index.d.ts.map +1 -0
  19. package/dist/index.js +5 -0
  20. package/dist/index.js.map +1 -0
  21. package/dist/pi-server-protocol.d.ts +13 -0
  22. package/dist/pi-server-protocol.d.ts.map +1 -0
  23. package/dist/pi-server-protocol.js +32 -0
  24. package/dist/pi-server-protocol.js.map +1 -0
  25. package/dist/request-chunks.d.ts +37 -0
  26. package/dist/request-chunks.d.ts.map +1 -0
  27. package/dist/request-chunks.js +167 -0
  28. package/dist/request-chunks.js.map +1 -0
  29. package/dist/server.d.ts +23 -0
  30. package/dist/server.d.ts.map +1 -0
  31. package/dist/server.js +735 -0
  32. package/dist/server.js.map +1 -0
  33. package/dist/session-persistence.d.ts +5 -0
  34. package/dist/session-persistence.d.ts.map +1 -0
  35. package/dist/session-persistence.js +259 -0
  36. package/dist/session-persistence.js.map +1 -0
  37. package/dist/session-store.d.ts +75 -0
  38. package/dist/session-store.d.ts.map +1 -0
  39. package/dist/session-store.js +293 -0
  40. package/dist/session-store.js.map +1 -0
  41. package/package.json +58 -0
package/CHANGELOG.md ADDED
@@ -0,0 +1,21 @@
1
+ # Changelog
2
+
3
+ ## [Unreleased]
4
+
5
+ ### Added
6
+
7
+ - Initial pi-server package: HTTP proxy server that stores session state and forwards incremental LLM requests to upstream providers.
8
+ - `/api/session/init` endpoint for initializing session static context.
9
+ - `/api/session/update` endpoint for updating session static context.
10
+ - `GET /api/session/:id/history` endpoint for reading full server-side session history without a request body.
11
+ - `/api/stream` endpoint for streaming incremental LLM requests with delta messages.
12
+ - `/api/request/chunk` endpoint for reassembling oversized client requests before dispatch.
13
+ - `DELETE /api/session/:id` endpoint for removing one server-side session.
14
+ - `/health` endpoint for health checks.
15
+ - `pi-server update` command with npm global package updates.
16
+ - Configurable via `PI_SERVER_CONFIG` or environment variables: `PI_SERVER_HOST`, `PI_SERVER_PORT`, `PI_SERVER_AUTH_TOKEN`.
17
+ - Persistent session tree storage under `PI_SERVER_SESSION_STORE_DIR`, including exact tree hashes in session responses.
18
+
19
+ ### Fixed
20
+
21
+ - Used `--legacy-peer-deps` for npm-global fork updates so existing upstream Pi installs do not trigger peer override warnings for forked prerelease aliases.
package/README.md ADDED
@@ -0,0 +1,46 @@
1
+ # @averyyy/pi-server
2
+
3
+ Server for `@averyyy/pi-client`. It stores session state and forwards client requests to upstream model providers.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm i -g @averyyy/pi-server
9
+ ```
10
+
11
+ ## Start
12
+
13
+ ```bash
14
+ pi-server
15
+ ```
16
+
17
+ By default it listens on `http://127.0.0.1:4217` and stores sessions under `.pi/pi-server/sessions` in the current directory.
18
+
19
+ ## Configure
20
+
21
+ Set provider keys in the server environment, then start the server:
22
+
23
+ ```bash
24
+ OPENAI_API_KEY=your-key pi-server
25
+ ```
26
+
27
+ Useful server settings:
28
+
29
+ ```bash
30
+ PI_SERVER_HOST=127.0.0.1
31
+ PI_SERVER_PORT=4217
32
+ PI_SERVER_AUTH_TOKEN=your-token
33
+ PI_SERVER_SESSION_STORE_DIR=/path/to/sessions
34
+ ```
35
+
36
+ ## Connect A Client
37
+
38
+ ```bash
39
+ PI_SERVER_URL=http://127.0.0.1:4217 pi-client
40
+ ```
41
+
42
+ With auth:
43
+
44
+ ```bash
45
+ PI_SERVER_AUTH_TOKEN=your-token PI_SERVER_URL=http://127.0.0.1:4217 pi-client
46
+ ```
@@ -0,0 +1,26 @@
1
+ #!/usr/bin/env node
2
+ import { spawnSync } from "node:child_process";
3
+ import { dirname, join } from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+
6
+ const args = process.argv.slice(2);
7
+ const modulePromise =
8
+ args[0] === "update"
9
+ ? import("./update.js").then(async ({ runPiServerUpdate }) => {
10
+ process.exitCode = await runPiServerUpdate(args.slice(1));
11
+ })
12
+ : runPiServer(args);
13
+
14
+ Promise.resolve(modulePromise).catch((e) => {
15
+ console.error(e);
16
+ process.exit(1);
17
+ });
18
+
19
+ function runPiServer(args) {
20
+ const packageRoot = dirname(dirname(fileURLToPath(import.meta.url)));
21
+ const result = spawnSync(process.execPath, [join(packageRoot, "dist", "cli.js"), ...args], {
22
+ stdio: "inherit",
23
+ });
24
+ if (result.error) throw result.error;
25
+ process.exitCode = result.status ?? (result.signal === "SIGINT" ? 130 : 1);
26
+ }
package/bin/update.js ADDED
@@ -0,0 +1,86 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import { readFileSync } from "node:fs";
3
+ import { dirname, resolve } from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+
6
+ function defaultPackageRoot() {
7
+ return dirname(dirname(fileURLToPath(import.meta.url)));
8
+ }
9
+
10
+ function defaultRepoRoot() {
11
+ return resolve(defaultPackageRoot(), "..", "..");
12
+ }
13
+
14
+ function readPackageMetadata(packageRoot) {
15
+ return JSON.parse(readFileSync(resolve(packageRoot, "package.json"), "utf-8"));
16
+ }
17
+
18
+ function runStep(runner, command, args, cwd, stdio = "inherit") {
19
+ return runner(command, args, { cwd, stdio, encoding: "utf-8" });
20
+ }
21
+
22
+ function isMissingGitCheckout(result) {
23
+ const text = `${String(result.stdout ?? "")}\n${String(result.stderr ?? "")}`;
24
+ return result.status !== 0 && text.includes("not a git repository");
25
+ }
26
+
27
+ async function runNpmGlobalUpdate(runner, repoRoot, stdout, stderr) {
28
+ stdout.write("Updating npm packages: @averyyy/pi-client@latest @averyyy/pi-server@latest\n");
29
+ const result = runStep(
30
+ runner,
31
+ "npm",
32
+ ["install", "-g", "--ignore-scripts", "--legacy-peer-deps", "@averyyy/pi-client@latest", "@averyyy/pi-server@latest"],
33
+ repoRoot,
34
+ );
35
+ if (result.status !== 0) {
36
+ stderr.write(
37
+ "pi-server update failed: npm install -g --ignore-scripts --legacy-peer-deps @averyyy/pi-client@latest @averyyy/pi-server@latest\n",
38
+ );
39
+ return result.status ?? 1;
40
+ }
41
+ stdout.write("pi-server update complete\n");
42
+ return 0;
43
+ }
44
+
45
+ export async function runPiServerUpdate(_args = [], options = {}) {
46
+ const packageRoot = options.packageRoot ?? defaultPackageRoot();
47
+ const repoRoot = options.repoRoot ?? defaultRepoRoot();
48
+ const runner = options.runner ?? spawnSync;
49
+ const stdout = options.stdout ?? process.stdout;
50
+ const stderr = options.stderr ?? process.stderr;
51
+ const pkg = readPackageMetadata(packageRoot);
52
+
53
+ stdout.write(`pi-server ${pkg.version}\n`);
54
+ stdout.write(`Updating checkout: ${repoRoot}\n`);
55
+
56
+ const status = runStep(runner, "git", ["status", "--porcelain"], repoRoot, "pipe");
57
+ if (status.status !== 0) {
58
+ if (isMissingGitCheckout(status)) {
59
+ return runNpmGlobalUpdate(runner, repoRoot, stdout, stderr);
60
+ }
61
+ stderr.write("pi-server update failed: unable to inspect git status\n");
62
+ return status.status ?? 1;
63
+ }
64
+ if (String(status.stdout ?? "").trim().length > 0) {
65
+ stderr.write("pi-server update failed: working tree has uncommitted changes\n");
66
+ return 1;
67
+ }
68
+
69
+ const steps = [
70
+ ["git", ["pull", "--ff-only"]],
71
+ ["npm", ["install", "--ignore-scripts"]],
72
+ ["npm", ["run", "install:pi-client"]],
73
+ ["npm", ["run", "install:pi-server"]],
74
+ ];
75
+
76
+ for (const [command, args] of steps) {
77
+ const result = runStep(runner, command, args, repoRoot);
78
+ if (result.status !== 0) {
79
+ stderr.write(`pi-server update failed: ${command} ${args.join(" ")}\n`);
80
+ return result.status ?? 1;
81
+ }
82
+ }
83
+
84
+ stdout.write("pi-server update complete\n");
85
+ return 0;
86
+ }
package/dist/cli.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ export {};
3
+ //# sourceMappingURL=cli.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"","sourcesContent":["#!/usr/bin/env node\nimport { startServer } from \"./server.ts\";\n\nstartServer();\n"]}
package/dist/cli.js ADDED
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env node
2
+ import { startServer } from "./server.js";
3
+ startServer();
4
+ //# sourceMappingURL=cli.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAE1C,WAAW,EAAE,CAAC","sourcesContent":["#!/usr/bin/env node\nimport { startServer } from \"./server.ts\";\n\nstartServer();\n"]}
@@ -0,0 +1,8 @@
1
+ export interface ServerConfig {
2
+ host: string;
3
+ port: number;
4
+ authToken: string | undefined;
5
+ sessionStoreDir: string;
6
+ }
7
+ export declare function loadConfig(overrides?: Partial<ServerConfig>): ServerConfig;
8
+ //# sourceMappingURL=config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAGA,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;CACxB;AAwBD,wBAAgB,UAAU,CAAC,SAAS,CAAC,EAAE,OAAO,CAAC,YAAY,CAAC,GAAG,YAAY,CAmB1E","sourcesContent":["import { existsSync, readFileSync } from \"node:fs\";\nimport { resolve } from \"node:path\";\n\nexport interface ServerConfig {\n\thost: string;\n\tport: number;\n\tauthToken: string | undefined;\n\tsessionStoreDir: 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};\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};\n}\n"]}
package/dist/config.js ADDED
@@ -0,0 +1,40 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { resolve } from "node:path";
3
+ const DEFAULT_CONFIG = {
4
+ host: "127.0.0.1",
5
+ port: 4217,
6
+ authToken: undefined,
7
+ sessionStoreDir: resolve(".pi", "pi-server", "sessions"),
8
+ };
9
+ function loadConfigFile() {
10
+ const configPath = process.env.PI_SERVER_CONFIG;
11
+ if (!configPath)
12
+ return {};
13
+ const resolved = resolve(configPath);
14
+ if (!existsSync(resolved)) {
15
+ throw new Error(`PI_SERVER_CONFIG file not found: ${resolved}`);
16
+ }
17
+ try {
18
+ const content = readFileSync(resolved, "utf-8");
19
+ return JSON.parse(content);
20
+ }
21
+ catch (err) {
22
+ throw new Error(`PI_SERVER_CONFIG file contains invalid JSON: ${resolved}`, { cause: err });
23
+ }
24
+ }
25
+ export function loadConfig(overrides) {
26
+ const fileConfig = loadConfigFile();
27
+ return {
28
+ host: overrides?.host ?? process.env.PI_SERVER_HOST ?? fileConfig.host ?? DEFAULT_CONFIG.host,
29
+ port: overrides?.port ??
30
+ (process.env.PI_SERVER_PORT ? parseInt(process.env.PI_SERVER_PORT, 10) : undefined) ??
31
+ fileConfig.port ??
32
+ DEFAULT_CONFIG.port,
33
+ authToken: overrides?.authToken ?? process.env.PI_SERVER_AUTH_TOKEN ?? fileConfig.authToken ?? DEFAULT_CONFIG.authToken,
34
+ sessionStoreDir: resolve(overrides?.sessionStoreDir ??
35
+ process.env.PI_SERVER_SESSION_STORE_DIR ??
36
+ fileConfig.sessionStoreDir ??
37
+ DEFAULT_CONFIG.sessionStoreDir),
38
+ };
39
+ }
40
+ //# sourceMappingURL=config.js.map
@@ -0,0 +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,WAAW,CAAC;AASpC,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;CACxD,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;KACD,CAAC;AAAA,CACF","sourcesContent":["import { existsSync, readFileSync } from \"node:fs\";\nimport { resolve } from \"node:path\";\n\nexport interface ServerConfig {\n\thost: string;\n\tport: number;\n\tauthToken: string | undefined;\n\tsessionStoreDir: 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};\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};\n}\n"]}
@@ -0,0 +1,5 @@
1
+ import type { ProxyAssistantMessageEvent } from "@earendil-works/pi-agent-core";
2
+ export declare function encodeProxyEvent(event: ProxyAssistantMessageEvent): string;
3
+ export declare function encodeErrorEvent(message: string): string;
4
+ export declare function parseProxyEvent(line: string): ProxyAssistantMessageEvent | undefined;
5
+ //# sourceMappingURL=event-encoding.d.ts.map
@@ -0,0 +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"]}
@@ -0,0 +1,15 @@
1
+ export function encodeProxyEvent(event) {
2
+ return `data: ${JSON.stringify(event)}\n\n`;
3
+ }
4
+ export function encodeErrorEvent(message) {
5
+ return `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`;
6
+ }
7
+ export function parseProxyEvent(line) {
8
+ if (!line.startsWith("data: "))
9
+ return undefined;
10
+ const data = line.slice(6).trim();
11
+ if (!data)
12
+ return undefined;
13
+ return JSON.parse(data);
14
+ }
15
+ //# sourceMappingURL=event-encoding.js.map
@@ -0,0 +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"]}
@@ -0,0 +1,5 @@
1
+ export { loadConfig, type ServerConfig } from "./config.ts";
2
+ export { encodeErrorEvent, encodeProxyEvent, parseProxyEvent } from "./event-encoding.ts";
3
+ export { createPiServer, startServer } from "./server.ts";
4
+ export { appendAssistantResponse, appendMessages, clearAllSessions, deleteSession, exportSessionState, getOrCreateSession, getSession, hashSessionEntries, listSessions, type PersistedSessionState, restoreSessionState, type SessionState, type SessionStaticContext, type SessionSummary, setStaticContext, } from "./session-store.ts";
5
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +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,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AAC1F,OAAO,EAAE,cAAc,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC1D,OAAO,EACN,uBAAuB,EACvB,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 { encodeErrorEvent, encodeProxyEvent, parseProxyEvent } from \"./event-encoding.ts\";\nexport { createPiServer, startServer } from \"./server.ts\";\nexport {\n\tappendAssistantResponse,\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"]}
package/dist/index.js ADDED
@@ -0,0 +1,5 @@
1
+ export { loadConfig } from "./config.js";
2
+ export { encodeErrorEvent, encodeProxyEvent, parseProxyEvent } from "./event-encoding.js";
3
+ export { createPiServer, startServer } from "./server.js";
4
+ export { appendAssistantResponse, appendMessages, clearAllSessions, deleteSession, exportSessionState, getOrCreateSession, getSession, hashSessionEntries, listSessions, restoreSessionState, setStaticContext, } from "./session-store.js";
5
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAqB,MAAM,aAAa,CAAC;AAC5D,OAAO,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AAC1F,OAAO,EAAE,cAAc,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC1D,OAAO,EACN,uBAAuB,EACvB,cAAc,EACd,gBAAgB,EAChB,aAAa,EACb,kBAAkB,EAClB,kBAAkB,EAClB,UAAU,EACV,kBAAkB,EAClB,YAAY,EAEZ,mBAAmB,EAInB,gBAAgB,GAChB,MAAM,oBAAoB,CAAC","sourcesContent":["export { loadConfig, type ServerConfig } from \"./config.ts\";\nexport { encodeErrorEvent, encodeProxyEvent, parseProxyEvent } from \"./event-encoding.ts\";\nexport { createPiServer, startServer } from \"./server.ts\";\nexport {\n\tappendAssistantResponse,\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"]}
@@ -0,0 +1,13 @@
1
+ import type { SessionTreeEntry } from "@earendil-works/pi-agent-core";
2
+ import type { Tool } from "@earendil-works/pi-ai";
3
+ export interface PiServerStaticContext {
4
+ systemPrompt?: string;
5
+ tools?: Tool[];
6
+ }
7
+ export declare function hashPiServerStaticContext(context: PiServerStaticContext | undefined): string;
8
+ export declare const PI_SERVER_EMPTY_TREE_HASH: string;
9
+ export declare function hashPiServerTreeEntry(entry: SessionTreeEntry): string;
10
+ export declare function appendPiServerTreeHash(previousHash: string, entryHash: string): string;
11
+ export declare function buildPiServerTreePrefixHashes(entries: SessionTreeEntry[]): string[];
12
+ export declare function hashPiServerSessionEntries(entries: SessionTreeEntry[]): string;
13
+ //# sourceMappingURL=pi-server-protocol.d.ts.map
@@ -0,0 +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"]}
@@ -0,0 +1,32 @@
1
+ import { createHash } from "node:crypto";
2
+ export function hashPiServerStaticContext(context) {
3
+ if (!context)
4
+ return "";
5
+ const canonical = {
6
+ systemPrompt: context.systemPrompt,
7
+ tools: context.tools?.map((tool) => ({
8
+ name: tool.name,
9
+ description: tool.description,
10
+ parameters: tool.parameters,
11
+ })),
12
+ };
13
+ return createHash("sha256").update(JSON.stringify(canonical)).digest("hex");
14
+ }
15
+ export const PI_SERVER_EMPTY_TREE_HASH = createHash("sha256").update("pi-tree-v1").digest("hex");
16
+ export function hashPiServerTreeEntry(entry) {
17
+ return createHash("sha256").update(JSON.stringify(entry)).digest("hex");
18
+ }
19
+ export function appendPiServerTreeHash(previousHash, entryHash) {
20
+ return createHash("sha256").update(`${previousHash}:${entryHash}`).digest("hex");
21
+ }
22
+ export function buildPiServerTreePrefixHashes(entries) {
23
+ const hashes = [PI_SERVER_EMPTY_TREE_HASH];
24
+ for (const entry of entries) {
25
+ hashes.push(appendPiServerTreeHash(hashes[hashes.length - 1], hashPiServerTreeEntry(entry)));
26
+ }
27
+ return hashes;
28
+ }
29
+ export function hashPiServerSessionEntries(entries) {
30
+ return buildPiServerTreePrefixHashes(entries)[entries.length];
31
+ }
32
+ //# sourceMappingURL=pi-server-protocol.js.map
@@ -0,0 +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"]}
@@ -0,0 +1,37 @@
1
+ export declare const CHUNK_ENDPOINT = "/api/request/chunk";
2
+ export interface RequestChunkBody {
3
+ requestId: string;
4
+ target: string;
5
+ chunkIndex: number;
6
+ totalChunks: number;
7
+ sha256: string;
8
+ chunk: string;
9
+ }
10
+ interface ChunkAck {
11
+ received: true;
12
+ requestId: string;
13
+ chunkIndex: number;
14
+ totalChunks: number;
15
+ }
16
+ interface PendingChunkResult {
17
+ complete: false;
18
+ ack: ChunkAck;
19
+ }
20
+ interface CompleteChunkResult {
21
+ complete: true;
22
+ target: string;
23
+ bodyJson: string;
24
+ }
25
+ export declare const REQUEST_CHUNK_PENDING_TTL_MS: number;
26
+ export declare const REQUEST_CHUNK_MAX_PENDING_BYTES: number;
27
+ export declare const REQUEST_CHUNK_COMPLETED_TTL_MS: number;
28
+ interface ReceiveRequestChunkOptions {
29
+ nowMs?: number;
30
+ pendingTtlMs?: number;
31
+ maxPendingBytes?: number;
32
+ completedTtlMs?: number;
33
+ }
34
+ export declare function receiveRequestChunk(body: RequestChunkBody, options?: ReceiveRequestChunkOptions): PendingChunkResult | CompleteChunkResult;
35
+ export declare function clearAllRequestChunks(): void;
36
+ export {};
37
+ //# sourceMappingURL=request-chunks.d.ts.map
@@ -0,0 +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,QAAmB,CAAC;AAChE,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/drop-last-assistant-error\",\n\t\"/api/session/compact\",\n\t\"/api/stream\",\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 = 64 * 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"]}
@@ -0,0 +1,167 @@
1
+ import { Buffer } from "node:buffer";
2
+ import { createHash } from "node:crypto";
3
+ export const CHUNK_ENDPOINT = "/api/request/chunk";
4
+ const ALLOWED_TARGETS = new Set([
5
+ "/api/session/init",
6
+ "/api/session/update",
7
+ "/api/session/sync",
8
+ "/api/session/append",
9
+ "/api/session/tree/sync",
10
+ "/api/session/tree/append",
11
+ "/api/session/tree/switch",
12
+ "/api/session/drop-last-assistant-error",
13
+ "/api/session/compact",
14
+ "/api/stream",
15
+ ]);
16
+ const pendingRequests = new Map();
17
+ const completedRequests = new Map();
18
+ let pendingRequestBytes = 0;
19
+ export const REQUEST_CHUNK_PENDING_TTL_MS = 5 * 60 * 1000;
20
+ export const REQUEST_CHUNK_MAX_PENDING_BYTES = 64 * 1024 * 1024;
21
+ export const REQUEST_CHUNK_COMPLETED_TTL_MS = 60 * 1000;
22
+ function sha256(value) {
23
+ return createHash("sha256").update(value).digest("hex");
24
+ }
25
+ function assertValidChunk(body) {
26
+ if (!body.requestId)
27
+ throw new Error("requestId is required");
28
+ if (!ALLOWED_TARGETS.has(body.target))
29
+ throw new Error(`Unsupported chunk target: ${body.target}`);
30
+ if (!Number.isInteger(body.totalChunks) || body.totalChunks <= 0) {
31
+ throw new Error("totalChunks must be a positive integer");
32
+ }
33
+ if (!Number.isInteger(body.chunkIndex) || body.chunkIndex < 0 || body.chunkIndex >= body.totalChunks) {
34
+ throw new Error("chunkIndex must be an integer within the chunk range");
35
+ }
36
+ if (typeof body.chunk !== "string")
37
+ throw new Error("chunk must be a string");
38
+ if (typeof body.sha256 !== "string" || !/^[a-f0-9]{64}$/i.test(body.sha256)) {
39
+ throw new Error("sha256 must be a 64-character hex string");
40
+ }
41
+ if (sha256(body.chunk) !== body.sha256) {
42
+ throw new Error(`Chunk checksum mismatch: ${body.chunkIndex}`);
43
+ }
44
+ }
45
+ function makeAck(body) {
46
+ return {
47
+ complete: false,
48
+ ack: {
49
+ received: true,
50
+ requestId: body.requestId,
51
+ chunkIndex: body.chunkIndex,
52
+ totalChunks: body.totalChunks,
53
+ },
54
+ };
55
+ }
56
+ function deletePendingRequest(requestId, pending) {
57
+ pendingRequests.delete(requestId);
58
+ pendingRequestBytes -= pending.receivedBytes;
59
+ }
60
+ function cleanupExpiredRequests(nowMs, pendingTtlMs, completedTtlMs) {
61
+ for (const [requestId, pending] of pendingRequests) {
62
+ if (nowMs - pending.updatedAtMs > pendingTtlMs) {
63
+ deletePendingRequest(requestId, pending);
64
+ }
65
+ }
66
+ for (const [requestId, completed] of completedRequests) {
67
+ if (nowMs - completed.completedAtMs > completedTtlMs) {
68
+ completedRequests.delete(requestId);
69
+ }
70
+ }
71
+ }
72
+ function cleanupForPendingBytes(extraBytes, maxPendingBytes, protectedRequestId) {
73
+ for (const [requestId, pending] of pendingRequests) {
74
+ if (pendingRequestBytes + extraBytes <= maxPendingBytes)
75
+ return;
76
+ if (requestId !== protectedRequestId) {
77
+ deletePendingRequest(requestId, pending);
78
+ }
79
+ }
80
+ if (pendingRequestBytes + extraBytes > maxPendingBytes) {
81
+ throw new Error("Request chunk pending bytes limit exceeded");
82
+ }
83
+ }
84
+ export function receiveRequestChunk(body, options = {}) {
85
+ assertValidChunk(body);
86
+ const nowMs = options.nowMs ?? Date.now();
87
+ cleanupExpiredRequests(nowMs, options.pendingTtlMs ?? REQUEST_CHUNK_PENDING_TTL_MS, options.completedTtlMs ?? REQUEST_CHUNK_COMPLETED_TTL_MS);
88
+ const completed = completedRequests.get(body.requestId);
89
+ if (completed) {
90
+ if (completed.target !== body.target || completed.totalChunks !== body.totalChunks) {
91
+ throw new Error("Chunk metadata does not match the completed request");
92
+ }
93
+ const existing = completed.chunks.get(body.chunkIndex);
94
+ if (!existing || existing.chunk !== body.chunk || existing.sha256 !== body.sha256) {
95
+ throw new Error(`Duplicate chunk index does not match: ${body.chunkIndex}`);
96
+ }
97
+ if (body.chunkIndex === completed.completedChunkIndex) {
98
+ return {
99
+ complete: true,
100
+ target: completed.target,
101
+ bodyJson: completed.bodyJson,
102
+ };
103
+ }
104
+ return makeAck(body);
105
+ }
106
+ let pending = pendingRequests.get(body.requestId);
107
+ if (pending) {
108
+ if (pending.target !== body.target || pending.totalChunks !== body.totalChunks) {
109
+ throw new Error("Chunk metadata does not match the pending request");
110
+ }
111
+ const existing = pending.chunks.get(body.chunkIndex);
112
+ if (existing) {
113
+ if (existing.chunk !== body.chunk || existing.sha256 !== body.sha256) {
114
+ throw new Error(`Duplicate chunk index does not match: ${body.chunkIndex}`);
115
+ }
116
+ pending.updatedAtMs = nowMs;
117
+ return makeAck(body);
118
+ }
119
+ }
120
+ const chunkBytes = Buffer.byteLength(body.chunk, "utf-8");
121
+ cleanupForPendingBytes(chunkBytes, options.maxPendingBytes ?? REQUEST_CHUNK_MAX_PENDING_BYTES, body.requestId);
122
+ if (!pending) {
123
+ pending = {
124
+ target: body.target,
125
+ totalChunks: body.totalChunks,
126
+ chunks: new Map(),
127
+ receivedBytes: 0,
128
+ updatedAtMs: nowMs,
129
+ };
130
+ pendingRequests.set(body.requestId, pending);
131
+ }
132
+ pending.chunks.set(body.chunkIndex, { chunk: body.chunk, sha256: body.sha256 });
133
+ pending.receivedBytes += chunkBytes;
134
+ pending.updatedAtMs = nowMs;
135
+ pendingRequestBytes += chunkBytes;
136
+ if (pending.chunks.size !== pending.totalChunks) {
137
+ return makeAck(body);
138
+ }
139
+ const encodedChunks = [];
140
+ for (let index = 0; index < pending.totalChunks; index++) {
141
+ const chunk = pending.chunks.get(index);
142
+ if (chunk === undefined)
143
+ throw new Error(`Missing chunk index: ${index}`);
144
+ encodedChunks.push(chunk.chunk);
145
+ }
146
+ deletePendingRequest(body.requestId, pending);
147
+ const bodyJson = Buffer.from(encodedChunks.join(""), "base64").toString("utf-8");
148
+ completedRequests.set(body.requestId, {
149
+ target: pending.target,
150
+ totalChunks: pending.totalChunks,
151
+ bodyJson,
152
+ completedChunkIndex: body.chunkIndex,
153
+ completedAtMs: nowMs,
154
+ chunks: pending.chunks,
155
+ });
156
+ return {
157
+ complete: true,
158
+ target: pending.target,
159
+ bodyJson,
160
+ };
161
+ }
162
+ export function clearAllRequestChunks() {
163
+ pendingRequests.clear();
164
+ completedRequests.clear();
165
+ pendingRequestBytes = 0;
166
+ }
167
+ //# sourceMappingURL=request-chunks.js.map