@luckydraw/cumulus 1.0.44 → 1.0.45

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.
@@ -9,6 +9,7 @@
9
9
  * GET /api/thread/:name/status — thread info + adaptive state
10
10
  * GET /api/threads — list all threads
11
11
  * DELETE /api/thread/:name — delete thread (requires X-Confirm: delete)
12
+ * POST /api/thread/:name/rename — rename a thread or co-thread (body: { name })
12
13
  * POST /api/migrate — sync thread + clone repo + set projectDir
13
14
  * POST /api/media/upload — upload file to media directory (returns URL)
14
15
  * GET /media/* — serve uploaded media files (no auth)
@@ -39,6 +40,7 @@ import { allNamespaceKeys, demoMintDenial, extraMcpServersForThread, licenseBann
39
40
  import { configureVapid, sendNotification, subscribe as pushSubscribe, unsubscribe as pushUnsubscribe, } from './push.js';
40
41
  import { automatedSender, isAutomatedSender, STALL_NUDGE_MARKER } from './senders.js';
41
42
  import { definiteStallForMessage, extractDanglingSentence, isStalled, pendingWakeReason, } from './stall-detection.js';
43
+ import { executeThreadRename, planThreadRename, THREAD_FILE_EXTENSIONS, ThreadRenameError, } from './thread-rename.js';
42
44
  import { validateTranscriptFlush, ingestTranscriptFlush } from './transcript-ingest.js';
43
45
  const threadQueues = new Map();
44
46
  const threadBusy = new Map();
@@ -175,6 +177,23 @@ let threadActivityListener = null;
175
177
  export function setThreadActivityListener(fn) {
176
178
  threadActivityListener = fn;
177
179
  }
180
+ // Task 186 — a rename has to reach every open tab, or the sidebar keeps a
181
+ // name that no longer exists and its next click 404s. Same shape and the same
182
+ // reason as the activity listener above: only the adapter can reach sockets.
183
+ let threadRenameListener = null;
184
+ export function setThreadRenameListener(fn) {
185
+ threadRenameListener = fn;
186
+ }
187
+ function notifyThreadRenamed(from, to) {
188
+ if (!threadRenameListener)
189
+ return;
190
+ try {
191
+ threadRenameListener(from, to);
192
+ }
193
+ catch (err) {
194
+ console.error(`[Gateway] thread rename listener failed for "${from}": ${err instanceof Error ? err.message : String(err)}`);
195
+ }
196
+ }
178
197
  /**
179
198
  * Announce a change in whether a thread is running a turn.
180
199
  *
@@ -339,6 +358,8 @@ let federationRouter;
339
358
  * out here — the same pattern `federationRouter` above already uses.
340
359
  */
341
360
  let jobRegistry;
