@lelouchhe/webagent 0.6.0 → 0.7.0

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/lib/routes.js CHANGED
@@ -10,6 +10,7 @@ import { createWriteStream } from "node:fs";
10
10
  import { handleShareRoutes } from "./share/routes.js";
11
11
  import { authenticate, isWhitelistedPath } from "./auth-middleware.js";
12
12
  import { enrichStoredEventsForDisplay } from "./attachment-labels.js";
13
+ import { agentCommandToken, resolveAgentCommand } from "./agent-commands.js";
13
14
  import { log } from "./log.js";
14
15
  const rlog = log.scope("routes");
15
16
  const plog = rlog.scope("prompt");
@@ -18,6 +19,7 @@ const mlog = rlog.scope("msg");
18
19
  import { signAttachmentUrl, verifyAttachmentSig, reSignAttachmentUrlsInJson, } from "./auth.js";
19
20
  import { buildContentDisposition, classifyKind, isInlineMime, mimeToExt, normalizeDisplayName, sniffMime, } from "./attachments.js";
20
21
  import { readImageDimensions } from "./image-dimensions.js";
22
+ import { HTTP_STATUS } from "./http-status.js";
21
23
  const IS_WIN = process.platform === "win32";
22
24
  const SAFE_ID = /^[a-zA-Z0-9_-]+$/;
23
25
  const MIME = {
@@ -172,7 +174,7 @@ async function handleAttachmentUpload(req, res, sessionId, deps) {
172
174
  const { store, dataDir, limits, sessions } = deps;
173
175
  const fileUploadLimit = limits.file_upload ?? 52_428_800;
174
176
  if (!store.getSession(sessionId)) {
175
- json(res, 404, { error: "Session not found" });
177
+ json(res, HTTP_STATUS.NOT_FOUND, { error: "Session not found" });
176
178
  return;
177
179
  }
178
180
  const dir = join(dataDir, "sessions", sessionId, "attachments");
@@ -233,7 +235,7 @@ async function handleAttachmentUpload(req, res, sessionId, deps) {
233
235
  });
234
236
  }
235
237
  catch {
236
- void finish(400, { error: "Invalid multipart" });
238
+ void finish(HTTP_STATUS.BAD_REQUEST, { error: "Invalid multipart" });
237
239
  return;
238
240
  }
