@jait/gateway 0.1.377 → 0.1.379
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.js +1 -1
- package/dist/routes/chat.js.map +1 -1
- package/dist/security/ssrf-guard.d.ts.map +1 -1
- package/dist/security/ssrf-guard.js +30 -24
- package/dist/security/ssrf-guard.js.map +1 -1
- package/dist/tools/thread-tools.d.ts +16 -0
- package/dist/tools/thread-tools.d.ts.map +1 -1
- package/dist/tools/thread-tools.js +266 -49
- package/dist/tools/thread-tools.js.map +1 -1
- package/package.json +1 -1
- package/web-dist/assets/{_basePickBy-Wzv49Akq.js → _basePickBy-BtWPVxzz.js} +1 -1
- package/web-dist/assets/{_baseUniq-BSZbCXUo.js → _baseUniq-vHqPKXCy.js} +1 -1
- package/web-dist/assets/{arc-DpjjQy8H.js → arc-mPAb9ypF.js} +1 -1
- package/web-dist/assets/{architectureDiagram-2XIMDMQ5-C6zyje39.js → architectureDiagram-2XIMDMQ5-CcE8yuVs.js} +1 -1
- package/web-dist/assets/{blockDiagram-WCTKOSBZ-Dg5vNEHV.js → blockDiagram-WCTKOSBZ-DYITtBod.js} +1 -1
- package/web-dist/assets/{c4Diagram-IC4MRINW-B-CGuYcX.js → c4Diagram-IC4MRINW-CIRb-Kh_.js} +1 -1
- package/web-dist/assets/channel-qsrWFAj-.js +1 -0
- package/web-dist/assets/{chunk-4BX2VUAB-CeqTXBvl.js → chunk-4BX2VUAB-hlKC2Pyx.js} +1 -1
- package/web-dist/assets/{chunk-55IACEB6-BjhCFTwZ.js → chunk-55IACEB6-C-pHltjx.js} +1 -1
- package/web-dist/assets/{chunk-FMBD7UC4-2VH4w1Cm.js → chunk-FMBD7UC4-CsOau2pA.js} +1 -1
- package/web-dist/assets/{chunk-JSJVCQXG-BC0wOWQH.js → chunk-JSJVCQXG-BRMz0YLo.js} +1 -1
- package/web-dist/assets/{chunk-KX2RTZJC-D9HDfnhZ.js → chunk-KX2RTZJC-Bor2JuuQ.js} +1 -1
- package/web-dist/assets/{chunk-NQ4KR5QH-SwWifZ9F.js → chunk-NQ4KR5QH-CpSvglTz.js} +1 -1
- package/web-dist/assets/{chunk-QZHKN3VN-C9r4087v.js → chunk-QZHKN3VN-D2h9DuqW.js} +1 -1
- package/web-dist/assets/{chunk-WL4C6EOR-BMdij3qU.js → chunk-WL4C6EOR-8PvymgEe.js} +1 -1
- package/web-dist/assets/classDiagram-VBA2DB6C-D8LZHmHs.js +1 -0
- package/web-dist/assets/classDiagram-v2-RAHNMMFH-D8LZHmHs.js +1 -0
- package/web-dist/assets/clone-DlHeuatR.js +1 -0
- package/web-dist/assets/{cose-bilkent-S5V4N54A-DfIv6ajn.js → cose-bilkent-S5V4N54A-N02lYkR9.js} +1 -1
- package/web-dist/assets/{dagre-KLK3FWXG-BJsr2FRT.js → dagre-KLK3FWXG-oCjcr5dJ.js} +1 -1
- package/web-dist/assets/{diagram-E7M64L7V-CzOQpmHA.js → diagram-E7M64L7V-BXXL3E2m.js} +1 -1
- package/web-dist/assets/{diagram-IFDJBPK2-DBOK9xY-.js → diagram-IFDJBPK2-B_vTdJEP.js} +1 -1
- package/web-dist/assets/{diagram-P4PSJMXO-BV4mV-cp.js → diagram-P4PSJMXO-lNygr5wq.js} +1 -1
- package/web-dist/assets/{erDiagram-INFDFZHY-Ba0tZtxl.js → erDiagram-INFDFZHY-NqUNNuKV.js} +1 -1
- package/web-dist/assets/{flowDiagram-PKNHOUZH-B9Jj988m.js → flowDiagram-PKNHOUZH-CNSXj35q.js} +1 -1
- package/web-dist/assets/{ganttDiagram-A5KZAMGK-CiMqI7tz.js → ganttDiagram-A5KZAMGK-BOtS9jUX.js} +1 -1
- package/web-dist/assets/{gitGraphDiagram-K3NZZRJ6-mi0Lj_tL.js → gitGraphDiagram-K3NZZRJ6-dPsLGV6U.js} +1 -1
- package/web-dist/assets/{graph-B1mSAN-r.js → graph-D3xE5oob.js} +1 -1
- package/web-dist/assets/{index-Coc8XQg5.js → index-TM7LRxSq.js} +304 -304
- package/web-dist/assets/{infoDiagram-LFFYTUFH-BBJROgHw.js → infoDiagram-LFFYTUFH-BAhs2wr3.js} +1 -1
- package/web-dist/assets/{ishikawaDiagram-PHBUUO56-CvD1jJdy.js → ishikawaDiagram-PHBUUO56-CV5mTlem.js} +1 -1
- package/web-dist/assets/{journeyDiagram-4ABVD52K-Cee9gxbo.js → journeyDiagram-4ABVD52K-Bx_Ce0gB.js} +1 -1
- package/web-dist/assets/{kanban-definition-K7BYSVSG-DZx02Jgo.js → kanban-definition-K7BYSVSG-COvXbKrD.js} +1 -1
- package/web-dist/assets/{layout-FD28oy1q.js → layout-DSUbOKAP.js} +1 -1
- package/web-dist/assets/{linear-BfIO_UGQ.js → linear-CG8Kj2Ds.js} +1 -1
- package/web-dist/assets/{mindmap-definition-YRQLILUH-BDhKfywd.js → mindmap-definition-YRQLILUH-DvE6jHkL.js} +1 -1
- package/web-dist/assets/{pieDiagram-SKSYHLDU-Ckqgoc0S.js → pieDiagram-SKSYHLDU-BP2_ppdA.js} +1 -1
- package/web-dist/assets/{quadrantDiagram-337W2JSQ-C-tMRbqn.js → quadrantDiagram-337W2JSQ-DbvBGrKb.js} +1 -1
- package/web-dist/assets/{requirementDiagram-Z7DCOOCP-KiArbbU4.js → requirementDiagram-Z7DCOOCP-qxDS52L6.js} +1 -1
- package/web-dist/assets/{sankeyDiagram-WA2Y5GQK-BSyF4GDm.js → sankeyDiagram-WA2Y5GQK-BdZdTJJw.js} +1 -1
- package/web-dist/assets/{sequenceDiagram-2WXFIKYE-ChU2W22g.js → sequenceDiagram-2WXFIKYE-DxGaQsU4.js} +1 -1
- package/web-dist/assets/{stateDiagram-RAJIS63D-CYfNyeS2.js → stateDiagram-RAJIS63D-DDY2Tu-v.js} +1 -1
- package/web-dist/assets/stateDiagram-v2-FVOUBMTO-fMhX8YPS.js +1 -0
- package/web-dist/assets/{timeline-definition-YZTLITO2-BNGiq9yT.js → timeline-definition-YZTLITO2-Bsll_GQh.js} +1 -1
- package/web-dist/assets/{treemap-KZPCXAKY-7f24nLHF.js → treemap-KZPCXAKY-L2Vte7Gd.js} +1 -1
- package/web-dist/assets/{vennDiagram-LZ73GAT5-B5T32YFj.js → vennDiagram-LZ73GAT5-sAfl6kSd.js} +1 -1
- package/web-dist/assets/{xychartDiagram-JWTSCODW-BsCy1_Gu.js → xychartDiagram-JWTSCODW-DDOPb45p.js} +1 -1
- package/web-dist/index.html +1 -1
- package/web-dist/assets/channel-Cfw0FcZ8.js +0 -1
- package/web-dist/assets/classDiagram-VBA2DB6C-luXaKhEX.js +0 -1
- package/web-dist/assets/classDiagram-v2-RAHNMMFH-luXaKhEX.js +0 -1
- package/web-dist/assets/clone-CotxwDmM.js +0 -1
- package/web-dist/assets/stateDiagram-v2-FVOUBMTO-CQ11roi0.js +0 -1
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
1
2
|
import { extractTodoResultItems } from "../providers/todo-result.js";
|
|
2
3
|
import { GitService, cleanupWorktreeRemoteAware } from "../services/git.js";
|
|
3
4
|
import { resolveThreadSelectionDefaults } from "../services/thread-defaults.js";
|
|
@@ -27,6 +28,21 @@ function normalizeThreadProviderId(value) {
|
|
|
27
28
|
return null;
|
|
28
29
|
}
|
|
29
30
|
}
|
|
31
|
+
function normalizePrompt(message, prompt) {
|
|
32
|
+
const value = typeof prompt === "string" && prompt.trim() ? prompt : message;
|
|
33
|
+
if (typeof value !== "string")
|
|
34
|
+
return undefined;
|
|
35
|
+
const trimmed = value.trim();
|
|
36
|
+
return trimmed ? trimmed : undefined;
|
|
37
|
+
}
|
|
38
|
+
function normalizeTimeoutMs(timeoutMinutes, defaultMinutes) {
|
|
39
|
+
const minutes = typeof timeoutMinutes === "number" && Number.isFinite(timeoutMinutes)
|
|
40
|
+
? timeoutMinutes
|
|
41
|
+
: defaultMinutes;
|
|
42
|
+
if (minutes === undefined || minutes <= 0)
|
|
43
|
+
return undefined;
|
|
44
|
+
return Math.max(1, Math.floor(minutes * 60_000));
|
|
45
|
+
}
|
|
30
46
|
export function createThreadControlTool(deps) {
|
|
31
47
|
const gitService = deps.gitService ?? new GitService();
|
|
32
48
|
const broadcastThreadEvent = (threadId, event, data) => {
|
|
@@ -43,6 +59,38 @@ export function createThreadControlTool(deps) {
|
|
|
43
59
|
const userId = context.userId?.trim();
|
|
44
60
|
return userId || "system";
|
|
45
61
|
};
|
|
62
|
+
const createManagedDeliveryWorktree = async (thread) => {
|
|
63
|
+
if (thread.kind !== "delivery")
|
|
64
|
+
return thread;
|
|
65
|
+
if (thread.branch)
|
|
66
|
+
return thread;
|
|
67
|
+
const cwd = thread.workingDirectory?.trim();
|
|
68
|
+
if (!cwd)
|
|
69
|
+
return thread;
|
|
70
|
+
if (!existsSync(cwd))
|
|
71
|
+
return thread;
|
|
72
|
+
if (!gitService.isRepo || !gitService.createWorktree)
|
|
73
|
+
return thread;
|
|
74
|
+
const isRepo = await gitService.isRepo(cwd).catch(() => false);
|
|
75
|
+
if (!isRepo)
|
|
76
|
+
return thread;
|
|
77
|
+
let remoteUrl = null;
|
|
78
|
+
if (gitService.getPreferredRemote && gitService.getRemoteUrl) {
|
|
79
|
+
const remote = await gitService.getPreferredRemote(cwd).catch(() => null);
|
|
80
|
+
if (remote) {
|
|
81
|
+
remoteUrl = await gitService.getRemoteUrl(cwd, remote).catch(() => null);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
const defaultBranch = gitService.resolveDefaultBranch
|
|
85
|
+
? await gitService.resolveDefaultBranch(cwd, remoteUrl).catch(() => "main")
|
|
86
|
+
: "main";
|
|
87
|
+
const branch = `jait/${thread.id.slice(-8)}`;
|
|
88
|
+
const worktree = await gitService.createWorktree(cwd, defaultBranch, branch);
|
|
89
|
+
return deps.threadService.update(thread.id, {
|
|
90
|
+
workingDirectory: worktree.path,
|
|
91
|
+
branch: worktree.branch,
|
|
92
|
+
}) ?? thread;
|
|
93
|
+
};
|
|
46
94
|
const resolveSelectedThreadDefaults = (context) => {
|
|
47
95
|
const defaults = resolveThreadSelectionDefaults({
|
|
48
96
|
userId: context.userId,
|
|
@@ -95,17 +143,23 @@ export function createThreadControlTool(deps) {
|
|
|
95
143
|
return undefined;
|
|
96
144
|
return thread;
|
|
97
145
|
};
|
|
98
|
-
const startThread = async (context, thread,
|
|
146
|
+
const startThread = async (context, thread, options = {}) => {
|
|
147
|
+
const message = normalizePrompt(options.message);
|
|
99
148
|
if (thread.status === "running" && thread.providerSessionId) {
|
|
100
149
|
return { ok: false, message: "Thread is already running" };
|
|
101
150
|
}
|
|
151
|
+
if (!message) {
|
|
152
|
+
return { ok: false, message: "start requires non-empty `prompt`." };
|
|
153
|
+
}
|
|
102
154
|
const resolvedProvider = resolveStoredThreadProvider(thread.providerId, context);
|
|
103
155
|
if (!resolvedProvider.providerId) {
|
|
104
156
|
return { ok: false, message: resolvedProvider.error ?? "Unable to resolve a provider for this thread." };
|
|
105
157
|
}
|
|
106
158
|
const effectiveProviderId = resolvedProvider.providerId;
|
|
107
159
|
const effectiveThread = thread;
|
|
108
|
-
const autoFinishAfterFirstTurn = effectiveThread.kind === "delegation"
|
|
160
|
+
const autoFinishAfterFirstTurn = effectiveThread.kind === "delegation" || options.autoStopAfterTurn === true;
|
|
161
|
+
const detach = options.detach === true || context.requestedBy === "scheduler";
|
|
162
|
+
const timeoutMs = normalizeTimeoutMs(options.timeoutMinutes, context.requestedBy === "scheduler" ? 240 : undefined);
|
|
109
163
|
const provider = deps.providerRegistry.get(effectiveProviderId);
|
|
110
164
|
if (!provider) {
|
|
111
165
|
return { ok: false, message: `Provider '${effectiveProviderId}' not found` };
|
|
@@ -125,7 +179,20 @@ export function createThreadControlTool(deps) {
|
|
|
125
179
|
model: effectiveThread.model ?? context.model ?? undefined,
|
|
126
180
|
mcpServers,
|
|
127
181
|
});
|
|
128
|
-
|
|
182
|
+
let timeout;
|
|
183
|
+
let unsubscribed = false;
|
|
184
|
+
let unsubscribe;
|
|
185
|
+
const cleanup = () => {
|
|
186
|
+
if (timeout) {
|
|
187
|
+
clearTimeout(timeout);
|
|
188
|
+
timeout = undefined;
|
|
189
|
+
}
|
|
190
|
+
if (unsubscribe && !unsubscribed) {
|
|
191
|
+
unsubscribed = true;
|
|
192
|
+
unsubscribe();
|
|
193
|
+
}
|
|
194
|
+
};
|
|
195
|
+
unsubscribe = provider.onEvent((event) => {
|
|
129
196
|
if (event.sessionId !== session.id) {
|
|
130
197
|
return;
|
|
131
198
|
}
|
|
@@ -149,12 +216,12 @@ export function createThreadControlTool(deps) {
|
|
|
149
216
|
deps.threadService.markCompleted(effectiveThread.id);
|
|
150
217
|
}
|
|
151
218
|
broadcastThreadEvent(effectiveThread.id, "status", { status: "completed" });
|
|
152
|
-
|
|
219
|
+
cleanup();
|
|
153
220
|
}
|
|
154
221
|
else if (event.type === "session.error") {
|
|
155
222
|
deps.threadService.markError(effectiveThread.id, event.error);
|
|
156
223
|
broadcastThreadEvent(effectiveThread.id, "status", { status: "error", error: event.error });
|
|
157
|
-
|
|
224
|
+
cleanup();
|
|
158
225
|
}
|
|
159
226
|
else if (event.type === "turn.started") {
|
|
160
227
|
// Re-assert running when a new turn begins
|
|
@@ -164,43 +231,77 @@ export function createThreadControlTool(deps) {
|
|
|
164
231
|
broadcastThreadEvent(effectiveThread.id, "status", { status: "running" });
|
|
165
232
|
}
|
|
166
233
|
}
|
|
167
|
-
else if (event.type === "turn.completed"
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
234
|
+
else if (event.type === "turn.completed") {
|
|
235
|
+
if (autoFinishAfterFirstTurn) {
|
|
236
|
+
void provider.stopSession(session.id).catch(() => {
|
|
237
|
+
// Best effort: one-shot threads should not linger if the provider
|
|
238
|
+
// leaves the session open after answering the first task.
|
|
239
|
+
});
|
|
240
|
+
deps.threadService.markCompletedAndClearSession(effectiveThread.id);
|
|
241
|
+
cleanup();
|
|
242
|
+
}
|
|
243
|
+
else {
|
|
244
|
+
deps.threadService.markCompleted(effectiveThread.id);
|
|
245
|
+
}
|
|
173
246
|
broadcastThreadEvent(effectiveThread.id, "status", { status: "completed" });
|
|
174
|
-
unsubscribe();
|
|
175
247
|
}
|
|
176
248
|
});
|
|
249
|
+
if (timeoutMs) {
|
|
250
|
+
timeout = setTimeout(() => {
|
|
251
|
+
void provider.stopSession(session.id).catch(() => { });
|
|
252
|
+
const activity = deps.threadService.addActivity(effectiveThread.id, "session", `Thread stopped after ${Math.round(timeoutMs / 60_000)} minute timeout`, { timeoutMs });
|
|
253
|
+
broadcastThreadEvent(effectiveThread.id, "activity", { activity });
|
|
254
|
+
deps.threadService.markInterrupted(effectiveThread.id);
|
|
255
|
+
broadcastThreadEvent(effectiveThread.id, "status", { status: "interrupted" });
|
|
256
|
+
cleanup();
|
|
257
|
+
}, timeoutMs);
|
|
258
|
+
if (typeof timeout === "object" && "unref" in timeout && typeof timeout.unref === "function") {
|
|
259
|
+
timeout.unref();
|
|
260
|
+
}
|
|
261
|
+
}
|
|
177
262
|
deps.threadService.markRunning(effectiveThread.id, session.id);
|
|
178
263
|
broadcastThreadEvent(effectiveThread.id, "status", { status: "running" });
|
|
179
264
|
const historyReplayPrompt = buildThreadHistoryReplayPrompt(deps.threadService, effectiveThread.id);
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
265
|
+
const userActivity = deps.threadService.addActivity(effectiveThread.id, "message", message.slice(0, 500), { role: "user" });
|
|
266
|
+
broadcastThreadEvent(effectiveThread.id, "activity", { activity: userActivity });
|
|
267
|
+
// Run thread router for the first turn
|
|
268
|
+
let turnMessage = message;
|
|
269
|
+
if (historyReplayPrompt) {
|
|
270
|
+
turnMessage = `${historyReplayPrompt}\n\n${turnMessage}`;
|
|
271
|
+
}
|
|
272
|
+
if (deps.skillRegistry) {
|
|
273
|
+
const availableSkills = deps.skillRegistry.listEnabled();
|
|
274
|
+
const routingPlan = routeThread({
|
|
275
|
+
message,
|
|
276
|
+
availableSkills,
|
|
277
|
+
pinnedSkillIds: effectiveThread.skillIds ? (typeof effectiveThread.skillIds === "string" ? JSON.parse(effectiveThread.skillIds) : effectiveThread.skillIds) : null,
|
|
278
|
+
kind: effectiveThread.kind,
|
|
279
|
+
});
|
|
280
|
+
deps.threadService.update(effectiveThread.id, { routingPlan });
|
|
281
|
+
broadcastThreadEvent(effectiveThread.id, "updated", { thread: deps.threadService.getById(effectiveThread.id) });
|
|
282
|
+
turnMessage = `${formatRoutingPlanForPrompt(routingPlan, message.length)}\n\n${message}`;
|
|
283
|
+
}
|
|
284
|
+
const sendTurn = async () => {
|
|
285
|
+
try {
|
|
286
|
+
await provider.sendTurn(session.id, turnMessage, options.attachments);
|
|
187
287
|
}
|
|
188
|
-
|
|
189
|
-
const
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
});
|
|
196
|
-
deps.threadService.update(effectiveThread.id, { routingPlan });
|
|
197
|
-
broadcastThreadEvent(effectiveThread.id, "updated", { thread: deps.threadService.getById(effectiveThread.id) });
|
|
198
|
-
turnMessage = `${formatRoutingPlanForPrompt(routingPlan, message.length)}\n\n${message}`;
|
|
288
|
+
catch (err) {
|
|
289
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
290
|
+
deps.threadService.markError(effectiveThread.id, errorMessage);
|
|
291
|
+
broadcastThreadEvent(effectiveThread.id, "status", { status: "error", error: errorMessage });
|
|
292
|
+
cleanup();
|
|
293
|
+
if (!detach)
|
|
294
|
+
throw err;
|
|
199
295
|
}
|
|
200
|
-
|
|
296
|
+
};
|
|
297
|
+
if (detach) {
|
|
298
|
+
void sendTurn();
|
|
299
|
+
}
|
|
300
|
+
else {
|
|
301
|
+
await sendTurn();
|
|
201
302
|
}
|
|
202
303
|
const updated = deps.threadService.getById(effectiveThread.id) ?? effectiveThread;
|
|
203
|
-
return { ok: true, message: "Thread started", thread: updated };
|
|
304
|
+
return { ok: true, message: detach ? "Thread started in background" : "Thread started", thread: updated };
|
|
204
305
|
}
|
|
205
306
|
catch (err) {
|
|
206
307
|
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
@@ -212,7 +313,10 @@ export function createThreadControlTool(deps) {
|
|
|
212
313
|
return {
|
|
213
314
|
name: ToolName.ThreadControl,
|
|
214
315
|
description: "Control agent threads end-to-end: create/list/update/delete threads, run them in parallel, " +
|
|
215
|
-
"send turns, stop/interrupt, and create pull requests with direct links."
|
|
316
|
+
"send turns, stop/interrupt, and create pull requests with direct links. " +
|
|
317
|
+
"Threads have two kinds: delivery threads are user-owned work items that can be completed, resumed, reviewed, and used to create PRs; " +
|
|
318
|
+
"delegation threads are helper/sub-agent tasks that auto-finish after one turn and cannot create PRs. " +
|
|
319
|
+
"Creating or starting a thread requires a non-empty prompt. Scheduled jobs should create delivery threads with detach=true so the scheduler returns after dispatching the turn.",
|
|
216
320
|
tier: "standard",
|
|
217
321
|
category: "agent",
|
|
218
322
|
source: "builtin",
|
|
@@ -243,16 +347,39 @@ export function createThreadControlTool(deps) {
|
|
|
243
347
|
title: { type: "string", description: "Thread title (create/update)." },
|
|
244
348
|
model: { type: "string", description: "Optional provider model." },
|
|
245
349
|
runtimeMode: { type: "string", enum: ["full-access", "supervised"], description: "Execution mode for thread runs." },
|
|
246
|
-
kind: {
|
|
350
|
+
kind: {
|
|
351
|
+
type: "string",
|
|
352
|
+
enum: ["delivery", "delegation"],
|
|
353
|
+
description: "Thread kind. Use delivery for user-owned work that can be reviewed, resumed, merged, or turned into a PR. " +
|
|
354
|
+
"Use delegation for helper/sub-agent work; delegation threads auto-finish after one turn and cannot create PRs.",
|
|
355
|
+
},
|
|
247
356
|
workingDirectory: { type: "string", description: "Working directory for the thread." },
|
|
248
357
|
branch: { type: "string", description: "Git branch metadata for the thread." },
|
|
249
|
-
|
|
358
|
+
prompt: {
|
|
359
|
+
type: "string",
|
|
360
|
+
description: "Required prompt for create/start/send. Prefer this over `message`; `message` is kept as a backward-compatible alias.",
|
|
361
|
+
},
|
|
362
|
+
message: { type: "string", description: "Backward-compatible alias for `prompt`." },
|
|
250
363
|
attachments: {
|
|
251
364
|
type: "array",
|
|
252
365
|
description: "Optional attachments for provider sendTurn.",
|
|
253
366
|
items: { type: "string" },
|
|
254
367
|
},
|
|
255
|
-
start: { type: "boolean", description: "For create/create_many: auto-start thread(s) after creation." },
|
|
368
|
+
start: { type: "boolean", description: "For create/create_many: auto-start thread(s) after creation. Requires `prompt`." },
|
|
369
|
+
detach: {
|
|
370
|
+
type: "boolean",
|
|
371
|
+
description: "Return after dispatching the first turn instead of waiting for provider.sendTurn. " +
|
|
372
|
+
"Use true for scheduled/cron-created delivery threads so the job creates a normal user-visible thread without blocking the scheduler. Scheduler-triggered starts detach automatically.",
|
|
373
|
+
},
|
|
374
|
+
autoStopAfterTurn: {
|
|
375
|
+
type: "boolean",
|
|
376
|
+
description: "Stop the provider session and clear resumability after the first turn completes. " +
|
|
377
|
+
"Delegation threads always do this. Leave false for normal delivery threads that the user may review, resume, merge, or delete later.",
|
|
378
|
+
},
|
|
379
|
+
timeoutMinutes: {
|
|
380
|
+
type: "number",
|
|
381
|
+
description: "Maximum runtime before stopping the provider session. Scheduler-triggered starts default to 240 minutes.",
|
|
382
|
+
},
|
|
256
383
|
threads: {
|
|
257
384
|
type: "array",
|
|
258
385
|
description: "For create_many: array of thread specs to create in one call.",
|
|
@@ -267,8 +394,12 @@ export function createThreadControlTool(deps) {
|
|
|
267
394
|
branch: { type: "string" },
|
|
268
395
|
sessionId: { type: "string" },
|
|
269
396
|
start: { type: "boolean" },
|
|
397
|
+
prompt: { type: "string" },
|
|
270
398
|
message: { type: "string" },
|
|
271
399
|
attachments: { type: "array", items: { type: "string" } },
|
|
400
|
+
detach: { type: "boolean" },
|
|
401
|
+
autoStopAfterTurn: { type: "boolean" },
|
|
402
|
+
timeoutMinutes: { type: "number" },
|
|
272
403
|
},
|
|
273
404
|
required: ["title"],
|
|
274
405
|
},
|
|
@@ -313,13 +444,17 @@ export function createThreadControlTool(deps) {
|
|
|
313
444
|
return { ok: true, message: "Thread loaded", data: { thread } };
|
|
314
445
|
}
|
|
315
446
|
case "create": {
|
|
447
|
+
const prompt = normalizePrompt(input.message, input.prompt);
|
|
448
|
+
if (!prompt) {
|
|
449
|
+
return { ok: false, message: "create requires non-empty `prompt`." };
|
|
450
|
+
}
|
|
316
451
|
const resolvedProvider = resolveProviderId(context);
|
|
317
452
|
if (!resolvedProvider.providerId) {
|
|
318
453
|
return { ok: false, message: resolvedProvider.error ?? "Unable to resolve a provider for this thread." };
|
|
319
454
|
}
|
|
320
455
|
const selectedProviderId = resolvedProvider.providerId;
|
|
321
456
|
const selectedDefaults = resolveSelectedThreadDefaults(context);
|
|
322
|
-
|
|
457
|
+
let thread = deps.threadService.create({
|
|
323
458
|
userId,
|
|
324
459
|
sessionId: input.sessionId,
|
|
325
460
|
title: input.title?.trim() || "New Thread",
|
|
@@ -331,10 +466,35 @@ export function createThreadControlTool(deps) {
|
|
|
331
466
|
branch: input.branch,
|
|
332
467
|
});
|
|
333
468
|
broadcastThreadEvent(thread.id, "created", { thread });
|
|
469
|
+
try {
|
|
470
|
+
const isolatedThread = await createManagedDeliveryWorktree(thread);
|
|
471
|
+
if (isolatedThread.id === thread.id && isolatedThread.workingDirectory !== thread.workingDirectory) {
|
|
472
|
+
thread = isolatedThread;
|
|
473
|
+
broadcastThreadEvent(thread.id, "updated", { thread });
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
catch (err) {
|
|
477
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
478
|
+
deps.threadService.markError(thread.id, message);
|
|
479
|
+
broadcastThreadEvent(thread.id, "status", { status: "error", error: message });
|
|
480
|
+
return {
|
|
481
|
+
ok: false,
|
|
482
|
+
message: `Thread created but failed to create delivery worktree: ${message}`,
|
|
483
|
+
data: { thread: deps.threadService.getById(thread.id) ?? thread },
|
|
484
|
+
};
|
|
485
|
+
}
|
|
334
486
|
if (!input.start) {
|
|
487
|
+
const activity = deps.threadService.addActivity(thread.id, "message", prompt.slice(0, 500), { role: "user" });
|
|
488
|
+
broadcastThreadEvent(thread.id, "activity", { activity });
|
|
335
489
|
return { ok: true, message: "Thread created", data: { thread } };
|
|
336
490
|
}
|
|
337
|
-
const started = await startThread(context, thread,
|
|
491
|
+
const started = await startThread(context, thread, {
|
|
492
|
+
message: prompt,
|
|
493
|
+
attachments: input.attachments,
|
|
494
|
+
detach: input.detach,
|
|
495
|
+
autoStopAfterTurn: input.autoStopAfterTurn,
|
|
496
|
+
timeoutMinutes: input.timeoutMinutes,
|
|
497
|
+
});
|
|
338
498
|
if (!started.ok) {
|
|
339
499
|
return {
|
|
340
500
|
ok: false,
|
|
@@ -352,6 +512,13 @@ export function createThreadControlTool(deps) {
|
|
|
352
512
|
if (!input.threads || input.threads.length === 0) {
|
|
353
513
|
return { ok: false, message: "create_many requires non-empty `threads`." };
|
|
354
514
|
}
|
|
515
|
+
const missingPrompt = input.threads.find((spec) => {
|
|
516
|
+
const prompt = normalizePrompt(spec.message, spec.prompt) ?? normalizePrompt(input.message, input.prompt);
|
|
517
|
+
return !prompt;
|
|
518
|
+
});
|
|
519
|
+
if (missingPrompt) {
|
|
520
|
+
return { ok: false, message: `create_many requires non-empty prompt for thread '${missingPrompt.title}'.` };
|
|
521
|
+
}
|
|
355
522
|
const validatedSpecs = input.threads.map((spec) => ({
|
|
356
523
|
spec,
|
|
357
524
|
resolvedProvider: resolveProviderId(context),
|
|
@@ -363,10 +530,11 @@ export function createThreadControlTool(deps) {
|
|
|
363
530
|
message: firstResolutionError.resolvedProvider.error ?? `Unable to resolve a provider for thread '${firstResolutionError.spec.title}'.`,
|
|
364
531
|
};
|
|
365
532
|
}
|
|
366
|
-
const created = validatedSpecs.map(({ spec, resolvedProvider }) => {
|
|
533
|
+
const created = await Promise.all(validatedSpecs.map(async ({ spec, resolvedProvider }) => {
|
|
367
534
|
const selectedProviderId = resolvedProvider.providerId;
|
|
368
535
|
const selectedDefaults = resolveSelectedThreadDefaults(context);
|
|
369
|
-
const
|
|
536
|
+
const prompt = (normalizePrompt(spec.message, spec.prompt) ?? normalizePrompt(input.message, input.prompt));
|
|
537
|
+
let thread = deps.threadService.create({
|
|
370
538
|
userId,
|
|
371
539
|
sessionId: spec.sessionId ?? input.sessionId,
|
|
372
540
|
title: spec.title?.trim() || "New Thread",
|
|
@@ -378,11 +546,44 @@ export function createThreadControlTool(deps) {
|
|
|
378
546
|
branch: spec.branch ?? input.branch,
|
|
379
547
|
});
|
|
380
548
|
broadcastThreadEvent(thread.id, "created", { thread });
|
|
381
|
-
|
|
382
|
-
|
|
549
|
+
try {
|
|
550
|
+
const isolatedThread = await createManagedDeliveryWorktree(thread);
|
|
551
|
+
if (isolatedThread.id === thread.id && isolatedThread.workingDirectory !== thread.workingDirectory) {
|
|
552
|
+
thread = isolatedThread;
|
|
553
|
+
broadcastThreadEvent(thread.id, "updated", { thread });
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
catch (err) {
|
|
557
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
558
|
+
deps.threadService.markError(thread.id, message);
|
|
559
|
+
broadcastThreadEvent(thread.id, "status", { status: "error", error: message });
|
|
560
|
+
return { thread: deps.threadService.getById(thread.id) ?? thread, spec, prompt, worktreeError: message };
|
|
561
|
+
}
|
|
562
|
+
return { thread, spec, prompt };
|
|
563
|
+
}));
|
|
564
|
+
const worktreeFailures = created.filter((entry) => entry.worktreeError);
|
|
565
|
+
if (worktreeFailures.length > 0) {
|
|
566
|
+
return {
|
|
567
|
+
ok: false,
|
|
568
|
+
message: `Created ${created.length} thread(s), ${worktreeFailures.length} failed to create delivery worktrees.`,
|
|
569
|
+
data: {
|
|
570
|
+
threads: created.map((entry) => entry.thread),
|
|
571
|
+
failedWorktrees: worktreeFailures.map((entry) => ({
|
|
572
|
+
threadId: entry.thread.id,
|
|
573
|
+
message: entry.worktreeError,
|
|
574
|
+
})),
|
|
575
|
+
},
|
|
576
|
+
};
|
|
577
|
+
}
|
|
383
578
|
const startTargets = created.filter(({ spec }) => spec.start === true || input.start === true);
|
|
384
|
-
const startResults = await Promise.all(startTargets.map(async ({ thread, spec }) => {
|
|
385
|
-
const started = await startThread(context, thread,
|
|
579
|
+
const startResults = await Promise.all(startTargets.map(async ({ thread, spec, prompt }) => {
|
|
580
|
+
const started = await startThread(context, thread, {
|
|
581
|
+
message: prompt,
|
|
582
|
+
attachments: spec.attachments ?? input.attachments,
|
|
583
|
+
detach: spec.detach ?? input.detach,
|
|
584
|
+
autoStopAfterTurn: spec.autoStopAfterTurn ?? input.autoStopAfterTurn,
|
|
585
|
+
timeoutMinutes: spec.timeoutMinutes ?? input.timeoutMinutes,
|
|
586
|
+
});
|
|
386
587
|
return {
|
|
387
588
|
threadId: thread.id,
|
|
388
589
|
ok: started.ok,
|
|
@@ -395,6 +596,12 @@ export function createThreadControlTool(deps) {
|
|
|
395
596
|
for (const started of startResults) {
|
|
396
597
|
threadById.set(started.threadId, started.thread);
|
|
397
598
|
}
|
|
599
|
+
for (const { thread, prompt, spec } of created) {
|
|
600
|
+
if (spec.start === true || input.start === true)
|
|
601
|
+
continue;
|
|
602
|
+
const activity = deps.threadService.addActivity(thread.id, "message", prompt.slice(0, 500), { role: "user" });
|
|
603
|
+
broadcastThreadEvent(thread.id, "activity", { activity });
|
|
604
|
+
}
|
|
398
605
|
const threads = [...threadById.values()];
|
|
399
606
|
return {
|
|
400
607
|
ok: failedStarts.length === 0,
|
|
@@ -464,10 +671,19 @@ export function createThreadControlTool(deps) {
|
|
|
464
671
|
case "start": {
|
|
465
672
|
if (!input.threadId)
|
|
466
673
|
return { ok: false, message: "start requires `threadId`." };
|
|
674
|
+
const prompt = normalizePrompt(input.message, input.prompt);
|
|
675
|
+
if (!prompt)
|
|
676
|
+
return { ok: false, message: "start requires non-empty `prompt`." };
|
|
467
677
|
const thread = getAccessibleThread(input.threadId, userId);
|
|
468
678
|
if (!thread)
|
|
469
679
|
return { ok: false, message: "Thread not found." };
|
|
470
|
-
const started = await startThread(context, thread,
|
|
680
|
+
const started = await startThread(context, thread, {
|
|
681
|
+
message: prompt,
|
|
682
|
+
attachments: input.attachments,
|
|
683
|
+
detach: input.detach,
|
|
684
|
+
autoStopAfterTurn: input.autoStopAfterTurn,
|
|
685
|
+
timeoutMinutes: input.timeoutMinutes,
|
|
686
|
+
});
|
|
471
687
|
return {
|
|
472
688
|
ok: started.ok,
|
|
473
689
|
message: started.message,
|
|
@@ -477,8 +693,9 @@ export function createThreadControlTool(deps) {
|
|
|
477
693
|
case "send": {
|
|
478
694
|
if (!input.threadId)
|
|
479
695
|
return { ok: false, message: "send requires `threadId`." };
|
|
480
|
-
|
|
481
|
-
|
|
696
|
+
const prompt = normalizePrompt(input.message, input.prompt);
|
|
697
|
+
if (!prompt)
|
|
698
|
+
return { ok: false, message: "send requires non-empty `prompt`." };
|
|
482
699
|
const thread = getAccessibleThread(input.threadId, userId);
|
|
483
700
|
if (!thread)
|
|
484
701
|
return { ok: false, message: "Thread not found." };
|
|
@@ -490,8 +707,8 @@ export function createThreadControlTool(deps) {
|
|
|
490
707
|
return { ok: false, message: `Provider '${thread.providerId}' not found` };
|
|
491
708
|
deps.threadService.update(thread.id, { status: "running", error: null, completedAt: null });
|
|
492
709
|
broadcastThreadEvent(thread.id, "status", { status: "running" });
|
|
493
|
-
await provider.sendTurn(thread.providerSessionId,
|
|
494
|
-
const activity = deps.threadService.addActivity(thread.id, "message",
|
|
710
|
+
await provider.sendTurn(thread.providerSessionId, prompt, input.attachments);
|
|
711
|
+
const activity = deps.threadService.addActivity(thread.id, "message", prompt.slice(0, 500), { role: "user" });
|
|
495
712
|
broadcastThreadEvent(thread.id, "activity", { activity });
|
|
496
713
|
return { ok: true, message: "Turn sent", data: { threadId: thread.id } };
|
|
497
714
|
}
|