@jait/gateway 0.1.518 → 0.1.520

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (89) hide show
  1. package/dist/index.d.ts.map +1 -1
  2. package/dist/index.js +19 -1
  3. package/dist/index.js.map +1 -1
  4. package/dist/providers/remote-cli-provider.d.ts.map +1 -1
  5. package/dist/providers/remote-cli-provider.js +44 -5
  6. package/dist/providers/remote-cli-provider.js.map +1 -1
  7. package/dist/routes/chat.d.ts.map +1 -1
  8. package/dist/routes/chat.js +157 -60
  9. package/dist/routes/chat.js.map +1 -1
  10. package/dist/routes/terminals.d.ts.map +1 -1
  11. package/dist/routes/terminals.js +23 -2
  12. package/dist/routes/terminals.js.map +1 -1
  13. package/dist/services/primary-link.d.ts +2 -0
  14. package/dist/services/primary-link.d.ts.map +1 -1
  15. package/dist/services/primary-link.js +81 -0
  16. package/dist/services/primary-link.js.map +1 -1
  17. package/dist/surfaces/index.d.ts +1 -0
  18. package/dist/surfaces/index.d.ts.map +1 -1
  19. package/dist/surfaces/index.js +1 -0
  20. package/dist/surfaces/index.js.map +1 -1
  21. package/dist/surfaces/remote-terminal.d.ts +44 -0
  22. package/dist/surfaces/remote-terminal.d.ts.map +1 -0
  23. package/dist/surfaces/remote-terminal.js +157 -0
  24. package/dist/surfaces/remote-terminal.js.map +1 -0
  25. package/dist/tools/agent-loop.d.ts +8 -2
  26. package/dist/tools/agent-loop.d.ts.map +1 -1
  27. package/dist/tools/agent-loop.js +19 -12
  28. package/dist/tools/agent-loop.js.map +1 -1
  29. package/dist/ws.d.ts +16 -0
  30. package/dist/ws.d.ts.map +1 -1
  31. package/dist/ws.js +89 -0
  32. package/dist/ws.js.map +1 -1
  33. package/package.json +1 -1
  34. package/web-dist/assets/{_basePickBy-4GXZ6Ixc.js → _basePickBy-D97xdOAU.js} +1 -1
  35. package/web-dist/assets/{_baseUniq-C8rcHDOr.js → _baseUniq-WW7JAj1l.js} +1 -1
  36. package/web-dist/assets/{arc-Ighm_O0d.js → arc-3mhc1c6Z.js} +1 -1
  37. package/web-dist/assets/{architectureDiagram-2XIMDMQ5-BoQ0ku_A.js → architectureDiagram-2XIMDMQ5-BNi--qrb.js} +1 -1
  38. package/web-dist/assets/{blockDiagram-WCTKOSBZ-D6l3Qis1.js → blockDiagram-WCTKOSBZ-GRQWMiTX.js} +1 -1
  39. package/web-dist/assets/{c4Diagram-IC4MRINW-Cnjd1IUe.js → c4Diagram-IC4MRINW-C42ZAGzO.js} +1 -1
  40. package/web-dist/assets/channel-C8_OwGkq.js +1 -0
  41. package/web-dist/assets/{chunk-4BX2VUAB-PS_u7IOQ.js → chunk-4BX2VUAB-DBagJPoH.js} +1 -1
  42. package/web-dist/assets/{chunk-55IACEB6-C6pB5keO.js → chunk-55IACEB6-Bb2d5l7K.js} +1 -1
  43. package/web-dist/assets/{chunk-FMBD7UC4-CN3x1VQ0.js → chunk-FMBD7UC4-CS4028UO.js} +1 -1
  44. package/web-dist/assets/{chunk-JSJVCQXG-KHgB7WcM.js → chunk-JSJVCQXG-pEPkRBug.js} +1 -1
  45. package/web-dist/assets/{chunk-KX2RTZJC-hrFhSS46.js → chunk-KX2RTZJC-DPZPReEB.js} +1 -1
  46. package/web-dist/assets/{chunk-NQ4KR5QH-DxnqGGgs.js → chunk-NQ4KR5QH-CxkV1_9p.js} +1 -1
  47. package/web-dist/assets/{chunk-QZHKN3VN-G6IBm1-B.js → chunk-QZHKN3VN-CwRRfkgR.js} +1 -1
  48. package/web-dist/assets/{chunk-WL4C6EOR-CCbc-Xur.js → chunk-WL4C6EOR-fFqRBlWe.js} +1 -1
  49. package/web-dist/assets/classDiagram-VBA2DB6C-C78vWo8s.js +1 -0
  50. package/web-dist/assets/classDiagram-v2-RAHNMMFH-C78vWo8s.js +1 -0
  51. package/web-dist/assets/clone-Cyv8PlV4.js +1 -0
  52. package/web-dist/assets/{cose-bilkent-S5V4N54A-CQB1kRU6.js → cose-bilkent-S5V4N54A-DN3v9QU8.js} +1 -1
  53. package/web-dist/assets/{dagre-KLK3FWXG-DrmxWwMD.js → dagre-KLK3FWXG-MbLMo82j.js} +1 -1
  54. package/web-dist/assets/{diagram-E7M64L7V-2voGx62g.js → diagram-E7M64L7V--9bssKST.js} +1 -1
  55. package/web-dist/assets/{diagram-IFDJBPK2-Bva2BWRv.js → diagram-IFDJBPK2-6cDHg5fI.js} +1 -1
  56. package/web-dist/assets/{diagram-P4PSJMXO-Brc9UEH9.js → diagram-P4PSJMXO-CnppAHOs.js} +1 -1
  57. package/web-dist/assets/{erDiagram-INFDFZHY-BEWpAybG.js → erDiagram-INFDFZHY-TLp6z50h.js} +1 -1
  58. package/web-dist/assets/{flowDiagram-PKNHOUZH-Hz_dbESe.js → flowDiagram-PKNHOUZH-DWGC6P2V.js} +1 -1
  59. package/web-dist/assets/{ganttDiagram-A5KZAMGK-Cm9_LZlr.js → ganttDiagram-A5KZAMGK-xRTztCVN.js} +1 -1
  60. package/web-dist/assets/{gitGraphDiagram-K3NZZRJ6-BzAOIXCz.js → gitGraphDiagram-K3NZZRJ6-C71gFMh3.js} +1 -1
  61. package/web-dist/assets/{graph-CJOpfjgA.js → graph-llADkI2L.js} +1 -1
  62. package/web-dist/assets/{index-DRBaHTGk.css → index-BZrUCLxt.css} +1 -1
  63. package/web-dist/assets/{index-C61DE3ai.js → index-CdADtvED.js} +1 -1
  64. package/web-dist/assets/{index-Zx1B8JUl.js → index-DF1xQK9v.js} +374 -374
  65. package/web-dist/assets/{index-Ct5S701c.js → index-qskLMmip.js} +1 -1
  66. package/web-dist/assets/{infoDiagram-LFFYTUFH-Bt7Hd4ca.js → infoDiagram-LFFYTUFH-4auIkcGV.js} +1 -1
  67. package/web-dist/assets/{ishikawaDiagram-PHBUUO56-kRYwr5j6.js → ishikawaDiagram-PHBUUO56-QNl96mQj.js} +1 -1
  68. package/web-dist/assets/{journeyDiagram-4ABVD52K-D62phOj4.js → journeyDiagram-4ABVD52K-CRkJoa9h.js} +1 -1
  69. package/web-dist/assets/{kanban-definition-K7BYSVSG-CDphYUb1.js → kanban-definition-K7BYSVSG-Bfoj75nt.js} +1 -1
  70. package/web-dist/assets/{layout-BVxY5QRl.js → layout-ZxnBXXOL.js} +1 -1
  71. package/web-dist/assets/{linear-CVsEUrat.js → linear-BkTIMqg7.js} +1 -1
  72. package/web-dist/assets/{mindmap-definition-YRQLILUH-BMKbEMM2.js → mindmap-definition-YRQLILUH-2y6NaKAS.js} +1 -1
  73. package/web-dist/assets/{pieDiagram-SKSYHLDU-CeASSviv.js → pieDiagram-SKSYHLDU-C7mUVJbX.js} +1 -1
  74. package/web-dist/assets/{quadrantDiagram-337W2JSQ-DyyBTffn.js → quadrantDiagram-337W2JSQ-CdZLSE27.js} +1 -1
  75. package/web-dist/assets/{requirementDiagram-Z7DCOOCP-CtNWH2d-.js → requirementDiagram-Z7DCOOCP-D1ICfZ5y.js} +1 -1
  76. package/web-dist/assets/{sankeyDiagram-WA2Y5GQK-BKwJNuto.js → sankeyDiagram-WA2Y5GQK-B5xjy5zy.js} +1 -1
  77. package/web-dist/assets/{sequenceDiagram-2WXFIKYE-CKaypO_N.js → sequenceDiagram-2WXFIKYE-rUHHa6Ef.js} +1 -1
  78. package/web-dist/assets/{stateDiagram-RAJIS63D-DPg4aXWw.js → stateDiagram-RAJIS63D-B0snaZqf.js} +1 -1
  79. package/web-dist/assets/stateDiagram-v2-FVOUBMTO-BYlXc09O.js +1 -0
  80. package/web-dist/assets/{timeline-definition-YZTLITO2-C-Uv37-w.js → timeline-definition-YZTLITO2-YTe8muSH.js} +1 -1
  81. package/web-dist/assets/{treemap-KZPCXAKY-CTpBRroI.js → treemap-KZPCXAKY-BaJ0_K6k.js} +1 -1
  82. package/web-dist/assets/{vennDiagram-LZ73GAT5-BpbnxQAX.js → vennDiagram-LZ73GAT5-Clg7FJSd.js} +1 -1
  83. package/web-dist/assets/{xychartDiagram-JWTSCODW-B4A0fzY8.js → xychartDiagram-JWTSCODW-CN5F0XWs.js} +1 -1
  84. package/web-dist/index.html +2 -2
  85. package/web-dist/assets/channel-Bb2O48C6.js +0 -1
  86. package/web-dist/assets/classDiagram-VBA2DB6C-ByArUuoT.js +0 -1
  87. package/web-dist/assets/classDiagram-v2-RAHNMMFH-ByArUuoT.js +0 -1
  88. package/web-dist/assets/clone-R6yqfY1r.js +0 -1
  89. package/web-dist/assets/stateDiagram-v2-FVOUBMTO-BJoc4OMi.js +0 -1
