@jait/gateway 0.1.519 → 0.1.521
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/index.d.ts.map +1 -1
- package/dist/index.js +19 -1
- package/dist/index.js.map +1 -1
- package/dist/providers/remote-cli-provider.d.ts.map +1 -1
- package/dist/providers/remote-cli-provider.js +44 -5
- package/dist/providers/remote-cli-provider.js.map +1 -1
- package/dist/routes/chat.d.ts.map +1 -1
- package/dist/routes/chat.js +181 -56
- package/dist/routes/chat.js.map +1 -1
- package/dist/routes/terminals.d.ts.map +1 -1
- package/dist/routes/terminals.js +23 -2
- package/dist/routes/terminals.js.map +1 -1
- package/dist/services/primary-link.d.ts +2 -0
- package/dist/services/primary-link.d.ts.map +1 -1
- package/dist/services/primary-link.js +81 -0
- package/dist/services/primary-link.js.map +1 -1
- package/dist/surfaces/index.d.ts +1 -0
- package/dist/surfaces/index.d.ts.map +1 -1
- package/dist/surfaces/index.js +1 -0
- package/dist/surfaces/index.js.map +1 -1
- package/dist/surfaces/remote-terminal.d.ts +44 -0
- package/dist/surfaces/remote-terminal.d.ts.map +1 -0
- package/dist/surfaces/remote-terminal.js +157 -0
- package/dist/surfaces/remote-terminal.js.map +1 -0
- package/dist/tools/agent-loop.d.ts +2 -2
- package/dist/tools/agent-loop.d.ts.map +1 -1
- package/dist/tools/agent-loop.js +6 -4
- package/dist/tools/agent-loop.js.map +1 -1
- package/dist/ws.d.ts +16 -0
- package/dist/ws.d.ts.map +1 -1
- package/dist/ws.js +89 -0
- package/dist/ws.js.map +1 -1
- package/package.json +1 -1
- package/web-dist/assets/{_basePickBy-4GXZ6Ixc.js → _basePickBy-D97xdOAU.js} +1 -1
- package/web-dist/assets/{_baseUniq-C8rcHDOr.js → _baseUniq-WW7JAj1l.js} +1 -1
- package/web-dist/assets/{arc-Ighm_O0d.js → arc-3mhc1c6Z.js} +1 -1
- package/web-dist/assets/{architectureDiagram-2XIMDMQ5-BoQ0ku_A.js → architectureDiagram-2XIMDMQ5-BNi--qrb.js} +1 -1
- package/web-dist/assets/{blockDiagram-WCTKOSBZ-D6l3Qis1.js → blockDiagram-WCTKOSBZ-GRQWMiTX.js} +1 -1
- package/web-dist/assets/{c4Diagram-IC4MRINW-Cnjd1IUe.js → c4Diagram-IC4MRINW-C42ZAGzO.js} +1 -1
- package/web-dist/assets/channel-C8_OwGkq.js +1 -0
- package/web-dist/assets/{chunk-4BX2VUAB-PS_u7IOQ.js → chunk-4BX2VUAB-DBagJPoH.js} +1 -1
- package/web-dist/assets/{chunk-55IACEB6-C6pB5keO.js → chunk-55IACEB6-Bb2d5l7K.js} +1 -1
- package/web-dist/assets/{chunk-FMBD7UC4-CN3x1VQ0.js → chunk-FMBD7UC4-CS4028UO.js} +1 -1
- package/web-dist/assets/{chunk-JSJVCQXG-KHgB7WcM.js → chunk-JSJVCQXG-pEPkRBug.js} +1 -1
- package/web-dist/assets/{chunk-KX2RTZJC-hrFhSS46.js → chunk-KX2RTZJC-DPZPReEB.js} +1 -1
- package/web-dist/assets/{chunk-NQ4KR5QH-DxnqGGgs.js → chunk-NQ4KR5QH-CxkV1_9p.js} +1 -1
- package/web-dist/assets/{chunk-QZHKN3VN-G6IBm1-B.js → chunk-QZHKN3VN-CwRRfkgR.js} +1 -1
- package/web-dist/assets/{chunk-WL4C6EOR-CCbc-Xur.js → chunk-WL4C6EOR-fFqRBlWe.js} +1 -1
- package/web-dist/assets/classDiagram-VBA2DB6C-C78vWo8s.js +1 -0
- package/web-dist/assets/classDiagram-v2-RAHNMMFH-C78vWo8s.js +1 -0
- package/web-dist/assets/clone-Cyv8PlV4.js +1 -0
- package/web-dist/assets/{cose-bilkent-S5V4N54A-CQB1kRU6.js → cose-bilkent-S5V4N54A-DN3v9QU8.js} +1 -1
- package/web-dist/assets/{dagre-KLK3FWXG-DrmxWwMD.js → dagre-KLK3FWXG-MbLMo82j.js} +1 -1
- package/web-dist/assets/{diagram-E7M64L7V-2voGx62g.js → diagram-E7M64L7V--9bssKST.js} +1 -1
- package/web-dist/assets/{diagram-IFDJBPK2-Bva2BWRv.js → diagram-IFDJBPK2-6cDHg5fI.js} +1 -1
- package/web-dist/assets/{diagram-P4PSJMXO-Brc9UEH9.js → diagram-P4PSJMXO-CnppAHOs.js} +1 -1
- package/web-dist/assets/{erDiagram-INFDFZHY-BEWpAybG.js → erDiagram-INFDFZHY-TLp6z50h.js} +1 -1
- package/web-dist/assets/{flowDiagram-PKNHOUZH-Hz_dbESe.js → flowDiagram-PKNHOUZH-DWGC6P2V.js} +1 -1
- package/web-dist/assets/{ganttDiagram-A5KZAMGK-Cm9_LZlr.js → ganttDiagram-A5KZAMGK-xRTztCVN.js} +1 -1
- package/web-dist/assets/{gitGraphDiagram-K3NZZRJ6-BzAOIXCz.js → gitGraphDiagram-K3NZZRJ6-C71gFMh3.js} +1 -1
- package/web-dist/assets/{graph-CJOpfjgA.js → graph-llADkI2L.js} +1 -1
- package/web-dist/assets/{index-DRBaHTGk.css → index-BZrUCLxt.css} +1 -1
- package/web-dist/assets/{index-C61DE3ai.js → index-CdADtvED.js} +1 -1
- package/web-dist/assets/{index-Zx1B8JUl.js → index-DF1xQK9v.js} +374 -374
- package/web-dist/assets/{index-Ct5S701c.js → index-qskLMmip.js} +1 -1
- package/web-dist/assets/{infoDiagram-LFFYTUFH-Bt7Hd4ca.js → infoDiagram-LFFYTUFH-4auIkcGV.js} +1 -1
- package/web-dist/assets/{ishikawaDiagram-PHBUUO56-kRYwr5j6.js → ishikawaDiagram-PHBUUO56-QNl96mQj.js} +1 -1
- package/web-dist/assets/{journeyDiagram-4ABVD52K-D62phOj4.js → journeyDiagram-4ABVD52K-CRkJoa9h.js} +1 -1
- package/web-dist/assets/{kanban-definition-K7BYSVSG-CDphYUb1.js → kanban-definition-K7BYSVSG-Bfoj75nt.js} +1 -1
- package/web-dist/assets/{layout-BVxY5QRl.js → layout-ZxnBXXOL.js} +1 -1
- package/web-dist/assets/{linear-CVsEUrat.js → linear-BkTIMqg7.js} +1 -1
- package/web-dist/assets/{mindmap-definition-YRQLILUH-BMKbEMM2.js → mindmap-definition-YRQLILUH-2y6NaKAS.js} +1 -1
- package/web-dist/assets/{pieDiagram-SKSYHLDU-CeASSviv.js → pieDiagram-SKSYHLDU-C7mUVJbX.js} +1 -1
- package/web-dist/assets/{quadrantDiagram-337W2JSQ-DyyBTffn.js → quadrantDiagram-337W2JSQ-CdZLSE27.js} +1 -1
- package/web-dist/assets/{requirementDiagram-Z7DCOOCP-CtNWH2d-.js → requirementDiagram-Z7DCOOCP-D1ICfZ5y.js} +1 -1
- package/web-dist/assets/{sankeyDiagram-WA2Y5GQK-BKwJNuto.js → sankeyDiagram-WA2Y5GQK-B5xjy5zy.js} +1 -1
- package/web-dist/assets/{sequenceDiagram-2WXFIKYE-CKaypO_N.js → sequenceDiagram-2WXFIKYE-rUHHa6Ef.js} +1 -1
- package/web-dist/assets/{stateDiagram-RAJIS63D-DPg4aXWw.js → stateDiagram-RAJIS63D-B0snaZqf.js} +1 -1
- package/web-dist/assets/stateDiagram-v2-FVOUBMTO-BYlXc09O.js +1 -0
- package/web-dist/assets/{timeline-definition-YZTLITO2-C-Uv37-w.js → timeline-definition-YZTLITO2-YTe8muSH.js} +1 -1
- package/web-dist/assets/{treemap-KZPCXAKY-CTpBRroI.js → treemap-KZPCXAKY-BaJ0_K6k.js} +1 -1
- package/web-dist/assets/{vennDiagram-LZ73GAT5-BpbnxQAX.js → vennDiagram-LZ73GAT5-Clg7FJSd.js} +1 -1
- package/web-dist/assets/{xychartDiagram-JWTSCODW-B4A0fzY8.js → xychartDiagram-JWTSCODW-CN5F0XWs.js} +1 -1
- package/web-dist/index.html +2 -2
- package/web-dist/assets/channel-Bb2O48C6.js +0 -1
- package/web-dist/assets/classDiagram-VBA2DB6C-ByArUuoT.js +0 -1
- package/web-dist/assets/classDiagram-v2-RAHNMMFH-ByArUuoT.js +0 -1
- package/web-dist/assets/clone-R6yqfY1r.js +0 -1
- package/web-dist/assets/stateDiagram-v2-FVOUBMTO-BJoc4OMi.js +0 -1
package/dist/routes/chat.js
CHANGED
|
@@ -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)
|
|
@@ -1253,6 +1342,9 @@ export function registerChatRoutes(app, config, depsOrDb, sessionServiceArg) {
|
|
|
1253
1342
|
"Content-Type": "text/event-stream",
|
|
1254
1343
|
"Cache-Control": "no-cache",
|
|
1255
1344
|
Connection: "keep-alive",
|
|
1345
|
+
// Prevent reverse proxies (nginx etc.) from buffering the SSE stream,
|
|
1346
|
+
// which would otherwise swallow chunks and let the connection time out.
|
|
1347
|
+
"X-Accel-Buffering": "no",
|
|
1256
1348
|
"Access-Control-Allow-Origin": reqOrigin,
|
|
1257
1349
|
"Access-Control-Allow-Credentials": "true",
|
|
1258
1350
|
});
|
|
@@ -1374,6 +1466,21 @@ export function registerChatRoutes(app, config, depsOrDb, sessionServiceArg) {
|
|
|
1374
1466
|
}
|
|
1375
1467
|
}
|
|
1376
1468
|
};
|
|
1469
|
+
// ── SSE keepalive heartbeat ──
|
|
1470
|
+
// While the agentic loop runs a long tool or waits for a slow LLM response,
|
|
1471
|
+
// no SSE data events are emitted and the connection can go idle for tens of
|
|
1472
|
+
// seconds. Browsers and reverse proxies then close the socket, surfacing as
|
|
1473
|
+
// "fetch failed" on the frontend and dropping the in-progress assistant
|
|
1474
|
+
// message. Emit an SSE comment (": keepalive\n\n") every 15s — comments are
|
|
1475
|
+
// ignored by the EventSource/reader parser but keep the TCP connection alive.
|
|
1476
|
+
const keepalive = setInterval(() => {
|
|
1477
|
+
if (clientDisconnected) {
|
|
1478
|
+
clearInterval(keepalive);
|
|
1479
|
+
return;
|
|
1480
|
+
}
|
|
1481
|
+
safeWrite(`: keepalive\n\n`);
|
|
1482
|
+
}, 15_000);
|
|
1483
|
+
reply.raw.on("close", () => { clearInterval(keepalive); });
|
|
1377
1484
|
const matchedSkillIds = new Set(matchSkills(content, promptCtx.skills ?? []));
|
|
1378
1485
|
const matchedSkills = (promptCtx.skills ?? []).filter((skill) => matchedSkillIds.has(skill.id));
|
|
1379
1486
|
const turnSkillToolCall = buildSyntheticSkillToolCall(matchedSkills);
|
|
@@ -1447,6 +1554,7 @@ export function registerChatRoutes(app, config, depsOrDb, sessionServiceArg) {
|
|
|
1447
1554
|
cliProvider = providerRegistry.get(requestProvider) ?? null;
|
|
1448
1555
|
}
|
|
1449
1556
|
if (!cliProvider) {
|
|
1557
|
+
clearInterval(keepalive);
|
|
1450
1558
|
safeWrite(`data: ${JSON.stringify({ type: "error", message: `Unknown provider: ${requestProvider}` })}\n\n`);
|
|
1451
1559
|
reply.raw.end();
|
|
1452
1560
|
return;
|
|
@@ -1695,9 +1803,13 @@ export function registerChatRoutes(app, config, depsOrDb, sessionServiceArg) {
|
|
|
1695
1803
|
}
|
|
1696
1804
|
break;
|
|
1697
1805
|
}
|
|
1698
|
-
case "tool.approval-required":
|
|
1699
|
-
|
|
1806
|
+
case "tool.approval-required": {
|
|
1807
|
+
const callId = accumulateToolApproval(sessionId, event.requestId, event.tool, event.args);
|
|
1808
|
+
const approvalEvent = { type: "approval_required", call_id: callId, request_id: event.requestId, tool: event.tool, args: event.args };
|
|
1809
|
+
safeWrite(`data: ${JSON.stringify(approvalEvent)}\n\n`);
|
|
1810
|
+
emitToSubscribers(sessionId, approvalEvent);
|
|
1700
1811
|
break;
|
|
1812
|
+
}
|
|
1701
1813
|
case "message":
|
|
1702
1814
|
if (event.role === "assistant" && event.content) {
|
|
1703
1815
|
flushToolGroup();
|
|
@@ -2104,6 +2216,8 @@ export function registerChatRoutes(app, config, depsOrDb, sessionServiceArg) {
|
|
|
2104
2216
|
const tcJson = partialToolCalls.length > 0 ? JSON.stringify(partialToolCalls) : undefined;
|
|
2105
2217
|
persistMessage(sessionId, "assistant", fullContent || "", tcJson, resultSegmentsJson, contextFlowJson);
|
|
2106
2218
|
}
|
|
2219
|
+
// Stop the SSE keepalive heartbeat now that the turn is finishing.
|
|
2220
|
+
clearInterval(keepalive);
|
|
2107
2221
|
activeStreams.delete(sessionId);
|
|
2108
2222
|
sessionAbortControllers.delete(sessionId);
|
|
2109
2223
|
sessionSteeringControllers.delete(sessionId);
|
|
@@ -2161,6 +2275,30 @@ export function registerChatRoutes(app, config, depsOrDb, sessionServiceArg) {
|
|
|
2161
2275
|
}
|
|
2162
2276
|
void drainQueuedChatMessages(sessionId);
|
|
2163
2277
|
});
|
|
2278
|
+
app.post("/api/sessions/:sessionId/approve", async (request, reply) => {
|
|
2279
|
+
const authUser = await requireAuth(request, reply, config.jwtSecret);
|
|
2280
|
+
if (!authUser)
|
|
2281
|
+
return;
|
|
2282
|
+
const { sessionId } = request.params;
|
|
2283
|
+
const body = request.body;
|
|
2284
|
+
const requestId = typeof body["requestId"] === "string" ? body["requestId"] : "";
|
|
2285
|
+
const approved = body["approved"] !== false;
|
|
2286
|
+
if (!requestId) {
|
|
2287
|
+
return reply.status(400).send({ error: "VALIDATION_ERROR", details: "requestId is required" });
|
|
2288
|
+
}
|
|
2289
|
+
if (sessionService) {
|
|
2290
|
+
const session = sessionService.getById(sessionId, authUser.id);
|
|
2291
|
+
if (!session) {
|
|
2292
|
+
return reply.status(404).send({ error: "NOT_FOUND", details: "Session not found" });
|
|
2293
|
+
}
|
|
2294
|
+
}
|
|
2295
|
+
const activeCliSession = activeCliSessions.get(sessionId);
|
|
2296
|
+
if (!activeCliSession) {
|
|
2297
|
+
return reply.status(409).send({ error: "CONFLICT", details: "No active provider session for approval" });
|
|
2298
|
+
}
|
|
2299
|
+
await activeCliSession.provider.respondToApproval(activeCliSession.providerSessionId, requestId, approved);
|
|
2300
|
+
return reply.status(200).send({ ok: true });
|
|
2301
|
+
});
|
|
2164
2302
|
// Cancel an active stream for a session
|
|
2165
2303
|
app.post("/api/sessions/:sessionId/cancel", async (request, reply) => {
|
|
2166
2304
|
const authUser = await requireAuth(request, reply, config.jwtSecret);
|
|
@@ -2257,7 +2395,7 @@ export function registerChatRoutes(app, config, depsOrDb, sessionServiceArg) {
|
|
|
2257
2395
|
.select()
|
|
2258
2396
|
.from(messagesTable)
|
|
2259
2397
|
.where(eq(messagesTable.sessionId, sessionId))
|
|
2260
|
-
.orderBy(messagesTable.createdAt)
|
|
2398
|
+
.orderBy(messagesTable.createdAt, messagesTable.id)
|
|
2261
2399
|
.all();
|
|
2262
2400
|
const rowsToDelete = rows.slice(Math.max(0, target.historyIndex - 1));
|
|
2263
2401
|
for (const row of rowsToDelete) {
|
|
@@ -2299,13 +2437,18 @@ export function registerChatRoutes(app, config, depsOrDb, sessionServiceArg) {
|
|
|
2299
2437
|
: typeof query?.before === "string"
|
|
2300
2438
|
? Number.parseInt(query.before, 10)
|
|
2301
2439
|
: undefined;
|
|
2302
|
-
|
|
2303
|
-
const
|
|
2304
|
-
|
|
2305
|
-
|
|
2440
|
+
const streaming = activeStreams.has(sessionId);
|
|
2441
|
+
const windowed = db && !streaming
|
|
2442
|
+
? persistedMessageWindow(db, sessionId, limit, Number.isFinite(before) ? before : undefined)
|
|
2443
|
+
: (() => {
|
|
2444
|
+
hydrateSession(sessionId);
|
|
2445
|
+
const history = sessionHistory.get(sessionId) ?? [];
|
|
2446
|
+
const visible = buildVisibleHistoryMessages(sessionId, history, { includePendingAssistantToolCalls: streaming });
|
|
2447
|
+
return windowMessages(visible, limit, Number.isFinite(before) ? before : undefined);
|
|
2448
|
+
})();
|
|
2306
2449
|
return {
|
|
2307
2450
|
sessionId,
|
|
2308
|
-
streaming
|
|
2451
|
+
streaming,
|
|
2309
2452
|
total: windowed.total,
|
|
2310
2453
|
hasMore: windowed.hasMore,
|
|
2311
2454
|
limit,
|
|
@@ -2332,59 +2475,26 @@ export function registerChatRoutes(app, config, depsOrDb, sessionServiceArg) {
|
|
|
2332
2475
|
"Content-Type": "text/event-stream",
|
|
2333
2476
|
"Cache-Control": "no-cache",
|
|
2334
2477
|
Connection: "keep-alive",
|
|
2478
|
+
"X-Accel-Buffering": "no",
|
|
2335
2479
|
"Access-Control-Allow-Origin": reqOrigin,
|
|
2336
2480
|
"Access-Control-Allow-Credentials": "true",
|
|
2337
2481
|
});
|
|
2338
|
-
hydrateSession(sessionId);
|
|
2339
|
-
const history = sessionHistory.get(sessionId) ?? [];
|
|
2340
2482
|
const isStreaming = activeStreams.has(sessionId);
|
|
2483
|
+
// Only the DB-windowed idle path can skip hydrating. Sessions without a DB
|
|
2484
|
+
// (in-memory history) still need their completed messages loaded from the
|
|
2485
|
+
// sessionHistory map, and streaming sessions need partial content too.
|
|
2486
|
+
let history = [];
|
|
2487
|
+
if (!db || isStreaming) {
|
|
2488
|
+
hydrateSession(sessionId);
|
|
2489
|
+
history = sessionHistory.get(sessionId) ?? [];
|
|
2490
|
+
}
|
|
2341
2491
|
// Build snapshot. While streaming, prefer in-memory history so partial assistant
|
|
2342
2492
|
// content is visible immediately (DB persistence may lag until stream completion).
|
|
2343
2493
|
let snapshotMessages;
|
|
2344
2494
|
let total = 0;
|
|
2345
2495
|
let hasMore = false;
|
|
2346
2496
|
if (db && !isStreaming) {
|
|
2347
|
-
const
|
|
2348
|
-
.select()
|
|
2349
|
-
.from(messagesTable)
|
|
2350
|
-
.where(eq(messagesTable.sessionId, sessionId))
|
|
2351
|
-
.orderBy(messagesTable.createdAt)
|
|
2352
|
-
.all();
|
|
2353
|
-
const allMessages = rows
|
|
2354
|
-
.filter((r) => r.role === "user" || r.role === "assistant")
|
|
2355
|
-
.map((r, i) => {
|
|
2356
|
-
const msg = {
|
|
2357
|
-
id: `${sessionId}-${i}`,
|
|
2358
|
-
role: r.role,
|
|
2359
|
-
content: typeof r.content === "string" ? r.content : String(r.content ?? ""),
|
|
2360
|
-
};
|
|
2361
|
-
if (r.toolCalls) {
|
|
2362
|
-
try {
|
|
2363
|
-
msg.toolCalls = JSON.parse(r.toolCalls);
|
|
2364
|
-
}
|
|
2365
|
-
catch { /* ignore */ }
|
|
2366
|
-
}
|
|
2367
|
-
if (r.segments) {
|
|
2368
|
-
try {
|
|
2369
|
-
msg.segments = JSON.parse(r.segments);
|
|
2370
|
-
}
|
|
2371
|
-
catch { /* ignore */ }
|
|
2372
|
-
}
|
|
2373
|
-
if (r.thinking) {
|
|
2374
|
-
msg.thinking = r.thinking;
|
|
2375
|
-
}
|
|
2376
|
-
if (r.contextFlow) {
|
|
2377
|
-
try {
|
|
2378
|
-
const parsed = JSON.parse(r.contextFlow);
|
|
2379
|
-
if (parsed && typeof parsed === "object" && Array.isArray(parsed.rounds)) {
|
|
2380
|
-
msg.contextFlow = parsed;
|
|
2381
|
-
}
|
|
2382
|
-
}
|
|
2383
|
-
catch { /* ignore */ }
|
|
2384
|
-
}
|
|
2385
|
-
return msg;
|
|
2386
|
-
});
|
|
2387
|
-
const windowed = windowMessages(allMessages, limit);
|
|
2497
|
+
const windowed = persistedMessageWindow(db, sessionId, limit);
|
|
2388
2498
|
snapshotMessages = windowed.messages;
|
|
2389
2499
|
total = windowed.total;
|
|
2390
2500
|
hasMore = windowed.hasMore;
|
|
@@ -2415,10 +2525,13 @@ export function registerChatRoutes(app, config, depsOrDb, sessionServiceArg) {
|
|
|
2415
2525
|
const snapshotSeq = sessionStreamSeq.get(sessionId) ?? 0;
|
|
2416
2526
|
let closed = false;
|
|
2417
2527
|
let unsubscribe = () => { };
|
|
2528
|
+
let keepalive = null;
|
|
2418
2529
|
const closeStream = () => {
|
|
2419
2530
|
if (closed)
|
|
2420
2531
|
return;
|
|
2421
2532
|
closed = true;
|
|
2533
|
+
if (keepalive)
|
|
2534
|
+
clearInterval(keepalive);
|
|
2422
2535
|
unsubscribe();
|
|
2423
2536
|
try {
|
|
2424
2537
|
reply.raw.end();
|
|
@@ -2428,6 +2541,18 @@ export function registerChatRoutes(app, config, depsOrDb, sessionServiceArg) {
|
|
|
2428
2541
|
reply.raw.on("close", () => {
|
|
2429
2542
|
closeStream();
|
|
2430
2543
|
});
|
|
2544
|
+
// Keep the SSE connection alive during idle periods (long tool runs / slow
|
|
2545
|
+
// LLM responses) so browsers/proxies don't drop it with "fetch failed".
|
|
2546
|
+
keepalive = setInterval(() => {
|
|
2547
|
+
if (closed)
|
|
2548
|
+
return;
|
|
2549
|
+
try {
|
|
2550
|
+
reply.raw.write(`: keepalive\n\n`);
|
|
2551
|
+
}
|
|
2552
|
+
catch {
|
|
2553
|
+
closeStream();
|
|
2554
|
+
}
|
|
2555
|
+
}, 15_000);
|
|
2431
2556
|
unsubscribe = subscribe(sessionId, snapshotSeq, (event) => {
|
|
2432
2557
|
if (closed)
|
|
2433
2558
|
return;
|
|
@@ -2491,7 +2616,7 @@ export function registerChatRoutes(app, config, depsOrDb, sessionServiceArg) {
|
|
|
2491
2616
|
.select()
|
|
2492
2617
|
.from(messagesTable)
|
|
2493
2618
|
.where(eq(messagesTable.sessionId, sessionId))
|
|
2494
|
-
.orderBy(messagesTable.createdAt)
|
|
2619
|
+
.orderBy(messagesTable.createdAt, messagesTable.id)
|
|
2495
2620
|
.all();
|
|
2496
2621
|
const lastAssistant = [...rows].reverse().find((r) => r.role === "assistant");
|
|
2497
2622
|
if (lastAssistant) {
|