@jait/gateway 0.1.83 → 0.1.85
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/routes/chat.d.ts.map +1 -1
- package/dist/routes/chat.js +202 -72
- package/dist/routes/chat.js.map +1 -1
- package/dist/routes/threads.d.ts.map +1 -1
- package/dist/routes/threads.js +97 -98
- package/dist/routes/threads.js.map +1 -1
- package/dist/services/workspace-sync.d.ts +56 -0
- package/dist/services/workspace-sync.d.ts.map +1 -0
- package/dist/services/workspace-sync.js +266 -0
- package/dist/services/workspace-sync.js.map +1 -0
- package/dist/tools/remote-executor.d.ts +44 -0
- package/dist/tools/remote-executor.d.ts.map +1 -0
- package/dist/tools/remote-executor.js +107 -0
- package/dist/tools/remote-executor.js.map +1 -0
- package/dist/ws.d.ts +12 -0
- package/dist/ws.d.ts.map +1 -1
- package/dist/ws.js +61 -0
- package/dist/ws.js.map +1 -1
- package/package.json +1 -1
- package/web-dist/assets/{index-BPlsWZxZ.js → index-Cc_KBEXt.js} +197 -197
- package/web-dist/dist/assets/{index-BPlsWZxZ.js → index-Cc_KBEXt.js} +197 -197
- package/web-dist/dist/index.html +1 -1
- package/web-dist/index.html +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"chat.d.ts","sourceRoot":"","sources":["../../src/routes/chat.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAC/C,OAAO,EAAE,KAAK,SAAS,EAAsB,MAAM,cAAc,CAAC;AAClE,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,gBAAgB,CAAC;AAC7C,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,yBAAyB,CAAC;AAC9D,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AACxD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AACzD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAC;AACzD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AACxD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAC;AACxD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAC;AAC5D,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC;AAE/D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AAC/C,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,8BAA8B,CAAC;AACxE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAC;
|
|
1
|
+
{"version":3,"file":"chat.d.ts","sourceRoot":"","sources":["../../src/routes/chat.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAC/C,OAAO,EAAE,KAAK,SAAS,EAAsB,MAAM,cAAc,CAAC;AAClE,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,gBAAgB,CAAC;AAC7C,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,yBAAyB,CAAC;AAC9D,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AACxD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AACzD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAC;AACzD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AACxD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAC;AACxD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAC;AAC5D,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC;AAE/D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AAC/C,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,8BAA8B,CAAC;AACxE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAC;AAuZjE,MAAM,WAAW,aAAa;IAC5B,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,cAAc,CAAC,EAAE,cAAc,CAAC;IAChC,WAAW,CAAC,EAAE,WAAW,CAAC;IAC1B,YAAY,CAAC,EAAE,YAAY,CAAC;IAC5B,eAAe,CAAC,EAAE,eAAe,CAAC;IAClC,KAAK,CAAC,EAAE,WAAW,CAAC;IACpB,aAAa,CAAC,EAAE,aAAa,CAAC;IAC9B,EAAE,CAAC,EAAE,cAAc,CAAC;IACpB,YAAY,CAAC,EAAE,mBAAmB,CAAC;IACnC,gBAAgB,CAAC,EAAE,gBAAgB,CAAC;IACpC,YAAY,CAAC,EAAE,CACb,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,OAAO,EACd,OAAO,EAAE,WAAW,EACpB,OAAO,CAAC,EAAE;QAAE,MAAM,CAAC,EAAE,OAAO,CAAC;QAAC,gBAAgB,CAAC,EAAE,MAAM,CAAA;KAAE,KACtD,OAAO,CAAC,UAAU,CAAC,CAAC;CAC1B;AAED,wBAAgB,kBAAkB,CAChC,GAAG,EAAE,eAAe,EACpB,MAAM,EAAE,SAAS,EACjB,QAAQ,CAAC,EAAE,MAAM,GAAG,aAAa,EACjC,iBAAiB,CAAC,EAAE,cAAc,QAy4CnC"}
|
package/dist/routes/chat.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { inferContextWindow } from "../config.js";
|
|
3
3
|
import { FileSystemSurface } from "../surfaces/filesystem.js";
|
|
4
|
-
import { RemoteCliProvider } from "../providers/remote-cli-provider.js";
|
|
5
4
|
import { resolveWorkspaceRoot } from "../tools/core/get-fs.js";
|
|
5
|
+
import { resolveRemoteNodeForSession, createRemoteToolExecutor } from "../tools/remote-executor.js";
|
|
6
|
+
import { WorkspaceSync } from "../services/workspace-sync.js";
|
|
6
7
|
import { existsSync } from "node:fs";
|
|
7
8
|
import { messages as messagesTable } from "../db/schema.js";
|
|
8
9
|
import { eq } from "drizzle-orm";
|
|
@@ -15,6 +16,63 @@ import { buildSystemPrompt } from "../tools/prompts/index.js";
|
|
|
15
16
|
const sessionHistory = new Map();
|
|
16
17
|
const activeStreams = new Set();
|
|
17
18
|
const sessionAbortControllers = new Map();
|
|
19
|
+
const sessionStreamingState = new Map();
|
|
20
|
+
function getOrCreateAccumulator(sessionId) {
|
|
21
|
+
let acc = sessionStreamingState.get(sessionId);
|
|
22
|
+
if (!acc) {
|
|
23
|
+
acc = { content: "", toolCalls: [], segments: [] };
|
|
24
|
+
sessionStreamingState.set(sessionId, acc);
|
|
25
|
+
}
|
|
26
|
+
return acc;
|
|
27
|
+
}
|
|
28
|
+
/** Append a text token to the streaming accumulator */
|
|
29
|
+
function accumulateToken(sessionId, token) {
|
|
30
|
+
const acc = getOrCreateAccumulator(sessionId);
|
|
31
|
+
acc.content += token;
|
|
32
|
+
const last = acc.segments[acc.segments.length - 1];
|
|
33
|
+
if (last?.type === "text") {
|
|
34
|
+
acc.segments[acc.segments.length - 1] = { type: "text", content: last.content + token };
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
acc.segments.push({ type: "text", content: token });
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
/** Record a tool call start in the streaming accumulator */
|
|
41
|
+
function accumulateToolStart(sessionId, callId, tool, args) {
|
|
42
|
+
const acc = getOrCreateAccumulator(sessionId);
|
|
43
|
+
acc.toolCalls.push({ callId, tool, args, ok: true, message: "", startedAt: Date.now() });
|
|
44
|
+
const last = acc.segments[acc.segments.length - 1];
|
|
45
|
+
if (last?.type === "toolGroup") {
|
|
46
|
+
if (!last.callIds.includes(callId)) {
|
|
47
|
+
acc.segments[acc.segments.length - 1] = { type: "toolGroup", callIds: [...last.callIds, callId] };
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
acc.segments.push({ type: "toolGroup", callIds: [callId] });
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
/** Record streaming output for a tool call */
|
|
55
|
+
function accumulateToolOutput(sessionId, callId, content) {
|
|
56
|
+
const acc = sessionStreamingState.get(sessionId);
|
|
57
|
+
if (!acc)
|
|
58
|
+
return;
|
|
59
|
+
const tc = acc.toolCalls.find(t => t.callId === callId);
|
|
60
|
+
if (tc)
|
|
61
|
+
tc.message = (tc.message || "") + content;
|
|
62
|
+
}
|
|
63
|
+
/** Record a tool call completion */
|
|
64
|
+
function accumulateToolResult(sessionId, callId, ok, message, data) {
|
|
65
|
+
const acc = sessionStreamingState.get(sessionId);
|
|
66
|
+
if (!acc)
|
|
67
|
+
return;
|
|
68
|
+
const tc = acc.toolCalls.find(t => t.callId === callId);
|
|
69
|
+
if (tc) {
|
|
70
|
+
tc.ok = ok;
|
|
71
|
+
tc.message = message;
|
|
72
|
+
tc.data = data;
|
|
73
|
+
tc.completedAt = Date.now();
|
|
74
|
+
}
|
|
75
|
+
}
|
|
18
76
|
/** Persistent CLI provider sessions — kept alive across turns so the agent retains conversation context */
|
|
19
77
|
const activeCliSessions = new Map();
|
|
20
78
|
const sessionSubscribers = new Map();
|
|
@@ -165,13 +223,37 @@ function buildVisibleHistoryEntries(sessionId, history, options) {
|
|
|
165
223
|
return out;
|
|
166
224
|
}
|
|
167
225
|
function buildVisibleHistoryMessages(sessionId, history, options) {
|
|
168
|
-
|
|
226
|
+
const msgs = buildVisibleHistoryEntries(sessionId, history, options).map(({ id, role, content, toolCalls, segments }) => ({
|
|
169
227
|
id,
|
|
170
228
|
role,
|
|
171
229
|
content,
|
|
172
230
|
toolCalls,
|
|
173
231
|
segments,
|
|
174
232
|
}));
|
|
233
|
+
// If there is a live streaming accumulator for this session, inject a
|
|
234
|
+
// synthetic assistant message so that reconnecting clients see the partial
|
|
235
|
+
// content that has been streamed so far.
|
|
236
|
+
const acc = sessionStreamingState.get(sessionId);
|
|
237
|
+
if (acc && (acc.content || acc.toolCalls.length > 0)) {
|
|
238
|
+
const last = msgs[msgs.length - 1];
|
|
239
|
+
// Merge into the last assistant message if it exists and has no content yet
|
|
240
|
+
// (the snapshot builder may have emitted a stub). Otherwise append a new one.
|
|
241
|
+
if (last && last.role === "assistant" && !last.content && !last.toolCalls) {
|
|
242
|
+
last.content = acc.content;
|
|
243
|
+
last.toolCalls = acc.toolCalls.length > 0 ? mapPersistedToolCallsForUI(acc.toolCalls) : undefined;
|
|
244
|
+
last.segments = acc.segments.length > 0 ? acc.segments : undefined;
|
|
245
|
+
}
|
|
246
|
+
else {
|
|
247
|
+
msgs.push({
|
|
248
|
+
id: `${sessionId}-streaming`,
|
|
249
|
+
role: "assistant",
|
|
250
|
+
content: acc.content,
|
|
251
|
+
toolCalls: acc.toolCalls.length > 0 ? mapPersistedToolCallsForUI(acc.toolCalls) : undefined,
|
|
252
|
+
segments: acc.segments.length > 0 ? acc.segments : undefined,
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
return msgs;
|
|
175
257
|
}
|
|
176
258
|
// ── System prompt ────────────────────────────────────────────────────
|
|
177
259
|
const SYSTEM_PROMPT = `You are Jait — Just Another Intelligent Tool. You are a capable AI assistant that can run shell commands, read/write files, and manage system surfaces.
|
|
@@ -329,48 +411,53 @@ export function registerChatRoutes(app, config, depsOrDb, sessionServiceArg) {
|
|
|
329
411
|
}
|
|
330
412
|
}
|
|
331
413
|
// ── Tool execution helper ──────────────────────────────────────────
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
if (signal?.aborted) {
|
|
337
|
-
return { ok: false, message: "Cancelled" };
|
|
338
|
-
}
|
|
339
|
-
const context = {
|
|
340
|
-
sessionId,
|
|
341
|
-
actionId: uuidv7(),
|
|
342
|
-
workspaceRoot: surfaceRegistry
|
|
343
|
-
? resolveWorkspaceRoot(surfaceRegistry, sessionId)
|
|
344
|
-
: process.cwd(),
|
|
345
|
-
requestedBy: "agent",
|
|
346
|
-
userId: auth?.userId,
|
|
347
|
-
apiKeys: auth?.apiKeys,
|
|
348
|
-
onOutputChunk,
|
|
349
|
-
signal,
|
|
350
|
-
};
|
|
351
|
-
try {
|
|
352
|
-
const toolPromise = toolExecutor
|
|
353
|
-
? toolExecutor(toolName, args, context)
|
|
354
|
-
: toolRegistry.execute(toolName, args, context, audit);
|
|
355
|
-
// Race the tool execution against the abort signal so a stuck tool
|
|
356
|
-
// (e.g. browser launch hanging) doesn't block the cancel flow forever.
|
|
357
|
-
if (signal && !signal.aborted) {
|
|
358
|
-
const abortPromise = new Promise((resolve) => {
|
|
359
|
-
const onAbort = () => resolve({ ok: false, message: "Cancelled" });
|
|
360
|
-
signal.addEventListener("abort", onAbort, { once: true });
|
|
361
|
-
// Clean up if the tool finishes first
|
|
362
|
-
toolPromise.finally(() => signal.removeEventListener("abort", onAbort));
|
|
363
|
-
});
|
|
364
|
-
return await Promise.race([toolPromise, abortPromise]);
|
|
414
|
+
function makeExecuteTool(executor) {
|
|
415
|
+
return async function executeTool(toolName, args, sessionId, auth, onOutputChunk, signal) {
|
|
416
|
+
if (!toolRegistry) {
|
|
417
|
+
return { ok: false, message: "Tool registry not available" };
|
|
365
418
|
}
|
|
366
|
-
|
|
367
|
-
}
|
|
368
|
-
catch (err) {
|
|
369
|
-
if (signal?.aborted)
|
|
419
|
+
if (signal?.aborted) {
|
|
370
420
|
return { ok: false, message: "Cancelled" };
|
|
371
|
-
|
|
372
|
-
|
|
421
|
+
}
|
|
422
|
+
const context = {
|
|
423
|
+
sessionId,
|
|
424
|
+
actionId: uuidv7(),
|
|
425
|
+
workspaceRoot: surfaceRegistry
|
|
426
|
+
? resolveWorkspaceRoot(surfaceRegistry, sessionId)
|
|
427
|
+
: process.cwd(),
|
|
428
|
+
requestedBy: "agent",
|
|
429
|
+
userId: auth?.userId,
|
|
430
|
+
apiKeys: auth?.apiKeys,
|
|
431
|
+
onOutputChunk,
|
|
432
|
+
signal,
|
|
433
|
+
};
|
|
434
|
+
try {
|
|
435
|
+
const effectiveExec = executor ?? toolExecutor;
|
|
436
|
+
const toolPromise = effectiveExec
|
|
437
|
+
? effectiveExec(toolName, args, context)
|
|
438
|
+
: toolRegistry.execute(toolName, args, context, audit);
|
|
439
|
+
// Race the tool execution against the abort signal so a stuck tool
|
|
440
|
+
// (e.g. browser launch hanging) doesn't block the cancel flow forever.
|
|
441
|
+
if (signal && !signal.aborted) {
|
|
442
|
+
const abortPromise = new Promise((resolve) => {
|
|
443
|
+
const onAbort = () => resolve({ ok: false, message: "Cancelled" });
|
|
444
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
445
|
+
// Clean up if the tool finishes first
|
|
446
|
+
toolPromise.finally(() => signal.removeEventListener("abort", onAbort));
|
|
447
|
+
});
|
|
448
|
+
return await Promise.race([toolPromise, abortPromise]);
|
|
449
|
+
}
|
|
450
|
+
return await toolPromise;
|
|
451
|
+
}
|
|
452
|
+
catch (err) {
|
|
453
|
+
if (signal?.aborted)
|
|
454
|
+
return { ok: false, message: "Cancelled" };
|
|
455
|
+
return { ok: false, message: err instanceof Error ? err.message : String(err) };
|
|
456
|
+
}
|
|
457
|
+
};
|
|
373
458
|
}
|
|
459
|
+
/** Default executeTool using the module-level toolExecutor */
|
|
460
|
+
const executeTool = makeExecuteTool();
|
|
374
461
|
// ══ POST /api/chat — Main chat endpoint with agentic tool loop ═════
|
|
375
462
|
app.post("/api/chat", async (request, reply) => {
|
|
376
463
|
const authUser = await requireAuth(request, reply, config.jwtSecret);
|
|
@@ -434,6 +521,12 @@ export function registerChatRoutes(app, config, depsOrDb, sessionServiceArg) {
|
|
|
434
521
|
? resolveWorkspaceRoot(surfaceRegistry, sessionId, sessionRecord?.workspacePath)
|
|
435
522
|
: (sessionRecord?.workspacePath?.trim() || process.cwd());
|
|
436
523
|
const promptCtx = { workspaceRoot: wsRoot };
|
|
524
|
+
// Detect if the workspace lives on a remote node and resolve the
|
|
525
|
+
// node ID so tool execution can be delegated there.
|
|
526
|
+
const remoteNodeId = ws ? resolveRemoteNodeForSession(ws, wsRoot) : null;
|
|
527
|
+
if (remoteNodeId) {
|
|
528
|
+
console.log(`[chat] session=${sessionId} workspace="${wsRoot}" → remote node ${remoteNodeId}`);
|
|
529
|
+
}
|
|
437
530
|
if (!sessionHistory.has(sessionId)) {
|
|
438
531
|
sessionHistory.set(sessionId, [
|
|
439
532
|
{ role: "system", content: buildSystemPrompt(chatMode, modelEndpoint, promptCtx) },
|
|
@@ -456,11 +549,19 @@ export function registerChatRoutes(app, config, depsOrDb, sessionServiceArg) {
|
|
|
456
549
|
catch { /* session may not exist */ }
|
|
457
550
|
const streamAbort = new AbortController();
|
|
458
551
|
sessionAbortControllers.set(sessionId, streamAbort);
|
|
552
|
+
// Build a tool executor that delegates to the remote node when needed.
|
|
553
|
+
// For remote workspaces, tools like terminal.run and file.write will be
|
|
554
|
+
// proxied to the node via the tool.op-request/response protocol.
|
|
555
|
+
const effectiveToolExecutor = (remoteNodeId && ws && toolExecutor)
|
|
556
|
+
? createRemoteToolExecutor({ ws, localExecutor: toolExecutor }, remoteNodeId)
|
|
557
|
+
: toolExecutor;
|
|
459
558
|
let fullContent = "";
|
|
460
559
|
let partialToolCalls = [];
|
|
461
560
|
let resultSegmentsJson;
|
|
462
561
|
let hitMaxRounds = false;
|
|
463
562
|
activeStreams.add(sessionId);
|
|
563
|
+
// Reset streaming accumulator for this turn so reload snapshots start fresh
|
|
564
|
+
sessionStreamingState.delete(sessionId);
|
|
464
565
|
let clientDisconnected = false;
|
|
465
566
|
reply.raw.on("close", () => { clientDisconnected = true; });
|
|
466
567
|
const safeWrite = (data) => {
|
|
@@ -487,32 +588,31 @@ export function registerChatRoutes(app, config, depsOrDb, sessionServiceArg) {
|
|
|
487
588
|
const cliWsRoot = surfaceRegistry
|
|
488
589
|
? resolveWorkspaceRoot(surfaceRegistry, sessionId, sessionRecord?.workspacePath)
|
|
489
590
|
: (sessionRecord?.workspacePath?.trim() || process.cwd());
|
|
490
|
-
//
|
|
491
|
-
//
|
|
492
|
-
// trying to spawn codex/claude-code locally on the gateway.
|
|
591
|
+
// CLI providers always run on the gateway. If the workspace path
|
|
592
|
+
// is on a remote node, sync the files locally first.
|
|
493
593
|
const pathExistsLocally = existsSync(cliWsRoot);
|
|
494
|
-
let
|
|
495
|
-
let
|
|
594
|
+
let effectiveCwd = cliWsRoot;
|
|
595
|
+
let workspaceSync = null;
|
|
496
596
|
if (!pathExistsLocally && ws) {
|
|
497
|
-
//
|
|
498
|
-
const
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
597
|
+
// Find a connected remote node that owns this path
|
|
598
|
+
const nodeId = resolveRemoteNodeForSession(ws, cliWsRoot);
|
|
599
|
+
if (nodeId) {
|
|
600
|
+
workspaceSync = new WorkspaceSync({ ws, nodeId, remotePath: cliWsRoot, sessionId });
|
|
601
|
+
try {
|
|
602
|
+
safeWrite(`data: ${JSON.stringify({ type: "token", content: "Syncing workspace from remote node...\n" })}\n\n`);
|
|
603
|
+
await workspaceSync.pull();
|
|
604
|
+
effectiveCwd = workspaceSync.localDir;
|
|
605
|
+
console.log(`[chat/cli] Synced remote workspace to ${effectiveCwd}`);
|
|
606
|
+
}
|
|
607
|
+
catch (err) {
|
|
608
|
+
safeWrite(`data: ${JSON.stringify({ type: "error", message: `Failed to sync remote workspace: ${err instanceof Error ? err.message : String(err)}` })}\n\n`);
|
|
609
|
+
reply.raw.end();
|
|
610
|
+
return;
|
|
611
|
+
}
|
|
510
612
|
}
|
|
511
613
|
}
|
|
512
|
-
//
|
|
513
|
-
|
|
514
|
-
cliProvider = providerRegistry.get(requestProvider) ?? null;
|
|
515
|
-
}
|
|
614
|
+
// Always use the local provider — CLI agents run on the gateway
|
|
615
|
+
let cliProvider = providerRegistry.get(requestProvider) ?? null;
|
|
516
616
|
if (!cliProvider) {
|
|
517
617
|
safeWrite(`data: ${JSON.stringify({ type: "error", message: `Unknown provider: ${requestProvider}` })}\n\n`);
|
|
518
618
|
reply.raw.end();
|
|
@@ -520,11 +620,11 @@ export function registerChatRoutes(app, config, depsOrDb, sessionServiceArg) {
|
|
|
520
620
|
}
|
|
521
621
|
const available = await cliProvider.checkAvailability();
|
|
522
622
|
if (!available) {
|
|
523
|
-
safeWrite(`data: ${JSON.stringify({ type: "error", message: `Provider ${requestProvider} is not available
|
|
623
|
+
safeWrite(`data: ${JSON.stringify({ type: "error", message: `Provider ${requestProvider} is not available: ${cliProvider.info.unavailableReason ?? "CLI not found"}` })}\n\n`);
|
|
524
624
|
reply.raw.end();
|
|
525
625
|
return;
|
|
526
626
|
}
|
|
527
|
-
console.log(`[chat/cli] session=${sessionId} wsRoot="${cliWsRoot}" session.workspacePath="${sessionRecord?.workspacePath}" surfaces=${surfaceRegistry?.getBySession(sessionId)?.length ?? 0}`);
|
|
627
|
+
console.log(`[chat/cli] session=${sessionId} wsRoot="${cliWsRoot}" effectiveCwd="${effectiveCwd}" session.workspacePath="${sessionRecord?.workspacePath}" surfaces=${surfaceRegistry?.getBySession(sessionId)?.length ?? 0}`);
|
|
528
628
|
// Ensure a FileSystemSurface exists for this session so we can
|
|
529
629
|
// back up files before CLI providers (Codex/Claude) write them,
|
|
530
630
|
// enabling the keep/discard (undo) flow.
|
|
@@ -542,7 +642,7 @@ export function registerChatRoutes(app, config, depsOrDb, sessionServiceArg) {
|
|
|
542
642
|
surfaceRegistry.onSurfaceStarted = null;
|
|
543
643
|
const started = await surfaceRegistry.startSurface("filesystem", fsId, {
|
|
544
644
|
sessionId,
|
|
545
|
-
workspaceRoot:
|
|
645
|
+
workspaceRoot: effectiveCwd,
|
|
546
646
|
});
|
|
547
647
|
surfaceRegistry.onSurfaceStarted = prevHandler;
|
|
548
648
|
cliFsSurface = started;
|
|
@@ -550,8 +650,8 @@ export function registerChatRoutes(app, config, depsOrDb, sessionServiceArg) {
|
|
|
550
650
|
catch { /* best effort */ }
|
|
551
651
|
}
|
|
552
652
|
}
|
|
553
|
-
//
|
|
554
|
-
const mcpServers =
|
|
653
|
+
// CLI providers run locally on the gateway — MCP always available
|
|
654
|
+
const mcpServers = [providerRegistry.buildJaitMcpServerRef(config)];
|
|
555
655
|
// ── Reuse an existing CLI session if one is alive for this Jait session ──
|
|
556
656
|
const cachedCliSession = activeCliSessions.get(sessionId);
|
|
557
657
|
let providerSessionId;
|
|
@@ -572,14 +672,14 @@ export function registerChatRoutes(app, config, depsOrDb, sessionServiceArg) {
|
|
|
572
672
|
}
|
|
573
673
|
const session = await cliProvider.startSession({
|
|
574
674
|
threadId: sessionId,
|
|
575
|
-
workingDirectory:
|
|
675
|
+
workingDirectory: effectiveCwd,
|
|
576
676
|
mode: "full-access",
|
|
577
677
|
model: typeof body["model"] === "string" ? body["model"] : undefined,
|
|
578
678
|
mcpServers,
|
|
579
679
|
});
|
|
580
680
|
providerSessionId = session.id;
|
|
581
681
|
activeCliSessions.set(sessionId, { providerId: requestProvider, providerSessionId, provider: cliProvider });
|
|
582
|
-
console.log(`[chat/cli] Started new ${requestProvider}
|
|
682
|
+
console.log(`[chat/cli] Started new ${requestProvider} session ${providerSessionId} for ${sessionId}`);
|
|
583
683
|
}
|
|
584
684
|
// Collect full content from CLI provider events
|
|
585
685
|
const contentChunks = [];
|
|
@@ -629,6 +729,7 @@ export function registerChatRoutes(app, config, depsOrDb, sessionServiceArg) {
|
|
|
629
729
|
contentChunks.push(event.content);
|
|
630
730
|
tokenBytesThisBlock += event.content.length;
|
|
631
731
|
lastSegmentWasText = false; // new text arrived
|
|
732
|
+
accumulateToken(sessionId, event.content);
|
|
632
733
|
safeWrite(`data: ${JSON.stringify({ type: "token", content: event.content })}\n\n`);
|
|
633
734
|
emitToSubscribers(sessionId, { type: "token", content: event.content });
|
|
634
735
|
break;
|
|
@@ -657,6 +758,7 @@ export function registerChatRoutes(app, config, depsOrDb, sessionServiceArg) {
|
|
|
657
758
|
}
|
|
658
759
|
safeWrite(`data: ${JSON.stringify({ type: "tool_start", call_id: callId, tool: event.tool, args: event.args })}\n\n`);
|
|
659
760
|
emitToSubscribers(sessionId, { type: "tool_start", call_id: callId, tool: event.tool, args: event.args });
|
|
761
|
+
accumulateToolStart(sessionId, callId, event.tool, event.args ?? {});
|
|
660
762
|
break;
|
|
661
763
|
}
|
|
662
764
|
case "tool.output": {
|
|
@@ -665,6 +767,7 @@ export function registerChatRoutes(app, config, depsOrDb, sessionServiceArg) {
|
|
|
665
767
|
if (tc) {
|
|
666
768
|
tc.message = (tc.message || "") + event.content;
|
|
667
769
|
}
|
|
770
|
+
accumulateToolOutput(sessionId, event.callId ?? "", event.content);
|
|
668
771
|
safeWrite(`data: ${JSON.stringify({ type: "tool_output", call_id: event.callId, content: event.content })}\n\n`);
|
|
669
772
|
break;
|
|
670
773
|
}
|
|
@@ -680,6 +783,7 @@ export function registerChatRoutes(app, config, depsOrDb, sessionServiceArg) {
|
|
|
680
783
|
}
|
|
681
784
|
safeWrite(`data: ${JSON.stringify({ type: "tool_result", call_id: resultCallId, tool: event.tool, ok: event.ok, message: event.message, data: event.data })}\n\n`);
|
|
682
785
|
emitToSubscribers(sessionId, { type: "tool_result", call_id: resultCallId, tool: event.tool, ok: event.ok, message: event.message });
|
|
786
|
+
accumulateToolResult(sessionId, resultCallId, event.ok, event.message || "", event.data);
|
|
683
787
|
// Emit file_changed for successful edits → drives the keep/discard UI
|
|
684
788
|
if (event.ok && event.tool === "edit") {
|
|
685
789
|
const editPath = String(tc?.args ? tc.args.path ?? "" : "");
|
|
@@ -744,7 +848,7 @@ export function registerChatRoutes(app, config, depsOrDb, sessionServiceArg) {
|
|
|
744
848
|
activeCliSessions.delete(sessionId);
|
|
745
849
|
const freshSession = await cliProvider.startSession({
|
|
746
850
|
threadId: sessionId,
|
|
747
|
-
workingDirectory:
|
|
851
|
+
workingDirectory: effectiveCwd,
|
|
748
852
|
mode: "full-access",
|
|
749
853
|
model: typeof body["model"] === "string" ? body["model"] : undefined,
|
|
750
854
|
mcpServers,
|
|
@@ -789,6 +893,18 @@ export function registerChatRoutes(app, config, depsOrDb, sessionServiceArg) {
|
|
|
789
893
|
// Persist assistant message with tool calls and segments
|
|
790
894
|
history.push({ role: "assistant", content: fullContent, uiToolCalls: cliToolCalls.length > 0 ? cliToolCalls : undefined });
|
|
791
895
|
persistMessage(sessionId, "assistant", fullContent, cliTcJson, cliSegJson);
|
|
896
|
+
// Push changed files back to the remote node if workspace was synced
|
|
897
|
+
if (workspaceSync) {
|
|
898
|
+
try {
|
|
899
|
+
const { pushed } = await workspaceSync.push();
|
|
900
|
+
if (pushed > 0) {
|
|
901
|
+
console.log(`[chat/cli] Pushed ${pushed} changed file(s) back to remote node`);
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
catch (err) {
|
|
905
|
+
console.error(`[chat/cli] Failed to push changes back:`, err);
|
|
906
|
+
}
|
|
907
|
+
}
|
|
792
908
|
// Session stays alive for the next turn — do NOT stop it.
|
|
793
909
|
// It will be cleaned up on session error, provider switch, or server shutdown.
|
|
794
910
|
}
|
|
@@ -805,6 +921,15 @@ export function registerChatRoutes(app, config, depsOrDb, sessionServiceArg) {
|
|
|
805
921
|
const onEvent = (event) => {
|
|
806
922
|
emitToSubscribers(sessionId, event);
|
|
807
923
|
safeWrite(`data: ${JSON.stringify(event)}\n\n`);
|
|
924
|
+
// ── Update streaming accumulator so reload snapshots include partial content ──
|
|
925
|
+
if (event.type === "token")
|
|
926
|
+
accumulateToken(sessionId, event.content);
|
|
927
|
+
else if (event.type === "tool_start")
|
|
928
|
+
accumulateToolStart(sessionId, event.call_id, event.tool, event.args);
|
|
929
|
+
else if (event.type === "tool_output")
|
|
930
|
+
accumulateToolOutput(sessionId, event.call_id, event.content);
|
|
931
|
+
else if (event.type === "tool_result")
|
|
932
|
+
accumulateToolResult(sessionId, event.call_id, event.ok, event.message, event.data);
|
|
808
933
|
// ── Cross-client sync: persist & broadcast state changes ──
|
|
809
934
|
const ev = event;
|
|
810
935
|
// Broadcast todo list updates to all session clients and persist to DB
|
|
@@ -864,7 +989,11 @@ export function registerChatRoutes(app, config, depsOrDb, sessionServiceArg) {
|
|
|
864
989
|
onEvent,
|
|
865
990
|
onPersist: (sid, role, content, tc, seg) => persistMessage(sid, role, content, tc, seg),
|
|
866
991
|
log: app.log,
|
|
867
|
-
},
|
|
992
|
+
},
|
|
993
|
+
// Use remote-aware executor when workspace is on a remote node
|
|
994
|
+
effectiveToolExecutor !== toolExecutor
|
|
995
|
+
? makeExecuteTool(effectiveToolExecutor)
|
|
996
|
+
: executeTool, steering);
|
|
868
997
|
fullContent = result.content;
|
|
869
998
|
partialToolCalls = result.executedToolCalls;
|
|
870
999
|
resultSegmentsJson = result.segments.length > 0 ? JSON.stringify(result.segments) : undefined;
|
|
@@ -913,6 +1042,7 @@ export function registerChatRoutes(app, config, depsOrDb, sessionServiceArg) {
|
|
|
913
1042
|
activeStreams.delete(sessionId);
|
|
914
1043
|
sessionAbortControllers.delete(sessionId);
|
|
915
1044
|
sessionSteeringControllers.delete(sessionId);
|
|
1045
|
+
sessionStreamingState.delete(sessionId);
|
|
916
1046
|
// Clean up in-memory history: remove any dangling assistant tool_calls
|
|
917
1047
|
// messages that never got a text response (e.g. cancelled mid-tool-call).
|
|
918
1048
|
// This prevents them from showing as "running" on reload.
|