@averyyy/pi-server 0.84.2-piclient.1 → 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/dist/server.js CHANGED
@@ -4,11 +4,12 @@ import { compactLegacy, DEFAULT_COMPACTION_SETTINGS, prepareLegacyCompaction, }
4
4
  import { createModels, createProvider, } from "@earendil-works/pi-ai";
5
5
  import { streamSimple } from "@earendil-works/pi-ai/compat";
6
6
  import { loadConfig } from "./config.js";
7
+ import { PiServerError, PiServerErrorCode } from "./error-codes.js";
7
8
  import { encodeErrorEvent, encodeProxyEvent } from "./event-encoding.js";
8
9
  import { ReceiveUploadError, receiveUpload } from "./receive-upload.js";
9
10
  import { CHUNK_ENDPOINT, receiveRequestChunk } from "./request-chunks.js";
10
11
  import { deletePersistedSession, loadPersistedSessions, savePersistedSession } from "./session-persistence.js";
11
- import { appendCompactionEntry, appendMessages, appendSessionEntries, deleteSession as deleteSessionFromStore, dropLastAssistantError, getOrCreateSession, getSession, getSessionBranch, listSessions, replaceMessages, replaceSessionTree, setStaticContext, switchSessionLeaf, } from "./session-store.js";
12
+ import { appendCompactionEntry, appendMessages, appendSessionEntries, deleteSession as deleteSessionFromStore, getOrCreateSession, getSession, getSessionBranch, listSessions, replaceMessages, replaceSessionTree, setStaticContext, switchSessionLeaf, } from "./session-store.js";
12
13
  export { loadConfig } from "./config.js";
13
14
  const packageMetadata = createRequire(import.meta.url)("../package.json");
14
15
  const PI_SERVER_VERSION = packageMetadata.version;
