@averyyy/pi-server 0.84.1-piclient.3 → 0.84.2-piclient.3

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
@@ -19,6 +19,11 @@
19
19
 
20
20
  ### Fixed
21
21
 
22
+ - Recovered and truncated torn WAL tail records without hiding complete invalid records or allowing one corrupt session file to prevent server startup.
23
+ - Preserved complete authoritative tool-call metadata through proxy streams and completed-run replay.
24
+ - Bounded stream-run journals with TTL cleanup, final-message-only storage, same-run recovery, and session-delete cleanup.
25
+ - Returned structured error codes for session/tree recovery and removed the obsolete assistant-error deletion endpoint.
26
+ - Updated pi-server runtime dependencies to the current lockstep fork packages.
22
27
  - Used `--legacy-peer-deps` for npm-global fork updates so existing upstream Pi installs do not trigger peer override warnings for forked prerelease aliases.
23
28
  - Increased the pending chunk budget to 1 GiB so large session-tree appends do not fail at the previous 64 MiB Base64 limit.
24
29
 
@@ -0,0 +1,27 @@
1
+ /** Structured error codes for the pi-server HTTP protocol. */
2
+ export declare const PiServerErrorCode: {
3
+ readonly SESSION_NOT_FOUND: "SESSION_NOT_FOUND";
4
+ readonly SESSION_NO_STATIC_CONTEXT: "SESSION_NO_STATIC_CONTEXT";
5
+ readonly PARENT_ENTRY_NOT_FOUND: "PARENT_ENTRY_NOT_FOUND";
6
+ readonly LEAF_ID_NOT_FOUND: "LEAF_ID_NOT_FOUND";
7
+ readonly ENTRY_ALREADY_EXISTS: "ENTRY_ALREADY_EXISTS";
8
+ readonly INVALID_REQUEST: "INVALID_REQUEST";
9
+ readonly REQUIRED_FIELD_MISSING: "REQUIRED_FIELD_MISSING";
10
+ readonly RUN_NOT_FOUND: "RUN_NOT_FOUND";
11
+ readonly RUN_IN_PROGRESS: "RUN_IN_PROGRESS";
12
+ readonly INTERNAL_ERROR: "INTERNAL_ERROR";
13
+ };
14
+ export type PiServerErrorCode = (typeof PiServerErrorCode)[keyof typeof PiServerErrorCode];
15
+ export interface PiServerErrorResponse {
16
+ error: string;
17
+ code?: PiServerErrorCode;
18
+ details?: Record<string, unknown>;
19
+ }
20
+ export declare class PiServerError extends Error {
21
+ readonly code: PiServerErrorCode;
22
+ readonly details?: Record<string, unknown>;
23
+ constructor(message: string, code: PiServerErrorCode, details?: Record<string, unknown>);
24
+ }
25
+ export declare function isRecoverableTreeDivergenceCode(code: PiServerErrorCode | undefined): boolean;
26
+ export declare function isRecoverableMissingServerStateCode(code: PiServerErrorCode | undefined): boolean;
27
+ //# sourceMappingURL=error-codes.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"error-codes.d.ts","sourceRoot":"","sources":["../src/error-codes.ts"],"names":[],"mappings":"AAAA,8DAA8D;AAE9D,eAAO,MAAM,iBAAiB;;;;;;;;;;;CAWpB,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\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"]}
@@ -0,0 +1,36 @@
1
+ /** Structured error codes for the pi-server HTTP protocol. */
2
+ export const PiServerErrorCode = {
3
+ SESSION_NOT_FOUND: "SESSION_NOT_FOUND",
4
+ SESSION_NO_STATIC_CONTEXT: "SESSION_NO_STATIC_CONTEXT",
5
+ PARENT_ENTRY_NOT_FOUND: "PARENT_ENTRY_NOT_FOUND",
6
+ LEAF_ID_NOT_FOUND: "LEAF_ID_NOT_FOUND",
7
+ ENTRY_ALREADY_EXISTS: "ENTRY_ALREADY_EXISTS",
8
+ INVALID_REQUEST: "INVALID_REQUEST",
9
+ REQUIRED_FIELD_MISSING: "REQUIRED_FIELD_MISSING",
10
+ RUN_NOT_FOUND: "RUN_NOT_FOUND",
11
+ RUN_IN_PROGRESS: "RUN_IN_PROGRESS",
12
+ INTERNAL_ERROR: "INTERNAL_ERROR",
13
+ };
14
+ export class PiServerError extends Error {
15
+ code;
16
+ details;
17
+ constructor(message, code, details) {
18
+ super(message);
19
+ this.name = "PiServerError";
20
+ this.code = code;
21
+ this.details = details;
22
+ }
23
+ }
24
+ export function isRecoverableTreeDivergenceCode(code) {
25
+ return (code === PiServerErrorCode.PARENT_ENTRY_NOT_FOUND ||
26
+ code === PiServerErrorCode.LEAF_ID_NOT_FOUND ||
27
+ code === PiServerErrorCode.ENTRY_ALREADY_EXISTS);
28
+ }
29
+ export function isRecoverableMissingServerStateCode(code) {
30
+ return (code === PiServerErrorCode.SESSION_NOT_FOUND ||
31
+ code === PiServerErrorCode.SESSION_NO_STATIC_CONTEXT ||
32
+ code === PiServerErrorCode.PARENT_ENTRY_NOT_FOUND ||
33
+ code === PiServerErrorCode.LEAF_ID_NOT_FOUND ||
34
+ code === PiServerErrorCode.ENTRY_ALREADY_EXISTS);
35
+ }
36
+ //# sourceMappingURL=error-codes.js.map
@@ -0,0 +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,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\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"]}
package/dist/index.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  export { loadConfig, type ServerConfig } from "./config.ts";
2
+ export { isRecoverableMissingServerStateCode, isRecoverableTreeDivergenceCode, PiServerError, PiServerErrorCode, type PiServerErrorResponse, } from "./error-codes.ts";
2
3
  export { encodeErrorEvent, encodeProxyEvent, parseProxyEvent } from "./event-encoding.ts";