239
241
  bb.on("file", (fieldName, stream, info) => {
@@ -246,7 +248,9 @@ async function handleAttachmentUpload(req, res, sessionId, deps) {
246
248
  sawFile = true;
247
249
  if (fieldName !== "file") {
248
250
  stream.resume();
249
- void finish(400, { error: "Unexpected field name" });
251
+ void finish(HTTP_STATUS.BAD_REQUEST, {
252
+ error: "Unexpected field name",
253
+ });
250
254
  return;
251
255
  }
252
256
  fileMime = (info.mimeType || "application/octet-stream").toLowerCase();
@@ -295,22 +299,28 @@ async function handleAttachmentUpload(req, res, sessionId, deps) {
295
299
  await writeDone;
296
300
  if (aborted) {
297
301
  await cleanupTmp();
298
- await finish(400, { error: "Upload aborted" });
302
+ await finish(HTTP_STATUS.BAD_REQUEST, { error: "Upload aborted" });
299
303
  return;
300
304
  }
301
305
  if (limitExceeded) {
302
306
  await cleanupTmp();
303
- await finish(413, { error: "Upload too large" });
307
+ await finish(HTTP_STATUS.PAYLOAD_TOO_LARGE, {
308
+ error: "Upload too large",
309
+ });
304
310
  return;
305
311
  }
306
312
  if (writeError) {
307
313
  await cleanupTmp();
308
- await finish(500, { error: "Upload failed" });
314
+ await finish(HTTP_STATUS.INTERNAL_SERVER_ERROR, {
315
+ error: "Upload failed",
316
+ });
309
317
  return;
310
318
  }
311
319
  if (!sawFile || !tmpPath || !finalPath || !displayName) {
312
320
  await cleanupTmp();
313
- await finish(400, { error: "Missing file part" });
321
+ await finish(HTTP_STATUS.BAD_REQUEST, {
322
+ error: "Missing file part",
323
+ });
314
324
  return;
315
325
  }
316
326
  // Sniff the actual mime from file content (magic bytes + UTF-8
@@ -362,7 +372,7 @@ async function handleAttachmentUpload(req, res, sessionId, deps) {
362
372
  const fileUrl = deps.attachmentSecret
363
373
  ? `${basePath}?${signAttachmentUrl(basePath, deps.attachmentSecret, 3600)}`
364
374
  : basePath;
365
- await finish(200, {
375
+ await finish(HTTP_STATUS.OK, {
366
376
  attachmentId: row.id,
367
377
  displayName: row.name,
368
378
  mimeType: row.mime,
@@ -376,7 +386,9 @@ async function handleAttachmentUpload(req, res, sessionId, deps) {
376
386
  }
377
387
  catch (err) {
378
388
  await cleanupTmp();
379
- await finish(500, { error: errorMessage(err) });
389
+ await finish(HTTP_STATUS.INTERNAL_SERVER_ERROR, {
390
+ error: errorMessage(err),
391
+ });
380
392
  }
381
393
  })();
382
394
  });
@@ -395,7 +407,7 @@ export function createRequestHandler(deps) {
395
407
  if (!isWhitelistedPath(method, path)) {
396
408
  const result = authenticate(req.headers, deps.authStore);
397
409
  if (!result.ok) {
398
- res.writeHead(401, {
410
+ res.writeHead(HTTP_STATUS.UNAUTHORIZED, {
399
411
  "Content-Type": "application/json",
400
412
  "WWW-Authenticate": "Bearer",
401
413
  });
@@ -427,7 +439,7 @@ export function createRequestHandler(deps) {
427
439
  res.setHeader("Content-Type", "application/json");
428
440
  // GET /api/v1 — discovery endpoint
429
441
  if (url === "/api/v1" && req.method === "GET") {
430
- json(res, 200, {
442
+ json(res, HTTP_STATUS.OK, {
431
443
  version: "v1",
432
444
  endpoints: {
433
445
  sessions: "/api/v1/sessions",
@@ -452,7 +464,7 @@ export function createRequestHandler(deps) {
452
464
  }
453
465
  // --- GET /api/v1/config ---
454
466
  if (url === "/api/v1/config" && req.method === "GET") {
455
- json(res, 200, {
467
+ json(res, HTTP_STATUS.OK, {
456
468
  configOptions: sessions?.cachedConfigOptions ?? [],
457
469
  cancelTimeout: deps.limits.cancel_timeout ?? 0,
458
470
  recentPathsLimit: deps.limits.recent_paths ?? 10,
@@ -469,12 +481,12 @@ export function createRequestHandler(deps) {
469
481
  limit: isNaN(limit) ? 0 : limit,
470
482
  ttlDays,
471
483
  });
472
- json(res, 200, paths);
484
+ json(res, HTTP_STATUS.OK, paths);
473
485
  return;
474
486
  }
475
487
  // GET /api/v1/version
476
488
  if (url === "/api/v1/version" && req.method === "GET") {
477
- json(res, 200, {
489
+ json(res, HTTP_STATUS.OK, {
478
490
  server: deps.serverVersion ?? "unknown",
479
491
  agent: sessions?.agentInfo ?? null,
480
492
  });
@@ -484,10 +496,10 @@ export function createRequestHandler(deps) {
484
496
  if (url === "/api/v1/auth/verify" && req.method === "GET") {
485
497
  const principal = principalByRequest.get(req);
486
498
  if (!principal) {
487
- json(res, 401, { error: "Unauthorized" });
499
+ json(res, HTTP_STATUS.UNAUTHORIZED, { error: "Unauthorized" });
488
500
  return;
489
501
  }
490
- json(res, 200, {
502
+ json(res, HTTP_STATUS.OK, {
491
503
  ok: true,
492
504
  name: principal.name,
493
505
  scope: principal.scope,
@@ -498,18 +510,20 @@ export function createRequestHandler(deps) {
498
510
  if (url === "/api/v1/sse-ticket" && req.method === "POST") {
499
511
  const principal = principalByRequest.get(req);
500
512
  if (!principal) {
501
- json(res, 401, { error: "Unauthorized" });
513
+ json(res, HTTP_STATUS.UNAUTHORIZED, { error: "Unauthorized" });
502
514
  return;
503
515
  }
504
516
  if (!deps.ticketStore) {
505
- json(res, 501, { error: "SSE not available" });
517
+ json(res, HTTP_STATUS.NOT_IMPLEMENTED, {
518
+ error: "SSE not available",
519
+ });
506
520
  return;
507
521
  }
508
522
  const ticket = deps.ticketStore.mint({
509
523
  tokenName: principal.name,
510
524
  scope: principal.scope,
511
525
  });
512
- json(res, 200, { ticket, expiresIn: 60 });
526
+ json(res, HTTP_STATUS.OK, { ticket, expiresIn: 60 });
513
527
  return;
514
528
  }
515
529
  // --- Token management (admin scope) ---
@@ -519,7 +533,7 @@ export function createRequestHandler(deps) {
519
533
  if (url === "/api/v1/tokens" && req.method === "GET") {
520
534
  const principal = principalByRequest.get(req);
521
535
  if (!deps.authStore || !principal) {
522
- json(res, 401, { error: "Unauthorized" });
536
+ json(res, HTTP_STATUS.UNAUTHORIZED, { error: "Unauthorized" });
523
537
  return;
524
538
  }
525
539
  const all = deps.authStore.list();
@@ -533,18 +547,18 @@ export function createRequestHandler(deps) {
533
547
  lastUsedAt: t.lastUsedAt,
534
548
  isSelf: t.name === principal.name,
535
549
  }));
536
- json(res, 200, list);
550
+ json(res, HTTP_STATUS.OK, list);
537
551
  return;
538
552
  }
539
553
  // POST /api/v1/tokens — create new api-scope token, return raw value once
540
554
  if (url === "/api/v1/tokens" && req.method === "POST") {
541
555
  const principal = principalByRequest.get(req);
542
556
  if (!deps.authStore || !principal) {
543
- json(res, 401, { error: "Unauthorized" });
557
+ json(res, HTTP_STATUS.UNAUTHORIZED, { error: "Unauthorized" });
544
558
  return;
545
559
  }
546
560
  if (principal.scope !== "admin") {
547
- json(res, 403, { error: "Forbidden" });
561
+ json(res, HTTP_STATUS.FORBIDDEN, { error: "Forbidden" });
548
562
  return;
549
563
  }
550
564
  let body;
@@ -552,13 +566,13 @@ export function createRequestHandler(deps) {
552
566
  body = JSON.parse(await readBody(req));
553
567
  }
554
568
  catch {
555
- json(res, 400, { error: "Invalid JSON" });
569
+ json(res, HTTP_STATUS.BAD_REQUEST, { error: "Invalid JSON" });
556
570
  return;
557
571
  }
558
572
  const name = typeof body.name === "string" ? body.name : "";
559
573
  try {
560
574
  const created = await deps.authStore.addToken(name, "api");
561
- json(res, 201, {
575
+ json(res, HTTP_STATUS.CREATED, {
562
576
  token: created.token,
563
577
  name: created.record.name,
564
578
  scope: created.record.scope,
@@ -567,10 +581,10 @@ export function createRequestHandler(deps) {
567
581
  catch (err) {
568
582
  const msg = errorMessage(err);
569
583
  if (/already exists|duplicate/i.test(msg)) {
570
- json(res, 409, { error: msg });
584
+ json(res, HTTP_STATUS.CONFLICT, { error: msg });
571
585
  }
572
586
  else {
573
- json(res, 400, { error: msg });
587
+ json(res, HTTP_STATUS.BAD_REQUEST, { error: msg });
574
588
  }
575
589
  }
576
590
  return;
@@ -580,33 +594,35 @@ export function createRequestHandler(deps) {
580
594
  if (tokenDelMatch && req.method === "DELETE") {
581
595
  const principal = principalByRequest.get(req);
582
596
  if (!deps.authStore || !principal) {
583
- json(res, 401, { error: "Unauthorized" });
597
+ json(res, HTTP_STATUS.UNAUTHORIZED, { error: "Unauthorized" });
584
598
  return;
585
599
  }
586
600
  if (principal.scope !== "admin") {
587
- json(res, 403, { error: "Forbidden" });
601
+ json(res, HTTP_STATUS.FORBIDDEN, { error: "Forbidden" });
588
602
  return;
589
603
  }
590
604
  const name = decodeURIComponent(tokenDelMatch[1]);
591
605
  if (!/^[A-Za-z0-9_-]{1,64}$/.test(name)) {
592
- json(res, 400, { error: "Invalid token name" });
606
+ json(res, HTTP_STATUS.BAD_REQUEST, { error: "Invalid token name" });
593
607
  return;
594
608
  }
595
609
  if (name === principal.name) {
596
- json(res, 400, { error: "Cannot revoke the token you are using" });
610
+ json(res, HTTP_STATUS.BAD_REQUEST, {
611
+ error: "Cannot revoke the token you are using",
612
+ });
597
613
  return;
598
614
  }
599
615
  try {
600
616
  const ok = await deps.authStore.revokeToken(name);
601
617
  if (!ok) {
602
- json(res, 404, { error: "Token not found" });
618
+ json(res, HTTP_STATUS.NOT_FOUND, { error: "Token not found" });
603
619
  return;
604
620
  }
605
- res.writeHead(204);
621
+ res.writeHead(HTTP_STATUS.NO_CONTENT);
606
622
  res.end();
607
623
  }
608
624
  catch (err) {
609
- json(res, 400, { error: errorMessage(err) });
625
+ json(res, HTTP_STATUS.BAD_REQUEST, { error: errorMessage(err) });
610
626
  }
611
627
  return;
612
628
  }
@@ -614,19 +630,23 @@ export function createRequestHandler(deps) {
614
630
  if (url === "/api/v1/bridge/reload" && req.method === "POST") {
615
631
  const bridge = getBridge?.();
616
632
  if (!bridge) {
617
- json(res, 503, { error: "Agent not ready yet" });
633
+ json(res, HTTP_STATUS.SERVICE_UNAVAILABLE, {
634
+ error: "Agent not ready yet",
635
+ });
618
636
  return;
619
637
  }
620
638
  if (bridge.reloading) {
621
- json(res, 409, { error: "Already reloading" });
639
+ json(res, HTTP_STATUS.CONFLICT, { error: "Already reloading" });
622
640
  return;
623
641
  }
624
642
  try {
625
643
  await bridge.restart(sessions, titleService);
626
- json(res, 200, { ok: true });
644
+ json(res, HTTP_STATUS.OK, { ok: true });
627
645
  }
628
646
  catch (err) {
629
- json(res, 500, { error: errorMessage(err) });
647
+ json(res, HTTP_STATUS.INTERNAL_SERVER_ERROR, {
648
+ error: errorMessage(err),
649
+ });
630
650
  }
631
651
  return;
632
652
  }
@@ -636,7 +656,7 @@ export function createRequestHandler(deps) {
636
656
  if (permListMatch && req.method === "GET") {
637
657
  const sessionId = decodeURIComponent(permListMatch[1]);
638
658
  const perms = sessions?.getPendingPermissions(sessionId) ?? [];
639
- json(res, 200, perms);
659
+ json(res, HTTP_STATUS.OK, perms);
640
660
  return;
641
661
  }
642
662
  // POST /api/v1/sessions/:id/permissions/:reqId
@@ -649,16 +669,18 @@ export function createRequestHandler(deps) {
649
669
  return;
650
670
  const perm = sessions?.pendingPermissions.get(requestId);
651
671
  if (!perm) {
652
- json(res, 404, { error: "Permission not found" });
672
+ json(res, HTTP_STATUS.NOT_FOUND, { error: "Permission not found" });
653
673
  return;
654
674
  }
655
675
  if (perm.sessionId !== sessionId) {
656
- json(res, 400, { error: "Session ID mismatch" });
676
+ json(res, HTTP_STATUS.BAD_REQUEST, { error: "Session ID mismatch" });
657
677
  return;
658
678
  }
659
679
  const bridge = getBridge?.();
660
680
  if (!bridge) {
661
- json(res, 503, { error: "Agent not ready yet" });
681
+ json(res, HTTP_STATUS.SERVICE_UNAVAILABLE, {
682
+ error: "Agent not ready yet",
683
+ });
662
684
  return;
663
685
  }
664
686
  let body;
@@ -666,11 +688,13 @@ export function createRequestHandler(deps) {
666
688
  body = JSON.parse(await readBody(req));
667
689
  }
668
690
  catch {
669
- json(res, 400, { error: "Invalid JSON" });
691
+ json(res, HTTP_STATUS.BAD_REQUEST, { error: "Invalid JSON" });
670
692
  return;
671
693
  }
672
694
  if (!body.optionId && !body.denied) {
673
- json(res, 400, { error: "Provide optionId or denied:true" });
695
+ json(res, HTTP_STATUS.BAD_REQUEST, {
696
+ error: "Provide optionId or denied:true",
697
+ });
674
698
  return;
675
699
  }
676
700
  const denied = Boolean(body.denied);
@@ -699,8 +723,8 @@ export function createRequestHandler(deps) {
699
723
  void deps.pushService.sendClose(`sess-${perm.sessionId}-perm-${requestId}`);
700
724
  }
701
725
  const okBody = { ok: true };
702
- saveClientOpResult(store, opId, sessionId, 200, okBody);
703
- json(res, 200, okBody);
726
+ saveClientOpResult(store, opId, sessionId, HTTP_STATUS.OK, okBody);
727
+ json(res, HTTP_STATUS.OK, okBody);
704
728
  return;
705
729
  }
706
730
  // --- POST /api/v1/sessions/:id/cancel ---
@@ -709,12 +733,14 @@ export function createRequestHandler(deps) {
709
733
  const sessionId = decodeURIComponent(cancelMatch[1]);
710
734
  const session = store.getSession(sessionId);
711
735
  if (!session) {
712
- json(res, 404, { error: "Session not found" });
736
+ json(res, HTTP_STATUS.NOT_FOUND, { error: "Session not found" });
713
737
  return;
714
738
  }
715
739
  const bridge = getBridge?.();
716
740
  if (!bridge) {
717
- json(res, 503, { error: "Agent not ready yet" });
741
+ json(res, HTTP_STATUS.SERVICE_UNAVAILABLE, {
742
+ error: "Agent not ready yet",
743
+ });
718
744
  return;
719
745
  }
720
746
  const { opId, replayed } = tryReplayClientOp(req, res, store, sessionId);
@@ -739,8 +765,8 @@ export function createRequestHandler(deps) {
739
765
  sessions.state.armCancelSafety(sessionId, cancelTimeout);
740
766
  sessions?.syncBusy(sessionId);
741
767
  const okBody = { ok: true };
742
- saveClientOpResult(store, opId, sessionId, 200, okBody);
743
- json(res, 200, okBody);
768
+ saveClientOpResult(store, opId, sessionId, HTTP_STATUS.OK, okBody);
769
+ json(res, HTTP_STATUS.OK, okBody);
744
770
  return;
745
771
  }
746
772
  // --- GET /api/v1/sessions/:id/status ---
@@ -749,12 +775,12 @@ export function createRequestHandler(deps) {
749
775
  const sessionId = decodeURIComponent(statusMatch[1]);
750
776
  const session = store.getSession(sessionId);
751
777
  if (!session) {
752
- json(res, 404, { error: "Session not found" });
778
+ json(res, HTTP_STATUS.NOT_FOUND, { error: "Session not found" });
753
779
  return;
754
780
  }
755
781
  const busyKind = sessions?.getBusyKind(sessionId) ?? null;
756
782
  const pendingPerms = sessions?.getPendingPermissions(sessionId) ?? [];
757
- json(res, 200, {
783
+ json(res, HTTP_STATUS.OK, {
758
784
  busy: busyKind != null,
759
785
  busyKind,
760
786
  pendingPermissions: pendingPerms,
@@ -771,20 +797,37 @@ export function createRequestHandler(deps) {
771
797
  const sessionId = decodeURIComponent(snapshotMatch[1]);
772
798
  const session = store.getSession(sessionId);
773
799
  if (!session) {
774
- json(res, 404, { error: "Session not found" });
800
+ json(res, HTTP_STATUS.NOT_FOUND, { error: "Session not found" });
775
801
  return;
776
802
  }
777
803
  if (!sessions) {
778
- json(res, 503, { error: "Session manager not available" });
804
+ json(res, HTTP_STATUS.SERVICE_UNAVAILABLE, {
805
+ error: "Session manager not available",
806
+ });
779
807
  return;
780
808
  }
809
+ const bridge = getBridge?.();
810
+ if (bridge && !sessions.liveSessions.has(sessionId)) {
811
+ try {
812
+ // Command discovery happens during session/load. Snapshot is the
813
+ // authoritative hydration boundary, so it must join any in-flight
814
+ // restore before reading the per-session command state.
815
+ await sessions.ensureResumed(bridge, sessionId);
816
+ }
817
+ catch {
818
+ json(res, HTTP_STATUS.SERVICE_UNAVAILABLE, {
819
+ error: "Failed to restore session",
820
+ });
821
+ return;
822
+ }
823
+ }
781
824
  // Make sure runtime reflects the current activePrompts/bash state even
782
825
  // if no patch has been emitted yet for this session.
783
826
  sessions.syncBusy(sessionId);
784
827
  sessions.syncPendingPermissions(sessionId);
785
828
  const runtimeState = sessions.state.getState(sessionId);
786
829
  const lastEventSeq = store.getLastEventSeq(sessionId);
787
- json(res, 200, {
830
+ json(res, HTTP_STATUS.OK, {
788
831
  version: 1,
789
832
  seq: runtimeState.seq,
790
833
  session: {
@@ -797,6 +840,7 @@ export function createRequestHandler(deps) {
797
840
  lastEventSeq,
798
841
  },
799
842
  runtime: runtimeState.runtime,
843
+ agentCommands: sessions.getAgentCommands(sessionId),
800
844
  }, req);
801
845
  return;
802
846
  }
@@ -809,32 +853,36 @@ export function createRequestHandler(deps) {
809
853
  if (!session) {
810
854
  logPromptRejectBeforeSave({
811
855
  sessionId,
812
- status: 404,
856
+ status: HTTP_STATUS.NOT_FOUND,
813
857
  reason: "session_not_found",
814
858
  opId: requestOpId,
815
859
  });
816
- json(res, 404, { error: "Session not found" });
860
+ json(res, HTTP_STATUS.NOT_FOUND, { error: "Session not found" });
817
861
  return;
818
862
  }
819
863
  const bridge = getBridge?.();
820
864
  if (!bridge) {
821
865
  logPromptRejectBeforeSave({
822
866
  sessionId,
823
- status: 503,
867
+ status: HTTP_STATUS.SERVICE_UNAVAILABLE,
824
868
  reason: "agent_not_ready",
825
869
  opId: requestOpId,
826
870
  });
827
- json(res, 503, { error: "Agent not ready yet" });
871
+ json(res, HTTP_STATUS.SERVICE_UNAVAILABLE, {
872
+ error: "Agent not ready yet",
873
+ });
828
874
  return;
829
875
  }
830
876
  if (!sessions) {
831
877
  logPromptRejectBeforeSave({
832
878
  sessionId,
833
- status: 503,
879
+ status: HTTP_STATUS.SERVICE_UNAVAILABLE,
834
880
  reason: "session_manager_unavailable",
835
881
  opId: requestOpId,
836
882
  });
837
- json(res, 503, { error: "Session manager not available" });
883
+ json(res, HTTP_STATUS.SERVICE_UNAVAILABLE, {
884
+ error: "Session manager not available",
885
+ });
838
886
  return;
839
887
  }
840
888
  const { opId, replayed } = tryReplayClientOp(req, res, store, sessionId);
@@ -847,12 +895,12 @@ export function createRequestHandler(deps) {
847
895
  catch (err) {
848
896
  logPromptRejectBeforeSave({
849
897
  sessionId,
850
- status: 500,
898
+ status: HTTP_STATUS.INTERNAL_SERVER_ERROR,
851
899
  reason: "resume_failed",
852
900
  opId,
853
901
  error: errorMessage(err),
854
902
  });
855
- json(res, 500, {
903
+ json(res, HTTP_STATUS.INTERNAL_SERVER_ERROR, {
856
904
  error: `Failed to resume session: ${err instanceof Error ? err.message : String(err)}`,
857
905
  });
858
906
  return;
@@ -862,12 +910,15 @@ export function createRequestHandler(deps) {
862
910
  if (busyKind) {
863
911
  logPromptRejectBeforeSave({
864
912
  sessionId,
865
- status: 409,
913
+ status: HTTP_STATUS.CONFLICT,
866
914
  reason: "session_busy",
867
915
  opId,
868
916
  busyKind,
869
917
  });
870
- json(res, 409, { error: "Session is busy", busyKind });
918
+ json(res, HTTP_STATUS.CONFLICT, {
919
+ error: "Session is busy",
920
+ busyKind,
921
+ });
871
922
  return;
872
923
  }
873
924
  let body;
@@ -877,17 +928,17 @@ export function createRequestHandler(deps) {
877
928
  catch {
878
929
  logPromptRejectBeforeSave({
879
930
  sessionId,
880
- status: 400,
931
+ status: HTTP_STATUS.BAD_REQUEST,
881
932
  reason: "invalid_json",
882
933
  opId,
883
934
  });
884
- json(res, 400, { error: "Invalid JSON" });
935
+ json(res, HTTP_STATUS.BAD_REQUEST, { error: "Invalid JSON" });
885
936
  return;
886
937
  }
887
938
  if (!body.text) {
888
939
  logPromptRejectBeforeSave({
889
940
  sessionId,
890
- status: 400,
941
+ status: HTTP_STATUS.BAD_REQUEST,
891
942
  reason: "missing_text",
892
943
  opId,
893
944
  textLength: 0,
@@ -895,7 +946,9 @@ export function createRequestHandler(deps) {
895
946
  ? body.attachments.length
896
947
  : undefined,
897
948
  });
898
- json(res, 400, { error: "Missing required field: text" });
949
+ json(res, HTTP_STATUS.BAD_REQUEST, {
950
+ error: "Missing required field: text",
951
+ });
899
952
  return;
900
953
  }
901
954
  // Validate attachment shape: client must NEVER supply uri/data/path,
@@ -906,12 +959,14 @@ export function createRequestHandler(deps) {
906
959
  if (!Array.isArray(attachments)) {
907
960
  logPromptRejectBeforeSave({
908
961
  sessionId,
909
- status: 400,
962
+ status: HTTP_STATUS.BAD_REQUEST,
910
963
  reason: "attachments_not_array",
911
964
  opId,
912
965
  textLength: body.text.length,
913
966
  });
914
- json(res, 400, { error: "attachments must be an array" });
967
+ json(res, HTTP_STATUS.BAD_REQUEST, {
968
+ error: "attachments must be an array",
969
+ });
915
970
  return;
916
971
  }
917
972
  for (const raw of attachments) {
@@ -924,13 +979,15 @@ export function createRequestHandler(deps) {
924
979
  typeof att.mimeType !== "string") {
925
980
  logPromptRejectBeforeSave({
926
981
  sessionId,
927
- status: 400,
982
+ status: HTTP_STATUS.BAD_REQUEST,
928
983
  reason: "invalid_attachment_entry",
929
984
  opId,
930
985
  textLength: body.text.length,
931
986
  attachmentCount: attachments.length,
932
987
  });
933
- json(res, 400, { error: "Invalid attachment entry" });
988
+ json(res, HTTP_STATUS.BAD_REQUEST, {
989
+ error: "Invalid attachment entry",
990
+ });
934
991
  return;
935
992
  }
936
993
  if (typeof att.uri === "string" ||
@@ -940,19 +997,41 @@ export function createRequestHandler(deps) {
940
997
  typeof att.height === "number") {
941
998
  logPromptRejectBeforeSave({
942
999
  sessionId,
943
- status: 400,
1000
+ status: HTTP_STATUS.BAD_REQUEST,
944
1001
  reason: "client_supplied_attachment_data",
945
1002
  opId,
946
1003
  textLength: body.text.length,
947
1004
  attachmentCount: attachments.length,
948
1005
  });
949
- json(res, 400, {
1006
+ json(res, HTTP_STATUS.BAD_REQUEST, {
950
1007
  error: "Client must not supply uri/data/path/width/height",
951
1008
  });
952
1009
  return;
953
1010
  }
954
1011
  }
955
1012
  }
1013
+ let agentText = body.text;
1014
+ if (body.text.startsWith("//")) {
1015
+ const resolved = resolveAgentCommand(body.text, sessions.getAgentCommands(sessionId).commands);
1016
+ if (!resolved) {
1017
+ const command = agentCommandToken(body.text);
1018
+ logPromptRejectBeforeSave({
1019
+ sessionId,
1020
+ status: HTTP_STATUS.UNPROCESSABLE_CONTENT,
1021
+ reason: "unknown_command",
1022
+ opId,
1023
+ textLength: body.text.length,
1024
+ attachmentCount: attachments?.length,
1025
+ });
1026
+ json(res, HTTP_STATUS.UNPROCESSABLE_CONTENT, {
1027
+ error: "Unknown command",
1028
+ command,
1029
+ prefix: "//",
1030
+ });
1031
+ return;
1032
+ }
1033
+ agentText = resolved.agentText;
1034
+ }
956
1035
  // Stored shape mirrors the wire shape PLUS a server-derived `path`
957
1036
  // for renderers. The path is the unsigned base URL
958
1037
  // (`/api/v1/sessions/<sid>/attachments/<filename>`); reSign on
@@ -1010,7 +1089,7 @@ export function createRequestHandler(deps) {
1010
1089
  sessions.activePrompts.add(sessionId);
1011
1090
  sessions.syncBusy(sessionId);
1012
1091
  bridge
1013
- .prompt(sessionId, body.text, attachments)
1092
+ .prompt(sessionId, agentText, attachments)
1014
1093
  .catch((err) => {
1015
1094
  plog.error("error", { sessionId, error: err });
1016
1095
  })
@@ -1019,8 +1098,8 @@ export function createRequestHandler(deps) {
1019
1098
  sessions.syncBusy(sessionId);
1020
1099
  });
1021
1100
  const acceptedBody = { status: "accepted" };
1022
- saveClientOpResult(store, opId, sessionId, 202, acceptedBody);
1023
- json(res, 202, acceptedBody);
1101
+ saveClientOpResult(store, opId, sessionId, HTTP_STATUS.ACCEPTED, acceptedBody);
1102
+ json(res, HTTP_STATUS.ACCEPTED, acceptedBody);
1024
1103
  return;
1025
1104
  }
1026
1105
  // --- POST /api/v1/sessions/:id/bash ---
@@ -1029,15 +1108,17 @@ export function createRequestHandler(deps) {
1029
1108
  const sessionId = decodeURIComponent(bashMatch[1]);
1030
1109
  const session = store.getSession(sessionId);
1031
1110
  if (!session) {
1032
- json(res, 404, { error: "Session not found" });
1111
+ json(res, HTTP_STATUS.NOT_FOUND, { error: "Session not found" });
1033
1112
  return;
1034
1113
  }
1035
1114
  if (!sessions) {
1036
- json(res, 503, { error: "Session manager not available" });
1115
+ json(res, HTTP_STATUS.SERVICE_UNAVAILABLE, {
1116
+ error: "Session manager not available",
1117
+ });
1037
1118
  return;
1038
1119
  }
1039
1120
  if (sessions.runningBashProcs.has(sessionId)) {
1040
- json(res, 409, {
1121
+ json(res, HTTP_STATUS.CONFLICT, {
1041
1122
  error: "A bash command is already running in this session",
1042
1123
  });
1043
1124
  return;
@@ -1047,11 +1128,13 @@ export function createRequestHandler(deps) {
1047
1128
  body = JSON.parse(await readBody(req));
1048
1129
  }
1049
1130
  catch {
1050
- json(res, 400, { error: "Invalid JSON" });
1131
+ json(res, HTTP_STATUS.BAD_REQUEST, { error: "Invalid JSON" });
1051
1132
  return;
1052
1133
  }
1053
1134
  if (!body.command) {
1054
- json(res, 400, { error: "Missing required field: command" });
1135
+ json(res, HTTP_STATUS.BAD_REQUEST, {
1136
+ error: "Missing required field: command",
1137
+ });
1055
1138
  return;
1056
1139
  }
1057
1140
  const cwd = sessions.getSessionCwd(sessionId);
@@ -1128,7 +1211,7 @@ export function createRequestHandler(deps) {
1128
1211
  };
1129
1212
  sseManager.broadcast(bashErrEvent);
1130
1213
  });
1131
- json(res, 202, { status: "accepted" });
1214
+ json(res, HTTP_STATUS.ACCEPTED, { status: "accepted" });
1132
1215
  return;
1133
1216
  }
1134
1217
  // --- POST /api/v1/sessions/:id/bash/cancel ---
@@ -1137,11 +1220,11 @@ export function createRequestHandler(deps) {
1137
1220
  const sessionId = decodeURIComponent(bashCancelMatch[1]);
1138
1221
  const session = store.getSession(sessionId);
1139
1222
  if (!session) {
1140
- json(res, 404, { error: "Session not found" });
1223
+ json(res, HTTP_STATUS.NOT_FOUND, { error: "Session not found" });
1141
1224
  return;
1142
1225
  }
1143
1226
  interruptBashProc(sessions?.runningBashProcs.get(sessionId));
1144
- json(res, 200, { ok: true });
1227
+ json(res, HTTP_STATUS.OK, { ok: true });
1145
1228
  return;
1146
1229
  }
1147
1230
  // --- PUT /api/v1/sessions/:id/{model,mode,reasoning-effort} ---
@@ -1155,12 +1238,14 @@ export function createRequestHandler(deps) {
1155
1238
  const configId = configPath.replace(/-/g, "_");
1156
1239
  const session = store.getSession(sessionId);
1157
1240
  if (!session) {
1158
- json(res, 404, { error: "Session not found" });
1241
+ json(res, HTTP_STATUS.NOT_FOUND, { error: "Session not found" });
1159
1242
  return;
1160
1243
  }
1161
1244
  const bridge = getBridge?.();
1162
1245
  if (!bridge) {
1163
- json(res, 503, { error: "Agent not ready yet" });
1246
+ json(res, HTTP_STATUS.SERVICE_UNAVAILABLE, {
1247
+ error: "Agent not ready yet",
1248
+ });
1164
1249
  return;
1165
1250
  }
1166
1251
  let body;
@@ -1168,11 +1253,13 @@ export function createRequestHandler(deps) {
1168
1253
  body = JSON.parse(await readBody(req));
1169
1254
  }
1170
1255
  catch {
1171
- json(res, 400, { error: "Invalid JSON" });
1256
+ json(res, HTTP_STATUS.BAD_REQUEST, { error: "Invalid JSON" });
1172
1257
  return;
1173
1258
  }
1174
1259
  if (body.value === undefined) {
1175
- json(res, 400, { error: "Missing required field: value" });
1260
+ json(res, HTTP_STATUS.BAD_REQUEST, {
1261
+ error: "Missing required field: value",
1262
+ });
1176
1263
  return;
1177
1264
  }
1178
1265
  try {
@@ -1193,10 +1280,10 @@ export function createRequestHandler(deps) {
1193
1280
  configId,
1194
1281
  value: body.value,
1195
1282
  });
1196
- json(res, 200, { configOptions });
1283
+ json(res, HTTP_STATUS.OK, { configOptions });
1197
1284
  }
1198
1285
  catch (err) {
1199
- json(res, 500, {
1286
+ json(res, HTTP_STATUS.INTERNAL_SERVER_ERROR, {
1200
1287
  error: `Failed to set ${configId}: ${err instanceof Error ? err.message : String(err)}`,
1201
1288
  });
1202
1289
  }
@@ -1208,7 +1295,7 @@ export function createRequestHandler(deps) {
1208
1295
  const sessionId = decodeURIComponent(titlePutMatch[1]);
1209
1296
  const session = store.getSession(sessionId);
1210
1297
  if (!session) {
1211
- json(res, 404, { error: "Session not found" });
1298
+ json(res, HTTP_STATUS.NOT_FOUND, { error: "Session not found" });
1212
1299
  return;
1213
1300
  }
1214
1301
  let body;
@@ -1216,11 +1303,13 @@ export function createRequestHandler(deps) {
1216
1303
  body = JSON.parse(await readBody(req));
1217
1304
  }
1218
1305
  catch {
1219
- json(res, 400, { error: "Invalid JSON" });
1306
+ json(res, HTTP_STATUS.BAD_REQUEST, { error: "Invalid JSON" });
1220
1307
  return;
1221
1308
  }
1222
1309
  if (!body.value) {
1223
- json(res, 400, { error: "Missing required field: value" });
1310
+ json(res, HTTP_STATUS.BAD_REQUEST, {
1311
+ error: "Missing required field: value",
1312
+ });
1224
1313
  return;
1225
1314
  }
1226
1315
  store.updateSessionTitle(sessionId, body.value);
@@ -1235,7 +1324,7 @@ export function createRequestHandler(deps) {
1235
1324
  title: body.value,
1236
1325
  };
1237
1326
  sseManager.broadcast(titleEvent);
1238
- json(res, 200, { title: body.value });
1327
+ json(res, HTTP_STATUS.OK, { title: body.value });
1239
1328
  return;
1240
1329
  }
1241
1330
  // --- Session CRUD: /api/v1/sessions/:id ---
@@ -1248,7 +1337,7 @@ export function createRequestHandler(deps) {
1248
1337
  if (req.method === "GET") {
1249
1338
  const session = store.getSession(sessionId);
1250
1339
  if (!session) {
1251
- json(res, 404, { error: "Session not found" });
1340
+ json(res, HTTP_STATUS.NOT_FOUND, { error: "Session not found" });
1252
1341
  return;
1253
1342
  }
1254
1343
  // Start resume in background (non-blocking) so the client gets metadata fast.
@@ -1353,7 +1442,7 @@ export function createRequestHandler(deps) {
1353
1442
  return opts;
1354
1443
  })()
1355
1444
  : [];
1356
- json(res, 200, {
1445
+ json(res, HTTP_STATUS.OK, {
1357
1446
  id: freshSession.id,
1358
1447
  cwd: freshSession.cwd,
1359
1448
  title: freshSession.title,
@@ -1368,7 +1457,7 @@ export function createRequestHandler(deps) {
1368
1457
  if (req.method === "DELETE") {
1369
1458
  const session = store.getSession(sessionId);
1370
1459
  if (!session) {
1371
- json(res, 404, { error: "Session not found" });
1460
+ json(res, HTTP_STATUS.NOT_FOUND, { error: "Session not found" });
1372
1461
  return;
1373
1462
  }
1374
1463
  if (sessions) {
@@ -1378,7 +1467,7 @@ export function createRequestHandler(deps) {
1378
1467
  store.deleteSession(sessionId);
1379
1468
  }
1380
1469
  sseManager.broadcast({ type: "session_deleted", sessionId });
1381
- res.writeHead(204);
1470
+ res.writeHead(HTTP_STATUS.NO_CONTENT);
1382
1471
  res.end();
1383
1472
  return;
1384
1473
  }
@@ -1387,11 +1476,15 @@ export function createRequestHandler(deps) {
1387
1476
  if (url === "/api/v1/sessions" && req.method === "POST") {
1388
1477
  const bridge = getBridge?.();
1389
1478
  if (!bridge) {
1390
- json(res, 503, { error: "Agent not ready yet" });
1479
+ json(res, HTTP_STATUS.SERVICE_UNAVAILABLE, {
1480
+ error: "Agent not ready yet",
1481
+ });
1391
1482
  return;
1392
1483
  }
1393
1484
  if (!sessions) {
1394
- json(res, 503, { error: "Session manager not available" });
1485
+ json(res, HTTP_STATUS.SERVICE_UNAVAILABLE, {
1486
+ error: "Session manager not available",
1487
+ });
1395
1488
  return;
1396
1489
  }
1397
1490
  let body;
@@ -1399,7 +1492,7 @@ export function createRequestHandler(deps) {
1399
1492
  body = JSON.parse(await readBody(req));
1400
1493
  }
1401
1494
  catch {
1402
- json(res, 400, { error: "Invalid JSON" });
1495
+ json(res, HTTP_STATUS.BAD_REQUEST, { error: "Invalid JSON" });
1403
1496
  return;
1404
1497
  }
1405
1498
  const source = body.source ?? "auto";
@@ -1412,6 +1505,7 @@ export function createRequestHandler(deps) {
1412
1505
  cwd: session?.cwd,
1413
1506
  title: session?.title,
1414
1507
  configOptions,
1508
+ agentCommands: sessions.getAgentCommands(sessionId),
1415
1509
  };
1416
1510
  sseManager.broadcast(sessionCreatedEvent);
1417
1511
  // ACP's session_created event fires before inheritance runs, so
@@ -1423,21 +1517,22 @@ export function createRequestHandler(deps) {
1423
1517
  configOptions,
1424
1518
  });
1425
1519
  }
1426
- json(res, 201, {
1520
+ json(res, HTTP_STATUS.CREATED, {
1427
1521
  id: sessionId,
1428
1522
  cwd: session?.cwd ?? body.cwd,
1429
1523
  title: session?.title ?? null,
1430
1524
  source: session?.source ?? source,
1431
1525
  configOptions,
1526
+ agentCommands: sessions.getAgentCommands(sessionId),
1432
1527
  });
1433
1528
  }
1434
1529
  catch (err) {
1435
1530
  const msg = err instanceof Error ? err.message : String(err);
1436
1531
  if (msg.includes("does not exist")) {
1437
- json(res, 400, { error: msg });
1532
+ json(res, HTTP_STATUS.BAD_REQUEST, { error: msg });
1438
1533
  }
1439
1534
  else {
1440
- json(res, 500, { error: msg });
1535
+ json(res, HTTP_STATUS.INTERNAL_SERVER_ERROR, { error: msg });
1441
1536
  }
1442
1537
  }
1443
1538
  return;
@@ -1459,7 +1554,7 @@ export function createRequestHandler(deps) {
1459
1554
  : undefined;
1460
1555
  const session = store.getSession(sessionId);
1461
1556
  if (!session) {
1462
- json(res, 404, { error: "Session not found" });
1557
+ json(res, HTTP_STATUS.NOT_FOUND, { error: "Session not found" });
1463
1558
  return;
1464
1559
  }
1465
1560
  // Flush pending buffers so their content becomes part of the event list.
@@ -1520,7 +1615,7 @@ export function createRequestHandler(deps) {
1520
1615
  envelope.total = total;
1521
1616
  envelope.hasMore = hasMore;
1522
1617
  }
1523
- json(res, 200, envelope, req);
1618
+ json(res, HTTP_STATUS.OK, envelope, req);
1524
1619
  return;
1525
1620
  }
1526
1621
  // --- SSE stream endpoints ---
@@ -1534,13 +1629,15 @@ export function createRequestHandler(deps) {
1534
1629
  const ticket = new URLSearchParams(url.split("?")[1] ?? "").get("ticket") ?? "";
1535
1630
  const principal = deps.ticketStore?.consume(ticket);
1536
1631
  if (!principal) {
1537
- json(res, 401, { error: "Invalid or expired ticket" });
1632
+ json(res, HTTP_STATUS.UNAUTHORIZED, {
1633
+ error: "Invalid or expired ticket",
1634
+ });
1538
1635
  return;
1539
1636
  }
1540
1637
  tokenName = principal.tokenName;
1541
1638
  }
1542
1639
  const clientId = sseManager.generateClientId();
1543
- res.writeHead(200, {
1640
+ res.writeHead(HTTP_STATUS.OK, {
1544
1641
  "Content-Type": "text/event-stream",
1545
1642
  "Cache-Control": "no-cache",
1546
1643
  Connection: "keep-alive",
@@ -1569,18 +1666,20 @@ export function createRequestHandler(deps) {
1569
1666
  const ticket = new URLSearchParams(url.split("?")[1] ?? "").get("ticket") ?? "";
1570
1667
  const principal = deps.ticketStore?.consume(ticket);
1571
1668
  if (!principal) {
1572
- json(res, 401, { error: "Invalid or expired ticket" });
1669
+ json(res, HTTP_STATUS.UNAUTHORIZED, {
1670
+ error: "Invalid or expired ticket",
1671
+ });
1573
1672
  return;
1574
1673
  }
1575
1674
  tokenName = principal.tokenName;
1576
1675
  }
1577
1676
  const session = store.getSession(sessionId);
1578
1677
  if (!session) {
1579
- json(res, 404, { error: "Session not found" });
1678
+ json(res, HTTP_STATUS.NOT_FOUND, { error: "Session not found" });
1580
1679
  return;
1581
1680
  }
1582
1681
  const clientId = sseManager.generateClientId();
1583
- res.writeHead(200, {
1682
+ res.writeHead(HTTP_STATUS.OK, {
1584
1683
  "Content-Type": "text/event-stream",
1585
1684
  "Cache-Control": "no-cache",
1586
1685
  Connection: "keep-alive",
@@ -1632,12 +1731,14 @@ export function createRequestHandler(deps) {
1632
1731
  if (imgUploadMatch && req.method === "POST") {
1633
1732
  const sessionId = decodeURIComponent(imgUploadMatch[1]);
1634
1733
  if (!SAFE_ID.test(sessionId)) {
1635
- json(res, 400, { error: "Invalid session ID" });
1734
+ json(res, HTTP_STATUS.BAD_REQUEST, { error: "Invalid session ID" });
1636
1735
  return;
1637
1736
  }
1638
1737
  const ctype = req.headers["content-type"] ?? "";
1639
1738
  if (!ctype.toLowerCase().startsWith("multipart/form-data")) {
1640
- json(res, 400, { error: "Expected multipart/form-data" });
1739
+ json(res, HTTP_STATUS.BAD_REQUEST, {
1740
+ error: "Expected multipart/form-data",
1741
+ });
1641
1742
  return;
1642
1743
  }
1643
1744
  await handleAttachmentUpload(req, res, sessionId, deps);
@@ -1660,14 +1761,16 @@ export function createRequestHandler(deps) {
1660
1761
  const exp = params.get("exp") ?? "";
1661
1762
  const basePath = `/api/v1/sessions/${sessionId}/attachments/${file}`;
1662
1763
  if (!verifyAttachmentSig(basePath, exp, sig, deps.attachmentSecret)) {
1663
- res.writeHead(401, { "Content-Type": "application/json" });
1764
+ res.writeHead(HTTP_STATUS.UNAUTHORIZED, {
1765
+ "Content-Type": "application/json",
1766
+ });
1664
1767
  res.end(JSON.stringify({ error: "Unauthorized" }));
1665
1768
  return;
1666
1769
  }
1667
1770
  }
1668
1771
  const filePath = join(deps.dataDir, "sessions", sessionId, "attachments", file);
1669
1772
  if (!filePath.startsWith(join(deps.dataDir, "sessions"))) {
1670
- res.writeHead(403);
1773
+ res.writeHead(HTTP_STATUS.FORBIDDEN);
1671
1774
  res.end("Forbidden");
1672
1775
  return;
1673
1776
  }
@@ -1688,11 +1791,11 @@ export function createRequestHandler(deps) {
1688
1791
  const disposition = isInlineMime(mime) ? "inline" : "attachment";
1689
1792
  headers["Content-Disposition"] = buildContentDisposition(disposition, row.name);
1690
1793
  }
1691
- res.writeHead(200, headers);
1794
+ res.writeHead(HTTP_STATUS.OK, headers);
1692
1795
  res.end(fileData);
1693
1796
  }
1694
1797
  catch {
1695
- res.writeHead(404);
1798
+ res.writeHead(HTTP_STATUS.NOT_FOUND);
1696
1799
  res.end("Not found");
1697
1800
  }
1698
1801
  return;
@@ -1711,7 +1814,7 @@ export function createRequestHandler(deps) {
1711
1814
  raw = await readBody(req);
1712
1815
  }
1713
1816
  catch {
1714
- json(res, 400, { error: "Failed to read body" });
1817
+ json(res, HTTP_STATUS.BAD_REQUEST, { error: "Failed to read body" });
1715
1818
  return;
1716
1819
  }
1717
1820
  let parsed;
@@ -1719,12 +1822,12 @@ export function createRequestHandler(deps) {
1719
1822
  parsed = JSON.parse(raw);
1720
1823
  }
1721
1824
  catch {
1722
- json(res, 400, { error: "Invalid JSON" });
1825
+ json(res, HTTP_STATUS.BAD_REQUEST, { error: "Invalid JSON" });
1723
1826
  return;
1724
1827
  }
1725
1828
  const validation = MessageIngressSchema.safeParse(parsed);
1726
1829
  if (!validation.success) {
1727
- json(res, 400, {
1830
+ json(res, HTTP_STATUS.BAD_REQUEST, {
1728
1831
  error: "Invalid body",
1729
1832
  issues: validation.error.issues,
1730
1833
  });
@@ -1736,7 +1839,7 @@ export function createRequestHandler(deps) {
1736
1839
  const targetSid = input.to.slice("session:".length);
1737
1840
  const session = store.getSession(targetSid);
1738
1841
  if (!session) {
1739
- json(res, 400, { error: "session_not_found" });
1842
+ json(res, HTTP_STATUS.BAD_REQUEST, { error: "session_not_found" });
1740
1843
  return;
1741
1844
  }
1742
1845
  sessions?.flushBuffers(targetSid);
@@ -1772,8 +1875,8 @@ export function createRequestHandler(deps) {
1772
1875
  sess_id: targetSid.slice(0, 8),
1773
1876
  });
1774
1877
  const boundBody = { id, delivered: "session" };
1775
- saveClientOpResult(store, opId, "__ingress__", 200, boundBody);
1776
- json(res, 200, boundBody);
1878
+ saveClientOpResult(store, opId, "__ingress__", HTTP_STATUS.OK, boundBody);
1879
+ json(res, HTTP_STATUS.OK, boundBody);
1777
1880
  return;
1778
1881
  }
1779
1882
  // Unbound: to=user → rows in `messages` table
@@ -1819,13 +1922,13 @@ export function createRequestHandler(deps) {
1819
1922
  from_ref: input.from_ref,
1820
1923
  });
1821
1924
  const unboundBody = { id, delivered: "pending" };
1822
- saveClientOpResult(store, opId, "__ingress__", 200, unboundBody);
1823
- json(res, 200, unboundBody);
1925
+ saveClientOpResult(store, opId, "__ingress__", HTTP_STATUS.OK, unboundBody);
1926
+ json(res, HTTP_STATUS.OK, unboundBody);
1824
1927
  return;
1825
1928
  }
1826
1929
  // GET /api/v1/messages — list unprocessed
1827
1930
  if (url === "/api/v1/messages" && req.method === "GET") {
1828
- json(res, 200, { messages: store.listUnprocessed() });
1931
+ json(res, HTTP_STATUS.OK, { messages: store.listUnprocessed() });
1829
1932
  return;
1830
1933
  }
1831
1934
  // /api/v1/messages/:id... — GET single, POST :id/consume, POST :id/ack, DELETE :id
@@ -1841,7 +1944,7 @@ export function createRequestHandler(deps) {
1841
1944
  }
1842
1945
  catch (err) {
1843
1946
  if (/message not found/.test(errorMessage(err))) {
1844
- json(res, 404, { error: "Message not found" });
1947
+ json(res, HTTP_STATUS.NOT_FOUND, { error: "Message not found" });
1845
1948
  return;
1846
1949
  }
1847
1950
  throw err;
@@ -1859,7 +1962,7 @@ export function createRequestHandler(deps) {
1859
1962
  sess_id: out.sessionId.slice(0, 8),
1860
1963
  already_consumed: out.alreadyConsumed,
1861
1964
  });
1862
- json(res, 200, {
1965
+ json(res, HTTP_STATUS.OK, {
1863
1966
  sessionId: out.sessionId,
1864
1967
  alreadyConsumed: out.alreadyConsumed,
1865
1968
  });
@@ -1873,28 +1976,28 @@ export function createRequestHandler(deps) {
1873
1976
  const id = decodeURIComponent((ackPost ?? idOnly)[1]);
1874
1977
  const changes = store.deleteMessage(id);
1875
1978
  if (changes === 0) {
1876
- json(res, 404, { error: "Message not found" });
1979
+ json(res, HTTP_STATUS.NOT_FOUND, { error: "Message not found" });
1877
1980
  return;
1878
1981
  }
1879
1982
  sseManager.broadcast({ type: "message_acked", messageId: id });
1880
1983
  if (deps.pushService)
1881
1984
  void deps.pushService.sendClose(id);
1882
1985
  mlog.info("ack", { msg_id: id });
1883
- json(res, 200, { ok: true });
1986
+ json(res, HTTP_STATUS.OK, { ok: true });
1884
1987
  return;
1885
1988
  }
1886
1989
  if (idOnly && req.method === "GET") {
1887
1990
  const id = decodeURIComponent(idOnly[1]);
1888
1991
  const row = store.getMessage(id);
1889
1992
  if (!row) {
1890
- json(res, 404, { error: "Message not found" });
1993
+ json(res, HTTP_STATUS.NOT_FOUND, { error: "Message not found" });
1891
1994
  return;
1892
1995
  }
1893
- json(res, 200, row);
1996
+ json(res, HTTP_STATUS.OK, row);
1894
1997
  return;
1895
1998
  }
1896
1999
  }
1897
- json(res, 404, { error: "Not found" });
2000
+ json(res, HTTP_STATUS.NOT_FOUND, { error: "Not found" });
1898
2001
  return;
1899
2002
  }
1900
2003
  // --- Beta API routes ---
@@ -1903,12 +2006,16 @@ export function createRequestHandler(deps) {
1903
2006
  // POST /api/beta/prompt — quick one-shot prompt (create temp session + send)
1904
2007
  if (url === "/api/beta/prompt" && req.method === "POST") {
1905
2008
  if (!sessions || !getBridge) {
1906
- json(res, 503, { error: "Agent not available" });
2009
+ json(res, HTTP_STATUS.SERVICE_UNAVAILABLE, {
2010
+ error: "Agent not available",
2011
+ });
1907
2012
  return;
1908
2013
  }
1909
2014
  const bridge = getBridge();
1910
2015
  if (!bridge) {
1911
- json(res, 503, { error: "Agent not available" });
2016
+ json(res, HTTP_STATUS.SERVICE_UNAVAILABLE, {
2017
+ error: "Agent not available",
2018
+ });
1912
2019
  return;
1913
2020
  }
1914
2021
  let body;
@@ -1916,18 +2023,20 @@ export function createRequestHandler(deps) {
1916
2023
  body = JSON.parse(await readBody(req));
1917
2024
  }
1918
2025
  catch {
1919
- json(res, 400, { error: "Invalid JSON" });
2026
+ json(res, HTTP_STATUS.BAD_REQUEST, { error: "Invalid JSON" });
1920
2027
  return;
1921
2028
  }
1922
2029
  const text = body.text;
1923
2030
  if (!text || typeof text !== "string") {
1924
- json(res, 400, { error: "Missing required field: text" });
2031
+ json(res, HTTP_STATUS.BAD_REQUEST, {
2032
+ error: "Missing required field: text",
2033
+ });
1925
2034
  return;
1926
2035
  }
1927
2036
  const cwd = typeof body.cwd === "string" ? body.cwd : undefined;
1928
2037
  const { sessionId } = await sessions.createSession(bridge, cwd, undefined, "auto");
1929
2038
  const streamUrl = `/api/v1/sessions/${sessionId}/events/stream`;
1930
- json(res, 202, { sessionId, streamUrl });
2039
+ json(res, HTTP_STATUS.ACCEPTED, { sessionId, streamUrl });
1931
2040
  // Fire-and-forget: send the prompt asynchronously, tracking busy state
1932
2041
  sessions.activePrompts.add(sessionId);
1933
2042
  sessions.syncBusy(sessionId);
@@ -1963,7 +2072,7 @@ export function createRequestHandler(deps) {
1963
2072
  const clientKnown = sseManager.clients.has(clientId) ||
1964
2073
  deps.clientRegistry?.get(clientId) !== undefined;
1965
2074
  if (!clientKnown) {
1966
- json(res, 404, { error: "Client not found" });
2075
+ json(res, HTTP_STATUS.NOT_FOUND, { error: "Client not found" });
1967
2076
  return;
1968
2077
  }
1969
2078
  let body;
@@ -1971,11 +2080,13 @@ export function createRequestHandler(deps) {
1971
2080
  body = JSON.parse(await readBody(req));
1972
2081
  }
1973
2082
  catch {
1974
- json(res, 400, { error: "Invalid JSON" });
2083
+ json(res, HTTP_STATUS.BAD_REQUEST, { error: "Invalid JSON" });
1975
2084
  return;
1976
2085
  }
1977
2086
  if (typeof body.visible !== "boolean") {
1978
- json(res, 400, { error: "Missing or invalid 'visible' field" });
2087
+ json(res, HTTP_STATUS.BAD_REQUEST, {
2088
+ error: "Missing or invalid 'visible' field",
2089
+ });
1979
2090
  return;
1980
2091
  }
1981
2092
  // sessionId patch semantics: absent = preserve, null = clear,
@@ -2022,23 +2133,25 @@ export function createRequestHandler(deps) {
2022
2133
  }
2023
2134
  }
2024
2135
  }
2025
- json(res, 200, { ok: true });
2136
+ json(res, HTTP_STATUS.OK, { ok: true });
2026
2137
  return;
2027
2138
  }
2028
2139
  // --- Push notification routes ---
2029
2140
  // GET /api/beta/push/vapid-key
2030
2141
  if (url === "/api/beta/push/vapid-key" && req.method === "GET") {
2031
2142
  if (!deps.pushService) {
2032
- json(res, 404, { error: "Push not configured" });
2143
+ json(res, HTTP_STATUS.NOT_FOUND, { error: "Push not configured" });
2033
2144
  return;
2034
2145
  }
2035
- json(res, 200, { publicKey: deps.pushService.getPublicKey() });
2146
+ json(res, HTTP_STATUS.OK, {
2147
+ publicKey: deps.pushService.getPublicKey(),
2148
+ });
2036
2149
  return;
2037
2150
  }
2038
2151
  // POST /api/beta/push/subscribe
2039
2152
  if (url === "/api/beta/push/subscribe" && req.method === "POST") {
2040
2153
  if (!deps.pushService) {
2041
- json(res, 404, { error: "Push not configured" });
2154
+ json(res, HTTP_STATUS.NOT_FOUND, { error: "Push not configured" });
2042
2155
  return;
2043
2156
  }
2044
2157
  const chunks = [];
@@ -2049,11 +2162,13 @@ export function createRequestHandler(deps) {
2049
2162
  body = JSON.parse(Buffer.concat(chunks).toString());
2050
2163
  }
2051
2164
  catch {
2052
- json(res, 400, { error: "Invalid JSON" });
2165
+ json(res, HTTP_STATUS.BAD_REQUEST, { error: "Invalid JSON" });
2053
2166
  return;
2054
2167
  }
2055
2168
  if (!body.endpoint || !body.keys?.auth || !body.keys.p256dh) {
2056
- json(res, 400, { error: "Missing endpoint or keys (auth, p256dh)" });
2169
+ json(res, HTTP_STATUS.BAD_REQUEST, {
2170
+ error: "Missing endpoint or keys (auth, p256dh)",
2171
+ });
2057
2172
  return;
2058
2173
  }
2059
2174
  store.saveSubscription(body.endpoint, body.keys.auth, body.keys.p256dh);
@@ -2061,13 +2176,13 @@ export function createRequestHandler(deps) {
2061
2176
  if (body.clientId && deps.pushService) {
2062
2177
  deps.pushService.registerClient(body.clientId, body.endpoint);
2063
2178
  }
2064
- json(res, 201, { ok: true });
2179
+ json(res, HTTP_STATUS.CREATED, { ok: true });
2065
2180
  return;
2066
2181
  }
2067
2182
  // POST /api/beta/push/register-client — associate clientId with push endpoint
2068
2183
  if (url === "/api/beta/push/register-client" && req.method === "POST") {
2069
2184
  if (!deps.pushService) {
2070
- json(res, 404, { error: "Push not configured" });
2185
+ json(res, HTTP_STATUS.NOT_FOUND, { error: "Push not configured" });
2071
2186
  return;
2072
2187
  }
2073
2188
  const chunks = [];
@@ -2078,21 +2193,23 @@ export function createRequestHandler(deps) {
2078
2193
  body = JSON.parse(Buffer.concat(chunks).toString());
2079
2194
  }
2080
2195
  catch {
2081
- json(res, 400, { error: "Invalid JSON" });
2196
+ json(res, HTTP_STATUS.BAD_REQUEST, { error: "Invalid JSON" });
2082
2197
  return;
2083
2198
  }
2084
2199
  if (!body.clientId || !body.endpoint) {
2085
- json(res, 400, { error: "Missing clientId or endpoint" });
2200
+ json(res, HTTP_STATUS.BAD_REQUEST, {
2201
+ error: "Missing clientId or endpoint",
2202
+ });
2086
2203
  return;
2087
2204
  }
2088
2205
  deps.pushService.registerClient(body.clientId, body.endpoint);
2089
- json(res, 200, { ok: true });
2206
+ json(res, HTTP_STATUS.OK, { ok: true });
2090
2207
  return;
2091
2208
  }
2092
2209
  // POST /api/beta/push/unsubscribe
2093
2210
  if (url === "/api/beta/push/unsubscribe" && req.method === "POST") {
2094
2211
  if (!deps.pushService) {
2095
- json(res, 404, { error: "Push not configured" });
2212
+ json(res, HTTP_STATUS.NOT_FOUND, { error: "Push not configured" });
2096
2213
  return;
2097
2214
  }
2098
2215
  const chunks = [];
@@ -2103,16 +2220,16 @@ export function createRequestHandler(deps) {
2103
2220
  body = JSON.parse(Buffer.concat(chunks).toString());
2104
2221
  }
2105
2222
  catch {
2106
- json(res, 400, { error: "Invalid JSON" });
2223
+ json(res, HTTP_STATUS.BAD_REQUEST, { error: "Invalid JSON" });
2107
2224
  return;
2108
2225
  }
2109
2226
  if (body.endpoint) {
2110
2227
  store.removeSubscription(body.endpoint);
2111
2228
  }
2112
- json(res, 200, { ok: true });
2229
+ json(res, HTTP_STATUS.OK, { ok: true });
2113
2230
  return;
2114
2231
  }
2115
- json(res, 404, { error: "Not found" });
2232
+ json(res, HTTP_STATUS.NOT_FOUND, { error: "Not found" });
2116
2233
  return;
2117
2234
  }
2118
2235
  // --- Static files ---
@@ -2122,7 +2239,7 @@ export function createRequestHandler(deps) {
2122
2239
  staticPath = "/" + htmlEntry.file;
2123
2240
  const filePath = join(deps.publicDir, staticPath);
2124
2241
  if (!filePath.startsWith(deps.publicDir)) {
2125
- res.writeHead(403);
2242
+ res.writeHead(HTTP_STATUS.FORBIDDEN);
2126
2243
  res.end("Forbidden");
2127
2244
  return;
2128
2245
  }
@@ -2145,11 +2262,11 @@ export function createRequestHandler(deps) {
2145
2262
  // CSP applies to HTML entrypoints (where script/style execute).
2146
2263
  if (htmlEntry)
2147
2264
  headers["Content-Security-Policy"] = CSP_POLICY;
2148
- res.writeHead(200, headers);
2265
+ res.writeHead(HTTP_STATUS.OK, headers);
2149
2266
  res.end(data);
2150
2267
  }
2151
2268
  catch {
2152
- res.writeHead(404);
2269
+ res.writeHead(HTTP_STATUS.NOT_FOUND);
2153
2270
  res.end("Not found");
2154
2271
  }
2155
2272
  };