@@ -34,6 +35,7 @@ function createRequestModels(model, options) {
34
35
  const STREAM_HEARTBEAT = ": keep-alive\n\n";
35
36
  const JSON_HEARTBEAT = " \n";
36
37
  const STREAM_HEARTBEAT_INTERVAL_MS = 25_000;
38
+ const STREAM_RUN_TTL_MS = 60 * 60 * 1000; // 1 hour
37
39
  const streamRuns = new Map();
38
40
  function readBody(req) {
39
41
  return new Promise((resolve, reject) => {
@@ -48,6 +50,14 @@ function sendJson(res, status, body) {
48
50
  res.writeHead(status, { "Content-Type": "application/json", "Content-Length": Buffer.byteLength(data) });
49
51
  res.end(data);
50
52
  }
53
+ function sendError(res, status, message, code, details) {
54
+ const body = { error: message };
55
+ if (code)
56
+ body.code = code;
57
+ if (details)
58
+ body.details = details;
59
+ sendJson(res, status, body);
60
+ }
51
61
  function logRequestError(req, error) {
52
62
  const message = error instanceof Error ? error.stack || error.message : String(error);
53
63
  console.error(`${req.method ?? "UNKNOWN"} ${req.url ?? "/"} failed: ${message}`);
@@ -83,27 +93,58 @@ function getStreamRun(sessionId, runId) {
83
93
  }
84
94
  function startStreamRun(sessionId, runId) {
85
95
  const existing = getStreamRun(sessionId, runId);
86
- if (existing?.status === "completed" || existing?.status === "failed")
96
+ if (existing?.status === "completed")
87
97
  return existing;
88
98
  const now = Date.now();
89
99
  const run = existing ?? {
90
100
  sessionId,
91
101
  runId,
92
102
  status: "running",
93
- events: [],
94
103
  createdAt: now,
95
104
  updatedAt: now,
96
105
  };
106
+ if (existing) {
107
+ run.message = undefined;
108
+ run.errorMessage = undefined;
109
+ }
97
110
  run.status = "running";
98
111
  run.updatedAt = now;
99
112
  streamRuns.set(runKey(sessionId, runId), run);
100
113
  return run;
101
114
  }
102
- function recordStreamRunEvent(run, event) {
103
- if (!run)
104
- return;
105
- run.events.push(event);
106
- run.updatedAt = Date.now();
115
+ function replayStreamRun(run) {
116
+ if (!run.message)
117
+ return [];
118
+ if (run.message.stopReason !== "stop" &&
119
+ run.message.stopReason !== "length" &&
120
+ run.message.stopReason !== "toolUse" &&
121
+ run.message.stopReason !== "deferred") {
122
+ return [];
123
+ }
124
+ const events = [{ type: "start" }];
125
+ for (const [contentIndex, content] of run.message.content.entries()) {
126
+ switch (content.type) {
127
+ case "text":
128
+ events.push({ type: "text_start", contentIndex }, { type: "text_delta", contentIndex, delta: content.text }, { type: "text_end", contentIndex, contentSignature: content.textSignature });
129
+ break;
130
+ case "thinking":
131
+ events.push({ type: "thinking_start", contentIndex }, { type: "thinking_delta", contentIndex, delta: content.thinking }, { type: "thinking_end", contentIndex, contentSignature: content.thinkingSignature });
132
+ break;
133
+ case "toolCall":
134
+ events.push({ type: "toolcall_start", contentIndex, id: content.id, toolName: content.name }, { type: "toolcall_delta", contentIndex, delta: JSON.stringify(content.arguments) }, { type: "toolcall_end", contentIndex, toolCall: content });
135
+ break;
136
+ default:
137
+ break;
138
+ }
139
+ }
140
+ events.push({
141
+ type: "done",
142
+ reason: run.message.stopReason,
143
+ usage: run.message.usage,
144
+ deferred: run.message.deferred,
145
+ message: run.message,
146
+ });
147
+ return events;
107
148
  }
108
149
  function completeStreamRun(run, message) {
109
150
  if (!run)
@@ -120,6 +161,40 @@ function failStreamRun(run, errorMessage) {
120
161
  run.errorMessage = errorMessage;
121
162
  run.updatedAt = Date.now();
122
163
  }
164
+ function touchStreamRun(run) {
165
+ if (run)
166
+ run.updatedAt = Date.now();
167
+ }
168
+ function writeStreamEvent(res, event) {
169
+ if (!res.writableEnded && !res.destroyed) {
170
+ res.write(encodeProxyEvent(event));
171
+ }
172
+ }
173
+ function writeStreamError(res, message) {
174
+ if (!res.writableEnded && !res.destroyed) {
175
+ res.write(encodeErrorEvent(message));
176
+ }
177
+ }
178
+ function endStreamResponse(res) {
179
+ if (!res.writableEnded && !res.destroyed) {
180
+ res.end();
181
+ }
182
+ }
183
+ function cleanupExpiredStreamRuns(nowMs) {
184
+ for (const [key, run] of streamRuns) {
185
+ if (nowMs - run.updatedAt > STREAM_RUN_TTL_MS) {
186
+ streamRuns.delete(key);
187
+ }
188
+ }
189
+ }
190
+ function deleteStreamRunsForSession(sessionId) {
191
+ for (const [key, run] of streamRuns) {
192
+ if (run.sessionId === sessionId)
193
+ streamRuns.delete(key);
194
+ }
195
+ }
196
+ const streamRunCleanupTimer = setInterval(() => cleanupExpiredStreamRuns(Date.now()), STREAM_RUN_TTL_MS / 4);
197
+ streamRunCleanupTimer.unref();
123
198
  function sessionHistoryFullResponseBody(session, baseMessageCount) {
124
199
  return {
125
200
  sessionId: session.sessionId,
@@ -156,12 +231,9 @@ function sessionTreePatchResponseBody(session, baseMessageCount, entriesFrom, ba
156
231
  },
157
232
  };
158
233
  }
159
- export function resolveStreamOptions(_config, model, body) {
160
- return { model, options: { ...(body.options ?? {}) } };
161
- }
162
234
  function handleSessionInit(config, body, res) {
163
235
  if (!body.sessionId) {
164
- sendJson(res, 400, { error: "sessionId is required" });
236
+ sendError(res, 400, "sessionId is required", PiServerErrorCode.REQUIRED_FIELD_MISSING);
165
237
  return;
166
238
  }
167
239
  if (body.staticContext) {
@@ -176,11 +248,11 @@ function handleSessionInit(config, body, res) {
176
248
  }
177
249
  function handleSessionUpdate(config, body, res) {
178
250
  if (!body.sessionId) {
179
- sendJson(res, 400, { error: "sessionId is required" });
251
+ sendError(res, 400, "sessionId is required", PiServerErrorCode.REQUIRED_FIELD_MISSING);
180
252
  return;
181
253
  }
182
254
  if (!body.staticContext) {
183
- sendJson(res, 400, { error: "staticContext is required for update" });
255
+ sendError(res, 400, "staticContext is required for update", PiServerErrorCode.REQUIRED_FIELD_MISSING);
184
256
  return;
185
257
  }
186
258
  setStaticContext(body.sessionId, body.staticContext);
@@ -190,11 +262,11 @@ function handleSessionUpdate(config, body, res) {
190
262
  }
191
263
  function handleSessionSync(config, body, res) {
192
264
  if (!body.sessionId) {
193
- sendJson(res, 400, { error: "sessionId is required" });
265
+ sendError(res, 400, "sessionId is required", PiServerErrorCode.REQUIRED_FIELD_MISSING);
194
266
  return;
195
267
  }
196
268
  if (!Array.isArray(body.messages)) {
197
- sendJson(res, 400, { error: "messages is required" });
269
+ sendError(res, 400, "messages is required", PiServerErrorCode.REQUIRED_FIELD_MISSING);
198
270
  return;
199
271
  }
200
272
  if (body.staticContext) {
@@ -206,11 +278,11 @@ function handleSessionSync(config, body, res) {
206
278
  }
207
279
  function handleSessionAppend(config, body, res) {
208
280
  if (!body.sessionId) {
209
- sendJson(res, 400, { error: "sessionId is required" });
281
+ sendError(res, 400, "sessionId is required", PiServerErrorCode.REQUIRED_FIELD_MISSING);
210
282
  return;
211
283
  }
212
284
  if (!Array.isArray(body.messages)) {
213
- sendJson(res, 400, { error: "messages is required" });
285
+ sendError(res, 400, "messages is required", PiServerErrorCode.REQUIRED_FIELD_MISSING);
214
286
  return;
215
287
  }
216
288
  if (body.staticContext) {
@@ -222,11 +294,11 @@ function handleSessionAppend(config, body, res) {
222
294
  }
223
295
  function handleSessionTreeSync(config, body, res) {
224
296
  if (!body.sessionId) {
225
- sendJson(res, 400, { error: "sessionId is required" });
297
+ sendError(res, 400, "sessionId is required", PiServerErrorCode.REQUIRED_FIELD_MISSING);
226
298
  return;
227
299
  }
228
300
  if (!Array.isArray(body.entries)) {
229
- sendJson(res, 400, { error: "entries is required" });
301
+ sendError(res, 400, "entries is required", PiServerErrorCode.REQUIRED_FIELD_MISSING);
230
302
  return;
231
303
  }
232
304
  if (body.staticContext) {
@@ -238,28 +310,50 @@ function handleSessionTreeSync(config, body, res) {
238
310
  }
239
311
  function handleSessionTreeAppend(config, body, res) {
240
312
  if (!body.sessionId) {
241
- sendJson(res, 400, { error: "sessionId is required" });
313
+ sendError(res, 400, "sessionId is required", PiServerErrorCode.REQUIRED_FIELD_MISSING);
242
314
  return;
243
315
  }
244
316
  if (!Array.isArray(body.entries)) {
245
- sendJson(res, 400, { error: "entries is required" });
317
+ sendError(res, 400, "entries is required", PiServerErrorCode.REQUIRED_FIELD_MISSING);
246
318
  return;
247
319
  }
248
- if (body.staticContext) {
249
- setStaticContext(body.sessionId, body.staticContext);
320
+ try {
321
+ if (body.staticContext) {
322
+ setStaticContext(body.sessionId, body.staticContext);
323
+ }
324
+ const session = appendSessionEntries(body.sessionId, body.entries, body.leafId ?? null);
325
+ persistSession(config, session);
326
+ sendJson(res, 200, sessionResponseBody(session));
327
+ }
328
+ catch (error) {
329
+ if (error instanceof PiServerError) {
330
+ sendError(res, 400, error.message, error.code, error.details);
331
+ }
332
+ else {
333
+ const message = error instanceof Error ? error.message : String(error);
334
+ sendError(res, 500, message, PiServerErrorCode.INTERNAL_ERROR);
335
+ }
250
336
  }
251
- const session = appendSessionEntries(body.sessionId, body.entries, body.leafId ?? null);
252
- persistSession(config, session);
253
- sendJson(res, 200, sessionResponseBody(session));
254
337
  }
255
338
  function handleSessionTreeSwitch(config, body, res) {
256
339
  if (!body.sessionId) {
257
- sendJson(res, 400, { error: "sessionId is required" });
340
+ sendError(res, 400, "sessionId is required", PiServerErrorCode.REQUIRED_FIELD_MISSING);
258
341
  return;
259
342
  }
260
- const session = switchSessionLeaf(body.sessionId, body.leafId ?? null);
261
- persistSession(config, session);
262
- sendJson(res, 200, sessionResponseBody(session));
343
+ try {
344
+ const session = switchSessionLeaf(body.sessionId, body.leafId ?? null);
345
+ persistSession(config, session);
346
+ sendJson(res, 200, sessionResponseBody(session));
347
+ }
348
+ catch (error) {
349
+ if (error instanceof PiServerError) {
350
+ sendError(res, 400, error.message, error.code, error.details);
351
+ }
352
+ else {
353
+ const message = error instanceof Error ? error.message : String(error);
354
+ sendError(res, 500, message, PiServerErrorCode.INTERNAL_ERROR);
355
+ }
356
+ }
263
357
  }
264
358
  function prepareSessionCompact(body) {
265
359
  if (!body.sessionId) {
@@ -270,7 +364,7 @@ function prepareSessionCompact(body) {
270
364
  }
271
365
  const session = getSession(body.sessionId);
272
366
  if (!session) {
273
- return { status: 404, body: { error: "session not found" } };
367
+ return { status: 404, body: { error: "session not found", code: PiServerErrorCode.SESSION_NOT_FOUND } };
274
368
  }
275
369
  const entries = getSessionBranch(session);
276
370
  const preparationResult = prepareLegacyCompaction(entries, body.settings ?? DEFAULT_COMPACTION_SETTINGS, body.preparation);
@@ -290,7 +384,7 @@ function prepareSessionCompact(body) {
290
384
  async function completeSessionCompact(config, body, prepared) {
291
385
  const result = await compactLegacy(prepared.preparation, createRequestModels(body.model, prepared.options), body.model, body.customInstructions, undefined, prepared.options.reasoning);
292
386
  if (!result.ok) {
293
- return { status: 500, body: { error: result.error.message } };
387
+ return { status: 500, body: { error: result.error.message, code: PiServerErrorCode.INTERNAL_ERROR } };
294
388
  }
295
389
  const baseTreeHash = prepared.session.treeHash;
296
390
  const baseEntryCount = prepared.session.entries.length;
@@ -399,23 +493,10 @@ async function handleSessionCompact(config, body, res) {
399
493
  }
400
494
  await handleSessionCompactJsonStream(config, body, prepared, res);
401
495
  }
402
- function handleDropLastAssistantError(config, body, res) {
403
- if (!body.sessionId) {
404
- sendJson(res, 400, { error: "sessionId is required" });
405
- return;
406
- }
407
- const dropped = dropLastAssistantError(body.sessionId);
408
- const session = getSession(body.sessionId);
409
- if (session) {
410
- persistSession(config, session);
411
- }
412
- const messageCount = session?.messages.length ?? 0;
413
- sendJson(res, 200, { success: true, dropped, messageCount });
414
- }
415
496
  function handleSessionHistory(sessionId, from, entriesFrom, revision, baseTreeHash, res) {
416
497
  const session = getSession(sessionId);
417
498
  if (!session) {
418
- sendJson(res, 404, { error: "session not found" });
499
+ sendError(res, 404, "session not found", PiServerErrorCode.SESSION_NOT_FOUND);
419
500
  return;
420
501
  }
421
502
  const baseMessageCount = from ?? 0;
@@ -431,7 +512,7 @@ function handleSessionHistory(sessionId, from, entriesFrom, revision, baseTreeHa
431
512
  function handleSessionRun(sessionId, runId, res) {
432
513
  const run = getStreamRun(sessionId, runId);
433
514
  if (!run) {
434
- sendJson(res, 404, { error: "run not found" });
515
+ sendError(res, 404, "run not found", PiServerErrorCode.RUN_NOT_FOUND);
435
516
  return;
436
517
  }
437
518
  sendJson(res, 200, run);
@@ -446,33 +527,53 @@ export function buildStreamContext(session, body) {
446
527
  }
447
528
  function handleStream(config, body, res) {
448
529
  if (!body.sessionId) {
449
- sendJson(res, 400, { error: "sessionId is required" });
530
+ sendError(res, 400, "sessionId is required", PiServerErrorCode.REQUIRED_FIELD_MISSING);
450
531
  return;
451
532
  }
452
533
  if (!body.model) {
453
- sendJson(res, 400, { error: "model is required" });
534
+ sendError(res, 400, "model is required", PiServerErrorCode.REQUIRED_FIELD_MISSING);
454
535
  return;
455
536
  }
537
+ cleanupExpiredStreamRuns(Date.now());
456
538
  const session = getOrCreateSession(body.sessionId);
457
539
  if (body.staticContext) {
458
540
  setStaticContext(body.sessionId, body.staticContext);
459
541
  persistSession(config, session);
460
542
  }
461
543
  if (!session.staticContext && !body.staticContext) {
462
- sendJson(res, 400, { error: "Session has no static context. Initialize with /api/session/init first." });
544
+ sendError(res, 400, "Session has no static context. Initialize with /api/session/init first.", PiServerErrorCode.SESSION_NO_STATIC_CONTEXT);
463
545
  return;
464
546
  }
465
547
  if (body.ephemeralMessages !== undefined && !Array.isArray(body.ephemeralMessages)) {
466
- sendJson(res, 400, { error: "ephemeralMessages must be an array" });
548
+ sendError(res, 400, "ephemeralMessages must be an array", PiServerErrorCode.INVALID_REQUEST);
467
549
  return;
468
550
  }
469
551
  if (body.contextOverlay !== undefined && !Array.isArray(body.contextOverlay)) {
470
- sendJson(res, 400, { error: "contextOverlay must be an array" });
552
+ sendError(res, 400, "contextOverlay must be an array", PiServerErrorCode.INVALID_REQUEST);
471
553
  return;
472
554
  }
473
555
  const context = buildStreamContext(session, body);
474
556
  const existingRun = body.runId ? getStreamRun(body.sessionId, body.runId) : undefined;
475
- const { model: resolvedModel, options: streamOptions } = resolveStreamOptions(config, body.model, body);
557
+ const resolvedModel = body.model;
558
+ const streamOptions = { ...(body.options ?? {}) };
559
+ if (existingRun?.status === "completed") {
560
+ res.writeHead(200, {
561
+ "Content-Type": "text/event-stream",
562
+ "Cache-Control": "no-cache",
563
+ Connection: "keep-alive",
564
+ });
565
+ res.flushHeaders();
566
+ res.write(STREAM_HEARTBEAT);
567
+ for (const event of replayStreamRun(existingRun)) {
568
+ writeStreamEvent(res, event);
569
+ }
570
+ endStreamResponse(res);
571
+ return;
572
+ }
573
+ if (existingRun?.status === "running") {
574
+ sendError(res, 409, "A stream with this runId is already in progress", PiServerErrorCode.RUN_IN_PROGRESS);
575
+ return;
576
+ }
476
577
  res.writeHead(200, {
477
578
  "Content-Type": "text/event-stream",
478
579
  "Cache-Control": "no-cache",
@@ -480,16 +581,10 @@ function handleStream(config, body, res) {
480
581
  });
481
582
  res.flushHeaders();
482
583
  res.write(STREAM_HEARTBEAT);
483
- if (existingRun?.status === "completed") {
484
- for (const event of existingRun.events) {
485
- res.write(encodeProxyEvent(event));
486
- }
487
- res.end();
488
- return;
489
- }
490
584
  const run = body.runId ? startStreamRun(body.sessionId, body.runId) : undefined;
491
585
  const heartbeat = setInterval(() => {
492
- if (!res.writableEnded) {
586
+ touchStreamRun(run);
587
+ if (!res.writableEnded && !res.destroyed) {
493
588
  res.write(STREAM_HEARTBEAT);
494
589
  }
495
590
  }, STREAM_HEARTBEAT_INTERVAL_MS);
@@ -502,8 +597,8 @@ function handleStream(config, body, res) {
502
597
  clearInterval(heartbeat);
503
598
  const message = err instanceof Error ? err.message : String(err);
504
599
  failStreamRun(run, message);
505
- res.write(encodeErrorEvent(message));
506
- res.end();
600
+ writeStreamError(res, message);
601
+ endStreamResponse(res);
507
602
  return;
508
603
  }
509
604
  (async () => {
@@ -511,8 +606,8 @@ function handleStream(config, body, res) {
511
606
  for await (const event of stream) {
512
607
  const proxyEvent = toProxyEvent(event);
513
608
  if (proxyEvent) {
514
- recordStreamRunEvent(run, proxyEvent);
515
- res.write(encodeProxyEvent(proxyEvent));
609
+ touchStreamRun(run);
610
+ writeStreamEvent(res, proxyEvent);
516
611
  }
517
612
  if (event.type === "done") {
518
613
  completeStreamRun(run, event.message);
@@ -525,13 +620,13 @@ function handleStream(config, body, res) {
525
620
  finally {
526
621
  clearInterval(heartbeat);
527
622
  }
528
- res.end();
623
+ endStreamResponse(res);
529
624
  })().catch((err) => {
530
625
  clearInterval(heartbeat);
531
626
  const message = err instanceof Error ? err.message : String(err);
532
627
  failStreamRun(run, message);
533
- res.write(encodeErrorEvent(message));
534
- res.end();
628
+ writeStreamError(res, message);
629
+ endStreamResponse(res);
535
630
  });
536
631
  }
537
632
  async function handlePostRequest(config, pathname, body, res) {
@@ -574,10 +669,6 @@ async function handlePostRequest(config, pathname, body, res) {
574
669
  handleSessionTreeSwitch(config, body, res);
575
670
  return true;
576
671
  }
577
- if (pathname === "/api/session/drop-last-assistant-error") {
578
- handleDropLastAssistantError(config, body, res);
579
- return true;
580
- }
581
672
  if (pathname === "/api/session/compact") {
582
673
  await handleSessionCompact(config, body, res);
583
674
  return true;
@@ -618,19 +709,19 @@ export function createPiServer(configOverride) {
618
709
  const fromParam = url.searchParams.get("from");
619
710
  const from = fromParam === null ? undefined : Number(fromParam);
620
711
  if (from !== undefined && (!Number.isInteger(from) || from < 0)) {
621
- sendJson(res, 400, { error: "from must be a non-negative integer" });
712
+ sendError(res, 400, "from must be a non-negative integer", PiServerErrorCode.INVALID_REQUEST);
622
713
  return;
623
714
  }
624
715
  const entriesFromParam = url.searchParams.get("entriesFrom");
625
716
  const entriesFrom = entriesFromParam === null ? undefined : Number(entriesFromParam);
626
717
  if (entriesFrom !== undefined && (!Number.isInteger(entriesFrom) || entriesFrom < 0)) {
627
- sendJson(res, 400, { error: "entriesFrom must be a non-negative integer" });
718
+ sendError(res, 400, "entriesFrom must be a non-negative integer", PiServerErrorCode.INVALID_REQUEST);
628
719
  return;
629
720
  }
630
721
  const revisionParam = url.searchParams.get("revision");
631
722
  const revision = revisionParam === null ? undefined : Number(revisionParam);
632
723
  if (revision !== undefined && (!Number.isInteger(revision) || revision < 0)) {
633
- sendJson(res, 400, { error: "revision must be a non-negative integer" });
724
+ sendError(res, 400, "revision must be a non-negative integer", PiServerErrorCode.INVALID_REQUEST);
634
725
  return;
635
726
  }
636
727
  handleSessionHistory(decodeURIComponent(encodedSessionId), from, entriesFrom, revision, url.searchParams.get("baseTreeHash") ?? undefined, res);
@@ -644,12 +735,15 @@ export function createPiServer(configOverride) {
644
735
  sendJson(res, 200, chunkResult.ack);
645
736
  return;
646
737
  }
647
- await handlePostRequest(config, chunkResult.target, JSON.parse(chunkResult.bodyJson), res);
738
+ const handled = await handlePostRequest(config, chunkResult.target, JSON.parse(chunkResult.bodyJson), res);
739
+ if (!handled && !res.headersSent) {
740
+ sendError(res, 404, "Not found", PiServerErrorCode.INVALID_REQUEST);
741
+ }
648
742
  }
649
743
  catch (err) {
650
744
  logRequestError(req, err);
651
745
  if (!res.headersSent) {
652
- sendJson(res, 400, { error: err instanceof Error ? err.message : String(err) });
746
+ sendError(res, 400, err instanceof Error ? err.message : String(err), PiServerErrorCode.INVALID_REQUEST);
653
747
  }
654
748
  else {
655
749
  res.write(encodeErrorEvent(err instanceof Error ? err.message : String(err)));
@@ -663,11 +757,15 @@ export function createPiServer(configOverride) {
663
757
  const body = JSON.parse(await readBody(req));
664
758
  if (await handlePostRequest(config, url.pathname, body, res))
665
759
  return;
760
+ if (!res.headersSent) {
761
+ sendError(res, 404, "Not found", PiServerErrorCode.INVALID_REQUEST);
762
+ return;
763
+ }
666
764
  }
667
765
  catch (err) {
668
766
  logRequestError(req, err);
669
767
  if (!res.headersSent) {
670
- sendJson(res, 500, { error: err instanceof Error ? err.message : String(err) });
768
+ sendError(res, 500, err instanceof Error ? err.message : String(err), PiServerErrorCode.INTERNAL_ERROR);
671
769
  }
672
770
  else {
673
771
  res.write(encodeErrorEvent(err instanceof Error ? err.message : String(err)));
@@ -679,11 +777,12 @@ export function createPiServer(configOverride) {
679
777
  if (req.method === "DELETE" && url.pathname.startsWith("/api/session/")) {
680
778
  const sessionId = decodeURIComponent(url.pathname.slice("/api/session/".length));
681
779
  deleteSessionFromStore(sessionId);
780
+ deleteStreamRunsForSession(sessionId);
682
781
  deletePersistedSession(config.sessionStoreDir, sessionId);
683
782
  sendJson(res, 200, { deleted: sessionId });
684
783
  return;
685
784
  }
686
- sendJson(res, 404, { error: "Not found" });
785
+ sendError(res, 404, "Not found", PiServerErrorCode.INVALID_REQUEST);
687
786
  });
688
787
  return server;
689
788
  }