@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.
Files changed (63) hide show
  1. package/dist/routes/chat.js +1 -1
  2. package/dist/routes/chat.js.map +1 -1
  3. package/dist/security/ssrf-guard.d.ts.map +1 -1
  4. package/dist/security/ssrf-guard.js +30 -24
  5. package/dist/security/ssrf-guard.js.map +1 -1
  6. package/dist/tools/thread-tools.d.ts +16 -0
  7. package/dist/tools/thread-tools.d.ts.map +1 -1
  8. package/dist/tools/thread-tools.js +266 -49
  9. package/dist/tools/thread-tools.js.map +1 -1
  10. package/package.json +1 -1
  11. package/web-dist/assets/{_basePickBy-Wzv49Akq.js → _basePickBy-BtWPVxzz.js} +1 -1
  12. package/web-dist/assets/{_baseUniq-BSZbCXUo.js → _baseUniq-vHqPKXCy.js} +1 -1
  13. package/web-dist/assets/{arc-DpjjQy8H.js → arc-mPAb9ypF.js} +1 -1
  14. package/web-dist/assets/{architectureDiagram-2XIMDMQ5-C6zyje39.js → architectureDiagram-2XIMDMQ5-CcE8yuVs.js} +1 -1
  15. package/web-dist/assets/{blockDiagram-WCTKOSBZ-Dg5vNEHV.js → blockDiagram-WCTKOSBZ-DYITtBod.js} +1 -1
  16. package/web-dist/assets/{c4Diagram-IC4MRINW-B-CGuYcX.js → c4Diagram-IC4MRINW-CIRb-Kh_.js} +1 -1
  17. package/web-dist/assets/channel-qsrWFAj-.js +1 -0
  18. package/web-dist/assets/{chunk-4BX2VUAB-CeqTXBvl.js → chunk-4BX2VUAB-hlKC2Pyx.js} +1 -1
  19. package/web-dist/assets/{chunk-55IACEB6-BjhCFTwZ.js → chunk-55IACEB6-C-pHltjx.js} +1 -1
  20. package/web-dist/assets/{chunk-FMBD7UC4-2VH4w1Cm.js → chunk-FMBD7UC4-CsOau2pA.js} +1 -1
  21. package/web-dist/assets/{chunk-JSJVCQXG-BC0wOWQH.js → chunk-JSJVCQXG-BRMz0YLo.js} +1 -1
  22. package/web-dist/assets/{chunk-KX2RTZJC-D9HDfnhZ.js → chunk-KX2RTZJC-Bor2JuuQ.js} +1 -1
  23. package/web-dist/assets/{chunk-NQ4KR5QH-SwWifZ9F.js → chunk-NQ4KR5QH-CpSvglTz.js} +1 -1
  24. package/web-dist/assets/{chunk-QZHKN3VN-C9r4087v.js → chunk-QZHKN3VN-D2h9DuqW.js} +1 -1
  25. package/web-dist/assets/{chunk-WL4C6EOR-BMdij3qU.js → chunk-WL4C6EOR-8PvymgEe.js} +1 -1
  26. package/web-dist/assets/classDiagram-VBA2DB6C-D8LZHmHs.js +1 -0
  27. package/web-dist/assets/classDiagram-v2-RAHNMMFH-D8LZHmHs.js +1 -0
  28. package/web-dist/assets/clone-DlHeuatR.js +1 -0
  29. package/web-dist/assets/{cose-bilkent-S5V4N54A-DfIv6ajn.js → cose-bilkent-S5V4N54A-N02lYkR9.js} +1 -1
  30. package/web-dist/assets/{dagre-KLK3FWXG-BJsr2FRT.js → dagre-KLK3FWXG-oCjcr5dJ.js} +1 -1
  31. package/web-dist/assets/{diagram-E7M64L7V-CzOQpmHA.js → diagram-E7M64L7V-BXXL3E2m.js} +1 -1
  32. package/web-dist/assets/{diagram-IFDJBPK2-DBOK9xY-.js → diagram-IFDJBPK2-B_vTdJEP.js} +1 -1
  33. package/web-dist/assets/{diagram-P4PSJMXO-BV4mV-cp.js → diagram-P4PSJMXO-lNygr5wq.js} +1 -1
  34. package/web-dist/assets/{erDiagram-INFDFZHY-Ba0tZtxl.js → erDiagram-INFDFZHY-NqUNNuKV.js} +1 -1
  35. package/web-dist/assets/{flowDiagram-PKNHOUZH-B9Jj988m.js → flowDiagram-PKNHOUZH-CNSXj35q.js} +1 -1
  36. package/web-dist/assets/{ganttDiagram-A5KZAMGK-CiMqI7tz.js → ganttDiagram-A5KZAMGK-BOtS9jUX.js} +1 -1
  37. package/web-dist/assets/{gitGraphDiagram-K3NZZRJ6-mi0Lj_tL.js → gitGraphDiagram-K3NZZRJ6-dPsLGV6U.js} +1 -1
  38. package/web-dist/assets/{graph-B1mSAN-r.js → graph-D3xE5oob.js} +1 -1
  39. package/web-dist/assets/{index-Coc8XQg5.js → index-TM7LRxSq.js} +304 -304
  40. package/web-dist/assets/{infoDiagram-LFFYTUFH-BBJROgHw.js → infoDiagram-LFFYTUFH-BAhs2wr3.js} +1 -1
  41. package/web-dist/assets/{ishikawaDiagram-PHBUUO56-CvD1jJdy.js → ishikawaDiagram-PHBUUO56-CV5mTlem.js} +1 -1
  42. package/web-dist/assets/{journeyDiagram-4ABVD52K-Cee9gxbo.js → journeyDiagram-4ABVD52K-Bx_Ce0gB.js} +1 -1
  43. package/web-dist/assets/{kanban-definition-K7BYSVSG-DZx02Jgo.js → kanban-definition-K7BYSVSG-COvXbKrD.js} +1 -1
  44. package/web-dist/assets/{layout-FD28oy1q.js → layout-DSUbOKAP.js} +1 -1
  45. package/web-dist/assets/{linear-BfIO_UGQ.js → linear-CG8Kj2Ds.js} +1 -1
  46. package/web-dist/assets/{mindmap-definition-YRQLILUH-BDhKfywd.js → mindmap-definition-YRQLILUH-DvE6jHkL.js} +1 -1
  47. package/web-dist/assets/{pieDiagram-SKSYHLDU-Ckqgoc0S.js → pieDiagram-SKSYHLDU-BP2_ppdA.js} +1 -1
  48. package/web-dist/assets/{quadrantDiagram-337W2JSQ-C-tMRbqn.js → quadrantDiagram-337W2JSQ-DbvBGrKb.js} +1 -1
  49. package/web-dist/assets/{requirementDiagram-Z7DCOOCP-KiArbbU4.js → requirementDiagram-Z7DCOOCP-qxDS52L6.js} +1 -1
  50. package/web-dist/assets/{sankeyDiagram-WA2Y5GQK-BSyF4GDm.js → sankeyDiagram-WA2Y5GQK-BdZdTJJw.js} +1 -1
  51. package/web-dist/assets/{sequenceDiagram-2WXFIKYE-ChU2W22g.js → sequenceDiagram-2WXFIKYE-DxGaQsU4.js} +1 -1
  52. package/web-dist/assets/{stateDiagram-RAJIS63D-CYfNyeS2.js → stateDiagram-RAJIS63D-DDY2Tu-v.js} +1 -1
  53. package/web-dist/assets/stateDiagram-v2-FVOUBMTO-fMhX8YPS.js +1 -0
  54. package/web-dist/assets/{timeline-definition-YZTLITO2-BNGiq9yT.js → timeline-definition-YZTLITO2-Bsll_GQh.js} +1 -1
  55. package/web-dist/assets/{treemap-KZPCXAKY-7f24nLHF.js → treemap-KZPCXAKY-L2Vte7Gd.js} +1 -1
  56. package/web-dist/assets/{vennDiagram-LZ73GAT5-B5T32YFj.js → vennDiagram-LZ73GAT5-sAfl6kSd.js} +1 -1
  57. package/web-dist/assets/{xychartDiagram-JWTSCODW-BsCy1_Gu.js → xychartDiagram-JWTSCODW-DDOPb45p.js} +1 -1
  58. package/web-dist/index.html +1 -1
  59. package/web-dist/assets/channel-Cfw0FcZ8.js +0 -1
  60. package/web-dist/assets/classDiagram-VBA2DB6C-luXaKhEX.js +0 -1
  61. package/web-dist/assets/classDiagram-v2-RAHNMMFH-luXaKhEX.js +0 -1
  62. package/web-dist/assets/clone-CotxwDmM.js +0 -1
  63. 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, message, attachments) => {
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" && typeof message === "string" && message.trim().length > 0;
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
- const unsubscribe = provider.onEvent((event) => {
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
- unsubscribe();
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
- unsubscribe();
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" && autoFinishAfterFirstTurn) {
168
- void provider.stopSession(session.id).catch(() => {
169
- // Best effort: delegation threads should not linger if the provider
170
- // leaves the session open after answering the first task.
171
- });
172
- deps.threadService.markCompletedAndClearSession(effectiveThread.id);
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
- if (message) {
181
- const userActivity = deps.threadService.addActivity(effectiveThread.id, "message", message.slice(0, 500), { role: "user" });
182
- broadcastThreadEvent(effectiveThread.id, "activity", { activity: userActivity });
183
- // Run thread router for the first turn
184
- let turnMessage = message;
185
- if (historyReplayPrompt) {
186
- turnMessage = `${historyReplayPrompt}\n\n${turnMessage}`;
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
- if (deps.skillRegistry) {
189
- const availableSkills = deps.skillRegistry.listEnabled();
190
- const routingPlan = routeThread({
191
- message,
192
- availableSkills,
193
- pinnedSkillIds: effectiveThread.skillIds ? (typeof effectiveThread.skillIds === "string" ? JSON.parse(effectiveThread.skillIds) : effectiveThread.skillIds) : null,
194
- kind: effectiveThread.kind,
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
- await provider.sendTurn(session.id, turnMessage, attachments);
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: { type: "string", enum: ["delivery", "delegation"], description: "Thread kind. Delegation threads are helper workers and do not create PRs." },
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
- message: { type: "string", description: "Initial or follow-up user message for start/send." },
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
- const thread = deps.threadService.create({
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, input.message, input.attachments);
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 thread = deps.threadService.create({
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
- return { thread, spec };
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, spec.message ?? input.message, spec.attachments ?? input.attachments);
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, input.message, input.attachments);
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
- if (!input.message?.trim())
481
- return { ok: false, message: "send requires non-empty `message`." };
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, input.message, input.attachments);
494
- const activity = deps.threadService.addActivity(thread.id, "message", input.message.slice(0, 500), { role: "user" });
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
  }