@@ -6,7 +6,7 @@ import { RemoteCliProvider } from "../providers/remote-cli-provider.js";
6
6
  import { resolveProjectRoot } from "../tools/core/get-fs.js";
7
7
  import { existsSync } from "node:fs";
8
8
  import { messages as messagesTable } from "../db/schema.js";
9
- import { eq } from "drizzle-orm";
9
+ import { eq, sql } from "drizzle-orm";
10
10
  import { uuidv7 } from "../db/uuidv7.js";
11
11
  import { requireAuth } from "../security/http-auth.js";
12
12
  import { signAuthToken } from "../security/http-auth.js";
@@ -541,6 +541,34 @@ function accumulateToolStart(sessionId, callId, tool, args, parentCallId) {
541
541
  acc.segments.push({ type: "toolGroup", callIds: [callId] });
542
542
  }
543
543
  }
544
+ function accumulateToolApproval(sessionId, requestId, tool, args) {
545
+ const callId = `approval-${requestId}`;
546
+ const acc = getOrCreateAccumulator(sessionId);
547
+ const existing = acc.toolCalls.find(t => t.callId === callId);
548
+ if (!existing) {
549
+ acc.toolCalls.push({
550
+ callId,
551
+ tool,
552
+ args,
553
+ ok: true,
554
+ message: "Awaiting approval",
555
+ status: "pending",
556
+ approvalRequestId: requestId,
557
+ approvalState: "pending",
558
+ startedAt: Date.now(),
559
+ });
560
+ }
561
+ const last = acc.segments[acc.segments.length - 1];
562
+ if (last?.type === "toolGroup") {
563
+ if (!last.callIds.includes(callId)) {
564
+ acc.segments[acc.segments.length - 1] = { type: "toolGroup", callIds: [...last.callIds, callId] };
565
+ }
566
+ }
567
+ else {
568
+ acc.segments.push({ type: "toolGroup", callIds: [callId] });
569
+ }
570
+ return callId;
571
+ }
544
572
  /** Record streaming output for a tool call */