3
4
  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
+ export { appendMessages, clearAllSessions, deleteSession, exportSessionState, getOrCreateSession, getSession, hashSessionEntries, listSessions, type PersistedSessionState, restoreSessionState, type SessionState, type SessionStaticContext, type SessionSummary, setStaticContext, } from "./session-store.ts";
5
6
  //# sourceMappingURL=index.d.ts.map
@@ -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,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"]}
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"]}
package/dist/index.js CHANGED
@@ -1,5 +1,6 @@
1
1
  export { loadConfig } from "./config.js";
2
+ export { isRecoverableMissingServerStateCode, isRecoverableTreeDivergenceCode, PiServerError, PiServerErrorCode, } from "./error-codes.js";
2
3
  export { encodeErrorEvent, encodeProxyEvent, parseProxyEvent } from "./event-encoding.js";
3
4
  export { createPiServer, startServer } from "./server.js";
4
- export { appendAssistantResponse, appendMessages, clearAllSessions, deleteSession, exportSessionState, getOrCreateSession, getSession, hashSessionEntries, listSessions, restoreSessionState, setStaticContext, } from "./session-store.js";
5
+ export { appendMessages, clearAllSessions, deleteSession, exportSessionState, getOrCreateSession, getSession, hashSessionEntries, listSessions, restoreSessionState, setStaticContext, } from "./session-store.js";
5
6
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +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"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAqB,MAAM,aAAa,CAAC;AAC5D,OAAO,EACN,mCAAmC,EACnC,+BAA+B,EAC/B,aAAa,EACb,iBAAiB,GAEjB,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,EAEZ,mBAAmB,EAInB,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 +1 @@
1
- {"version":3,"file":"request-chunks.d.ts","sourceRoot":"","sources":["../src/request-chunks.ts"],"names":[],"mappings":"AAGA,eAAO,MAAM,cAAc,uBAAuB,CAAC;AAgBnD,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/drop-last-assistant-error\",\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","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"]}
@@ -9,7 +9,6 @@ const ALLOWED_TARGETS = new Set([
9
9
  "/api/session/tree/sync",
10
10
  "/api/session/tree/append",
11
11
  "/api/session/tree/switch",
12
- "/api/session/drop-last-assistant-error",
13
12
  "/api/session/compact",
14
13
  "/api/stream",
15
14
  "/api/receive",
@@ -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/drop-last-assistant-error\",\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"]}
package/dist/server.d.ts CHANGED
@@ -12,11 +12,6 @@ interface StreamRequestBody {
12
12
  ephemeralMessages?: Message[];
13
13
  contextOverlay?: Message[];
14
14
  }