361
+ /** Module-level mirror of the scheduler handle: `handleRenameThread` re-arms timers (task 186). */
362
+ let schedulerHandle;
342
363
  /**
343
364
  * Pipeline options for spawning turns outside a request context (agent queue
344
365
  * drains). Set once at server startup; the object is shared with the running
@@ -1386,6 +1407,87 @@ async function handleDeleteThread(threadName, req, res) {
1386
1407
  jsonResponse(res, 500, { error: String(err) });
1387
1408
  }
1388
1409
  }
1410
+ /**
1411
+ * Re-key one server-side map from `from` to `to`, if it has an entry.
1412
+ * Exists so `handleRenameThread` lists every map once and cannot half-move one.
1413
+ */
1414
+ function moveMapKey(map, from, to) {
1415
+ if (!map.has(from))
1416
+ return;
1417
+ map.set(to, map.get(from));
1418
+ map.delete(from);
1419
+ }
1420
+ /**
1421
+ * POST /api/thread/:name/rename — body `{ name }` (task 186).
1422
+ *
1423
+ * `planThreadRename` refuses anything illegal before a byte moves;
1424
+ * `executeThreadRename` moves the on-disk rows. What is left for this handler
1425
+ * is everything that lives only in this process: caches, queues, the stall
1426
+ * timer, jobs, schedules — and telling the sockets. Each one is listed here
1427
+ * and nowhere else, so the rename module stays free of server state and this
1428
+ * handler stays the single place a name changes at runtime.
1429
+ */
1430
+ async function handleRenameThread(threadName, req, res, namespaces) {
1431
+ let body;
1432
+ try {
1433
+ body = JSON.parse(await readBody(req));
1434
+ }
1435
+ catch {
1436
+ jsonResponse(res, 400, { error: 'Invalid JSON body' });
1437
+ return;
1438
+ }
1439
+ const newName = (body.name ?? '').trim();
1440
+ if (!newName) {
1441
+ jsonResponse(res, 400, { error: 'Missing "name"' });
1442
+ return;
1443
+ }
1444
+ const exists = (name) => THREAD_FILE_EXTENSIONS.some(ext => fs.existsSync(path.join(THREADS_DIR, `${name}${ext}`)));
1445
+ try {
1446
+ const plan = await planThreadRename(threadName, newName, {
1447
+ namespaces,
1448
+ threadExists: exists,
1449
+ threadBusy: name => threadBusy.get(name) === true,
1450
+ coThreadMasterOf: resolveCoThreadMaster,
1451
+ coThreadsOf: async (master) => (await loadThreadConfigExact(master)).coThreads ?? [],
1452
+ });
1453
+ const result = await executeThreadRename(plan, {
1454
+ threadsDir: THREADS_DIR,
1455
+ projectDirFor,
1456
+ });
1457
+ for (const move of plan.moves) {
1458
+ clearThreadCache(move.from);
1459
+ threadBusy.delete(move.from);
1460
+ moveMapKey(threadQueues, move.from, move.to);
1461
+ moveMapKey(userQueues, move.from, move.to);
1462
+ moveMapKey(agentQueues, move.from, move.to);
1463
+ moveMapKey(bridgeContexts, move.from, move.to);
1464
+ moveMapKey(stallNudgeCounts, move.from, move.to);
1465
+ // A pending stall check would read the old name's history, which is gone.
1466
+ // Cancelled rather than moved: the thread is idle (rule 6) and the next
1467
+ // busy→idle edge arms a fresh one under the new name.
1468
+ const stall = stallTimers.get(move.from);
1469
+ if (stall) {
1470
+ clearTimeout(stall);
1471
+ stallTimers.delete(move.from);
1472
+ }
1473
+ jobRegistry?.rename(move.from, move.to);
1474
+ // The moved config carries the schedules; the old name's config is gone.
1475
+ schedulerHandle?.reloadThread(move.from);
1476
+ schedulerHandle?.reloadThread(move.to);
1477
+ notifyThreadRenamed(move.from, move.to);
1478
+ }
1479
+ console.log(`[Gateway] renamed thread "${plan.from}" → "${plan.to}"` +
1480
+ (plan.coThreads.length ? ` with ${plan.coThreads.length} co-thread(s)` : ''));
1481
+ jsonResponse(res, 200, { renamed: true, ...result });
1482
+ }
1483
+ catch (err) {
1484
+ if (err instanceof ThreadRenameError) {
1485
+ jsonResponse(res, err.status, { error: err.message });
1486
+ return;
1487
+ }
1488
+ jsonResponse(res, 500, { error: String(err) });
1489
+ }
1490
+ }
1389
1491
  async function handleDeleteMessages(threadName, req, res) {
1390
1492
  const confirm = req.headers['x-confirm'];
1391
1493
  if (confirm !== 'delete') {
@@ -2707,6 +2809,11 @@ export async function startGatewayServer(options) {
2707
2809
  await handleDeleteThread(params.name, req, res);
2708
2810
  return;
2709
2811
  }
2812
+ // POST /api/thread/:name/rename — rename a thread or co-thread (task 186)
2813
+ if (routePath === '/api/thread/:name/rename' && req.method === 'POST') {
2814
+ await handleRenameThread(params.name, req, res, namespaces);
2815
+ return;
2816
+ }
2710
2817
  // DELETE /api/thread/:name/messages
2711
2818
  if (routePath === '/api/thread/:name/messages' && req.method === 'DELETE') {
2712
2819
  await handleDeleteMessages(params.name, req, res);
@@ -3073,6 +3180,7 @@ export async function startGatewayServer(options) {
3073
3180
  },
3074
3181
  setScheduler: (s) => {
3075
3182
  scheduler = s;
3183
+ schedulerHandle = s;
3076
3184
  },
3077
3185
  setJobRegistry: (r) => {
3078
3186
  jobRegistry = r;