545
573
  function accumulateToolOutput(sessionId, callId, content) {
546
574
  const acc = sessionStreamingState.get(sessionId);
@@ -623,9 +651,11 @@ function mapPersistedToolCallsForUI(toolCalls) {
623
651
  parentCallId: tc.parentCallId,
624
652
  tool: tc.tool,
625
653
  args: (typeof tc.args === "object" && tc.args !== null ? tc.args : {}),
626
- status: tc.ok ? "success" : "error",
654
+ status: tc.status ?? (tc.ok ? "success" : "error"),
627
655
  ok: tc.ok,
628
656
  message: tc.message,
657
+ approvalRequestId: tc.approvalRequestId,
658
+ approvalState: tc.approvalState,
629
659
  output: tc.output,
630
660
  data: tc.data,
631
661
  startedAt: tc.startedAt,
@@ -716,6 +746,65 @@ function windowMessages(messages, limit, before) {
716
746
  hasMore: start > 0,
717
747
  };
718
748
  }
749
+ function rowToUIMsg(sessionId, row, visibleIndex) {
750
+ const msg = {
751
+ id: `${sessionId}-${visibleIndex}`,
752
+ role: row.role,
753
+ content: typeof row.content === "string" ? row.content : String(row.content ?? ""),
754
+ };
755
+ if (row.toolCalls) {
756
+ try {
757
+ msg.toolCalls = JSON.parse(row.toolCalls);
758
+ }
759
+ catch { /* ignore */ }
760
+ }
761
+ if (row.segments) {
762
+ try {
763
+ msg.segments = JSON.parse(row.segments);
764
+ }
765
+ catch { /* ignore */ }
766
+ }
767
+ if (row.thinking) {
768
+ msg.thinking = row.thinking;
769
+ }
770
+ if (row.contextFlow) {
771
+ try {
772
+ const parsed = JSON.parse(row.contextFlow);
773
+ if (parsed && typeof parsed === "object" && Array.isArray(parsed.rounds)) {
774
+ msg.contextFlow = parsed;
775
+ }
776
+ }
777
+ catch { /* ignore */ }
778
+ }
779
+ return msg;
780
+ }
781
+ function persistedMessageWindow(db, sessionId, limit, before) {
782
+ const totalRow = db
783
+ .select({ count: sql `count(*)` })
784
+ .from(messagesTable)
785
+ .where(eq(messagesTable.sessionId, sessionId))
786
+ .get();
787
+ const total = Number(totalRow?.count ?? 0);
788
+ const end = typeof before === "number" && Number.isFinite(before) && before >= 0 && before < total
789
+ ? Math.floor(before)
790
+ : total;
791
+ const start = Math.max(end - limit, 0);
792
+ const rows = start < end
793
+ ? db
794
+ .select()
795
+ .from(messagesTable)
796
+ .where(eq(messagesTable.sessionId, sessionId))
797
+ .orderBy(messagesTable.createdAt, messagesTable.id)
798
+ .limit(end - start)
799
+ .offset(start)
800
+ .all()
801
+ : [];
802
+ return {
803
+ messages: rows.map((row, index) => rowToUIMsg(sessionId, row, start + index)),
804
+ total,
805
+ hasMore: start > 0,
806
+ };
807
+ }
719
808
  function sleep(ms) {
720
809
  return new Promise((resolve) => setTimeout(resolve, ms));
721
810
  }
@@ -1015,7 +1104,7 @@ export function registerChatRoutes(app, config, depsOrDb, sessionServiceArg) {
1015
1104
  .select()
1016
1105
  .from(messagesTable)
1017
1106
  .where(eq(messagesTable.sessionId, sessionId))
1018
- .orderBy(messagesTable.createdAt)
1107
+ .orderBy(messagesTable.createdAt, messagesTable.id)
1019
1108
  .all();
1020
1109
  if (rows.length > 0) {
1021
1110
  sessionHistory.set(sessionId, [
@@ -1095,7 +1184,7 @@ export function registerChatRoutes(app, config, depsOrDb, sessionServiceArg) {
1095
1184
  .select()
1096
1185
  .from(messagesTable)
1097
1186
  .where(eq(messagesTable.sessionId, sessionId))
1098
- .orderBy(messagesTable.createdAt)
1187
+ .orderBy(messagesTable.createdAt, messagesTable.id)
1099
1188
  .all();
1100
1189
  const lastAssistant = [...rows].reverse().find((row) => row.role === "assistant");
1101
1190
  if (!lastAssistant)
@@ -1345,6 +1434,11 @@ export function registerChatRoutes(app, config, depsOrDb, sessionServiceArg) {
1345
1434
  let resultSegmentsJson;
1346
1435
  let contextFlowJson;
1347
1436
  let hitMaxRounds = false;
1437
+ // Whether the agentic loop already persisted the assistant message via its
1438
+ // onPersist callback. The post-loop fallback persists must be skipped when
1439
+ // this is true to avoid writing a duplicate DB row (which scrambled message
1440
+ // order on reload because the rows share createdAt down to the millisecond).
1441
+ let loopPersisted = false;
1348
1442
  activeStreams.add(sessionId);
1349
1443
  // Reset streaming accumulator for this turn so reload snapshots start fresh
1350
1444
  sessionStreamingState.delete(sessionId);
@@ -1690,9 +1784,13 @@ export function registerChatRoutes(app, config, depsOrDb, sessionServiceArg) {
1690
1784
  }
1691
1785
  break;
1692
1786
  }
1693
- case "tool.approval-required":
1694
- safeWrite(`data: ${JSON.stringify({ type: "approval_required", tool: event.tool, args: event.args, requestId: event.requestId })}\n\n`);
1787
+ case "tool.approval-required": {
1788
+ const callId = accumulateToolApproval(sessionId, event.requestId, event.tool, event.args);
1789
+ const approvalEvent = { type: "approval_required", call_id: callId, request_id: event.requestId, tool: event.tool, args: event.args };
1790
+ safeWrite(`data: ${JSON.stringify(approvalEvent)}\n\n`);
1791
+ emitToSubscribers(sessionId, approvalEvent);
1695
1792
  break;
1793
+ }
1696
1794
  case "message":
1697
1795
  if (event.role === "assistant" && event.content) {
1698
1796
  flushToolGroup();
@@ -2026,6 +2124,7 @@ export function registerChatRoutes(app, config, depsOrDb, sessionServiceArg) {
2026
2124
  }
2027
2125
  resultSegmentsJson = resultSegments.length > 0 ? JSON.stringify(resultSegments) : undefined;
2028
2126
  hitMaxRounds = result.hitMaxRounds;
2127
+ loopPersisted = result.persisted === true;
2029
2128
  // Persist fingerprints so a Continue turn reuses them
2030
2129
  sessionFingerprints.set(sessionId, result.fingerprints);
2031
2130
  // Re-serialize contextFlow now that round metrics have been attached
@@ -2068,12 +2167,13 @@ export function registerChatRoutes(app, config, depsOrDb, sessionServiceArg) {
2068
2167
  const wasCancelled = err instanceof Error && err.name === "AbortError";
2069
2168
  if (!wasCancelled)
2070
2169
  app.log.error(err, `${providerLabel} streaming error`);
2071
- // Save partial content for real (non-cancel) errors
2072
- if (!wasCancelled && (fullContent || partialToolCalls.length > 0)) {
2170
+ // Save partial content for real (non-cancel) errors — but only if the
2171
+ // agentic loop didn't already persist this turn's assistant message.
2172
+ if (!wasCancelled && !loopPersisted && (fullContent || partialToolCalls.length > 0)) {
2073
2173
  const tcJson = partialToolCalls.length > 0 ? JSON.stringify(partialToolCalls) : undefined;
2074
2174
  persistMessage(sessionId, "assistant", fullContent || "", tcJson, resultSegmentsJson, contextFlowJson);
2075
2175
  }
2076
- else if (!wasCancelled) {
2176
+ else if (!wasCancelled && !loopPersisted) {
2077
2177
  // No partial content — persist the error message itself so it's visible on reload.
2078
2178
  const errMsg2 = err instanceof Error ? err.message : `Failed to reach ${providerLabel}`;
2079
2179
  persistMessage(sessionId, "assistant", errMsg2, undefined, JSON.stringify([{ type: "error", content: errMsg2 }]), contextFlowJson);
@@ -2091,7 +2191,9 @@ export function registerChatRoutes(app, config, depsOrDb, sessionServiceArg) {
2091
2191
  }
2092
2192
  // Persist partial results BEFORE clearing stream state so that a reload
2093
2193
  // between these two steps loads the cancelled tool calls from the DB.
2094
- if (streamAbort.signal.aborted && (fullContent || partialToolCalls.length > 0)) {
2194
+ // Skip when the agentic loop already persisted via onPersist (avoids a
2195
+ // duplicate row that scrambled message order on reload).
2196
+ if (streamAbort.signal.aborted && !loopPersisted && (fullContent || partialToolCalls.length > 0)) {
2095
2197
  const tcJson = partialToolCalls.length > 0 ? JSON.stringify(partialToolCalls) : undefined;
2096
2198
  persistMessage(sessionId, "assistant", fullContent || "", tcJson, resultSegmentsJson, contextFlowJson);
2097
2199
  }
@@ -2152,6 +2254,30 @@ export function registerChatRoutes(app, config, depsOrDb, sessionServiceArg) {
2152
2254
  }
2153
2255
  void drainQueuedChatMessages(sessionId);
2154
2256
  });
2257
+ app.post("/api/sessions/:sessionId/approve", async (request, reply) => {
2258
+ const authUser = await requireAuth(request, reply, config.jwtSecret);
2259
+ if (!authUser)
2260
+ return;
2261
+ const { sessionId } = request.params;
2262
+ const body = request.body;
2263
+ const requestId = typeof body["requestId"] === "string" ? body["requestId"] : "";
2264
+ const approved = body["approved"] !== false;
2265
+ if (!requestId) {
2266
+ return reply.status(400).send({ error: "VALIDATION_ERROR", details: "requestId is required" });
2267
+ }
2268
+ if (sessionService) {
2269
+ const session = sessionService.getById(sessionId, authUser.id);
2270
+ if (!session) {
2271
+ return reply.status(404).send({ error: "NOT_FOUND", details: "Session not found" });
2272
+ }
2273
+ }
2274
+ const activeCliSession = activeCliSessions.get(sessionId);
2275
+ if (!activeCliSession) {
2276
+ return reply.status(409).send({ error: "CONFLICT", details: "No active provider session for approval" });
2277
+ }
2278
+ await activeCliSession.provider.respondToApproval(activeCliSession.providerSessionId, requestId, approved);
2279
+ return reply.status(200).send({ ok: true });
2280
+ });
2155
2281
  // Cancel an active stream for a session
2156
2282
  app.post("/api/sessions/:sessionId/cancel", async (request, reply) => {
2157
2283
  const authUser = await requireAuth(request, reply, config.jwtSecret);
@@ -2248,7 +2374,7 @@ export function registerChatRoutes(app, config, depsOrDb, sessionServiceArg) {
2248
2374
  .select()
2249
2375
  .from(messagesTable)
2250
2376
  .where(eq(messagesTable.sessionId, sessionId))
2251
- .orderBy(messagesTable.createdAt)
2377
+ .orderBy(messagesTable.createdAt, messagesTable.id)
2252
2378
  .all();
2253
2379
  const rowsToDelete = rows.slice(Math.max(0, target.historyIndex - 1));
2254
2380
  for (const row of rowsToDelete) {
@@ -2290,13 +2416,18 @@ export function registerChatRoutes(app, config, depsOrDb, sessionServiceArg) {
2290
2416
  : typeof query?.before === "string"
2291
2417
  ? Number.parseInt(query.before, 10)
2292
2418
  : undefined;
2293
- hydrateSession(sessionId);
2294
- const history = sessionHistory.get(sessionId) ?? [];
2295
- const visible = buildVisibleHistoryMessages(sessionId, history);
2296
- const windowed = windowMessages(visible, limit, Number.isFinite(before) ? before : undefined);
2419
+ const streaming = activeStreams.has(sessionId);
2420
+ const windowed = db && !streaming
2421
+ ? persistedMessageWindow(db, sessionId, limit, Number.isFinite(before) ? before : undefined)
2422
+ : (() => {
2423
+ hydrateSession(sessionId);
2424
+ const history = sessionHistory.get(sessionId) ?? [];
2425
+ const visible = buildVisibleHistoryMessages(sessionId, history, { includePendingAssistantToolCalls: streaming });
2426
+ return windowMessages(visible, limit, Number.isFinite(before) ? before : undefined);
2427
+ })();
2297
2428
  return {
2298
2429
  sessionId,
2299
- streaming: activeStreams.has(sessionId),
2430
+ streaming,
2300
2431
  total: windowed.total,
2301
2432
  hasMore: windowed.hasMore,
2302
2433
  limit,
@@ -2326,56 +2457,22 @@ export function registerChatRoutes(app, config, depsOrDb, sessionServiceArg) {
2326
2457
  "Access-Control-Allow-Origin": reqOrigin,
2327
2458
  "Access-Control-Allow-Credentials": "true",
2328
2459
  });
2329
- hydrateSession(sessionId);
2330
- const history = sessionHistory.get(sessionId) ?? [];
2331
2460
  const isStreaming = activeStreams.has(sessionId);
2461
+ // Only the DB-windowed idle path can skip hydrating. Sessions without a DB
2462
+ // (in-memory history) still need their completed messages loaded from the
2463
+ // sessionHistory map, and streaming sessions need partial content too.
2464
+ let history = [];
2465
+ if (!db || isStreaming) {
2466
+ hydrateSession(sessionId);
2467
+ history = sessionHistory.get(sessionId) ?? [];
2468
+ }
2332
2469
  // Build snapshot. While streaming, prefer in-memory history so partial assistant
2333
2470
  // content is visible immediately (DB persistence may lag until stream completion).
2334
2471
  let snapshotMessages;
2335
2472
  let total = 0;
2336
2473
  let hasMore = false;
2337
2474
  if (db && !isStreaming) {
2338
- const rows = db
2339
- .select()
2340
- .from(messagesTable)
2341
- .where(eq(messagesTable.sessionId, sessionId))
2342
- .orderBy(messagesTable.createdAt)
2343
- .all();
2344
- const allMessages = rows
2345
- .filter((r) => r.role === "user" || r.role === "assistant")
2346
- .map((r, i) => {
2347
- const msg = {
2348
- id: `${sessionId}-${i}`,
2349
- role: r.role,
2350
- content: typeof r.content === "string" ? r.content : String(r.content ?? ""),
2351
- };
2352
- if (r.toolCalls) {
2353
- try {
2354
- msg.toolCalls = JSON.parse(r.toolCalls);
2355
- }
2356
- catch { /* ignore */ }
2357
- }
2358
- if (r.segments) {
2359
- try {
2360
- msg.segments = JSON.parse(r.segments);
2361
- }
2362
- catch { /* ignore */ }
2363
- }
2364
- if (r.thinking) {
2365
- msg.thinking = r.thinking;
2366
- }
2367
- if (r.contextFlow) {
2368
- try {
2369
- const parsed = JSON.parse(r.contextFlow);
2370
- if (parsed && typeof parsed === "object" && Array.isArray(parsed.rounds)) {
2371
- msg.contextFlow = parsed;
2372
- }
2373
- }
2374
- catch { /* ignore */ }
2375
- }
2376
- return msg;
2377
- });
2378
- const windowed = windowMessages(allMessages, limit);
2475
+ const windowed = persistedMessageWindow(db, sessionId, limit);
2379
2476
  snapshotMessages = windowed.messages;
2380
2477
  total = windowed.total;
2381
2478
  hasMore = windowed.hasMore;
@@ -2482,7 +2579,7 @@ export function registerChatRoutes(app, config, depsOrDb, sessionServiceArg) {
2482
2579
  .select()
2483
2580
  .from(messagesTable)
2484
2581
  .where(eq(messagesTable.sessionId, sessionId))
2485
- .orderBy(messagesTable.createdAt)
2582
+ .orderBy(messagesTable.createdAt, messagesTable.id)
2486
2583
  .all();
2487
2584
  const lastAssistant = [...rows].reverse().find((r) => r.role === "assistant");
2488
2585
  if (lastAssistant) {