15
- interface ResolvedStream {
16
- model: Model<any>;
17
- options: SimpleStreamOptions;
18
- }
19
- export declare function resolveStreamOptions(_config: ServerConfig, model: Model<any>, body: StreamRequestBody): ResolvedStream;
20
15
  export declare function buildStreamContext(session: SessionState, body: Pick<StreamRequestBody, "contextOverlay" | "ephemeralMessages">): Context;
21
16
  export declare function createPiServer(configOverride?: Partial<ServerConfig>): HttpServer;
22
17
  export declare function startServer(configOverride?: Partial<ServerConfig>): HttpServer;
@@ -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,EACV,KAAK,mBAAmB,EACxB,MAAM,uBAAuB,CAAC;AAE/B,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAMhD,OAAO,EAYN,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;AA4GD,UAAU,cAAc;IACvB,KAAK,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC;IAClB,OAAO,EAAE,mBAAmB,CAAC;CAC7B;AAgJD,wBAAgB,oBAAoB,CACnC,OAAO,EAAE,YAAY,EACrB,KAAK,EAAE,KAAK,CAAC,GAAG,CAAC,EACjB,IAAI,EAAE,iBAAiB,GACrB,cAAc,CAEhB;AAsVD,wBAAgB,kBAAkB,CACjC,OAAO,EAAE,YAAY,EACrB,IAAI,EAAE,IAAI,CAAC,iBAAiB,EAAE,gBAAgB,GAAG,mBAAmB,CAAC,GACnE,OAAO,CAOT;AA4KD,wBAAgB,cAAc,CAAC,cAAc,CAAC,EAAE,OAAO,CAAC,YAAY,CAAC,GAAG,UAAU,CAgHjF;AAoED,wBAAgB,WAAW,CAAC,cAAc,CAAC,EAAE,OAAO,CAAC,YAAY,CAAC,GAAG,UAAU,CAO9E","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 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 { 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\tdropLastAssistantError,\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\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\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: (requestModel, context, streamOptions) => streamSimple(requestModel, context, streamOptions),\n\t\t\t\tstreamSimple: (requestModel, context, streamOptions) => streamSimple(requestModel, context, streamOptions),\n\t\t\t},\n\t\t}),\n\t);\n\treturn models;\n}\n\ninterface SessionIdBody {\n\tsessionId: string;\n}\n\ninterface StreamRunRecord {\n\tsessionId: string;\n\trunId: string;\n\tstatus: \"running\" | \"completed\" | \"failed\";\n\tevents: ProxyAssistantMessageEvent[];\n\tmessage?: AssistantMessage;\n\terrorMessage?: string;\n\tcreatedAt: number;\n\tupdatedAt: number;\n}\n\nconst STREAM_HEARTBEAT = \": keep-alive\\n\\n\";\nconst JSON_HEARTBEAT = \" \\n\";\nconst STREAM_HEARTBEAT_INTERVAL_MS = 25_000;\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 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\ninterface ResolvedStream {\n\tmodel: Model<any>;\n\toptions: SimpleStreamOptions;\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 startStreamRun(sessionId: string, runId: string): StreamRunRecord {\n\tconst existing = getStreamRun(sessionId, runId);\n\tif (existing?.status === \"completed\" || existing?.status === \"failed\") return existing;\n\tconst now = Date.now();\n\tconst run: StreamRunRecord = existing ?? {\n\t\tsessionId,\n\t\trunId,\n\t\tstatus: \"running\",\n\t\tevents: [],\n\t\tcreatedAt: now,\n\t\tupdatedAt: now,\n\t};\n\trun.status = \"running\";\n\trun.updatedAt = now;\n\tstreamRuns.set(runKey(sessionId, runId), run);\n\treturn run;\n}\n\nfunction recordStreamRunEvent(run: StreamRunRecord | undefined, event: ProxyAssistantMessageEvent): void {\n\tif (!run) return;\n\trun.events.push(event);\n\trun.updatedAt = Date.now();\n}\n\nfunction completeStreamRun(run: StreamRunRecord | undefined, message: AssistantMessage): void {\n\tif (!run) return;\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\trun.status = \"failed\";\n\trun.errorMessage = errorMessage;\n\trun.updatedAt = Date.now();\n}\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\tpreparation: PreparedCompaction;\n\toptions: SimpleStreamOptions;\n}\n\ninterface SessionCompactSuccessBody {\n\tsuccess: true;\n\tcompaction: LegacyCompactResult;\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: { error: string } | SessionCompactSuccessBody;\n}\n\nexport function resolveStreamOptions(\n\t_config: ServerConfig,\n\tmodel: Model<any>,\n\tbody: StreamRequestBody,\n): ResolvedStream {\n\treturn { model, options: { ...(body.options ?? {}) } };\n}\n\nfunction handleSessionInit(config: ServerConfig, body: SessionInitBody, res: ServerResponse): void {\n\tif (!body.sessionId) {\n\t\tsendJson(res, 400, { error: \"sessionId is required\" });\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\tsendJson(res, 400, { error: \"sessionId is required\" });\n\t\treturn;\n\t}\n\tif (!body.staticContext) {\n\t\tsendJson(res, 400, { error: \"staticContext is required for update\" });\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\tsendJson(res, 400, { error: \"sessionId is required\" });\n\t\treturn;\n\t}\n\tif (!Array.isArray(body.messages)) {\n\t\tsendJson(res, 400, { error: \"messages is required\" });\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\tsendJson(res, 400, { error: \"sessionId is required\" });\n\t\treturn;\n\t}\n\tif (!Array.isArray(body.messages)) {\n\t\tsendJson(res, 400, { error: \"messages is required\" });\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\tsendJson(res, 400, { error: \"sessionId is required\" });\n\t\treturn;\n\t}\n\tif (!Array.isArray(body.entries)) {\n\t\tsendJson(res, 400, { error: \"entries is required\" });\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\tsendJson(res, 400, { error: \"sessionId is required\" });\n\t\treturn;\n\t}\n\tif (!Array.isArray(body.entries)) {\n\t\tsendJson(res, 400, { error: \"entries is required\" });\n\t\treturn;\n\t}\n\tif (body.staticContext) {\n\t\tsetStaticContext(body.sessionId, body.staticContext);\n\t}\n\tconst session = appendSessionEntries(body.sessionId, body.entries, body.leafId ?? null);\n\tpersistSession(config, session);\n\tsendJson(res, 200, sessionResponseBody(session));\n}\n\nfunction handleSessionTreeSwitch(config: ServerConfig, body: SessionTreeSwitchBody, res: ServerResponse): void {\n\tif (!body.sessionId) {\n\t\tsendJson(res, 400, { error: \"sessionId is required\" });\n\t\treturn;\n\t}\n\tconst session = switchSessionLeaf(body.sessionId, body.leafId ?? null);\n\tpersistSession(config, session);\n\tsendJson(res, 200, sessionResponseBody(session));\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\" } };\n\t}\n\n\tconst entries = getSessionBranch(session);\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\tpreparation: preparationResult.value,\n\t\toptions,\n\t};\n}\n\nasync function completeSessionCompact(\n\tconfig: ServerConfig,\n\tbody: SessionCompactBody,\n\tprepared: PreparedSessionCompact,\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\tundefined,\n\t\tprepared.options.reasoning,\n\t);\n\tif (!result.ok) {\n\t\treturn { status: 500, body: { error: result.error.message } };\n\t}\n\n\tconst baseTreeHash = prepared.session.treeHash;\n\tconst baseEntryCount = prepared.session.entries.length;\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): 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\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);\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\twriteServerSentEvent(res, \"error\", { error: message });\n\t} finally {\n\t\tclearInterval(heartbeat);\n\t\tres.end();\n\t}\n}\n\nasync function handleSessionCompactJsonStream(\n\tconfig: ServerConfig,\n\tbody: SessionCompactBody,\n\tprepared: PreparedSessionCompact,\n\tres: ServerResponse,\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\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);\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\tres.write(JSON.stringify({ error: message }));\n\t} finally {\n\t\tclearInterval(heartbeat);\n\t\tres.end();\n\t}\n}\n\nasync function handleSessionCompact(\n\tconfig: ServerConfig,\n\tbody: SessionCompactBody,\n\tres: ServerResponse,\n): Promise<void> {\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\tif (body.streamResponse) {\n\t\tawait handleSessionCompactStream(config, body, prepared, res);\n\t\treturn;\n\t}\n\n\tawait handleSessionCompactJsonStream(config, body, prepared, res);\n}\n\nfunction handleDropLastAssistantError(config: ServerConfig, body: SessionIdBody, res: ServerResponse): void {\n\tif (!body.sessionId) {\n\t\tsendJson(res, 400, { error: \"sessionId is required\" });\n\t\treturn;\n\t}\n\tconst dropped = dropLastAssistantError(body.sessionId);\n\tconst session = getSession(body.sessionId);\n\tif (session) {\n\t\tpersistSession(config, session);\n\t}\n\tconst messageCount = session?.messages.length ?? 0;\n\tsendJson(res, 200, { success: true, dropped, messageCount });\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\tsendJson(res, 404, { error: \"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\tsendJson(res, 404, { error: \"run not found\" });\n\t\treturn;\n\t}\n\tsendJson(res, 200, 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\tsendJson(res, 400, { error: \"sessionId is required\" });\n\t\treturn;\n\t}\n\tif (!body.model) {\n\t\tsendJson(res, 400, { error: \"model is required\" });\n\t\treturn;\n\t}\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\tsendJson(res, 400, { error: \"Session has no static context. Initialize with /api/session/init first.\" });\n\t\treturn;\n\t}\n\n\tif (body.ephemeralMessages !== undefined && !Array.isArray(body.ephemeralMessages)) {\n\t\tsendJson(res, 400, { error: \"ephemeralMessages must be an array\" });\n\t\treturn;\n\t}\n\tif (body.contextOverlay !== undefined && !Array.isArray(body.contextOverlay)) {\n\t\tsendJson(res, 400, { error: \"contextOverlay must be an array\" });\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 { model: resolvedModel, options: streamOptions } = resolveStreamOptions(config, body.model, body);\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\tif (existingRun?.status === \"completed\") {\n\t\tfor (const event of existingRun.events) {\n\t\t\tres.write(encodeProxyEvent(event));\n\t\t}\n\t\tres.end();\n\t\treturn;\n\t}\n\n\tconst run = body.runId ? startStreamRun(body.sessionId, body.runId) : undefined;\n\n\tconst heartbeat = setInterval(() => {\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\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\tres.write(encodeErrorEvent(message));\n\t\tres.end();\n\t\treturn;\n\t}\n\n\t(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\trecordStreamRunEvent(run, proxyEvent);\n\t\t\t\t\tres.write(encodeProxyEvent(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} finally {\n\t\t\tclearInterval(heartbeat);\n\t\t}\n\n\t\tres.end();\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\tres.write(encodeErrorEvent(message));\n\t\tres.end();\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/drop-last-assistant-error\") {\n\t\thandleDropLastAssistantError(config, body as SessionIdBody, 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://${config.host}:${config.port}`);\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\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\tsendJson(res, 400, { error: \"from must be a non-negative integer\" });\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\tsendJson(res, 400, { error: \"entriesFrom must be a non-negative integer\" });\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\tsendJson(res, 400, { error: \"revision must be a non-negative integer\" });\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\tawait handlePostRequest(config, chunkResult.target, JSON.parse(chunkResult.bodyJson) as unknown, res);\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\tsendJson(res, 400, { error: err instanceof Error ? err.message : String(err) });\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} catch (err) {\n\t\t\t\tlogRequestError(req, err);\n\t\t\t\tif (!res.headersSent) {\n\t\t\t\t\tsendJson(res, 500, { error: err instanceof Error ? err.message : String(err) });\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\tdeletePersistedSession(config.sessionStoreDir, sessionId);\n\t\t\tsendJson(res, 200, { deleted: sessionId });\n\t\t\treturn;\n\t\t}\n\n\t\tsendJson(res, 404, { error: \"Not found\" });\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 };\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};\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\tconsole.log(`pi-server listening on ${config.host}:${config.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;AAY/G,OAAO,EAGN,KAAK,OAAO,EAGZ,KAAK,OAAO,EACZ,KAAK,KAAK,EACV,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+qBD,wBAAgB,kBAAkB,CACjC,OAAO,EAAE,YAAY,EACrB,IAAI,EAAE,IAAI,CAAC,iBAAiB,EAAE,gBAAgB,GAAG,mBAAmB,CAAC,GACnE,OAAO,CAOT;AA2LD,wBAAgB,cAAc,CAAC,cAAc,CAAC,EAAE,OAAO,CAAC,YAAY,CAAC,GAAG,UAAU,CA6HjF;AAoED,wBAAgB,WAAW,CAAC,cAAc,CAAC,EAAE,OAAO,CAAC,YAAY,CAAC,GAAG,UAAU,CAO9E","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 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\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\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: (requestModel, context, streamOptions) => streamSimple(requestModel, context, streamOptions),\n\t\t\t\tstreamSimple: (requestModel, context, streamOptions) => streamSimple(requestModel, context, streamOptions),\n\t\t\t},\n\t\t}),\n\t);\n\treturn models;\n}\n\ninterface StreamRunRecord {\n\tsessionId: string;\n\trunId: string;\n\tstatus: \"running\" | \"completed\" | \"failed\";\n\tmessage?: AssistantMessage;\n\terrorMessage?: string;\n\tcreatedAt: number;\n\tupdatedAt: number;\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 startStreamRun(sessionId: string, runId: string): StreamRunRecord {\n\tconst existing = getStreamRun(sessionId, runId);\n\tif (existing?.status === \"completed\") return existing;\n\tconst now = Date.now();\n\tconst run: StreamRunRecord = existing ?? {\n\t\tsessionId,\n\t\trunId,\n\t\tstatus: \"running\",\n\t\tcreatedAt: now,\n\t\tupdatedAt: now,\n\t};\n\tif (existing) {\n\t\trun.message = undefined;\n\t\trun.errorMessage = undefined;\n\t}\n\trun.status = \"running\";\n\trun.updatedAt = now;\n\tstreamRuns.set(runKey(sessionId, runId), run);\n\treturn run;\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\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\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\tpreparation: PreparedCompaction;\n\toptions: SimpleStreamOptions;\n}\n\ninterface SessionCompactSuccessBody {\n\tsuccess: true;\n\tcompaction: LegacyCompactResult;\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 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\tpreparation: preparationResult.value,\n\t\toptions,\n\t};\n}\n\nasync function completeSessionCompact(\n\tconfig: ServerConfig,\n\tbody: SessionCompactBody,\n\tprepared: PreparedSessionCompact,\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\tundefined,\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\n\tconst baseTreeHash = prepared.session.treeHash;\n\tconst baseEntryCount = prepared.session.entries.length;\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): 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\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);\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\twriteServerSentEvent(res, \"error\", { error: message });\n\t} finally {\n\t\tclearInterval(heartbeat);\n\t\tres.end();\n\t}\n}\n\nasync function handleSessionCompactJsonStream(\n\tconfig: ServerConfig,\n\tbody: SessionCompactBody,\n\tprepared: PreparedSessionCompact,\n\tres: ServerResponse,\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\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);\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\tres.write(JSON.stringify({ error: message }));\n\t} finally {\n\t\tclearInterval(heartbeat);\n\t\tres.end();\n\t}\n}\n\nasync function handleSessionCompact(\n\tconfig: ServerConfig,\n\tbody: SessionCompactBody,\n\tres: ServerResponse,\n): Promise<void> {\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\tif (body.streamResponse) {\n\t\tawait handleSessionCompactStream(config, body, prepared, res);\n\t\treturn;\n\t}\n\n\tawait handleSessionCompactJsonStream(config, body, prepared, res);\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, 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\tconst streamOptions: SimpleStreamOptions = { ...(body.options ?? {}) };\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 === \"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\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 run = body.runId ? startStreamRun(body.sessionId, body.runId) : undefined;\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\twriteStreamError(res, message);\n\t\tendStreamResponse(res);\n\t\treturn;\n\t}\n\n\t(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} finally {\n\t\t\tclearInterval(heartbeat);\n\t\t}\n\n\t\tendStreamResponse(res);\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\twriteStreamError(res, message);\n\t\tendStreamResponse(res);\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://${config.host}:${config.port}`);\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\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};\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\tconsole.log(`pi-server listening on ${config.host}:${config.port}`);\n\t});\n\treturn server;\n}\n"]}