claude-threads 1.25.1 → 1.26.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -12857,9 +12857,9 @@ GFS4: `);
12857
12857
  }
12858
12858
  }
12859
12859
  var fs$readdir = fs3.readdir;
12860
- fs3.readdir = readdir2;
12860
+ fs3.readdir = readdir;
12861
12861
  var noReaddirOptionVersions = /^v[0-5]\./;
12862
- function readdir2(path2, options, cb) {
12862
+ function readdir(path2, options, cb) {
12863
12863
  if (typeof options === "function")
12864
12864
  cb = options, options = null;
12865
12865
  var go$readdir = noReaddirOptionVersions.test(process.version) ? function go$readdir(path3, options2, cb2, startTime) {
@@ -14810,9 +14810,9 @@ GFS4: `);
14810
14810
  }
14811
14811
  }
14812
14812
  var fs$readdir = fs5.readdir;
14813
- fs5.readdir = readdir2;
14813
+ fs5.readdir = readdir;
14814
14814
  var noReaddirOptionVersions = /^v[0-5]\./;
14815
- function readdir2(path6, options, cb) {
14815
+ function readdir(path6, options, cb) {
14816
14816
  if (typeof options === "function")
14817
14817
  cb = options, options = null;
14818
14818
  var go$readdir = noReaddirOptionVersions.test(process.version) ? function go$readdir(path7, options2, cb2, startTime) {
@@ -20451,7 +20451,7 @@ var require_react_reconciler_development = __commonJS((exports, module) => {
20451
20451
  return hook.checkDCE ? true : false;
20452
20452
  }
20453
20453
  function setIsStrictModeForDevtools(newIsStrictMode) {
20454
- typeof log39 === "function" && unstable_setDisableYieldValue2(newIsStrictMode);
20454
+ typeof log43 === "function" && unstable_setDisableYieldValue2(newIsStrictMode);
20455
20455
  if (injectedHook && typeof injectedHook.setStrictMode === "function")
20456
20456
  try {
20457
20457
  injectedHook.setStrictMode(rendererID, newIsStrictMode);
@@ -28535,7 +28535,7 @@ Check the render method of %s.`, getComponentNameFromFiber(current) || "Unknown"
28535
28535
  var fiberStack = [];
28536
28536
  var index$jscomp$0 = -1, emptyContextObject = {};
28537
28537
  Object.freeze(emptyContextObject);
28538
- var clz32 = Math.clz32 ? Math.clz32 : clz32Fallback, log$1 = Math.log, LN2 = Math.LN2, nextTransitionUpdateLane = 256, nextTransitionDeferredLane = 262144, nextRetryLane = 4194304, scheduleCallback$3 = Scheduler.unstable_scheduleCallback, cancelCallback$1 = Scheduler.unstable_cancelCallback, shouldYield = Scheduler.unstable_shouldYield, requestPaint = Scheduler.unstable_requestPaint, now$1 = Scheduler.unstable_now, ImmediatePriority = Scheduler.unstable_ImmediatePriority, UserBlockingPriority = Scheduler.unstable_UserBlockingPriority, NormalPriority$1 = Scheduler.unstable_NormalPriority, IdlePriority = Scheduler.unstable_IdlePriority, log39 = Scheduler.log, unstable_setDisableYieldValue2 = Scheduler.unstable_setDisableYieldValue, rendererID = null, injectedHook = null, hasLoggedError = false, isDevToolsPresent = typeof __REACT_DEVTOOLS_GLOBAL_HOOK__ !== "undefined", lastResetTime = 0;
28538
+ var clz32 = Math.clz32 ? Math.clz32 : clz32Fallback, log$1 = Math.log, LN2 = Math.LN2, nextTransitionUpdateLane = 256, nextTransitionDeferredLane = 262144, nextRetryLane = 4194304, scheduleCallback$3 = Scheduler.unstable_scheduleCallback, cancelCallback$1 = Scheduler.unstable_cancelCallback, shouldYield = Scheduler.unstable_shouldYield, requestPaint = Scheduler.unstable_requestPaint, now$1 = Scheduler.unstable_now, ImmediatePriority = Scheduler.unstable_ImmediatePriority, UserBlockingPriority = Scheduler.unstable_UserBlockingPriority, NormalPriority$1 = Scheduler.unstable_NormalPriority, IdlePriority = Scheduler.unstable_IdlePriority, log43 = Scheduler.log, unstable_setDisableYieldValue2 = Scheduler.unstable_setDisableYieldValue, rendererID = null, injectedHook = null, hasLoggedError = false, isDevToolsPresent = typeof __REACT_DEVTOOLS_GLOBAL_HOOK__ !== "undefined", lastResetTime = 0;
28539
28539
  if (typeof performance === "object" && typeof performance.now === "function") {
28540
28540
  var localPerformance = performance;
28541
28541
  var getCurrentTime = function() {
@@ -51226,6 +51226,14 @@ function resolveMemoryConfig(value, fieldPath) {
51226
51226
  console.warn(`Invalid ${fieldPath ?? "memory"} config: expected boolean or {enabled, repoLayer, channelLayer, distillation}, got ${JSON.stringify(value)} — using defaults`);
51227
51227
  return DEFAULT_MEMORY_CONFIG;
51228
51228
  }
51229
+ function resolveRoutinesEnabled(value, fieldPath) {
51230
+ if (value === undefined || value === null || value === true)
51231
+ return true;
51232
+ if (value === false)
51233
+ return false;
51234
+ console.warn(`Invalid ${fieldPath ?? "routines"} config: expected boolean, got ${JSON.stringify(value)} — routines stay enabled`);
51235
+ return true;
51236
+ }
51229
51237
  var LIMITS_DEFAULTS = {
51230
51238
  maxSessions: 5,
51231
51239
  sessionTimeoutMinutes: 30,
@@ -51234,7 +51242,8 @@ var LIMITS_DEFAULTS = {
51234
51242
  maxWorktreeAgeHours: 24,
51235
51243
  cleanupWorktrees: true,
51236
51244
  permissionTimeoutSeconds: 120,
51237
- flushDelayMs: 500
51245
+ flushDelayMs: 500,
51246
+ maxRoutines: 10
51238
51247
  };
51239
51248
  function resolveLimits(limits) {
51240
51249
  const envMaxSessions = process.env.MAX_SESSIONS ? parseInt(process.env.MAX_SESSIONS, 10) : undefined;
@@ -51247,7 +51256,8 @@ function resolveLimits(limits) {
51247
51256
  maxWorktreeAgeHours: limits?.maxWorktreeAgeHours ?? LIMITS_DEFAULTS.maxWorktreeAgeHours,
51248
51257
  cleanupWorktrees: limits?.cleanupWorktrees ?? LIMITS_DEFAULTS.cleanupWorktrees,
51249
51258
  permissionTimeoutSeconds: limits?.permissionTimeoutSeconds ?? LIMITS_DEFAULTS.permissionTimeoutSeconds,
51250
- flushDelayMs: limits?.flushDelayMs ?? LIMITS_DEFAULTS.flushDelayMs
51259
+ flushDelayMs: limits?.flushDelayMs ?? LIMITS_DEFAULTS.flushDelayMs,
51260
+ maxRoutines: limits?.maxRoutines ?? LIMITS_DEFAULTS.maxRoutines
51251
51261
  };
51252
51262
  }
51253
51263
  function resolvePermissionMode(opts) {
@@ -56178,11 +56188,570 @@ async function resolveSessionMemory(memoryStore, memoryConfig, platformId, worki
56178
56188
  }
56179
56189
  }
56180
56190
 
56181
- // src/claude/account-pool.ts
56191
+ // src/persistence/routines-store.ts
56192
+ import { existsSync as existsSync8, mkdirSync as mkdirSync5, readFileSync as readFileSync7 } from "fs";
56193
+ import { homedir as homedir6 } from "os";
56194
+ import { join as join7 } from "path";
56195
+ import { randomUUID as randomUUID2 } from "crypto";
56182
56196
  init_logger();
56183
56197
 
56184
- // src/claude/usage-probe.ts
56185
- init_spawn();
56198
+ // src/persistence/atomic-file.ts
56199
+ import { chmodSync as chmodSync5, renameSync as renameSync4, writeFileSync as writeFileSync5 } from "fs";
56200
+
56201
+ class SerialQueue {
56202
+ tail = Promise.resolve();
56203
+ run(fn) {
56204
+ const next = this.tail.then(fn, fn);
56205
+ this.tail = next.catch(() => {
56206
+ return;
56207
+ });
56208
+ return next;
56209
+ }
56210
+ }
56211
+ function writeFileAtomic(file, content) {
56212
+ const tempFile = `${file}.tmp`;
56213
+ writeFileSync5(tempFile, content, { encoding: "utf-8", mode: 384 });
56214
+ renameSync4(tempFile, file);
56215
+ chmodSync5(file, 384);
56216
+ }
56217
+
56218
+ // src/persistence/routines-store.ts
56219
+ var log10 = createLogger("routines");
56220
+ var DEFAULT_CONFIG_DIR3 = join7(homedir6(), ".config", "claude-threads");
56221
+ var DEFAULT_FILE2 = join7(DEFAULT_CONFIG_DIR3, "routines.yaml");
56222
+ var STORE_VERSION3 = 1;
56223
+ var MAX_CONSECUTIVE_FAILURES = 3;
56224
+ var DEFAULT_MAX_ROUTINES = 10;
56225
+ var SCHEDULE_PRESETS = ["hourly", "daily", "weekdays", "weekly"];
56226
+ var TIME_RE = /^([01]\d|2[0-3]):[0-5]\d$/;
56227
+ function isValidTimezone(tz) {
56228
+ if (typeof tz !== "string" || !tz)
56229
+ return false;
56230
+ try {
56231
+ new Intl.DateTimeFormat("en-US", { timeZone: tz });
56232
+ return true;
56233
+ } catch {
56234
+ return false;
56235
+ }
56236
+ }
56237
+ function validateSchedule(schedule) {
56238
+ if (!SCHEDULE_PRESETS.includes(schedule.preset)) {
56239
+ return `unknown preset "${String(schedule.preset)}" (expected ${SCHEDULE_PRESETS.join("/")})`;
56240
+ }
56241
+ if (!isValidTimezone(schedule.timezone)) {
56242
+ return `invalid timezone "${String(schedule.timezone)}"`;
56243
+ }
56244
+ if (schedule.preset === "hourly") {
56245
+ return null;
56246
+ }
56247
+ if (!schedule.time || !TIME_RE.test(schedule.time)) {
56248
+ return `invalid time "${String(schedule.time)}" (expected HH:MM, 24h)`;
56249
+ }
56250
+ if (schedule.preset === "weekly") {
56251
+ const weekday = schedule.weekday;
56252
+ if (typeof weekday !== "number" || !Number.isInteger(weekday) || weekday < 1 || weekday > 7) {
56253
+ return `invalid weekday "${String(weekday)}" (expected 1=Mon … 7=Sun)`;
56254
+ }
56255
+ }
56256
+ return null;
56257
+ }
56258
+ function describeSchedule(schedule) {
56259
+ const WEEKDAYS = ["", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"];
56260
+ switch (schedule.preset) {
56261
+ case "hourly":
56262
+ return `hourly (${schedule.timezone})`;
56263
+ case "daily":
56264
+ return `daily at ${schedule.time} (${schedule.timezone})`;
56265
+ case "weekdays":
56266
+ return `weekdays at ${schedule.time} (${schedule.timezone})`;
56267
+ case "weekly":
56268
+ return `weekly on ${WEEKDAYS[schedule.weekday ?? 0] || "?"} at ${schedule.time} (${schedule.timezone})`;
56269
+ }
56270
+ }
56271
+
56272
+ class RoutinesStore {
56273
+ file;
56274
+ configDir;
56275
+ queue = new SerialQueue;
56276
+ constructor(filePath) {
56277
+ const effective = filePath ?? process.env.CLAUDE_THREADS_ROUTINES_PATH;
56278
+ if (effective) {
56279
+ this.file = effective;
56280
+ this.configDir = join7(effective, "..");
56281
+ } else {
56282
+ this.file = DEFAULT_FILE2;
56283
+ this.configDir = DEFAULT_CONFIG_DIR3;
56284
+ }
56285
+ if (!existsSync8(this.configDir)) {
56286
+ mkdirSync5(this.configDir, { recursive: true, mode: 448 });
56287
+ }
56288
+ }
56289
+ list(platformId) {
56290
+ return this.loadRaw().routines[platformId] ?? [];
56291
+ }
56292
+ get(platformId, id) {
56293
+ return this.list(platformId).find((r) => r.id === id);
56294
+ }
56295
+ add(platformId, routine, maxRoutines = DEFAULT_MAX_ROUTINES) {
56296
+ return this.runExclusive(() => {
56297
+ const scheduleError = validateSchedule(routine.schedule);
56298
+ if (scheduleError)
56299
+ return { ok: false, error: scheduleError };
56300
+ const name = routine.name.trim().slice(0, 80);
56301
+ const prompt = routine.prompt.trim().slice(0, 2000);
56302
+ if (!name || !prompt)
56303
+ return { ok: false, error: "name and prompt are required" };
56304
+ const data = this.loadRaw();
56305
+ const existing = data.routines[platformId] ?? [];
56306
+ if (existing.length >= maxRoutines) {
56307
+ return { ok: false, error: `routine limit reached (${maxRoutines}); delete one first` };
56308
+ }
56309
+ const full = {
56310
+ ...routine,
56311
+ name,
56312
+ prompt,
56313
+ id: randomUUID2().slice(0, 8),
56314
+ createdAt: new Date().toISOString(),
56315
+ enabled: true,
56316
+ consecutiveFailures: 0
56317
+ };
56318
+ data.routines[platformId] = [...existing, full];
56319
+ this.writeAtomic(data);
56320
+ log10.info(`Routine "${full.name}" created on ${platformId} by @${full.createdBy}`);
56321
+ return { ok: true, routine: full };
56322
+ });
56323
+ }
56324
+ update(platformId, id, patch) {
56325
+ return this.runExclusive(() => {
56326
+ const data = this.loadRaw();
56327
+ const routines = data.routines[platformId] ?? [];
56328
+ const idx = routines.findIndex((r) => r.id === id);
56329
+ if (idx < 0)
56330
+ return;
56331
+ routines[idx] = { ...routines[idx], ...patch };
56332
+ this.writeAtomic(data);
56333
+ return routines[idx];
56334
+ });
56335
+ }
56336
+ remove(platformId, id) {
56337
+ return this.runExclusive(() => {
56338
+ const data = this.loadRaw();
56339
+ const routines = data.routines[platformId] ?? [];
56340
+ const idx = routines.findIndex((r) => r.id === id);
56341
+ if (idx < 0)
56342
+ return;
56343
+ const [removed] = routines.splice(idx, 1);
56344
+ if (routines.length === 0)
56345
+ delete data.routines[platformId];
56346
+ this.writeAtomic(data);
56347
+ log10.info(`Routine "${removed.name}" removed from ${platformId}`);
56348
+ return removed;
56349
+ });
56350
+ }
56351
+ runExclusive(fn) {
56352
+ return this.queue.run(fn);
56353
+ }
56354
+ loadRaw() {
56355
+ if (!existsSync8(this.file)) {
56356
+ return { version: STORE_VERSION3, routines: {} };
56357
+ }
56358
+ try {
56359
+ const parsed = yaml.load(readFileSync7(this.file, "utf-8"));
56360
+ if (!parsed || typeof parsed !== "object") {
56361
+ return { version: STORE_VERSION3, routines: {} };
56362
+ }
56363
+ const routines = parsed.routines && typeof parsed.routines === "object" ? parsed.routines : {};
56364
+ for (const list of Object.values(routines)) {
56365
+ for (const r of list) {
56366
+ r.enabled = r.enabled ?? true;
56367
+ r.consecutiveFailures = r.consecutiveFailures ?? 0;
56368
+ }
56369
+ }
56370
+ return { version: parsed.version ?? STORE_VERSION3, routines };
56371
+ } catch (err) {
56372
+ log10.warn(`Failed to read ${this.file}: ${err.message} — starting empty`);
56373
+ return { version: STORE_VERSION3, routines: {} };
56374
+ }
56375
+ }
56376
+ writeAtomic(data) {
56377
+ writeFileAtomic(this.file, yaml.dump(data, { sortKeys: true, lineWidth: -1 }));
56378
+ }
56379
+ }
56380
+
56381
+ // src/routines/scheduler.ts
56382
+ init_logger();
56383
+ var log11 = createLogger("routines");
56384
+ var DEFAULT_INTERVAL_MS = 60 * 1000;
56385
+ var FIRE_WINDOW_MS = 5 * 60 * 1000;
56386
+ var WEEKDAY_TO_ISO = {
56387
+ Mon: 1,
56388
+ Tue: 2,
56389
+ Wed: 3,
56390
+ Thu: 4,
56391
+ Fri: 5,
56392
+ Sat: 6,
56393
+ Sun: 7
56394
+ };
56395
+ function getLocalParts(date, timeZone) {
56396
+ const fmt = new Intl.DateTimeFormat("en-US", {
56397
+ timeZone,
56398
+ year: "numeric",
56399
+ month: "2-digit",
56400
+ day: "2-digit",
56401
+ hour: "2-digit",
56402
+ minute: "2-digit",
56403
+ weekday: "short",
56404
+ hour12: false
56405
+ });
56406
+ const parts = {};
56407
+ for (const p of fmt.formatToParts(date)) {
56408
+ parts[p.type] = p.value;
56409
+ }
56410
+ return {
56411
+ year: parseInt(parts.year, 10),
56412
+ month: parseInt(parts.month, 10),
56413
+ day: parseInt(parts.day, 10),
56414
+ hour: parseInt(parts.hour, 10) % 24,
56415
+ minute: parseInt(parts.minute, 10),
56416
+ isoWeekday: WEEKDAY_TO_ISO[parts.weekday] ?? 0
56417
+ };
56418
+ }
56419
+ function periodKey(routine, date) {
56420
+ const p = getLocalParts(date, routine.schedule.timezone);
56421
+ const day = `${p.year}-${String(p.month).padStart(2, "0")}-${String(p.day).padStart(2, "0")}`;
56422
+ return routine.schedule.preset === "hourly" ? `${day}T${String(p.hour).padStart(2, "0")}` : day;
56423
+ }
56424
+ function isInFireWindow(routine, now) {
56425
+ const { schedule } = routine;
56426
+ const p = getLocalParts(now, schedule.timezone);
56427
+ if (schedule.preset === "hourly") {
56428
+ return p.minute * 60 * 1000 < FIRE_WINDOW_MS;
56429
+ }
56430
+ if (schedule.preset === "weekdays" && (p.isoWeekday < 1 || p.isoWeekday > 5))
56431
+ return false;
56432
+ if (schedule.preset === "weekly" && p.isoWeekday !== schedule.weekday)
56433
+ return false;
56434
+ const [hh, mm] = (schedule.time ?? "00:00").split(":").map((s) => parseInt(s, 10));
56435
+ const nowMs = (p.hour * 60 + p.minute) * 60 * 1000;
56436
+ const schedMs = (hh * 60 + mm) * 60 * 1000;
56437
+ if (nowMs >= schedMs && nowMs - schedMs < FIRE_WINDOW_MS)
56438
+ return true;
56439
+ const prev = getLocalParts(new Date(now.getTime() - FIRE_WINDOW_MS), schedule.timezone);
56440
+ const sameDay = prev.year === p.year && prev.month === p.month && prev.day === p.day;
56441
+ const prevMs = (prev.hour * 60 + prev.minute) * 60 * 1000;
56442
+ return sameDay && prevMs < schedMs && nowMs >= schedMs;
56443
+ }
56444
+ function isRoutineDue(routine, now) {
56445
+ if (!routine.enabled)
56446
+ return false;
56447
+ if (!isInFireWindow(routine, now))
56448
+ return false;
56449
+ if (routine.lastRunAt) {
56450
+ const last = new Date(routine.lastRunAt);
56451
+ if (!Number.isNaN(last.getTime()) && periodKey(routine, last) === periodKey(routine, now)) {
56452
+ return false;
56453
+ }
56454
+ }
56455
+ return true;
56456
+ }
56457
+
56458
+ class RoutineScheduler {
56459
+ opts;
56460
+ intervalMs;
56461
+ timer = null;
56462
+ ticking = false;
56463
+ constructor(opts) {
56464
+ this.opts = opts;
56465
+ this.intervalMs = opts.intervalMs ?? DEFAULT_INTERVAL_MS;
56466
+ }
56467
+ start() {
56468
+ if (this.timer)
56469
+ return;
56470
+ const safeTick = () => this.tick(new Date).catch((err) => {
56471
+ log11.error(`Routine scheduler tick failed: ${err.message}`);
56472
+ });
56473
+ this.timer = setInterval(safeTick, this.intervalMs);
56474
+ safeTick();
56475
+ log11.debug(`Routine scheduler started (interval: ${this.intervalMs / 1000}s)`);
56476
+ }
56477
+ stop() {
56478
+ if (this.timer) {
56479
+ clearInterval(this.timer);
56480
+ this.timer = null;
56481
+ }
56482
+ }
56483
+ async tick(now) {
56484
+ if (this.ticking)
56485
+ return;
56486
+ this.ticking = true;
56487
+ try {
56488
+ for (const platformId of this.opts.listPlatformIds()) {
56489
+ if (!this.opts.isRoutinesEnabled(platformId))
56490
+ continue;
56491
+ for (const routine of this.opts.store.list(platformId)) {
56492
+ if (!isRoutineDue(routine, now))
56493
+ continue;
56494
+ await this.fire(platformId, routine, now);
56495
+ }
56496
+ }
56497
+ } finally {
56498
+ this.ticking = false;
56499
+ }
56500
+ }
56501
+ async fire(platformId, routine, now, anchorPeriod = true) {
56502
+ let status;
56503
+ try {
56504
+ status = await this.opts.fireRoutine(platformId, routine);
56505
+ } catch (err) {
56506
+ log11.warn(`Routine "${routine.name}" (${platformId}) failed: ${err.message}`);
56507
+ status = "failed";
56508
+ }
56509
+ try {
56510
+ if (status === "unauthorized") {
56511
+ await this.opts.store.update(platformId, routine.id, { enabled: false, lastRunStatus: "failed" });
56512
+ await this.opts.notifyDisabled(platformId, routine, `its creator @${routine.createdBy} is no longer authorized on this platform`);
56513
+ } else if (status === "skipped") {
56514
+ await this.opts.store.update(platformId, routine.id, { lastRunStatus: "skipped" });
56515
+ } else if (!anchorPeriod) {
56516
+ await this.opts.store.update(platformId, routine.id, { lastRunStatus: status });
56517
+ } else {
56518
+ const failures = status === "failed" ? routine.consecutiveFailures + 1 : 0;
56519
+ await this.opts.store.update(platformId, routine.id, {
56520
+ lastRunAt: now.toISOString(),
56521
+ lastRunStatus: status,
56522
+ consecutiveFailures: failures
56523
+ });
56524
+ if (failures >= MAX_CONSECUTIVE_FAILURES) {
56525
+ await this.opts.store.update(platformId, routine.id, { enabled: false });
56526
+ await this.opts.notifyDisabled(platformId, routine, `${failures} consecutive runs failed`);
56527
+ }
56528
+ }
56529
+ } catch (err) {
56530
+ log11.error(`Routine "${routine.name}" (${platformId}) bookkeeping failed: ${err.message}`);
56531
+ }
56532
+ return status;
56533
+ }
56534
+ }
56535
+
56536
+ // src/session/authorization.ts
56537
+ function isAuthorizedForSession(check) {
56538
+ const { username, platform, sessionAllowedUsers } = check;
56539
+ if (!username || username === "unknown") {
56540
+ return false;
56541
+ }
56542
+ if (platform.isUserAllowed(username)) {
56543
+ return true;
56544
+ }
56545
+ return sessionAllowedUsers?.has(username) ?? false;
56546
+ }
56547
+
56548
+ // src/session/lifecycle-fsm.ts
56549
+ init_logger();
56550
+ var log12 = createLogger("fsm");
56551
+ var ALLOWED_TRANSITIONS = {
56552
+ starting: new Set(["active", "paused", "interrupted", "cancelling", "restarting"]),
56553
+ active: new Set([
56554
+ "active",
56555
+ "processing",
56556
+ "paused",
56557
+ "interrupted",
56558
+ "restarting",
56559
+ "cancelling",
56560
+ "ending"
56561
+ ]),
56562
+ processing: new Set([
56563
+ "active",
56564
+ "paused",
56565
+ "interrupted",
56566
+ "restarting",
56567
+ "cancelling"
56568
+ ]),
56569
+ paused: new Set(["active", "cancelling", "restarting"]),
56570
+ interrupted: new Set(["active", "cancelling", "restarting", "paused"]),
56571
+ restarting: new Set(["active", "paused", "cancelling"]),
56572
+ cancelling: new Set(["ending"]),
56573
+ ending: new Set
56574
+ };
56575
+ function checkTransition(from, to, sessionId) {
56576
+ const allowed = ALLOWED_TRANSITIONS[from];
56577
+ if (allowed.has(to))
56578
+ return;
56579
+ const msg = `illegal lifecycle transition ${from} -> ${to}`;
56580
+ const payload = {
56581
+ event: "fsm.illegal_transition",
56582
+ from,
56583
+ to,
56584
+ sessionId
56585
+ };
56586
+ if (process.env.CLAUDE_THREADS_FSM_STRICT === "1") {
56587
+ throw new Error(`${msg} (sessionId=${sessionId})`);
56588
+ }
56589
+ log12.warn(msg, payload);
56590
+ }
56591
+
56592
+ // src/session/timer-manager.ts
56593
+ function createSessionTimers() {
56594
+ return {
56595
+ updateTimer: null,
56596
+ typingTimer: null,
56597
+ statusBarTimer: null
56598
+ };
56599
+ }
56600
+ function clearAllTimers(timers) {
56601
+ if (timers.updateTimer) {
56602
+ clearTimeout(timers.updateTimer);
56603
+ timers.updateTimer = null;
56604
+ }
56605
+ if (timers.typingTimer) {
56606
+ clearInterval(timers.typingTimer);
56607
+ timers.typingTimer = null;
56608
+ }
56609
+ if (timers.statusBarTimer) {
56610
+ clearInterval(timers.statusBarTimer);
56611
+ timers.statusBarTimer = null;
56612
+ }
56613
+ }
56614
+
56615
+ // src/session/types.ts
56616
+ function createSessionLifecycle() {
56617
+ return {
56618
+ state: "starting",
56619
+ resumeFailCount: 0,
56620
+ hasClaudeResponded: false
56621
+ };
56622
+ }
56623
+ function createResumedLifecycle(resumeFailCount = 0) {
56624
+ return {
56625
+ state: "active",
56626
+ resumeFailCount,
56627
+ hasClaudeResponded: true
56628
+ };
56629
+ }
56630
+ function isSessionRestarting(session) {
56631
+ return session.lifecycle.state === "restarting";
56632
+ }
56633
+ function isSessionCancelled(session) {
56634
+ return session.lifecycle.state === "cancelling";
56635
+ }
56636
+ function transitionTo(session, newState) {
56637
+ checkTransition(session.lifecycle.state, newState, session.sessionId);
56638
+ session.lifecycle.state = newState;
56639
+ }
56640
+ function markClaudeResponded(session) {
56641
+ session.lifecycle.hasClaudeResponded = true;
56642
+ if (session.lifecycle.state === "starting") {
56643
+ session.lifecycle.state = "active";
56644
+ }
56645
+ }
56646
+ function getSessionStatus(session) {
56647
+ if (session.isProcessing) {
56648
+ return session.lifecycle.hasClaudeResponded ? "active" : "starting";
56649
+ }
56650
+ return "idle";
56651
+ }
56652
+
56653
+ // src/mcp/decision-bridge.ts
56654
+ import { createServer, createConnection } from "node:net";
56655
+ import { tmpdir } from "node:os";
56656
+ import { join as join8 } from "node:path";
56657
+ import { randomUUID as randomUUID3 } from "node:crypto";
56658
+ import { mkdtempSync } from "node:fs";
56659
+ import { rm } from "node:fs/promises";
56660
+
56661
+ class BridgeUnavailableError extends Error {
56662
+ }
56663
+ function bridgeSocketPath() {
56664
+ if (process.platform === "win32") {
56665
+ return `\\\\.\\pipe\\ctb-${randomUUID3()}`;
56666
+ }
56667
+ const dir = mkdtempSync(join8(tmpdir(), "ctb-"));
56668
+ return join8(dir, "b.sock");
56669
+ }
56670
+
56671
+ class DecisionBridgeServer {
56672
+ server;
56673
+ path;
56674
+ liveSockets = new Set;
56675
+ constructor(server, path2) {
56676
+ this.server = server;
56677
+ this.path = path2;
56678
+ }
56679
+ static async create(handler) {
56680
+ const path2 = bridgeSocketPath();
56681
+ const liveSockets = new Set;
56682
+ const server = createServer((socket) => {
56683
+ liveSockets.add(socket);
56684
+ socket.once("close", () => liveSockets.delete(socket));
56685
+ let buffer = "";
56686
+ const aborter = new AbortController;
56687
+ let responded = false;
56688
+ socket.on("data", (chunk) => {
56689
+ buffer += chunk.toString("utf8");
56690
+ const newline = buffer.indexOf(`
56691
+ `);
56692
+ if (newline === -1)
56693
+ return;
56694
+ const line = buffer.slice(0, newline);
56695
+ buffer = "";
56696
+ let request;
56697
+ try {
56698
+ request = JSON.parse(line);
56699
+ } catch {
56700
+ responded = true;
56701
+ socket.end(JSON.stringify({ behavior: "deny", message: "Malformed bridge request" }) + `
56702
+ `);
56703
+ return;
56704
+ }
56705
+ handler(request, aborter.signal).then((response) => {
56706
+ responded = true;
56707
+ socket.end(JSON.stringify(response) + `
56708
+ `);
56709
+ }).catch((err) => {
56710
+ responded = true;
56711
+ if (err instanceof BridgeUnavailableError) {
56712
+ socket.destroy();
56713
+ return;
56714
+ }
56715
+ socket.end(JSON.stringify({
56716
+ behavior: "deny",
56717
+ message: `Bridge handler failed: ${err instanceof Error ? err.message : String(err)}`
56718
+ }) + `
56719
+ `);
56720
+ });
56721
+ });
56722
+ socket.on("close", () => {
56723
+ if (!responded)
56724
+ aborter.abort();
56725
+ });
56726
+ socket.on("error", () => {});
56727
+ });
56728
+ try {
56729
+ await new Promise((resolve4, reject) => {
56730
+ server.once("error", reject);
56731
+ server.listen(path2, () => {
56732
+ server.removeListener("error", reject);
56733
+ resolve4();
56734
+ });
56735
+ });
56736
+ } catch (err) {
56737
+ if (process.platform !== "win32") {
56738
+ await rm(join8(path2, ".."), { recursive: true, force: true }).catch(() => {});
56739
+ }
56740
+ throw err;
56741
+ }
56742
+ const bridge = new DecisionBridgeServer(server, path2);
56743
+ bridge.liveSockets = liveSockets;
56744
+ return bridge;
56745
+ }
56746
+ async close() {
56747
+ for (const socket of this.liveSockets)
56748
+ socket.destroy();
56749
+ await new Promise((resolve4) => this.server.close(() => resolve4()));
56750
+ if (process.platform !== "win32") {
56751
+ await rm(join8(this.path, ".."), { recursive: true, force: true }).catch(() => {});
56752
+ }
56753
+ }
56754
+ }
56186
56755
 
56187
56756
  // src/claude/cli.ts
56188
56757
  init_spawn();
@@ -56190,9 +56759,9 @@ init_logger();
56190
56759
  import { EventEmitter as EventEmitter2 } from "events";
56191
56760
  import { resolve as resolve4, dirname as dirname6 } from "path";
56192
56761
  import { fileURLToPath as fileURLToPath3 } from "url";
56193
- import { existsSync as existsSync8, readFileSync as readFileSync7, watchFile, unwatchFile, unlinkSync, statSync, readdirSync, writeFileSync as writeFileSync5 } from "fs";
56194
- import { tmpdir } from "os";
56195
- import { join as join7 } from "path";
56762
+ import { existsSync as existsSync9, readFileSync as readFileSync8, watchFile, unwatchFile, unlinkSync, statSync, readdirSync, writeFileSync as writeFileSync6 } from "fs";
56763
+ import { tmpdir as tmpdir2 } from "os";
56764
+ import { join as join9 } from "path";
56196
56765
 
56197
56766
  // src/mcp/outbound-env.ts
56198
56767
  var OUTBOUND_ENV = {
@@ -56283,25 +56852,25 @@ function parseRateLimitEvent(event, now = Date.now()) {
56283
56852
  }
56284
56853
 
56285
56854
  // src/claude/cli.ts
56286
- var log10 = createLogger("claude");
56855
+ var log13 = createLogger("claude");
56287
56856
  function cleanupBrowserBridgeSockets() {
56288
56857
  try {
56289
- const tempDir = tmpdir();
56858
+ const tempDir = tmpdir2();
56290
56859
  const files = readdirSync(tempDir);
56291
56860
  for (const file of files) {
56292
56861
  if (file.startsWith("claude-mcp-browser-bridge-")) {
56293
- const filePath = join7(tempDir, file);
56862
+ const filePath = join9(tempDir, file);
56294
56863
  try {
56295
56864
  const stats = statSync(filePath);
56296
56865
  if (stats.isSocket()) {
56297
56866
  unlinkSync(filePath);
56298
- log10.debug(`Removed stale browser bridge socket: ${file}`);
56867
+ log13.debug(`Removed stale browser bridge socket: ${file}`);
56299
56868
  }
56300
56869
  } catch {}
56301
56870
  }
56302
56871
  }
56303
56872
  } catch (err) {
56304
- log10.debug(`Browser bridge cleanup failed: ${err}`);
56873
+ log13.debug(`Browser bridge cleanup failed: ${err}`);
56305
56874
  }
56306
56875
  }
56307
56876
  function buildClaudeChildEnv(parentEnv, account, opts) {
@@ -56359,9 +56928,9 @@ function materializeMcpConfig(config, sessionId, opts = {}) {
56359
56928
  if (opts.inline) {
56360
56929
  return { mode: "inline", value: JSON.stringify(config) };
56361
56930
  }
56362
- const dir = opts.tmpDirOverride ?? tmpdir();
56363
- const path2 = join7(dir, `claude-threads-mcp-${sessionId ?? process.pid}-${Date.now()}.json`);
56364
- writeFileSync5(path2, JSON.stringify(config), { mode: 384 });
56931
+ const dir = opts.tmpDirOverride ?? tmpdir2();
56932
+ const path2 = join9(dir, `claude-threads-mcp-${sessionId ?? process.pid}-${Date.now()}.json`);
56933
+ writeFileSync6(path2, JSON.stringify(config), { mode: 384 });
56365
56934
  return { mode: "file", path: path2 };
56366
56935
  }
56367
56936
  function buildPermissionArgs(opts) {
@@ -56461,8 +57030,8 @@ class ClaudeCli extends EventEmitter2 {
56461
57030
  if (!this.statusFilePath)
56462
57031
  return null;
56463
57032
  try {
56464
- if (existsSync8(this.statusFilePath)) {
56465
- const data = readFileSync7(this.statusFilePath, "utf8");
57033
+ if (existsSync9(this.statusFilePath)) {
57034
+ const data = readFileSync8(this.statusFilePath, "utf8");
56466
57035
  this.lastStatusData = JSON.parse(data);
56467
57036
  }
56468
57037
  } catch (err) {
@@ -56489,7 +57058,7 @@ class ClaudeCli extends EventEmitter2 {
56489
57058
  if (this.statusFilePath) {
56490
57059
  unwatchFile(this.statusFilePath);
56491
57060
  try {
56492
- if (existsSync8(this.statusFilePath)) {
57061
+ if (existsSync9(this.statusFilePath)) {
56493
57062
  unlinkSync(this.statusFilePath);
56494
57063
  }
56495
57064
  } catch {}
@@ -56543,7 +57112,7 @@ class ClaudeCli extends EventEmitter2 {
56543
57112
  }
56544
57113
  let statusLineCommand;
56545
57114
  if (this.options.sessionId) {
56546
- this.statusFilePath = join7(tmpdir(), `claude-threads-status-${this.options.sessionId}.json`);
57115
+ this.statusFilePath = join9(tmpdir2(), `claude-threads-status-${this.options.sessionId}.json`);
56547
57116
  const statusLineWriterPath = this.getStatusLineWriterPath();
56548
57117
  const runtime = runtimeForScriptPath(statusLineWriterPath);
56549
57118
  statusLineCommand = `${runtime} ${statusLineWriterPath} ${this.options.sessionId}`;
@@ -56780,15 +57349,15 @@ class ClaudeCli extends EventEmitter2 {
56780
57349
  const __filename2 = fileURLToPath3(import.meta.url);
56781
57350
  const __dirname4 = dirname6(__filename2);
56782
57351
  const bundledPath = resolve4(__dirname4, "mcp", "mcp-server.js");
56783
- if (existsSync8(bundledPath)) {
57352
+ if (existsSync9(bundledPath)) {
56784
57353
  return bundledPath;
56785
57354
  }
56786
57355
  const sourceLayoutPath = resolve4(__dirname4, "..", "mcp", "mcp-server.js");
56787
- if (existsSync8(sourceLayoutPath)) {
57356
+ if (existsSync9(sourceLayoutPath)) {
56788
57357
  return sourceLayoutPath;
56789
57358
  }
56790
57359
  const tsPath = resolve4(__dirname4, "..", "mcp", "mcp-server.ts");
56791
- if (existsSync8(tsPath)) {
57360
+ if (existsSync9(tsPath)) {
56792
57361
  return tsPath;
56793
57362
  }
56794
57363
  return sourceLayoutPath;
@@ -56797,288 +57366,28 @@ class ClaudeCli extends EventEmitter2 {
56797
57366
  const __filename2 = fileURLToPath3(import.meta.url);
56798
57367
  const __dirname4 = dirname6(__filename2);
56799
57368
  const bundledPath = resolve4(__dirname4, "statusline", "writer.js");
56800
- if (existsSync8(bundledPath)) {
57369
+ if (existsSync9(bundledPath)) {
56801
57370
  return bundledPath;
56802
57371
  }
56803
57372
  const sourceLayoutPath = resolve4(__dirname4, "..", "statusline", "writer.js");
56804
- if (existsSync8(sourceLayoutPath)) {
57373
+ if (existsSync9(sourceLayoutPath)) {
56805
57374
  return sourceLayoutPath;
56806
57375
  }
56807
57376
  const tsPath = resolve4(__dirname4, "..", "statusline", "writer.ts");
56808
- if (existsSync8(tsPath)) {
57377
+ if (existsSync9(tsPath)) {
56809
57378
  return tsPath;
56810
57379
  }
56811
57380
  return sourceLayoutPath;
56812
57381
  }
56813
57382
  }
56814
57383
 
56815
- // src/claude/usage-probe.ts
56816
- init_logger();
56817
- var log11 = createLogger("usage-probe");
56818
- var DEFAULT_USAGE_PROBE_TIMEOUT_MS = 30000;
56819
- function parseUsageOutput(text) {
56820
- if (!text)
56821
- return null;
56822
- const sessionMatch = text.match(/Current session:\s*(\d+)%\s*used(?:\s*·\s*resets\s*([^\n]+?))?\s*(?:\n|$)/i);
56823
- const weekAllMatch = text.match(/Current week(?: \(all models\))?:\s*(\d+)%\s*used(?:\s*·\s*resets\s*([^\n]+?))?\s*(?:\n|$)/i);
56824
- if (!sessionMatch && !weekAllMatch)
56825
- return null;
56826
- const sessionPct = sessionMatch ? clampPct(Number(sessionMatch[1])) : 0;
56827
- const weekAllModelsPct = weekAllMatch ? clampPct(Number(weekAllMatch[1])) : 0;
56828
- let weekPerModelPct = null;
56829
- const perModelRe = /Current week \((?!all models\))[^)]+\):\s*(\d+)%\s*used/gi;
56830
- for (const m of text.matchAll(perModelRe)) {
56831
- const pct = clampPct(Number(m[1]));
56832
- weekPerModelPct = weekPerModelPct === null ? pct : Math.max(weekPerModelPct, pct);
56833
- }
56834
- return {
56835
- sessionPct,
56836
- weekAllModelsPct,
56837
- weekPerModelPct,
56838
- sessionResetsAt: sessionMatch?.[2]?.trim() || null,
56839
- weekResetsAt: weekAllMatch?.[2]?.trim() || null
56840
- };
56841
- }
56842
- function usageLoadScore(usage) {
56843
- return Math.max(usage.sessionPct, usage.weekAllModelsPct, usage.weekPerModelPct ?? 0);
56844
- }
56845
- function clampPct(n) {
56846
- if (!Number.isFinite(n))
56847
- return 0;
56848
- return Math.max(0, Math.min(100, Math.round(n)));
56849
- }
56850
- async function probeAccountUsage(account, opts = {}) {
56851
- const timeoutMs = opts.timeoutMs ?? DEFAULT_USAGE_PROBE_TIMEOUT_MS;
56852
- const claudePath = getClaudePath();
56853
- const env = buildClaudeChildEnv(process.env, account);
56854
- return new Promise((resolve5) => {
56855
- let settled = false;
56856
- const finish = (value) => {
56857
- if (settled)
56858
- return;
56859
- settled = true;
56860
- clearTimeout(timer);
56861
- resolve5(value);
56862
- };
56863
- let child;
56864
- try {
56865
- child = crossSpawn(claudePath, ["-p", "/usage", "--output-format", "json"], {
56866
- env,
56867
- stdio: ["ignore", "pipe", "pipe"]
56868
- });
56869
- } catch (err) {
56870
- log11.warn(`Failed to spawn /usage probe for "${account.id}": ${err}`);
56871
- resolve5(null);
56872
- return;
56873
- }
56874
- const timer = setTimeout(() => {
56875
- log11.warn(`/usage probe for "${account.id}" timed out after ${timeoutMs}ms`);
56876
- try {
56877
- child.kill("SIGKILL");
56878
- } catch {}
56879
- finish(null);
56880
- }, timeoutMs);
56881
- let stdout = "";
56882
- child.stdout?.on("data", (chunk) => {
56883
- stdout += chunk.toString();
56884
- });
56885
- child.stderr?.on("data", () => {});
56886
- child.on("error", (err) => {
56887
- log11.warn(`/usage probe for "${account.id}" errored: ${err}`);
56888
- finish(null);
56889
- });
56890
- child.on("close", () => {
56891
- const usage = extractUsage(stdout);
56892
- if (!usage) {
56893
- log11.debug(`/usage probe for "${account.id}" returned no parseable usage`);
56894
- }
56895
- finish(usage);
56896
- });
56897
- });
56898
- }
56899
- function extractUsage(stdout) {
56900
- const trimmed = stdout.trim();
56901
- if (!trimmed)
56902
- return null;
56903
- let text = trimmed;
56904
- try {
56905
- const parsed = JSON.parse(trimmed);
56906
- if (typeof parsed.result === "string") {
56907
- text = parsed.result;
56908
- }
56909
- } catch {}
56910
- return parseUsageOutput(text);
56911
- }
56912
-
56913
- // src/claude/account-pool.ts
56914
- var log12 = createLogger("account-pool");
56915
- var ACTIVE_SESSION_LOAD_PENALTY = 5;
56916
- function hashThreadId(threadId) {
56917
- let h = 2166136261;
56918
- for (let i = 0;i < threadId.length; i++) {
56919
- h ^= threadId.charCodeAt(i);
56920
- h = Math.imul(h, 16777619);
56921
- }
56922
- return h >>> 0;
56923
- }
56924
-
56925
- class AccountPool {
56926
- accounts;
56927
- byId;
56928
- orderIndex;
56929
- activeCounts = new Map;
56930
- coolingUntil = new Map;
56931
- usage = new Map;
56932
- rrCursor = 0;
56933
- constructor(accounts) {
56934
- this.accounts = (accounts ?? []).filter((acc) => {
56935
- const hasAuth = !!acc.home || !!acc.apiKey;
56936
- if (!hasAuth) {
56937
- log12.warn(`Claude account ${acc.id} has neither home nor apiKey — ignoring`);
56938
- return false;
56939
- }
56940
- if (acc.home && acc.apiKey) {
56941
- log12.warn(`Claude account ${acc.id} has both home and apiKey set — must choose one; ignoring`);
56942
- return false;
56943
- }
56944
- return true;
56945
- });
56946
- this.byId = new Map(this.accounts.map((acc) => [acc.id, acc]));
56947
- this.orderIndex = new Map(this.accounts.map((acc, i) => [acc.id, i]));
56948
- for (const acc of this.accounts) {
56949
- this.activeCounts.set(acc.id, 0);
56950
- this.usage.set(acc.id, null);
56951
- }
56952
- }
56953
- get isEmpty() {
56954
- return this.accounts.length === 0;
56955
- }
56956
- get size() {
56957
- return this.accounts.length;
56958
- }
56959
- get all() {
56960
- return this.accounts;
56961
- }
56962
- acquire(preferredId, threadId, opts) {
56963
- if (this.isEmpty)
56964
- return null;
56965
- if (preferredId) {
56966
- const preferred = this.byId.get(preferredId);
56967
- if (preferred) {
56968
- this.incrementActive(preferred.id);
56969
- return preferred;
56970
- }
56971
- log12.warn(`Preferred account "${preferredId}" not in pool — falling back to usage balancing`);
56972
- }
56973
- const now = Date.now();
56974
- const n = this.accounts.length;
56975
- if (threadId && !opts?.balanceByUsage) {
56976
- const sticky = this.accounts[hashThreadId(threadId) % n];
56977
- const cooling = this.coolingUntil.get(sticky.id) ?? 0;
56978
- if (cooling <= now) {
56979
- this.incrementActive(sticky.id);
56980
- return sticky;
56981
- }
56982
- }
56983
- const chosen = this.selectLeastLoaded(now);
56984
- if (!chosen) {
56985
- log12.warn(`All ${n} accounts are in rate-limit cooldown`);
56986
- return null;
56987
- }
56988
- this.incrementActive(chosen.id);
56989
- return chosen;
56990
- }
56991
- selectLeastLoaded(now) {
56992
- const n = this.accounts.length;
56993
- let best = null;
56994
- let bestScore = Number.POSITIVE_INFINITY;
56995
- let bestIdx = -1;
56996
- for (let k = 0;k < n; k++) {
56997
- const idx = (this.rrCursor + k) % n;
56998
- const acc = this.accounts[idx];
56999
- if ((this.coolingUntil.get(acc.id) ?? 0) > now)
57000
- continue;
57001
- const score = this.effectiveLoad(acc.id);
57002
- if (best === null || score < bestScore) {
57003
- best = acc;
57004
- bestScore = score;
57005
- bestIdx = idx;
57006
- }
57007
- }
57008
- if (bestIdx >= 0)
57009
- this.rrCursor = (bestIdx + 1) % n;
57010
- return best;
57011
- }
57012
- loadScore(accountId) {
57013
- const u = this.usage.get(accountId);
57014
- return u ? usageLoadScore(u) : Number.POSITIVE_INFINITY;
57015
- }
57016
- effectiveLoad(accountId) {
57017
- const base = this.loadScore(accountId);
57018
- const active = this.activeCounts.get(accountId) ?? 0;
57019
- return base + ACTIVE_SESSION_LOAD_PENALTY * active;
57020
- }
57021
- release(accountId) {
57022
- const current = this.activeCounts.get(accountId);
57023
- if (current === undefined)
57024
- return;
57025
- this.activeCounts.set(accountId, Math.max(0, current - 1));
57026
- }
57027
- setUsage(accountId, usage) {
57028
- if (!this.byId.has(accountId))
57029
- return;
57030
- this.usage.set(accountId, usage);
57031
- if (usage) {
57032
- log12.debug(`Account "${accountId}" usage: ${usageLoadScore(usage)}% (load score)`);
57033
- }
57034
- }
57035
- markCooling(accountId, untilEpochMs) {
57036
- if (!this.byId.has(accountId)) {
57037
- log12.warn(`markCooling called for unknown account "${accountId}"`);
57038
- return;
57039
- }
57040
- const existing = this.coolingUntil.get(accountId) ?? 0;
57041
- if (untilEpochMs > existing) {
57042
- this.coolingUntil.set(accountId, untilEpochMs);
57043
- const minutes = Math.ceil((untilEpochMs - Date.now()) / 60000);
57044
- log12.info(`Account "${accountId}" cooling for ~${minutes}min`);
57045
- }
57046
- }
57047
- get(accountId) {
57048
- return this.byId.get(accountId);
57049
- }
57050
- status() {
57051
- const now = Date.now();
57052
- return this.accounts.map((acc) => {
57053
- const cooling = this.coolingUntil.get(acc.id) ?? 0;
57054
- const usage = this.usage.get(acc.id) ?? null;
57055
- return {
57056
- id: acc.id,
57057
- displayName: acc.displayName ?? acc.id,
57058
- activeSessions: this.activeCounts.get(acc.id) ?? 0,
57059
- coolingUntil: cooling > now ? cooling : null,
57060
- usagePercent: usage ? usageLoadScore(usage) : null
57061
- };
57062
- });
57063
- }
57064
- incrementActive(accountId) {
57065
- this.activeCounts.set(accountId, (this.activeCounts.get(accountId) ?? 0) + 1);
57066
- }
57067
- }
57068
-
57069
- // src/cleanup/scheduler.ts
57070
- init_logger();
57071
- import { existsSync as existsSync10 } from "fs";
57072
- import { readdir, rm } from "fs/promises";
57073
- import { join as join9 } from "path";
57074
-
57075
57384
  // src/persistence/thread-logger.ts
57076
57385
  init_logger();
57077
- import { existsSync as existsSync9, mkdirSync as mkdirSync5, appendFileSync, readdirSync as readdirSync2, statSync as statSync2, unlinkSync as unlinkSync2, rmdirSync, readFileSync as readFileSync8, chmodSync as chmodSync5 } from "fs";
57078
- import { homedir as homedir6 } from "os";
57079
- import { join as join8, dirname as dirname7 } from "path";
57080
- var log13 = createLogger("thread-log");
57081
- var LOGS_BASE_DIR = join8(homedir6(), ".claude-threads", "logs");
57386
+ import { existsSync as existsSync10, mkdirSync as mkdirSync6, appendFileSync, readdirSync as readdirSync2, statSync as statSync2, unlinkSync as unlinkSync2, rmdirSync, readFileSync as readFileSync9, chmodSync as chmodSync6 } from "fs";
57387
+ import { homedir as homedir7 } from "os";
57388
+ import { join as join10, dirname as dirname7 } from "path";
57389
+ var log14 = createLogger("thread-log");
57390
+ var LOGS_BASE_DIR = join10(homedir7(), ".claude-threads", "logs");
57082
57391
 
57083
57392
  class ThreadLoggerImpl {
57084
57393
  platformId;
@@ -57098,16 +57407,16 @@ class ThreadLoggerImpl {
57098
57407
  this.enabled = options?.enabled ?? true;
57099
57408
  this.bufferSize = options?.bufferSize ?? 10;
57100
57409
  this.flushIntervalMs = options?.flushIntervalMs ?? 1000;
57101
- this.logPath = join8(LOGS_BASE_DIR, platformId, `${claudeSessionId}.jsonl`);
57410
+ this.logPath = join10(LOGS_BASE_DIR, platformId, `${claudeSessionId}.jsonl`);
57102
57411
  if (this.enabled) {
57103
57412
  const dir = dirname7(this.logPath);
57104
- if (!existsSync9(dir)) {
57105
- mkdirSync5(dir, { recursive: true });
57413
+ if (!existsSync10(dir)) {
57414
+ mkdirSync6(dir, { recursive: true });
57106
57415
  }
57107
57416
  this.flushTimer = setInterval(() => {
57108
57417
  this.flushSync();
57109
57418
  }, this.flushIntervalMs);
57110
- log13.debug(`Thread logger initialized: ${this.logPath}`);
57419
+ log14.debug(`Thread logger initialized: ${this.logPath}`);
57111
57420
  }
57112
57421
  }
57113
57422
  isEnabled() {
@@ -57221,7 +57530,7 @@ class ThreadLoggerImpl {
57221
57530
  this.flushTimer = null;
57222
57531
  }
57223
57532
  this.flushSync();
57224
- log13.debug(`Thread logger closed: ${this.logPath}`);
57533
+ log14.debug(`Thread logger closed: ${this.logPath}`);
57225
57534
  }
57226
57535
  addEntry(entry) {
57227
57536
  this.buffer.push(entry);
@@ -57236,14 +57545,14 @@ class ThreadLoggerImpl {
57236
57545
  const lines = this.buffer.map((entry) => JSON.stringify(entry)).join(`
57237
57546
  `) + `
57238
57547
  `;
57239
- const isNewFile = !existsSync9(this.logPath);
57548
+ const isNewFile = !existsSync10(this.logPath);
57240
57549
  appendFileSync(this.logPath, lines, { encoding: "utf8", mode: 384 });
57241
57550
  if (isNewFile) {
57242
- chmodSync5(this.logPath, 384);
57551
+ chmodSync6(this.logPath, 384);
57243
57552
  }
57244
57553
  this.buffer = [];
57245
57554
  } catch (err) {
57246
- log13.error(`Failed to flush thread log: ${err}`);
57555
+ log14.error(`Failed to flush thread log: ${err}`);
57247
57556
  }
57248
57557
  }
57249
57558
  }
@@ -57274,13 +57583,13 @@ function createThreadLogger(platformId, threadId, claudeSessionId, options) {
57274
57583
  function cleanupOldLogs(retentionDays = 30) {
57275
57584
  const cutoffMs = Date.now() - retentionDays * 24 * 60 * 60 * 1000;
57276
57585
  let deletedCount = 0;
57277
- if (!existsSync9(LOGS_BASE_DIR)) {
57586
+ if (!existsSync10(LOGS_BASE_DIR)) {
57278
57587
  return 0;
57279
57588
  }
57280
57589
  try {
57281
57590
  const platformDirs = readdirSync2(LOGS_BASE_DIR);
57282
57591
  for (const platformId of platformDirs) {
57283
- const platformDir = join8(LOGS_BASE_DIR, platformId);
57592
+ const platformDir = join10(LOGS_BASE_DIR, platformId);
57284
57593
  const stat = statSync2(platformDir);
57285
57594
  if (!stat.isDirectory())
57286
57595
  continue;
@@ -57288,49 +57597,49 @@ function cleanupOldLogs(retentionDays = 30) {
57288
57597
  for (const file of logFiles) {
57289
57598
  if (!file.endsWith(".jsonl"))
57290
57599
  continue;
57291
- const filePath = join8(platformDir, file);
57600
+ const filePath = join10(platformDir, file);
57292
57601
  try {
57293
57602
  const fileStat = statSync2(filePath);
57294
57603
  if (fileStat.mtimeMs < cutoffMs) {
57295
57604
  unlinkSync2(filePath);
57296
57605
  deletedCount++;
57297
- log13.debug(`Deleted old log file: ${filePath}`);
57606
+ log14.debug(`Deleted old log file: ${filePath}`);
57298
57607
  }
57299
57608
  } catch (err) {
57300
- log13.warn(`Failed to check/delete log file ${filePath}: ${err}`);
57609
+ log14.warn(`Failed to check/delete log file ${filePath}: ${err}`);
57301
57610
  }
57302
57611
  }
57303
57612
  try {
57304
57613
  const remaining = readdirSync2(platformDir);
57305
57614
  if (remaining.length === 0) {
57306
57615
  rmdirSync(platformDir);
57307
- log13.debug(`Removed empty platform log directory: ${platformDir}`);
57616
+ log14.debug(`Removed empty platform log directory: ${platformDir}`);
57308
57617
  }
57309
57618
  } catch {}
57310
57619
  }
57311
57620
  if (deletedCount > 0) {
57312
- log13.info(`Cleaned up ${deletedCount} old log file(s)`);
57621
+ log14.info(`Cleaned up ${deletedCount} old log file(s)`);
57313
57622
  }
57314
57623
  } catch (err) {
57315
- log13.error(`Failed to clean up old logs: ${err}`);
57624
+ log14.error(`Failed to clean up old logs: ${err}`);
57316
57625
  }
57317
57626
  return deletedCount;
57318
57627
  }
57319
57628
  function getLogFilePath(platformId, sessionId) {
57320
- return join8(LOGS_BASE_DIR, platformId, `${sessionId}.jsonl`);
57629
+ return join10(LOGS_BASE_DIR, platformId, `${sessionId}.jsonl`);
57321
57630
  }
57322
57631
  function readRecentLogEntries(platformId, sessionId, maxLines = 50) {
57323
57632
  const logPath = getLogFilePath(platformId, sessionId);
57324
- log13.debug(`Reading log entries from: ${logPath}`);
57325
- if (!existsSync9(logPath)) {
57326
- log13.debug(`Log file does not exist: ${logPath}`);
57633
+ log14.debug(`Reading log entries from: ${logPath}`);
57634
+ if (!existsSync10(logPath)) {
57635
+ log14.debug(`Log file does not exist: ${logPath}`);
57327
57636
  return [];
57328
57637
  }
57329
57638
  try {
57330
- const content = readFileSync8(logPath, "utf8");
57639
+ const content = readFileSync9(logPath, "utf8");
57331
57640
  const lines = content.trim().split(`
57332
57641
  `);
57333
- log13.debug(`Log file has ${lines.length} lines`);
57642
+ log14.debug(`Log file has ${lines.length} lines`);
57334
57643
  const recentLines = lines.slice(-maxLines);
57335
57644
  const entries = [];
57336
57645
  for (const line of recentLines) {
@@ -57340,412 +57649,14 @@ function readRecentLogEntries(platformId, sessionId, maxLines = 50) {
57340
57649
  entries.push(JSON.parse(line));
57341
57650
  } catch {}
57342
57651
  }
57343
- log13.debug(`Parsed ${entries.length} log entries`);
57652
+ log14.debug(`Parsed ${entries.length} log entries`);
57344
57653
  return entries;
57345
57654
  } catch (err) {
57346
- log13.error(`Failed to read log file: ${err}`);
57655
+ log14.error(`Failed to read log file: ${err}`);
57347
57656
  return [];
57348
57657
  }
57349
57658
  }
57350
57659
 
57351
- // src/cleanup/scheduler.ts
57352
- init_worktree();
57353
- var log14 = createLogger("cleanup");
57354
- var DEFAULT_CLEANUP_INTERVAL_MS = 60 * 60 * 1000;
57355
- var MAX_WORKTREE_AGE_MS = 24 * 60 * 60 * 1000;
57356
-
57357
- class CleanupScheduler {
57358
- intervalMs;
57359
- logRetentionDays;
57360
- threadLogsEnabled;
57361
- sessionStore;
57362
- maxWorktreeAgeMs;
57363
- cleanupWorktrees;
57364
- timer = null;
57365
- isRunning = false;
57366
- constructor(options) {
57367
- this.intervalMs = options.intervalMs ?? DEFAULT_CLEANUP_INTERVAL_MS;
57368
- this.logRetentionDays = options.logRetentionDays ?? 30;
57369
- this.threadLogsEnabled = options.threadLogsEnabled ?? true;
57370
- this.sessionStore = options.sessionStore;
57371
- this.maxWorktreeAgeMs = options.maxWorktreeAgeMs ?? MAX_WORKTREE_AGE_MS;
57372
- this.cleanupWorktrees = options.cleanupWorktrees ?? true;
57373
- }
57374
- start() {
57375
- if (this.isRunning) {
57376
- log14.debug("Cleanup scheduler already running");
57377
- return;
57378
- }
57379
- this.isRunning = true;
57380
- log14.info(`Cleanup scheduler started (interval: ${Math.round(this.intervalMs / 60000)}min)`);
57381
- this.runCleanup().catch((err) => {
57382
- log14.warn(`Initial cleanup failed: ${err}`);
57383
- });
57384
- this.timer = setInterval(() => {
57385
- this.runCleanup().catch((err) => {
57386
- log14.warn(`Periodic cleanup failed: ${err}`);
57387
- });
57388
- }, this.intervalMs);
57389
- }
57390
- stop() {
57391
- if (this.timer) {
57392
- clearInterval(this.timer);
57393
- this.timer = null;
57394
- }
57395
- this.isRunning = false;
57396
- log14.debug("Cleanup scheduler stopped");
57397
- }
57398
- async runCleanup() {
57399
- const startTime = Date.now();
57400
- log14.debug("Running background cleanup...");
57401
- const stats = {
57402
- logsDeleted: 0,
57403
- worktreesCleaned: 0,
57404
- metadataCleaned: 0,
57405
- errors: []
57406
- };
57407
- const cleanupTasks = [
57408
- this.cleanupLogs().catch((err) => {
57409
- stats.errors.push(`Log cleanup: ${err}`);
57410
- return 0;
57411
- })
57412
- ];
57413
- if (this.cleanupWorktrees) {
57414
- cleanupTasks.push(this.cleanupOrphanedWorktrees().catch((err) => {
57415
- stats.errors.push(`Worktree cleanup: ${err}`);
57416
- return { cleaned: 0, metadata: 0 };
57417
- }));
57418
- }
57419
- const [logStats, worktreeStats = { cleaned: 0, metadata: 0 }] = await Promise.all(cleanupTasks);
57420
- stats.logsDeleted = logStats;
57421
- stats.worktreesCleaned = worktreeStats.cleaned;
57422
- stats.metadataCleaned = worktreeStats.metadata;
57423
- const elapsed = Date.now() - startTime;
57424
- const totalCleaned = stats.logsDeleted + stats.worktreesCleaned + stats.metadataCleaned;
57425
- if (totalCleaned > 0 || stats.errors.length > 0) {
57426
- log14.info(`Cleanup completed in ${elapsed}ms: ` + `${stats.logsDeleted} logs, ${stats.worktreesCleaned} worktrees, ${stats.metadataCleaned} metadata` + (stats.errors.length > 0 ? ` (${stats.errors.length} errors)` : ""));
57427
- } else {
57428
- log14.debug(`Cleanup completed in ${elapsed}ms (nothing to clean)`);
57429
- }
57430
- return stats;
57431
- }
57432
- async cleanupLogs() {
57433
- if (!this.threadLogsEnabled) {
57434
- return 0;
57435
- }
57436
- return new Promise((resolve5) => {
57437
- try {
57438
- const deleted = cleanupOldLogs(this.logRetentionDays);
57439
- resolve5(deleted);
57440
- } catch (err) {
57441
- log14.warn(`Log cleanup error: ${err}`);
57442
- resolve5(0);
57443
- }
57444
- });
57445
- }
57446
- async cleanupOrphanedWorktrees() {
57447
- const worktreesDir = getWorktreesDir();
57448
- const result = { cleaned: 0, metadata: 0 };
57449
- if (!existsSync10(worktreesDir)) {
57450
- log14.debug("No worktrees directory exists, nothing to clean");
57451
- return result;
57452
- }
57453
- const persisted = this.sessionStore.load();
57454
- const activeWorktrees = new Set;
57455
- for (const session of persisted.values()) {
57456
- if (session.worktreeInfo?.worktreePath) {
57457
- activeWorktrees.add(session.worktreeInfo.worktreePath);
57458
- }
57459
- }
57460
- const now = Date.now();
57461
- try {
57462
- const entries = await readdir(worktreesDir, { withFileTypes: true });
57463
- for (const entry of entries) {
57464
- if (!entry.isDirectory())
57465
- continue;
57466
- const worktreePath = join9(worktreesDir, entry.name);
57467
- if (activeWorktrees.has(worktreePath)) {
57468
- log14.debug(`Worktree in use by persisted session, skipping: ${entry.name}`);
57469
- continue;
57470
- }
57471
- const meta = await readWorktreeMetadata(worktreePath);
57472
- let shouldCleanup = false;
57473
- let cleanupReason = "";
57474
- if (meta) {
57475
- const lastActivity = new Date(meta.lastActivityAt).getTime();
57476
- const age = now - lastActivity;
57477
- if (meta.sessionId && age < this.maxWorktreeAgeMs) {
57478
- log14.debug(`Worktree has active session (${Math.round(age / 60000)}min old), skipping: ${entry.name}`);
57479
- continue;
57480
- }
57481
- const merged = age >= this.maxWorktreeAgeMs ? await isBranchMerged(meta.repoRoot, meta.branch).catch(() => false) : false;
57482
- if (merged) {
57483
- shouldCleanup = true;
57484
- cleanupReason = `branch "${meta.branch}" was merged`;
57485
- } else if (age >= this.maxWorktreeAgeMs) {
57486
- shouldCleanup = true;
57487
- cleanupReason = `inactive for ${Math.round(age / 3600000)}h`;
57488
- } else {
57489
- log14.debug(`Worktree recent (${Math.round(age / 60000)}min old), skipping: ${entry.name}`);
57490
- continue;
57491
- }
57492
- } else {
57493
- shouldCleanup = true;
57494
- cleanupReason = "no metadata";
57495
- }
57496
- if (!shouldCleanup)
57497
- continue;
57498
- log14.info(`Cleaning worktree (${cleanupReason}): ${entry.name}`);
57499
- try {
57500
- if (meta?.repoRoot) {
57501
- await removeWorktree(meta.repoRoot, worktreePath);
57502
- } else {
57503
- await rm(worktreePath, { recursive: true, force: true });
57504
- }
57505
- result.cleaned++;
57506
- await removeWorktreeMetadata(worktreePath);
57507
- result.metadata++;
57508
- } catch (err) {
57509
- log14.warn(`Failed to clean orphaned worktree ${entry.name}: ${err}`);
57510
- try {
57511
- await rm(worktreePath, { recursive: true, force: true });
57512
- result.cleaned++;
57513
- await removeWorktreeMetadata(worktreePath);
57514
- result.metadata++;
57515
- } catch (rmErr) {
57516
- log14.error(`Failed to force remove worktree ${entry.name}: ${rmErr}`);
57517
- }
57518
- }
57519
- }
57520
- } catch (err) {
57521
- log14.warn(`Failed to scan worktrees directory: ${err}`);
57522
- }
57523
- return result;
57524
- }
57525
- }
57526
- // src/operations/monitor/handler.ts
57527
- init_logger();
57528
-
57529
- // src/session/lifecycle-fsm.ts
57530
- init_logger();
57531
- var log15 = createLogger("fsm");
57532
- var ALLOWED_TRANSITIONS = {
57533
- starting: new Set(["active", "paused", "interrupted", "cancelling", "restarting"]),
57534
- active: new Set([
57535
- "active",
57536
- "processing",
57537
- "paused",
57538
- "interrupted",
57539
- "restarting",
57540
- "cancelling",
57541
- "ending"
57542
- ]),
57543
- processing: new Set([
57544
- "active",
57545
- "paused",
57546
- "interrupted",
57547
- "restarting",
57548
- "cancelling"
57549
- ]),
57550
- paused: new Set(["active", "cancelling", "restarting"]),
57551
- interrupted: new Set(["active", "cancelling", "restarting", "paused"]),
57552
- restarting: new Set(["active", "paused", "cancelling"]),
57553
- cancelling: new Set(["ending"]),
57554
- ending: new Set
57555
- };
57556
- function checkTransition(from, to, sessionId) {
57557
- const allowed = ALLOWED_TRANSITIONS[from];
57558
- if (allowed.has(to))
57559
- return;
57560
- const msg = `illegal lifecycle transition ${from} -> ${to}`;
57561
- const payload = {
57562
- event: "fsm.illegal_transition",
57563
- from,
57564
- to,
57565
- sessionId
57566
- };
57567
- if (process.env.CLAUDE_THREADS_FSM_STRICT === "1") {
57568
- throw new Error(`${msg} (sessionId=${sessionId})`);
57569
- }
57570
- log15.warn(msg, payload);
57571
- }
57572
-
57573
- // src/session/timer-manager.ts
57574
- function createSessionTimers() {
57575
- return {
57576
- updateTimer: null,
57577
- typingTimer: null,
57578
- statusBarTimer: null
57579
- };
57580
- }
57581
- function clearAllTimers(timers) {
57582
- if (timers.updateTimer) {
57583
- clearTimeout(timers.updateTimer);
57584
- timers.updateTimer = null;
57585
- }
57586
- if (timers.typingTimer) {
57587
- clearInterval(timers.typingTimer);
57588
- timers.typingTimer = null;
57589
- }
57590
- if (timers.statusBarTimer) {
57591
- clearInterval(timers.statusBarTimer);
57592
- timers.statusBarTimer = null;
57593
- }
57594
- }
57595
-
57596
- // src/session/types.ts
57597
- function createSessionLifecycle() {
57598
- return {
57599
- state: "starting",
57600
- resumeFailCount: 0,
57601
- hasClaudeResponded: false
57602
- };
57603
- }
57604
- function createResumedLifecycle(resumeFailCount = 0) {
57605
- return {
57606
- state: "active",
57607
- resumeFailCount,
57608
- hasClaudeResponded: true
57609
- };
57610
- }
57611
- function isSessionRestarting(session) {
57612
- return session.lifecycle.state === "restarting";
57613
- }
57614
- function isSessionCancelled(session) {
57615
- return session.lifecycle.state === "cancelling";
57616
- }
57617
- function transitionTo(session, newState) {
57618
- checkTransition(session.lifecycle.state, newState, session.sessionId);
57619
- session.lifecycle.state = newState;
57620
- }
57621
- function markClaudeResponded(session) {
57622
- session.lifecycle.hasClaudeResponded = true;
57623
- if (session.lifecycle.state === "starting") {
57624
- session.lifecycle.state = "active";
57625
- }
57626
- }
57627
- function getSessionStatus(session) {
57628
- if (session.isProcessing) {
57629
- return session.lifecycle.hasClaudeResponded ? "active" : "starting";
57630
- }
57631
- return "idle";
57632
- }
57633
-
57634
- // src/session/authorization.ts
57635
- function isAuthorizedForSession(check) {
57636
- const { username, platform, sessionAllowedUsers } = check;
57637
- if (!username || username === "unknown") {
57638
- return false;
57639
- }
57640
- if (platform.isUserAllowed(username)) {
57641
- return true;
57642
- }
57643
- return sessionAllowedUsers?.has(username) ?? false;
57644
- }
57645
-
57646
- // src/mcp/decision-bridge.ts
57647
- import { createServer, createConnection } from "node:net";
57648
- import { tmpdir as tmpdir2 } from "node:os";
57649
- import { join as join10 } from "node:path";
57650
- import { randomUUID as randomUUID2 } from "node:crypto";
57651
- import { mkdtempSync } from "node:fs";
57652
- import { rm as rm2 } from "node:fs/promises";
57653
-
57654
- class BridgeUnavailableError extends Error {
57655
- }
57656
- function bridgeSocketPath() {
57657
- if (process.platform === "win32") {
57658
- return `\\\\.\\pipe\\ctb-${randomUUID2()}`;
57659
- }
57660
- const dir = mkdtempSync(join10(tmpdir2(), "ctb-"));
57661
- return join10(dir, "b.sock");
57662
- }
57663
-
57664
- class DecisionBridgeServer {
57665
- server;
57666
- path;
57667
- liveSockets = new Set;
57668
- constructor(server, path2) {
57669
- this.server = server;
57670
- this.path = path2;
57671
- }
57672
- static async create(handler) {
57673
- const path2 = bridgeSocketPath();
57674
- const liveSockets = new Set;
57675
- const server = createServer((socket) => {
57676
- liveSockets.add(socket);
57677
- socket.once("close", () => liveSockets.delete(socket));
57678
- let buffer = "";
57679
- const aborter = new AbortController;
57680
- let responded = false;
57681
- socket.on("data", (chunk) => {
57682
- buffer += chunk.toString("utf8");
57683
- const newline = buffer.indexOf(`
57684
- `);
57685
- if (newline === -1)
57686
- return;
57687
- const line = buffer.slice(0, newline);
57688
- buffer = "";
57689
- let request;
57690
- try {
57691
- request = JSON.parse(line);
57692
- } catch {
57693
- responded = true;
57694
- socket.end(JSON.stringify({ behavior: "deny", message: "Malformed bridge request" }) + `
57695
- `);
57696
- return;
57697
- }
57698
- handler(request, aborter.signal).then((response) => {
57699
- responded = true;
57700
- socket.end(JSON.stringify(response) + `
57701
- `);
57702
- }).catch((err) => {
57703
- responded = true;
57704
- if (err instanceof BridgeUnavailableError) {
57705
- socket.destroy();
57706
- return;
57707
- }
57708
- socket.end(JSON.stringify({
57709
- behavior: "deny",
57710
- message: `Bridge handler failed: ${err instanceof Error ? err.message : String(err)}`
57711
- }) + `
57712
- `);
57713
- });
57714
- });
57715
- socket.on("close", () => {
57716
- if (!responded)
57717
- aborter.abort();
57718
- });
57719
- socket.on("error", () => {});
57720
- });
57721
- try {
57722
- await new Promise((resolve5, reject) => {
57723
- server.once("error", reject);
57724
- server.listen(path2, () => {
57725
- server.removeListener("error", reject);
57726
- resolve5();
57727
- });
57728
- });
57729
- } catch (err) {
57730
- if (process.platform !== "win32") {
57731
- await rm2(join10(path2, ".."), { recursive: true, force: true }).catch(() => {});
57732
- }
57733
- throw err;
57734
- }
57735
- const bridge = new DecisionBridgeServer(server, path2);
57736
- bridge.liveSockets = liveSockets;
57737
- return bridge;
57738
- }
57739
- async close() {
57740
- for (const socket of this.liveSockets)
57741
- socket.destroy();
57742
- await new Promise((resolve5) => this.server.close(() => resolve5()));
57743
- if (process.platform !== "win32") {
57744
- await rm2(join10(this.path, ".."), { recursive: true, force: true }).catch(() => {});
57745
- }
57746
- }
57747
- }
57748
-
57749
57660
  // src/commands/registry.ts
57750
57661
  var COMMAND_REGISTRY = [
57751
57662
  {
@@ -57874,6 +57785,28 @@ var COMMAND_REGISTRY = [
57874
57785
  { name: "forget", description: "Remove one entry (by number or matching text), or all", args: "<n|text> | all" }
57875
57786
  ]
57876
57787
  },
57788
+ {
57789
+ command: "routine",
57790
+ description: "Create a scheduled routine from a natural-language request (confirmed with \uD83D\uDC4D before saving)",
57791
+ args: "<schedule, task>",
57792
+ category: "settings",
57793
+ audience: "user",
57794
+ claudeNotes: "User decisions, not yours"
57795
+ },
57796
+ {
57797
+ command: "routines",
57798
+ description: "List scheduled routines; pause/resume/delete/run manage them",
57799
+ args: "[pause|resume|delete|run <n>]",
57800
+ category: "settings",
57801
+ audience: "user",
57802
+ claudeNotes: "User decisions, not yours",
57803
+ subcommands: [
57804
+ { name: "pause", description: "Pause a routine", args: "<n>" },
57805
+ { name: "resume", description: "Resume a paused routine", args: "<n>" },
57806
+ { name: "delete", description: "Delete a routine", args: "<n>" },
57807
+ { name: "run", description: "Run a routine now, outside its schedule", args: "<n>" }
57808
+ ]
57809
+ },
57877
57810
  {
57878
57811
  command: "update",
57879
57812
  description: "Show auto-update status",
@@ -57998,6 +57931,8 @@ var COMMAND_PATTERNS = [
57998
57931
  ["mentions", /^!mentions(?:\s+(on|off))?\s*$/i],
57999
57932
  ["remember", /^!remember\s+([\s\S]+)$/i],
58000
57933
  ["memory", /^!memory(?:\s+([\s\S]+))?$/i],
57934
+ ["routines", /^!routines(?:\s+([\s\S]+))?$/i],
57935
+ ["routine", /^!routine\s+([\s\S]+)$/i],
58001
57936
  ["update", /^!update(?:\s+(now|defer))?\s*$/i],
58002
57937
  ["context", /^!context\s*$/i],
58003
57938
  ["cost", /^!cost\s*$/i],
@@ -58134,7 +58069,7 @@ ${formatter.formatBold("Reactions:")}
58134
58069
  }
58135
58070
 
58136
58071
  // src/changelog.ts
58137
- import { readFileSync as readFileSync9, existsSync as existsSync11 } from "fs";
58072
+ import { readFileSync as readFileSync10, existsSync as existsSync11 } from "fs";
58138
58073
  import { dirname as dirname8, resolve as resolve5 } from "path";
58139
58074
  import { fileURLToPath as fileURLToPath4 } from "url";
58140
58075
  var __dirname4 = dirname8(fileURLToPath4(import.meta.url));
@@ -58154,7 +58089,7 @@ function getReleaseNotes(version) {
58154
58089
  return null;
58155
58090
  }
58156
58091
  try {
58157
- const content = readFileSync9(changelogPath, "utf-8");
58092
+ const content = readFileSync10(changelogPath, "utf-8");
58158
58093
  return parseChangelog(content, version);
58159
58094
  } catch {
58160
58095
  return null;
@@ -58376,6 +58311,30 @@ var handleMemory = async (ctx, args) => {
58376
58311
  await ctx.client.createPost(`⚠️ Usage: ${ctx.formatter.formatCode("!memory")} or ${ctx.formatter.formatCode("!memory forget <n|text>")} or ${ctx.formatter.formatCode("!memory forget all")}`, ctx.threadId);
58377
58312
  return { handled: true };
58378
58313
  };
58314
+ var handleRoutine = async (ctx, args) => {
58315
+ if (ctx.commandContext === "first-message") {
58316
+ return { handled: false };
58317
+ }
58318
+ if (!ctx.isAllowed) {
58319
+ return { handled: true };
58320
+ }
58321
+ if (!args?.trim()) {
58322
+ await ctx.client.createPost(`⚠️ Usage: ${ctx.formatter.formatCode("!routine every weekday at 9:00, <task>")}`, ctx.threadId);
58323
+ return { handled: true };
58324
+ }
58325
+ await ctx.sessionManager.createRoutine(ctx.threadId, args, ctx.username);
58326
+ return { handled: true };
58327
+ };
58328
+ var handleRoutines = async (ctx, args) => {
58329
+ if (ctx.commandContext === "first-message") {
58330
+ return { handled: false };
58331
+ }
58332
+ if (!ctx.isAllowed) {
58333
+ return { handled: true };
58334
+ }
58335
+ await ctx.sessionManager.manageRoutines(ctx.threadId, args, ctx.username);
58336
+ return { handled: true };
58337
+ };
58379
58338
  var handleCd = async (ctx, args) => {
58380
58339
  if (!args) {
58381
58340
  return { handled: false };
@@ -58567,6 +58526,8 @@ handlers.set("kick", handleKick);
58567
58526
  handlers.set("github-email", handleGitHubEmail);
58568
58527
  handlers.set("remember", handleRemember);
58569
58528
  handlers.set("memory", handleMemory);
58529
+ handlers.set("routine", handleRoutine);
58530
+ handlers.set("routines", handleRoutines);
58570
58531
  handlers.set("cd", handleCd);
58571
58532
  handlers.set("permissions", handlePermissions);
58572
58533
  handlers.set("mentions", handleMentions);
@@ -58610,7 +58571,7 @@ async function handleDynamicSlashCommand(command, args, ctx) {
58610
58571
  }
58611
58572
  // src/commands/system-prompt-generator.ts
58612
58573
  init_logger();
58613
- var log16 = createLogger("system-prompt");
58574
+ var log15 = createLogger("system-prompt");
58614
58575
  function formatUserCommand(cmd) {
58615
58576
  const cmdStr = cmd.args ? `\`!${cmd.command} ${cmd.args}\`` : `\`!${cmd.command}\``;
58616
58577
  const description = cmd.description;
@@ -58649,7 +58610,7 @@ async function resolveCollaborators(platform, platformId, ownerUsername, allowed
58649
58610
  continue;
58650
58611
  const email = githubEmailsStore.get(platformId, username);
58651
58612
  if (!email) {
58652
- log16.debug(`Collaborator @${username} has no registered GitHub noreply email — skipping`);
58613
+ log15.debug(`Collaborator @${username} has no registered GitHub noreply email — skipping`);
58653
58614
  continue;
58654
58615
  }
58655
58616
  let name = username;
@@ -58658,7 +58619,7 @@ async function resolveCollaborators(platform, platformId, ownerUsername, allowed
58658
58619
  if (user)
58659
58620
  name = user.displayName || user.username;
58660
58621
  } catch (err) {
58661
- log16.debug(`Display name lookup failed for @${username}: ${err.message}`);
58622
+ log15.debug(`Display name lookup failed for @${username}: ${err.message}`);
58662
58623
  }
58663
58624
  resolved.push({ username, name, email });
58664
58625
  }
@@ -58781,13 +58742,13 @@ ${avoidCommands.map((c) => `- \`!${c.command}\` - ${c.reason}`).join(`
58781
58742
  `.trim();
58782
58743
  }
58783
58744
  // src/session/lifecycle.ts
58784
- import { randomUUID as randomUUID5 } from "crypto";
58745
+ import { randomUUID as randomUUID6 } from "crypto";
58785
58746
  import { existsSync as existsSync13 } from "fs";
58786
58747
 
58787
58748
  // src/utils/keep-alive.ts
58788
58749
  init_logger();
58789
58750
  import { spawn as spawn2 } from "child_process";
58790
- var log17 = createLogger("keepalive");
58751
+ var log16 = createLogger("keepalive");
58791
58752
  function keepAliveSpawnSpec(platform, parentPid) {
58792
58753
  switch (platform) {
58793
58754
  case "darwin":
@@ -58843,7 +58804,7 @@ class KeepAliveManager {
58843
58804
  if (!enabled && this.keepAliveProcess) {
58844
58805
  this.stopKeepAlive();
58845
58806
  }
58846
- log17.debug(`Keep-alive ${enabled ? "enabled" : "disabled"}`);
58807
+ log16.debug(`Keep-alive ${enabled ? "enabled" : "disabled"}`);
58847
58808
  }
58848
58809
  isEnabled() {
58849
58810
  return this.enabled;
@@ -58853,7 +58814,7 @@ class KeepAliveManager {
58853
58814
  }
58854
58815
  sessionStarted() {
58855
58816
  this.activeSessionCount++;
58856
- log17.debug(`Session started (${this.activeSessionCount} active)`);
58817
+ log16.debug(`Session started (${this.activeSessionCount} active)`);
58857
58818
  if (this.activeSessionCount === 1) {
58858
58819
  this.startKeepAlive();
58859
58820
  }
@@ -58862,7 +58823,7 @@ class KeepAliveManager {
58862
58823
  if (this.activeSessionCount > 0) {
58863
58824
  this.activeSessionCount--;
58864
58825
  }
58865
- log17.debug(`Session ended (${this.activeSessionCount} active)`);
58826
+ log16.debug(`Session ended (${this.activeSessionCount} active)`);
58866
58827
  if (this.activeSessionCount === 0) {
58867
58828
  this.stopKeepAlive();
58868
58829
  }
@@ -58876,11 +58837,11 @@ class KeepAliveManager {
58876
58837
  }
58877
58838
  startKeepAlive() {
58878
58839
  if (!this.enabled) {
58879
- log17.debug("Keep-alive disabled, skipping");
58840
+ log16.debug("Keep-alive disabled, skipping");
58880
58841
  return;
58881
58842
  }
58882
58843
  if (this.keepAliveProcess) {
58883
- log17.debug("Keep-alive already running");
58844
+ log16.debug("Keep-alive already running");
58884
58845
  return;
58885
58846
  }
58886
58847
  switch (this.platform) {
@@ -58894,12 +58855,12 @@ class KeepAliveManager {
58894
58855
  this.startWindowsKeepAlive();
58895
58856
  break;
58896
58857
  default:
58897
- log17.warn(`Keep-alive not supported on ${this.platform}`);
58858
+ log16.warn(`Keep-alive not supported on ${this.platform}`);
58898
58859
  }
58899
58860
  }
58900
58861
  stopKeepAlive() {
58901
58862
  if (this.keepAliveProcess) {
58902
- log17.debug("Stopping keep-alive");
58863
+ log16.debug("Stopping keep-alive");
58903
58864
  this.keepAliveProcess.kill();
58904
58865
  this.keepAliveProcess = null;
58905
58866
  }
@@ -58914,18 +58875,18 @@ class KeepAliveManager {
58914
58875
  detached: false
58915
58876
  });
58916
58877
  this.keepAliveProcess.on("error", (err) => {
58917
- log17.error(`Failed to start caffeinate: ${err.message}`);
58878
+ log16.error(`Failed to start caffeinate: ${err.message}`);
58918
58879
  this.keepAliveProcess = null;
58919
58880
  });
58920
58881
  this.keepAliveProcess.on("exit", (code) => {
58921
58882
  if (code !== null && code !== 0 && this.activeSessionCount > 0) {
58922
- log17.debug(`caffeinate exited with code ${code}`);
58883
+ log16.debug(`caffeinate exited with code ${code}`);
58923
58884
  }
58924
58885
  this.keepAliveProcess = null;
58925
58886
  });
58926
- log17.info("Sleep prevention active (caffeinate)");
58887
+ log16.info("Sleep prevention active (caffeinate)");
58927
58888
  } catch (err) {
58928
- log17.error(`Failed to start caffeinate: ${err}`);
58889
+ log16.error(`Failed to start caffeinate: ${err}`);
58929
58890
  }
58930
58891
  }
58931
58892
  startLinuxKeepAlive() {
@@ -58938,19 +58899,19 @@ class KeepAliveManager {
58938
58899
  detached: false
58939
58900
  });
58940
58901
  this.keepAliveProcess.on("error", (err) => {
58941
- log17.debug(`systemd-inhibit not available: ${err.message}`);
58902
+ log16.debug(`systemd-inhibit not available: ${err.message}`);
58942
58903
  this.keepAliveProcess = null;
58943
58904
  this.startLinuxKeepAliveFallback();
58944
58905
  });
58945
58906
  this.keepAliveProcess.on("exit", (code) => {
58946
58907
  if (code !== null && code !== 0 && this.activeSessionCount > 0) {
58947
- log17.debug(`systemd-inhibit exited with code ${code}`);
58908
+ log16.debug(`systemd-inhibit exited with code ${code}`);
58948
58909
  }
58949
58910
  this.keepAliveProcess = null;
58950
58911
  });
58951
- log17.info("Sleep prevention active (systemd-inhibit)");
58912
+ log16.info("Sleep prevention active (systemd-inhibit)");
58952
58913
  } catch (err) {
58953
- log17.debug(`Failed to start systemd-inhibit: ${err}`);
58914
+ log16.debug(`Failed to start systemd-inhibit: ${err}`);
58954
58915
  this.startLinuxKeepAliveFallback();
58955
58916
  }
58956
58917
  }
@@ -58961,15 +58922,15 @@ class KeepAliveManager {
58961
58922
  detached: false
58962
58923
  });
58963
58924
  this.keepAliveProcess.on("error", (err) => {
58964
- log17.warn(`Linux keep-alive fallback not available: ${err.message}`);
58925
+ log16.warn(`Linux keep-alive fallback not available: ${err.message}`);
58965
58926
  this.keepAliveProcess = null;
58966
58927
  });
58967
58928
  this.keepAliveProcess.on("exit", () => {
58968
58929
  this.keepAliveProcess = null;
58969
58930
  });
58970
- log17.info("Sleep prevention active (xdg-screensaver)");
58931
+ log16.info("Sleep prevention active (xdg-screensaver)");
58971
58932
  } catch (err) {
58972
- log17.warn(`Linux keep-alive not available: ${err}`);
58933
+ log16.warn(`Linux keep-alive not available: ${err}`);
58973
58934
  }
58974
58935
  }
58975
58936
  startWindowsKeepAlive() {
@@ -58981,18 +58942,18 @@ class KeepAliveManager {
58981
58942
  windowsHide: true
58982
58943
  });
58983
58944
  this.keepAliveProcess.on("error", (err) => {
58984
- log17.warn(`Windows keep-alive not available: ${err.message}`);
58945
+ log16.warn(`Windows keep-alive not available: ${err.message}`);
58985
58946
  this.keepAliveProcess = null;
58986
58947
  });
58987
58948
  this.keepAliveProcess.on("exit", (code) => {
58988
58949
  if (code !== null && code !== 0 && this.activeSessionCount > 0) {
58989
- log17.debug(`PowerShell keep-alive exited with code ${code}`);
58950
+ log16.debug(`PowerShell keep-alive exited with code ${code}`);
58990
58951
  }
58991
58952
  this.keepAliveProcess = null;
58992
58953
  });
58993
- log17.info("Sleep prevention active (SetThreadExecutionState)");
58954
+ log16.info("Sleep prevention active (SetThreadExecutionState)");
58994
58955
  } catch (err) {
58995
- log17.warn(`Windows keep-alive not available: ${err}`);
58956
+ log16.warn(`Windows keep-alive not available: ${err}`);
58996
58957
  }
58997
58958
  }
58998
58959
  }
@@ -59000,7 +58961,7 @@ var keepAlive = new KeepAliveManager;
59000
58961
 
59001
58962
  // src/utils/error-handler/index.ts
59002
58963
  init_logger();
59003
- var log18 = createLogger("error");
58964
+ var log17 = createLogger("error");
59004
58965
 
59005
58966
  class SessionError extends Error {
59006
58967
  sessionId;
@@ -59026,19 +58987,19 @@ async function handleError(error, context, severity = "recoverable") {
59026
58987
  const sessionPart = sessionId ? ` (${formatShortId(sessionId)})` : "";
59027
58988
  const logMessage = `${context.action}${sessionPart}: ${message}`;
59028
58989
  if (severity === "recoverable") {
59029
- log18.warn(logMessage);
58990
+ log17.warn(logMessage);
59030
58991
  } else {
59031
- log18.error(logMessage, error instanceof Error ? error : undefined);
58992
+ log17.error(logMessage, error instanceof Error ? error : undefined);
59032
58993
  }
59033
58994
  if (context.details) {
59034
- log18.debugJson("Error details", context.details);
58995
+ log17.debugJson("Error details", context.details);
59035
58996
  }
59036
58997
  if (context.notifyUser && context.session) {
59037
58998
  try {
59038
58999
  const fmt = context.session.platform.getFormatter();
59039
59000
  await context.session.platform.createPost(`⚠️ ${fmt.formatBold("Error")}: ${context.action} failed - ${message}`, context.session.threadId);
59040
59001
  } catch (notifyError) {
59041
- log18.warn(`Could not notify user: ${notifyError}`);
59002
+ log17.warn(`Could not notify user: ${notifyError}`);
59042
59003
  }
59043
59004
  }
59044
59005
  if (severity === "session-fatal" || severity === "system-fatal") {
@@ -59065,7 +59026,7 @@ async function logAndNotify(error, context) {
59065
59026
  }
59066
59027
  function logSilentError(context, error) {
59067
59028
  const message = error instanceof Error ? error.message : String(error);
59068
- log18.debug(`[${context}] Silently caught: ${message}`);
59029
+ log17.debug(`[${context}] Silently caught: ${message}`);
59069
59030
  }
59070
59031
 
59071
59032
  // src/session/lifecycle.ts
@@ -59085,8 +59046,8 @@ function createSessionLog(baseLog) {
59085
59046
  init_logger();
59086
59047
  init_emoji();
59087
59048
  init_worktree();
59088
- var log19 = createLogger("helpers");
59089
- var sessionLog = createSessionLog(log19);
59049
+ var log18 = createLogger("helpers");
59050
+ var sessionLog = createSessionLog(log18);
59090
59051
  var POST_TYPES = {
59091
59052
  info: "",
59092
59053
  success: "✅",
@@ -59172,10 +59133,10 @@ function updateLastMessage(session, post2) {
59172
59133
 
59173
59134
  // src/operations/streaming/handler.ts
59174
59135
  init_logger();
59175
- import { lstat, mkdir as mkdir2, mkdtemp, rm as rm3, writeFile as writeFile2 } from "fs/promises";
59136
+ import { lstat, mkdir as mkdir2, mkdtemp, rm as rm2, writeFile as writeFile2 } from "fs/promises";
59176
59137
  import { tmpdir as tmpdir3 } from "os";
59177
59138
  import { join as join11 } from "path";
59178
- var log20 = createLogger("streaming");
59139
+ var log19 = createLogger("streaming");
59179
59140
  var UPLOAD_ROOT_DIR = "claude-threads-uploads";
59180
59141
  function safeIdSegment2(id) {
59181
59142
  return id.replace(/[^A-Za-z0-9._-]/g, "_");
@@ -59188,9 +59149,9 @@ async function cleanupSessionUploads(platformId, threadId) {
59188
59149
  return;
59189
59150
  const dir = getSessionUploadDir(platformId, threadId);
59190
59151
  try {
59191
- await rm3(dir, { recursive: true, force: true });
59152
+ await rm2(dir, { recursive: true, force: true });
59192
59153
  } catch (err) {
59193
- log20.debug(`Upload cleanup for ${platformId}:${threadId} failed (ignored): ${err}`);
59154
+ log19.debug(`Upload cleanup for ${platformId}:${threadId} failed (ignored): ${err}`);
59194
59155
  }
59195
59156
  }
59196
59157
  function sanitizeForPrompt(value) {
@@ -59211,7 +59172,7 @@ async function saveFilesToUploadDir(platform, uploadDir, files, debug = false) {
59211
59172
  for (const file of files) {
59212
59173
  skipped.push({ name: file.name, reason: "Refusing to write under symlinked upload directory" });
59213
59174
  }
59214
- log20.error(`Upload dir is a symlink, refusing all writes: ${uploadDir}`);
59175
+ log19.error(`Upload dir is a symlink, refusing all writes: ${uploadDir}`);
59215
59176
  return { saved, skipped };
59216
59177
  }
59217
59178
  const messageDir = await mkdtemp(join11(uploadDir, `${Date.now().toString(36)}-`));
@@ -59229,11 +59190,11 @@ async function saveFilesToUploadDir(platform, uploadDir, files, debug = false) {
59229
59190
  size: buffer.length
59230
59191
  });
59231
59192
  if (debug) {
59232
- log20.debug(`Saved ${file.name} → ${absolutePath} (${formatBytes(buffer.length)})`);
59193
+ log19.debug(`Saved ${file.name} → ${absolutePath} (${formatBytes(buffer.length)})`);
59233
59194
  }
59234
59195
  } catch (err) {
59235
59196
  const message = err instanceof Error ? err.message : String(err);
59236
- log20.error(`Failed to save uploaded file ${file.name}: ${message}`);
59197
+ log19.error(`Failed to save uploaded file ${file.name}: ${message}`);
59237
59198
  skipped.push({
59238
59199
  name: file.name,
59239
59200
  reason: `Download failed: ${message}`
@@ -59309,7 +59270,7 @@ function buildRestartCliOptions(session, ctx) {
59309
59270
  }
59310
59271
 
59311
59272
  // src/operations/commands/handler.ts
59312
- import { randomUUID as randomUUID4 } from "crypto";
59273
+ import { randomUUID as randomUUID5 } from "crypto";
59313
59274
  import { resolve as resolve6 } from "path";
59314
59275
  import { existsSync as existsSync12, statSync as statSync3 } from "fs";
59315
59276
 
@@ -59656,9 +59617,9 @@ node_default(Temp.purgeSyncAll);
59656
59617
  var temp_default = Temp;
59657
59618
 
59658
59619
  // node_modules/atomically/dist/index.js
59659
- function writeFileSync6(filePath, data, options = DEFAULT_WRITE_OPTIONS) {
59620
+ function writeFileSync7(filePath, data, options = DEFAULT_WRITE_OPTIONS) {
59660
59621
  if (isString(options))
59661
- return writeFileSync6(filePath, data, { encoding: options });
59622
+ return writeFileSync7(filePath, data, { encoding: options });
59662
59623
  const timeout = options.timeout ?? DEFAULT_TIMEOUT_SYNC;
59663
59624
  const retryOptions = { timeout };
59664
59625
  let tempDisposer = null;
@@ -59986,7 +59947,7 @@ class Configstore {
59986
59947
  }
59987
59948
  if (error.name === "SyntaxError") {
59988
59949
  if (this._clearInvalidConfig) {
59989
- writeFileSync6(this._path, "", writeFileOptions);
59950
+ writeFileSync7(this._path, "", writeFileOptions);
59990
59951
  return {};
59991
59952
  }
59992
59953
  throw error;
@@ -59998,7 +59959,7 @@ class Configstore {
59998
59959
  set all(value) {
59999
59960
  try {
60000
59961
  import_graceful_fs.default.mkdirSync(path5.dirname(this._path), mkdirOptions);
60001
- writeFileSync6(this._path, JSON.stringify(value, undefined, "\t"), writeFileOptions);
59962
+ writeFileSync7(this._path, JSON.stringify(value, undefined, "\t"), writeFileOptions);
60002
59963
  } catch (error) {
60003
59964
  handlePermissionError(error);
60004
59965
  }
@@ -62726,7 +62687,7 @@ init_emoji();
62726
62687
 
62727
62688
  // src/operations/bug-report/handler.ts
62728
62689
  import { execSync as execSync2 } from "child_process";
62729
- import { writeFileSync as writeFileSync7, unlinkSync as unlinkSync3 } from "fs";
62690
+ import { writeFileSync as writeFileSync8, unlinkSync as unlinkSync3 } from "fs";
62730
62691
  import { tmpdir as tmpdir4 } from "os";
62731
62692
  import { join as join12 } from "path";
62732
62693
 
@@ -63310,7 +63271,7 @@ async function createGitHubIssue(title, body, workingDir) {
63310
63271
  }
63311
63272
  const bodyFile = join12(tmpdir4(), `bug-body-${Date.now()}.md`);
63312
63273
  try {
63313
- writeFileSync7(bodyFile, body, "utf-8");
63274
+ writeFileSync8(bodyFile, body, "utf-8");
63314
63275
  const cmd = `gh issue create --repo "${GITHUB_REPO}" --title "${escapeShell(title)}" --body-file "${bodyFile}"`;
63315
63276
  const result = execSync2(cmd, {
63316
63277
  cwd: workingDir,
@@ -66967,7 +66928,8 @@ class PromptExecutor extends BaseExecutor {
66967
66928
  return {
66968
66929
  pendingContextPrompt: null,
66969
66930
  pendingExistingWorktreePrompt: null,
66970
- pendingUpdatePrompt: null
66931
+ pendingUpdatePrompt: null,
66932
+ pendingRoutinePrompt: null
66971
66933
  };
66972
66934
  }
66973
66935
  getInitialState() {
@@ -66977,14 +66939,16 @@ class PromptExecutor extends BaseExecutor {
66977
66939
  return {
66978
66940
  pendingContextPrompt: this.state.pendingContextPrompt ? { ...this.state.pendingContextPrompt } : null,
66979
66941
  pendingExistingWorktreePrompt: this.state.pendingExistingWorktreePrompt ? { ...this.state.pendingExistingWorktreePrompt } : null,
66980
- pendingUpdatePrompt: this.state.pendingUpdatePrompt ? { ...this.state.pendingUpdatePrompt } : null
66942
+ pendingUpdatePrompt: this.state.pendingUpdatePrompt ? { ...this.state.pendingUpdatePrompt } : null,
66943
+ pendingRoutinePrompt: this.state.pendingRoutinePrompt ? { ...this.state.pendingRoutinePrompt } : null
66981
66944
  };
66982
66945
  }
66983
66946
  hydrateState(persisted) {
66984
66947
  this.state = {
66985
66948
  pendingContextPrompt: persisted.pendingContextPrompt ?? null,
66986
66949
  pendingExistingWorktreePrompt: persisted.pendingExistingWorktreePrompt ?? null,
66987
- pendingUpdatePrompt: persisted.pendingUpdatePrompt ?? null
66950
+ pendingUpdatePrompt: persisted.pendingUpdatePrompt ?? null,
66951
+ pendingRoutinePrompt: null
66988
66952
  };
66989
66953
  }
66990
66954
  setPendingContextPrompt(prompt) {
@@ -67114,6 +67078,30 @@ class PromptExecutor extends BaseExecutor {
67114
67078
  }
67115
67079
  return true;
67116
67080
  }
67081
+ setPendingRoutinePrompt(prompt) {
67082
+ this.state.pendingRoutinePrompt = prompt;
67083
+ }
67084
+ hasPendingRoutinePrompt() {
67085
+ return this.state.pendingRoutinePrompt !== null;
67086
+ }
67087
+ async handleRoutinePromptResponse(postId, approved, username, ctx) {
67088
+ if (!this.state.pendingRoutinePrompt)
67089
+ return false;
67090
+ if (this.state.pendingRoutinePrompt.postId !== postId)
67091
+ return false;
67092
+ const { parsed, requestedBy } = this.state.pendingRoutinePrompt;
67093
+ const statusMessage = approved ? `✅ ${ctx.formatter.formatBold(`Routine "${parsed.name}" confirmed`)} by ${ctx.formatter.formatUserMention(username)} — saving...` : `❌ ${ctx.formatter.formatBold(`Routine "${parsed.name}" discarded`)} by ${ctx.formatter.formatUserMention(username)}`;
67094
+ try {
67095
+ await ctx.platform.updatePost(postId, statusMessage);
67096
+ } catch (err) {
67097
+ ctx.logger.debug(`Failed to update routine prompt post: ${err}`);
67098
+ }
67099
+ this.state.pendingRoutinePrompt = null;
67100
+ if (this.events) {
67101
+ this.events.emit("routine-prompt:complete", { approved, parsed, requestedBy, postId });
67102
+ }
67103
+ return true;
67104
+ }
67117
67105
  async handleReaction(postId, emoji, user, action, ctx) {
67118
67106
  ctx.logger.debug(`PromptExecutor.handleReaction: postId=${postId.substring(0, 8)}, emoji=${emoji}, user=${user}, action=${action}`);
67119
67107
  if (action !== "added") {
@@ -67174,6 +67162,18 @@ class PromptExecutor extends BaseExecutor {
67174
67162
  ctx.logger.debug(`PromptExecutor: emoji ${emoji} not valid for update prompt, ignoring`);
67175
67163
  return false;
67176
67164
  }
67165
+ if (this.state.pendingRoutinePrompt?.postId === postId) {
67166
+ if (isApprovalEmoji(emoji)) {
67167
+ ctx.logger.debug(`Routine prompt reaction from @${user}: approve`);
67168
+ return this.handleRoutinePromptResponse(postId, true, user, ctx);
67169
+ }
67170
+ if (isDenialEmoji(emoji)) {
67171
+ ctx.logger.debug(`Routine prompt reaction from @${user}: discard`);
67172
+ return this.handleRoutinePromptResponse(postId, false, user, ctx);
67173
+ }
67174
+ ctx.logger.debug(`PromptExecutor: emoji ${emoji} not valid for routine prompt, ignoring`);
67175
+ return false;
67176
+ }
67177
67177
  ctx.logger.debug(`PromptExecutor: no pending prompt state matches postId=${postId.substring(0, 8)}`);
67178
67178
  return false;
67179
67179
  }
@@ -67268,7 +67268,7 @@ class BugReportExecutor extends BaseExecutor {
67268
67268
  // src/operations/executors/worktree-prompt.ts
67269
67269
  init_emoji();
67270
67270
  init_logger();
67271
- var log21 = createLogger("wt-prompt");
67271
+ var log20 = createLogger("wt-prompt");
67272
67272
  // src/operations/message-manager.ts
67273
67273
  init_logger();
67274
67274
 
@@ -67348,7 +67348,7 @@ function formatRelativeTime(date) {
67348
67348
  return `${diffMin} min ago`;
67349
67349
  }
67350
67350
  // src/operations/message-manager.ts
67351
- var log22 = createLogger("msg-mgr");
67351
+ var log21 = createLogger("msg-mgr");
67352
67352
 
67353
67353
  class MessageManager {
67354
67354
  platform;
@@ -67441,7 +67441,7 @@ class MessageManager {
67441
67441
  });
67442
67442
  }
67443
67443
  async handleEvent(event) {
67444
- const logger = log22.forSession(this.sessionId);
67444
+ const logger = log21.forSession(this.sessionId);
67445
67445
  const transformCtx = {
67446
67446
  sessionId: this.sessionId,
67447
67447
  formatter: this.platform.getFormatter(),
@@ -67495,7 +67495,7 @@ class MessageManager {
67495
67495
  }
67496
67496
  }
67497
67497
  async executeOperation(op) {
67498
- const logger = log22.forSession(this.sessionId);
67498
+ const logger = log21.forSession(this.sessionId);
67499
67499
  const ctx = this.getExecutorContext();
67500
67500
  try {
67501
67501
  if (isContentOp(op)) {
@@ -67563,7 +67563,7 @@ class MessageManager {
67563
67563
  threadId: this.threadId,
67564
67564
  platform: this.platform,
67565
67565
  formatter: this.platform.getFormatter(),
67566
- logger: log22.forSession(this.sessionId),
67566
+ logger: log21.forSession(this.sessionId),
67567
67567
  postTracker: this.postTracker,
67568
67568
  contentBreaker: this.contentBreaker,
67569
67569
  threadLogger: this.session.threadLogger,
@@ -67682,6 +67682,9 @@ class MessageManager {
67682
67682
  clearPendingUpdatePrompt() {
67683
67683
  this.promptExecutor.clearPendingUpdatePrompt();
67684
67684
  }
67685
+ setPendingRoutinePrompt(prompt) {
67686
+ this.promptExecutor.setPendingRoutinePrompt(prompt);
67687
+ }
67685
67688
  setPendingBugReport(report) {
67686
67689
  this.bugReportExecutor.setPendingBugReport(report);
67687
67690
  }
@@ -67780,13 +67783,13 @@ class MessageManager {
67780
67783
  return this.systemExecutor.postSuccess(message, this.getExecutorContext());
67781
67784
  }
67782
67785
  async prepareForUserMessage() {
67783
- const logger = log22.forSession(this.sessionId);
67786
+ const logger = log21.forSession(this.sessionId);
67784
67787
  logger.debug("Preparing for new user message");
67785
67788
  await this.closeCurrentPost();
67786
67789
  await this.bumpTaskList();
67787
67790
  }
67788
67791
  async handleUserMessage(message, files, username, displayName) {
67789
- const logger = log22.forSession(this.sessionId);
67792
+ const logger = log21.forSession(this.sessionId);
67790
67793
  if (!this.session.claude.isRunning()) {
67791
67794
  logger.debug("Claude not running, ignoring user message");
67792
67795
  return false;
@@ -67829,7 +67832,7 @@ class MessageManager {
67829
67832
  ];
67830
67833
  }
67831
67834
  async handleReaction(postId, emoji, user, action) {
67832
- const logger = log22.forSession(this.sessionId);
67835
+ const logger = log21.forSession(this.sessionId);
67833
67836
  const ctx = this.getExecutorContext();
67834
67837
  logger.debug(`Routing reaction: postId=${postId}, emoji=${emoji}, user=${user}, action=${action}`);
67835
67838
  for (const { name, executor } of this.reactionDispatchList()) {
@@ -67927,7 +67930,7 @@ class MessageManager {
67927
67930
  }
67928
67931
  // src/operations/sticky-message/handler.ts
67929
67932
  init_logger();
67930
- var log23 = createLogger("sticky");
67933
+ var log22 = createLogger("sticky");
67931
67934
  var botStartedAt = new Date;
67932
67935
  function getPendingPrompts(session) {
67933
67936
  const prompts2 = [];
@@ -68002,21 +68005,21 @@ function initialize(store) {
68002
68005
  stickyPostIds.set(platformId, postId);
68003
68006
  }
68004
68007
  if (persistedIds.size > 0) {
68005
- log23.info(`\uD83D\uDCCC Restored ${persistedIds.size} sticky post ID(s) from persistence`);
68008
+ log22.info(`\uD83D\uDCCC Restored ${persistedIds.size} sticky post ID(s) from persistence`);
68006
68009
  }
68007
68010
  }
68008
68011
  function setPlatformPaused(platformId, paused) {
68009
68012
  if (paused) {
68010
68013
  pausedPlatforms.set(platformId, true);
68011
- log23.debug(`Platform ${platformId} marked as paused`);
68014
+ log22.debug(`Platform ${platformId} marked as paused`);
68012
68015
  } else {
68013
68016
  pausedPlatforms.delete(platformId);
68014
- log23.debug(`Platform ${platformId} marked as active`);
68017
+ log22.debug(`Platform ${platformId} marked as active`);
68015
68018
  }
68016
68019
  }
68017
68020
  function setShuttingDown(shuttingDown) {
68018
68021
  isShuttingDown = shuttingDown;
68019
- log23.debug(`Bot shutdown state: ${shuttingDown}`);
68022
+ log22.debug(`Bot shutdown state: ${shuttingDown}`);
68020
68023
  }
68021
68024
  function getTaskContent(session) {
68022
68025
  const taskState = session.messageManager?.getTaskListState();
@@ -68349,12 +68352,12 @@ async function validateLastMessageIds(platform, sessions) {
68349
68352
  try {
68350
68353
  const post2 = await platform.getPost(lastMessageId);
68351
68354
  if (!post2) {
68352
- log23.debug(`lastMessageId ${lastMessageId.substring(0, 8)} for session ${session.sessionId} was deleted, clearing`);
68355
+ log22.debug(`lastMessageId ${lastMessageId.substring(0, 8)} for session ${session.sessionId} was deleted, clearing`);
68353
68356
  session.lastMessageId = undefined;
68354
68357
  session.lastMessageTs = undefined;
68355
68358
  }
68356
68359
  } catch (err) {
68357
- log23.debug(`Failed to validate lastMessageId for session ${session.sessionId}, clearing: ${err}`);
68360
+ log22.debug(`Failed to validate lastMessageId for session ${session.sessionId}, clearing: ${err}`);
68358
68361
  session.lastMessageId = undefined;
68359
68362
  session.lastMessageTs = undefined;
68360
68363
  }
@@ -68371,7 +68374,7 @@ async function updateStickyMessageImpl(platform, sessions, config) {
68371
68374
  hiddenCleanupDone.add(platform.platformId);
68372
68375
  const existing = stickyPostIds.get(platform.platformId);
68373
68376
  if (existing) {
68374
- log23.info(`sticky[${platform.platformId}] hidden mode: removing leftover ${formatShortId(existing)}`);
68377
+ log22.info(`sticky[${platform.platformId}] hidden mode: removing leftover ${formatShortId(existing)}`);
68375
68378
  try {
68376
68379
  await platform.unpinPost(existing);
68377
68380
  } catch {}
@@ -68391,63 +68394,63 @@ async function updateStickyMessageImpl(platform, sessions, config) {
68391
68394
  return;
68392
68395
  }
68393
68396
  const platformSessions = [...sessions.values()].filter((s) => s.platformId === platform.platformId);
68394
- log23.debug(`updateStickyMessage for ${platform.platformId}, ${platformSessions.length} sessions`);
68397
+ log22.debug(`updateStickyMessage for ${platform.platformId}, ${platformSessions.length} sessions`);
68395
68398
  for (const s of platformSessions) {
68396
- log23.debug(` - ${s.sessionId}: title="${s.sessionTitle}" firstPrompt="${s.firstPrompt?.substring(0, 30)}..."`);
68399
+ log22.debug(` - ${s.sessionId}: title="${s.sessionTitle}" firstPrompt="${s.firstPrompt?.substring(0, 30)}..."`);
68397
68400
  }
68398
68401
  await validateLastMessageIds(platform, platformSessions);
68399
68402
  const formatter = platform.getFormatter();
68400
68403
  const content = await buildStickyMessage(sessions, platform.platformId, config, formatter, (threadId) => platform.getThreadLink(threadId));
68401
68404
  const existingPostId = stickyPostIds.get(platform.platformId);
68402
68405
  const shouldBump = needsBump.get(platform.platformId) ?? false;
68403
- log23.debug(`existingPostId: ${existingPostId || "(none)"}, needsBump: ${shouldBump}`);
68406
+ log22.debug(`existingPostId: ${existingPostId || "(none)"}, needsBump: ${shouldBump}`);
68404
68407
  try {
68405
68408
  if (existingPostId && !shouldBump) {
68406
- log23.debug(`Updating existing post in place...`);
68409
+ log22.debug(`Updating existing post in place...`);
68407
68410
  try {
68408
68411
  await platform.updatePost(existingPostId, content);
68409
68412
  try {
68410
68413
  await platform.pinPost(existingPostId);
68411
- log23.debug(`Re-pinned post`);
68414
+ log22.debug(`Re-pinned post`);
68412
68415
  } catch (pinErr) {
68413
- log23.debug(`Re-pin failed (might already be pinned): ${pinErr}`);
68416
+ log22.debug(`Re-pin failed (might already be pinned): ${pinErr}`);
68414
68417
  }
68415
- log23.debug(`Updated successfully`);
68418
+ log22.debug(`Updated successfully`);
68416
68419
  return;
68417
68420
  } catch (err) {
68418
- log23.debug(`Update failed, will create new: ${err}`);
68421
+ log22.debug(`Update failed, will create new: ${err}`);
68419
68422
  }
68420
68423
  }
68421
68424
  needsBump.set(platform.platformId, false);
68422
68425
  if (existingPostId) {
68423
- log23.debug(`Unpinning and deleting existing post ${existingPostId.substring(0, 8)}...`);
68426
+ log22.debug(`Unpinning and deleting existing post ${existingPostId.substring(0, 8)}...`);
68424
68427
  try {
68425
68428
  await platform.unpinPost(existingPostId);
68426
- log23.debug(`Unpinned successfully`);
68429
+ log22.debug(`Unpinned successfully`);
68427
68430
  } catch (err) {
68428
- log23.debug(`Unpin failed (probably already unpinned): ${err}`);
68431
+ log22.debug(`Unpin failed (probably already unpinned): ${err}`);
68429
68432
  }
68430
68433
  try {
68431
68434
  await platform.deletePost(existingPostId);
68432
- log23.debug(`Deleted successfully`);
68435
+ log22.debug(`Deleted successfully`);
68433
68436
  } catch (err) {
68434
- log23.debug(`Delete failed (probably already deleted): ${err}`);
68437
+ log22.debug(`Delete failed (probably already deleted): ${err}`);
68435
68438
  }
68436
68439
  stickyPostIds.delete(platform.platformId);
68437
68440
  }
68438
- log23.debug(`Creating new post...`);
68441
+ log22.debug(`Creating new post...`);
68439
68442
  const post2 = await platform.createPost(content);
68440
68443
  stickyPostIds.set(platform.platformId, post2.id);
68441
68444
  try {
68442
68445
  await platform.pinPost(post2.id);
68443
- log23.debug(`Pinned post successfully`);
68446
+ log22.debug(`Pinned post successfully`);
68444
68447
  } catch (err) {
68445
- log23.debug(`Failed to pin post: ${err}`);
68448
+ log22.debug(`Failed to pin post: ${err}`);
68446
68449
  }
68447
68450
  if (sessionStore) {
68448
68451
  sessionStore.saveStickyPostId(platform.platformId, post2.id);
68449
68452
  }
68450
- log23.info(`\uD83D\uDCCC Created sticky message for ${platform.platformId}: ${formatShortId(post2.id)}`);
68453
+ log22.info(`\uD83D\uDCCC Created sticky message for ${platform.platformId}: ${formatShortId(post2.id)}`);
68451
68454
  const excludePostIds = new Set;
68452
68455
  if (sessionStore) {
68453
68456
  for (const session of sessionStore.load().values()) {
@@ -68463,10 +68466,10 @@ async function updateStickyMessageImpl(platform, sessions, config) {
68463
68466
  }
68464
68467
  const botUser = await platform.getBotUser();
68465
68468
  cleanupOldStickyMessages(platform, botUser.id, false, excludePostIds).catch((err) => {
68466
- log23.debug(`Background cleanup failed: ${err}`);
68469
+ log22.debug(`Background cleanup failed: ${err}`);
68467
68470
  });
68468
68471
  } catch (err) {
68469
- log23.error(`Failed to update sticky message for ${platform.platformId}`, err instanceof Error ? err : undefined);
68472
+ log22.error(`Failed to update sticky message for ${platform.platformId}`, err instanceof Error ? err : undefined);
68470
68473
  }
68471
68474
  }
68472
68475
  async function updateAllStickyMessages(platforms, sessions, config, overheadByPlatform) {
@@ -68494,7 +68497,7 @@ async function cleanupOldStickyMessages(platform, botUserId, forceRun = false, e
68494
68497
  if (!forceRun) {
68495
68498
  const lastRun = lastCleanupTime.get(platformId) || 0;
68496
68499
  if (now - lastRun < CLEANUP_THROTTLE_MS) {
68497
- log23.debug(`Cleanup throttled for ${platformId} (last run ${Math.round((now - lastRun) / 1000)}s ago)`);
68500
+ log22.debug(`Cleanup throttled for ${platformId} (last run ${Math.round((now - lastRun) / 1000)}s ago)`);
68498
68501
  return;
68499
68502
  }
68500
68503
  }
@@ -68504,37 +68507,37 @@ async function cleanupOldStickyMessages(platform, botUserId, forceRun = false, e
68504
68507
  const pinnedPostIds = await platform.getPinnedPosts();
68505
68508
  const recentPinnedIds = pinnedPostIds.filter((id) => id !== currentStickyId && !excludePostIds?.has(id) && isRecentPost(id));
68506
68509
  if (recentPinnedIds.length === 0) {
68507
- log23.debug(`No recent pinned posts to check (${pinnedPostIds.length} total, current: ${currentStickyId?.substring(0, 8) || "(none)"})`);
68510
+ log22.debug(`No recent pinned posts to check (${pinnedPostIds.length} total, current: ${currentStickyId?.substring(0, 8) || "(none)"})`);
68508
68511
  return;
68509
68512
  }
68510
- log23.debug(`Checking ${recentPinnedIds.length} recent pinned posts (of ${pinnedPostIds.length} total)`);
68513
+ log22.debug(`Checking ${recentPinnedIds.length} recent pinned posts (of ${pinnedPostIds.length} total)`);
68511
68514
  for (const postId of recentPinnedIds) {
68512
68515
  try {
68513
68516
  const post2 = await platform.getPost(postId);
68514
68517
  if (!post2)
68515
68518
  continue;
68516
68519
  if (post2.userId === botUserId) {
68517
- log23.debug(`Cleaning up old sticky: ${postId.substring(0, 8)}...`);
68520
+ log22.debug(`Cleaning up old sticky: ${postId.substring(0, 8)}...`);
68518
68521
  try {
68519
68522
  await platform.unpinPost(postId);
68520
68523
  await platform.deletePost(postId);
68521
- log23.info(`\uD83E\uDDF9 Cleaned up old sticky message: ${postId.substring(0, 8)}...`);
68524
+ log22.info(`\uD83E\uDDF9 Cleaned up old sticky message: ${postId.substring(0, 8)}...`);
68522
68525
  } catch (err) {
68523
- log23.debug(`Failed to cleanup ${postId}: ${err}`);
68526
+ log22.debug(`Failed to cleanup ${postId}: ${err}`);
68524
68527
  }
68525
68528
  }
68526
68529
  } catch (err) {
68527
- log23.debug(`Could not check post ${postId}: ${err}`);
68530
+ log22.debug(`Could not check post ${postId}: ${err}`);
68528
68531
  }
68529
68532
  }
68530
68533
  } catch (err) {
68531
- log23.error(`Failed to cleanup old sticky messages`, err instanceof Error ? err : undefined);
68534
+ log22.error(`Failed to cleanup old sticky messages`, err instanceof Error ? err : undefined);
68532
68535
  }
68533
68536
  }
68534
68537
  // src/claude/quick-query.ts
68535
68538
  init_spawn();
68536
68539
  init_logger();
68537
- var log24 = createLogger("query");
68540
+ var log23 = createLogger("query");
68538
68541
  async function quickQuery(options) {
68539
68542
  const {
68540
68543
  prompt,
@@ -68549,7 +68552,7 @@ async function quickQuery(options) {
68549
68552
  if (systemPrompt) {
68550
68553
  args.push("--system-prompt", systemPrompt);
68551
68554
  }
68552
- log24.debug(`Quick query: model=${model}, timeout=${timeout2}ms, prompt="${prompt.substring(0, 50)}..."`);
68555
+ log23.debug(`Quick query: model=${model}, timeout=${timeout2}ms, prompt="${prompt.substring(0, 50)}..."`);
68553
68556
  return new Promise((resolve6) => {
68554
68557
  let stdout = "";
68555
68558
  let stderr = "";
@@ -68563,7 +68566,7 @@ async function quickQuery(options) {
68563
68566
  if (!resolved) {
68564
68567
  resolved = true;
68565
68568
  proc.kill("SIGTERM");
68566
- log24.debug(`Quick query timed out after ${timeout2}ms`);
68569
+ log23.debug(`Quick query timed out after ${timeout2}ms`);
68567
68570
  resolve6({
68568
68571
  success: false,
68569
68572
  error: "timeout",
@@ -68581,7 +68584,7 @@ async function quickQuery(options) {
68581
68584
  if (!resolved) {
68582
68585
  resolved = true;
68583
68586
  clearTimeout(timeoutId);
68584
- log24.debug(`Quick query error: ${err.message}`);
68587
+ log23.debug(`Quick query error: ${err.message}`);
68585
68588
  resolve6({
68586
68589
  success: false,
68587
68590
  error: err.message,
@@ -68595,14 +68598,14 @@ async function quickQuery(options) {
68595
68598
  clearTimeout(timeoutId);
68596
68599
  const durationMs = Date.now() - startTime;
68597
68600
  if (code === 0 && stdout.trim()) {
68598
- log24.debug(`Quick query success: ${durationMs}ms, ${stdout.length} chars`);
68601
+ log23.debug(`Quick query success: ${durationMs}ms, ${stdout.length} chars`);
68599
68602
  resolve6({
68600
68603
  success: true,
68601
68604
  response: stdout.trim(),
68602
68605
  durationMs
68603
68606
  });
68604
68607
  } else {
68605
- log24.debug(`Quick query failed: code=${code}, stderr=${stderr.substring(0, 100)}`);
68608
+ log23.debug(`Quick query failed: code=${code}, stderr=${stderr.substring(0, 100)}`);
68606
68609
  resolve6({
68607
68610
  success: false,
68608
68611
  error: stderr || `exit code ${code}`,
@@ -68621,7 +68624,7 @@ init_logger();
68621
68624
  import { exec as exec3 } from "child_process";
68622
68625
  import { promisify as promisify3 } from "util";
68623
68626
  var execAsync2 = promisify3(exec3);
68624
- var log25 = createLogger("branch");
68627
+ var log24 = createLogger("branch");
68625
68628
  var SUGGESTION_TIMEOUT = 15000;
68626
68629
  var MAX_SUGGESTIONS = 3;
68627
68630
  async function getCurrentBranch3(workingDir) {
@@ -68670,7 +68673,7 @@ function parseBranchSuggestions(response) {
68670
68673
  return lines.slice(0, MAX_SUGGESTIONS);
68671
68674
  }
68672
68675
  async function suggestBranchNames(workingDir, userMessage) {
68673
- log25.debug(`Suggesting branch names for: "${userMessage.substring(0, 50)}..."`);
68676
+ log24.debug(`Suggesting branch names for: "${userMessage.substring(0, 50)}..."`);
68674
68677
  try {
68675
68678
  const [currentBranch, recentCommits] = await Promise.all([
68676
68679
  getCurrentBranch3(workingDir),
@@ -68684,24 +68687,24 @@ async function suggestBranchNames(workingDir, userMessage) {
68684
68687
  workingDir
68685
68688
  });
68686
68689
  if (!result.success || !result.response) {
68687
- log25.debug(`Branch suggestion failed: ${result.error || "no response"}`);
68690
+ log24.debug(`Branch suggestion failed: ${result.error || "no response"}`);
68688
68691
  return [];
68689
68692
  }
68690
68693
  const suggestions = parseBranchSuggestions(result.response);
68691
- log25.debug(`Got ${suggestions.length} branch suggestions: ${suggestions.join(", ")}`);
68694
+ log24.debug(`Got ${suggestions.length} branch suggestions: ${suggestions.join(", ")}`);
68692
68695
  return suggestions;
68693
68696
  } catch (err) {
68694
- log25.debug(`Branch suggestion error: ${err}`);
68697
+ log24.debug(`Branch suggestion error: ${err}`);
68695
68698
  return [];
68696
68699
  }
68697
68700
  }
68698
68701
 
68699
68702
  // src/operations/worktree/handler.ts
68700
68703
  init_worktree();
68701
- import { randomUUID as randomUUID3 } from "crypto";
68704
+ import { randomUUID as randomUUID4 } from "crypto";
68702
68705
  init_logger();
68703
- var log26 = createLogger("worktree");
68704
- var sessionLog2 = createSessionLog(log26);
68706
+ var log25 = createLogger("worktree");
68707
+ var sessionLog2 = createSessionLog(log25);
68705
68708
  function parseWorktreeError(error) {
68706
68709
  const message = error instanceof Error ? error.message : String(error);
68707
68710
  const lowerMessage = message.toLowerCase();
@@ -68945,7 +68948,7 @@ async function createAndSwitchToWorktree(session, branch, username, options) {
68945
68948
  transitionTo(session, "restarting");
68946
68949
  await session.claude.kill();
68947
68950
  await options.flush(session);
68948
- const newSessionId = randomUUID3();
68951
+ const newSessionId = randomUUID4();
68949
68952
  session.claudeSessionId = newSessionId;
68950
68953
  const needsTitlePrompt = !session.sessionTitle;
68951
68954
  const memoryConfig = options.getPlatformMemoryConfig(session.platformId);
@@ -69044,7 +69047,7 @@ ${fmt.formatItalic("Claude Code restarted in the worktree")}`);
69044
69047
  transitionTo(session, "restarting");
69045
69048
  await session.claude.kill();
69046
69049
  await options.flush(session);
69047
- const newSessionId = randomUUID3();
69050
+ const newSessionId = randomUUID4();
69048
69051
  session.claudeSessionId = newSessionId;
69049
69052
  const needsTitlePrompt = !session.sessionTitle;
69050
69053
  const memoryConfig = options.getPlatformMemoryConfig(session.platformId);
@@ -69283,8 +69286,8 @@ async function cleanupWorktreeCommand(session, username, hasOtherSessionsUsingWo
69283
69286
  }
69284
69287
  // src/operations/events/handler.ts
69285
69288
  init_logger();
69286
- var log27 = createLogger("events");
69287
- var sessionLog3 = createSessionLog(log27);
69289
+ var log26 = createLogger("events");
69290
+ var sessionLog3 = createSessionLog(log26);
69288
69291
  function detectAndExecuteClaudeCommands(text, session, ctx) {
69289
69292
  const parsed = parseClaudeCommand(text);
69290
69293
  if (parsed && isClaudeAllowedCommand(parsed.command)) {
@@ -69417,7 +69420,11 @@ function handleEventPostProcessing(session, event, ctx, mainHandling) {
69417
69420
  ctx.ops.emitSessionUpdate(session.sessionId, { status: getSessionStatus(session) });
69418
69421
  updateUsageStats(session, event, ctx);
69419
69422
  if (mainHandling) {
69420
- mainHandling.catch(() => {}).then(() => ctx.ops.persistSession(session));
69423
+ mainHandling.catch(() => {}).then(() => {
69424
+ if (!ctx.state.sessions.has(session.sessionId))
69425
+ return;
69426
+ ctx.ops.persistSession(session);
69427
+ });
69421
69428
  } else {
69422
69429
  ctx.ops.persistSession(session);
69423
69430
  }
@@ -69593,6 +69600,56 @@ function updateUsageFromStatusLine(session) {
69593
69600
  sessionLog3(session).debug(`Updated from status line: context ${contextTokens}/${session.usageStats.contextWindowSize} (${contextPct}%)`);
69594
69601
  }
69595
69602
  }
69603
+ // src/operations/monitor/handler.ts
69604
+ init_logger();
69605
+ var log27 = createLogger("monitor");
69606
+ var DEFAULT_INTERVAL_MS2 = 60 * 1000;
69607
+
69608
+ class SessionMonitor {
69609
+ intervalMs;
69610
+ sessionTimeoutMs;
69611
+ sessionWarningMs;
69612
+ getContext;
69613
+ getSessionCount;
69614
+ updateStickyMessage;
69615
+ timer = null;
69616
+ isRunning = false;
69617
+ constructor(options) {
69618
+ this.intervalMs = options.intervalMs ?? DEFAULT_INTERVAL_MS2;
69619
+ this.sessionTimeoutMs = options.sessionTimeoutMs;
69620
+ this.sessionWarningMs = options.sessionWarningMs;
69621
+ this.getContext = options.getContext;
69622
+ this.getSessionCount = options.getSessionCount;
69623
+ this.updateStickyMessage = options.updateStickyMessage;
69624
+ }
69625
+ start() {
69626
+ if (this.isRunning) {
69627
+ log27.debug("Session monitor already running");
69628
+ return;
69629
+ }
69630
+ this.isRunning = true;
69631
+ log27.debug(`Session monitor started (interval: ${this.intervalMs / 1000}s)`);
69632
+ this.timer = setInterval(() => {
69633
+ this.runCheck().catch((err) => {
69634
+ log27.error(`Error during session monitoring: ${err}`);
69635
+ });
69636
+ }, this.intervalMs);
69637
+ }
69638
+ stop() {
69639
+ if (this.timer) {
69640
+ clearInterval(this.timer);
69641
+ this.timer = null;
69642
+ }
69643
+ this.isRunning = false;
69644
+ log27.debug("Session monitor stopped");
69645
+ }
69646
+ async runCheck() {
69647
+ await cleanupIdleSessions(this.sessionTimeoutMs, this.sessionWarningMs, this.getContext());
69648
+ if (this.getSessionCount() > 0) {
69649
+ await this.updateStickyMessage();
69650
+ }
69651
+ }
69652
+ }
69596
69653
  // src/operations/session-context/types.ts
69597
69654
  function createSessionContext(config, state, ops) {
69598
69655
  return {
@@ -69994,9 +70051,90 @@ async function suggestSessionMetadata(context) {
69994
70051
  return null;
69995
70052
  }
69996
70053
  }
70054
+ // src/routines/parser.ts
70055
+ init_logger();
70056
+ var log31 = createLogger("routines");
70057
+ var PARSE_TIMEOUT_MS = 15000;
70058
+ function hostTimezone() {
70059
+ return Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC";
70060
+ }
70061
+ function buildParsePrompt(request, defaultTimezone) {
70062
+ return `Parse this scheduled-routine request from a chat user into JSON.
70063
+
70064
+ Request: ${request}
70065
+
70066
+ Output ONLY a JSON object, no other text, with exactly these fields:
70067
+ - "name": short descriptive name for the routine (max 6 words)
70068
+ - "prompt": the task to perform on each run, as an instruction (everything that is not the schedule)
70069
+ - "preset": one of "hourly", "daily", "weekdays", "weekly" (the closest match; sub-hourly is not supported — if the user asked for more often than hourly, use "hourly")
70070
+ - "time": "HH:MM" 24-hour (omit for hourly)
70071
+ - "weekday": 1-7 where 1=Monday (only for weekly)
70072
+ - "timezone": IANA timezone ONLY if the user named one (e.g. "9am Pacific" -> "America/Los_Angeles"); otherwise omit and ${defaultTimezone} will be used
70073
+
70074
+ If the request is not actually asking for a recurring schedule, output exactly: {"error": "reason"}`;
70075
+ }
70076
+ function extractJsonObject(output) {
70077
+ const start = output.indexOf("{");
70078
+ const end = output.lastIndexOf("}");
70079
+ if (start < 0 || end <= start)
70080
+ return;
70081
+ try {
70082
+ const parsed = JSON.parse(output.slice(start, end + 1));
70083
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : undefined;
70084
+ } catch {
70085
+ return;
70086
+ }
70087
+ }
70088
+ function validateParsedRoutine(raw, defaultTimezone) {
70089
+ if (typeof raw.error === "string" && raw.error) {
70090
+ return { ok: false, error: raw.error };
70091
+ }
70092
+ const name = typeof raw.name === "string" ? raw.name.trim() : "";
70093
+ const prompt = typeof raw.prompt === "string" ? raw.prompt.trim() : "";
70094
+ const preset = raw.preset;
70095
+ if (!name)
70096
+ return { ok: false, error: "could not derive a routine name" };
70097
+ if (!prompt)
70098
+ return { ok: false, error: "could not tell what the routine should do" };
70099
+ if (!SCHEDULE_PRESETS.includes(preset)) {
70100
+ return { ok: false, error: `could not map the schedule to ${SCHEDULE_PRESETS.join("/")}` };
70101
+ }
70102
+ const timezoneDefaulted = !(typeof raw.timezone === "string" && raw.timezone);
70103
+ const timezone = timezoneDefaulted ? defaultTimezone : raw.timezone;
70104
+ if (!isValidTimezone(timezone)) {
70105
+ return { ok: false, error: `unknown timezone "${String(raw.timezone)}"` };
70106
+ }
70107
+ const schedule = {
70108
+ preset,
70109
+ timezone,
70110
+ ...preset !== "hourly" && typeof raw.time === "string" ? { time: raw.time } : {},
70111
+ ...preset === "weekly" && typeof raw.weekday === "number" ? { weekday: raw.weekday } : {}
70112
+ };
70113
+ const scheduleError = validateSchedule(schedule);
70114
+ if (scheduleError)
70115
+ return { ok: false, error: scheduleError };
70116
+ return { ok: true, parsed: { name, prompt, schedule }, timezoneDefaulted };
70117
+ }
70118
+ async function parseRoutineRequest(request, defaultTimezone = hostTimezone()) {
70119
+ const result = await quickQuery({
70120
+ prompt: buildParsePrompt(request, defaultTimezone),
70121
+ model: "haiku",
70122
+ timeout: PARSE_TIMEOUT_MS
70123
+ });
70124
+ if (!result.success || !result.response) {
70125
+ log31.debug(`Routine parse quickQuery failed: ${result.error ?? "no response"}`);
70126
+ return { ok: false, error: "could not reach the parsing model — try again in a moment" };
70127
+ }
70128
+ const raw = extractJsonObject(result.response);
70129
+ if (!raw) {
70130
+ return { ok: false, error: 'could not understand the schedule — try e.g. "every weekday at 9:00, <task>"' };
70131
+ }
70132
+ return validateParsedRoutine(raw, defaultTimezone);
70133
+ }
70134
+
69997
70135
  // src/operations/commands/handler.ts
69998
- var log31 = createLogger("commands");
69999
- var sessionLog5 = createSessionLog(log31);
70136
+ var log32 = createLogger("commands");
70137
+ var sessionLog5 = createSessionLog(log32);
70000
70138
  function sessionAccountOption(session, ctx) {
70001
70139
  if (!session.claudeAccountId)
70002
70140
  return;
@@ -70173,7 +70311,7 @@ async function changeDirectory(session, newDir, username, ctx) {
70173
70311
  sessionLog5(session).debug(`Stored work summary for context preservation`);
70174
70312
  }
70175
70313
  session.workingDir = absoluteDir;
70176
- const newSessionId = randomUUID4();
70314
+ const newSessionId = randomUUID5();
70177
70315
  session.claudeSessionId = newSessionId;
70178
70316
  const memoryConfig = ctx.ops.getPlatformMemoryConfig(session.platformId);
70179
70317
  const appendSystemPrompt = await buildAppendSystemPrompt(session.platform, session.platformId, absoluteDir, session.threadId, session.startedBy, session.sessionAllowedUsers, CHAT_PLATFORM_PROMPT, ctx.state.githubEmailsStore, memoryConfig.enabled && memoryConfig.channelLayer ? ctx.state.memoryStore : null, { userAttribution: session.userAttribution });
@@ -70463,6 +70601,125 @@ ${list}${more}`);
70463
70601
  await post(session, "warning", `\uD83E\uDDE0 No matching entry. Use ${formatter.formatCode("!memory")} to list entries, then ${formatter.formatCode("!memory forget <number>")}.`);
70464
70602
  }
70465
70603
  }
70604
+ async function requireRoutinesEnabled(session, ctx) {
70605
+ if (ctx.ops.isRoutinesEnabled(session.platformId))
70606
+ return true;
70607
+ await post(session, "info", `\uD83D\uDD58 Routines are disabled for this platform (see the \`routines\` option in config.yaml).`);
70608
+ return false;
70609
+ }
70610
+ async function createRoutine(session, request, username, ctx, parse = parseRoutineRequest) {
70611
+ if (!await requireRoutinesEnabled(session, ctx))
70612
+ return;
70613
+ if (!await requireSessionOwner(session, username, "create routines"))
70614
+ return;
70615
+ const formatter = session.platform.getFormatter();
70616
+ const trimmed = request.trim();
70617
+ if (!trimmed) {
70618
+ await post(session, "warning", `Usage: ${formatter.formatCode("!routine every weekday at 9:00, <task>")}`);
70619
+ return;
70620
+ }
70621
+ await post(session, "info", `\uD83D\uDD58 Parsing the schedule...`);
70622
+ const result = await parse(trimmed, hostTimezone());
70623
+ if (!result.ok) {
70624
+ await post(session, "warning", `\uD83D\uDD58 Could not create a routine: ${result.error}`);
70625
+ sessionLog5(session).warn(`\uD83D\uDD58 Routine parse failed for @${username}: ${result.error}`);
70626
+ return;
70627
+ }
70628
+ const { parsed, timezoneDefaulted } = result;
70629
+ const tzNote = timezoneDefaulted ? `
70630
+ ${formatter.formatItalic(`Timezone defaulted to the bot host's ${parsed.schedule.timezone} — name one explicitly ("9am Pacific") to override.`)}` : "";
70631
+ const confirmPost = await postInteractiveAndRegister(session, `\uD83D\uDD58 ${formatter.formatBold(`Create routine "${parsed.name}"?`)}
70632
+ ` + `${formatter.formatBold("Schedule:")} ${describeSchedule(parsed.schedule)}
70633
+ ` + `${formatter.formatBold("Task:")} ${parsed.prompt}${tzNote}
70634
+
70635
+ ` + `${formatter.formatItalic("Each run starts a full Claude session in a new thread. React \uD83D\uDC4D to save or \uD83D\uDC4E to discard.")}`, ["+1", "-1"], (postId, threadId) => ctx.ops.registerPost(postId, threadId));
70636
+ session.messageManager?.setPendingRoutinePrompt({
70637
+ postId: confirmPost.id,
70638
+ parsed,
70639
+ requestedBy: username
70640
+ });
70641
+ sessionLog5(session).info(`\uD83D\uDD58 Routine proposal posted for @${username}: "${parsed.name}"`);
70642
+ }
70643
+ function routineByIndex(ctx, platformId, arg) {
70644
+ if (!/^\d+$/.test(arg))
70645
+ return;
70646
+ const list = ctx.state.routinesStore.list(platformId);
70647
+ return list[parseInt(arg, 10) - 1];
70648
+ }
70649
+ async function manageRoutines(session, args, username, ctx) {
70650
+ if (!await requireRoutinesEnabled(session, ctx))
70651
+ return;
70652
+ const formatter = session.platform.getFormatter();
70653
+ const platformId = session.platformId;
70654
+ const trimmed = args?.trim();
70655
+ if (!trimmed) {
70656
+ const routines = ctx.state.routinesStore.list(platformId);
70657
+ if (routines.length === 0) {
70658
+ await post(session, "info", `\uD83D\uDD58 No routines yet. Create one with ${formatter.formatCode("!routine every weekday at 9:00, <task>")}.`);
70659
+ return;
70660
+ }
70661
+ const lines = routines.map((r, i) => {
70662
+ const status = r.enabled ? "" : " — ⏸️ paused";
70663
+ const last = r.lastRunAt ? ` · last run ${r.lastRunAt.slice(0, 16).replace("T", " ")}Z (${r.lastRunStatus})` : "";
70664
+ return `${i + 1}. ${formatter.formatBold(r.name)} — ${describeSchedule(r.schedule)} · by ${formatter.formatCode("@" + r.createdBy)}${status}${last}`;
70665
+ });
70666
+ await post(session, "info", `\uD83D\uDD58 ${formatter.formatBold(`Routines (${routines.length})`)} — each run starts a full Claude session in a new thread:
70667
+
70668
+ ` + `${lines.join(`
70669
+ `)}
70670
+
70671
+ ` + `${formatter.formatItalic(`Manage with ${"`!routines pause|resume|delete|run <n>`"}.`)}`);
70672
+ session.threadLogger?.logCommand("routines", "list", username);
70673
+ return;
70674
+ }
70675
+ const match = trimmed.match(/^(pause|resume|delete|run)\s+(\d+)$/i);
70676
+ if (!match) {
70677
+ await post(session, "warning", `\uD83D\uDD58 Usage: ${formatter.formatCode("!routines")} or ${formatter.formatCode("!routines pause|resume|delete|run <n>")}`);
70678
+ return;
70679
+ }
70680
+ const [, action, indexArg] = match;
70681
+ const routine = routineByIndex(ctx, platformId, indexArg);
70682
+ if (!routine) {
70683
+ await post(session, "warning", `\uD83D\uDD58 No routine ${indexArg}. See ${formatter.formatCode("!routines")}.`);
70684
+ return;
70685
+ }
70686
+ const lowered = action.toLowerCase();
70687
+ if (lowered !== "run" && !await requireSessionOwner(session, username, "manage routines")) {
70688
+ return;
70689
+ }
70690
+ if (lowered === "run" && !session.platform.isUserAllowed(username)) {
70691
+ await post(session, "warning", `\uD83D\uDD58 Only platform-allowed users can run routines (${formatter.formatCode("@" + username)} is invited to this session only).`);
70692
+ return;
70693
+ }
70694
+ switch (lowered) {
70695
+ case "pause":
70696
+ await ctx.state.routinesStore.update(platformId, routine.id, { enabled: false });
70697
+ await post(session, "success", `⏸️ Routine ${formatter.formatBold(routine.name)} paused.`);
70698
+ break;
70699
+ case "resume":
70700
+ await ctx.state.routinesStore.update(platformId, routine.id, { enabled: true, consecutiveFailures: 0 });
70701
+ await post(session, "success", `▶️ Routine ${formatter.formatBold(routine.name)} resumed.`);
70702
+ break;
70703
+ case "delete":
70704
+ await ctx.state.routinesStore.remove(platformId, routine.id);
70705
+ await post(session, "success", `\uD83D\uDDD1️ Routine ${formatter.formatBold(routine.name)} deleted.`);
70706
+ break;
70707
+ case "run": {
70708
+ await post(session, "info", `\uD83D\uDD58 Running ${formatter.formatBold(routine.name)} now — it will post in a new thread.`);
70709
+ const status = await ctx.ops.fireRoutineNow(platformId, routine);
70710
+ if (status === "skipped") {
70711
+ await post(session, "warning", `\uD83D\uDD58 Could not run now (session limit reached or platform busy) — try again shortly.`);
70712
+ } else if (status === "unauthorized") {
70713
+ await post(session, "warning", `\uD83D\uDD58 The routine's creator ${formatter.formatCode("@" + routine.createdBy)} is no longer authorized — routine disabled.`);
70714
+ } else if (status === "failed") {
70715
+ await post(session, "warning", `\uD83D\uDD58 The run failed to start — check the bot logs.`);
70716
+ }
70717
+ break;
70718
+ }
70719
+ }
70720
+ sessionLog5(session).info(`\uD83D\uDD58 @${username}: !routines ${lowered} ${indexArg} ("${routine.name}")`);
70721
+ session.threadLogger?.logCommand("routines", `${lowered} ${indexArg}`, username);
70722
+ }
70466
70723
  async function setSessionPermissionMode(session, username, mode, ctx) {
70467
70724
  if (!await requireSessionOwner(session, username, "change permissions")) {
70468
70725
  return;
@@ -70763,7 +71020,7 @@ init_worktree();
70763
71020
 
70764
71021
  // src/memory/distiller.ts
70765
71022
  init_logger();
70766
- var log32 = createLogger("memory");
71023
+ var log33 = createLogger("memory");
70767
71024
  var MIN_THREAD_MESSAGES = 4;
70768
71025
  var DISTILL_MESSAGE_LIMIT = 30;
70769
71026
  var MESSAGE_CHAR_CAP = 500;
@@ -70810,10 +71067,10 @@ function scheduleDistillation(session, ctx, reason) {
70810
71067
  const store = ctx.state.memoryStore;
70811
71068
  distillThread(store, platformId, threadId, platform).then((added) => {
70812
71069
  if (added > 0) {
70813
- log32.debug(`Distilled ${added} memory entries from ${platformId}:${threadId} (${reason})`);
71070
+ log33.debug(`Distilled ${added} memory entries from ${platformId}:${threadId} (${reason})`);
70814
71071
  }
70815
71072
  }).catch((err) => {
70816
- log32.debug(`Distillation failed for ${platformId}:${threadId}: ${err.message}`);
71073
+ log33.debug(`Distillation failed for ${platformId}:${threadId}: ${err.message}`);
70817
71074
  });
70818
71075
  }
70819
71076
  async function distillThread(store, platformId, threadId, platform) {
@@ -70840,8 +71097,8 @@ async function distillThread(store, platformId, threadId, platform) {
70840
71097
  }
70841
71098
 
70842
71099
  // src/session/lifecycle.ts
70843
- var log33 = createLogger("lifecycle");
70844
- var sessionLog6 = createSessionLog(log33);
71100
+ var log34 = createLogger("lifecycle");
71101
+ var sessionLog6 = createSessionLog(log34);
70845
71102
  function mutableSessions(ctx) {
70846
71103
  return ctx.state.sessions;
70847
71104
  }
@@ -70945,7 +71202,7 @@ async function createSessionDecisionBridge(ref) {
70945
71202
  return messageManager.handleBridgeRequest(request, signal);
70946
71203
  });
70947
71204
  } catch (err) {
70948
- log33.warn(`Decision bridge unavailable — falling back to legacy MCP prompts: ${err}`);
71205
+ log34.warn(`Decision bridge unavailable — falling back to legacy MCP prompts: ${err}`);
70949
71206
  return null;
70950
71207
  }
70951
71208
  }
@@ -71013,6 +71270,28 @@ function createMessageManager(session, ctx) {
71013
71270
  sessionLog6(session).info(`@${fromUser} invited to session by @${approvedBy}`);
71014
71271
  }
71015
71272
  });
71273
+ messageManager.events.on("routine-prompt:complete", async ({ approved, parsed, requestedBy, postId }) => {
71274
+ session.threadLogger?.logCommand("routine", approved ? "created" : "discarded", requestedBy);
71275
+ if (!approved) {
71276
+ sessionLog6(session).info(`\uD83D\uDD58 Routine "${parsed.name}" discarded before saving`);
71277
+ return;
71278
+ }
71279
+ let result;
71280
+ try {
71281
+ result = await ctx.state.routinesStore.add(session.platformId, { name: parsed.name, prompt: parsed.prompt, schedule: parsed.schedule, createdBy: requestedBy }, ctx.config.maxRoutines);
71282
+ } catch (err) {
71283
+ result = { ok: false, error: `could not write the routines file (${err.message})` };
71284
+ }
71285
+ const formatter = session.platform.getFormatter();
71286
+ if (result.ok) {
71287
+ const position = ctx.state.routinesStore.list(session.platformId).length;
71288
+ await withErrorHandling(() => session.platform.updatePost(postId, `✅ ${formatter.formatBold(`Routine ${position}: ${result.routine.name}`)} saved — it will post its runs as new threads in this channel. ` + `${formatter.formatItalic(`Manage with ${"`!routines`"}. Each run starts a full Claude session.`)}`), { action: "Update routine confirmation post", session });
71289
+ sessionLog6(session).info(`\uD83D\uDD58 Routine "${result.routine.name}" saved by @${requestedBy}`);
71290
+ } else {
71291
+ await withErrorHandling(() => session.platform.updatePost(postId, `⚠️ Could not save routine: ${result.error}`), { action: "Update routine confirmation post", session });
71292
+ sessionLog6(session).warn(`\uD83D\uDD58 Routine save failed: ${result.error}`);
71293
+ }
71294
+ });
71016
71295
  messageManager.events.on("context-prompt:complete", async ({ selection, queuedPrompt, queuedByUsername, queuedFiles: _queuedFiles, threadMessageCount: _threadMessageCount }) => {
71017
71296
  const userTurn = formatUserTurn(queuedPrompt, queuedByUsername, shouldAttribute(session.userAttribution, session.sessionAllowedUsers.size));
71018
71297
  let messageToSend = userTurn;
@@ -71210,7 +71489,7 @@ function resumeSessionHeaderMode(persisted, platformConfigured) {
71210
71489
  function resolveSessionHeaderMode(configured, replyToPostId, platformId) {
71211
71490
  const mode = configured ?? DEFAULT_OVERHEAD_VISIBILITY;
71212
71491
  if (mode === "hidden" && !replyToPostId) {
71213
- log33.error(`sessionHeader: hidden requires a replyToPostId for ${platformId}; ` + `downgrading this session to 'minimal' so the header post is still short.`);
71492
+ log34.error(`sessionHeader: hidden requires a replyToPostId for ${platformId}; ` + `downgrading this session to 'minimal' so the header post is still short.`);
71214
71493
  return "minimal";
71215
71494
  }
71216
71495
  return mode;
@@ -71229,7 +71508,7 @@ async function startSession(options, username, displayName, replyToPostId, platf
71229
71508
  throw new Error(`Platform '${platformId}' not found. Call addPlatform() first.`);
71230
71509
  }
71231
71510
  if (!isAuthorizedForSession({ username, platform, sessionAllowedUsers: undefined })) {
71232
- log33.warn(`auth.denied.startSession: @${username || "unknown"} not authorized to start session in ${threadId.substring(0, 8)}...`);
71511
+ log34.warn(`auth.denied.startSession: @${username || "unknown"} not authorized to start session in ${threadId.substring(0, 8)}...`);
71233
71512
  return;
71234
71513
  }
71235
71514
  const activeOrPending = ctx.state.sessions.size + pendingStartsCount;
@@ -71258,7 +71537,7 @@ async function startSession(options, username, displayName, replyToPostId, platf
71258
71537
  const actualThreadId = replyToPostId || (startPost ? startPost.id : "");
71259
71538
  const sessionId = ctx.ops.getSessionId(platformId, actualThreadId);
71260
71539
  platform.sendTyping(actualThreadId);
71261
- const claudeSessionId = randomUUID5();
71540
+ const claudeSessionId = randomUUID6();
71262
71541
  let workingDir = ctx.config.workingDir;
71263
71542
  let permissionMode = ctx.config.permissionMode;
71264
71543
  let forceInteractivePermissions = false;
@@ -71290,17 +71569,17 @@ async function startSession(options, username, displayName, replyToPostId, platf
71290
71569
  return;
71291
71570
  }
71292
71571
  workingDir = resolvedDir;
71293
- log33.info(`Starting session in directory: ${workingDir} (from !cd command)`);
71572
+ log34.info(`Starting session in directory: ${workingDir} (from !cd command)`);
71294
71573
  }
71295
71574
  if (initialOptions?.permissionMode) {
71296
71575
  permissionMode = initialOptions.permissionMode;
71297
71576
  forceInteractivePermissions = permissionMode === "default";
71298
71577
  sessionPermissionModeOverride = permissionMode;
71299
- log33.info(`Starting session with permission mode "${permissionMode}" (from !permissions command)`);
71578
+ log34.info(`Starting session with permission mode "${permissionMode}" (from !permissions command)`);
71300
71579
  } else if (initialOptions?.forceInteractivePermissions) {
71301
71580
  forceInteractivePermissions = true;
71302
71581
  permissionMode = "default";
71303
- log33.info(`Starting session with interactive permissions (from !permissions command)`);
71582
+ log34.info(`Starting session with interactive permissions (from !permissions command)`);
71304
71583
  }
71305
71584
  const userAttribution = ctx.config.userAttribution ?? true;
71306
71585
  const memoryConfig = ctx.ops.getPlatformMemoryConfig(platformId);
@@ -71311,7 +71590,7 @@ async function startSession(options, username, displayName, replyToPostId, platf
71311
71590
  balanceByUsage: true
71312
71591
  });
71313
71592
  if (claudeAccount) {
71314
- log33.info(`Session ${sessionId.substring(0, 20)} reserved Claude account "${claudeAccount.id}"`);
71593
+ log34.info(`Session ${sessionId.substring(0, 20)} reserved Claude account "${claudeAccount.id}"`);
71315
71594
  }
71316
71595
  const bridgeSessionRef = {};
71317
71596
  const decisionBridge = await createSessionDecisionBridge(bridgeSessionRef);
@@ -71442,28 +71721,28 @@ async function resumeSession(state, ctx) {
71442
71721
  !state.claudeSessionId && "claudeSessionId",
71443
71722
  !state.workingDir && "workingDir"
71444
71723
  ].filter(Boolean).join(", ");
71445
- log33.warn(`Skipping session with missing required fields: ${missing}`);
71724
+ log34.warn(`Skipping session with missing required fields: ${missing}`);
71446
71725
  return;
71447
71726
  }
71448
71727
  const shortId = state.threadId.substring(0, 8);
71449
71728
  const platforms = ctx.state.platforms;
71450
71729
  const platform = platforms.get(state.platformId);
71451
71730
  if (!platform) {
71452
- log33.warn(`Platform ${state.platformId} not registered, skipping resume for ${shortId}...`);
71731
+ log34.warn(`Platform ${state.platformId} not registered, skipping resume for ${shortId}...`);
71453
71732
  return;
71454
71733
  }
71455
71734
  const threadPost = await platform.getPost(state.threadId);
71456
71735
  if (!threadPost) {
71457
- log33.warn(`Thread ${shortId}... deleted, skipping resume`);
71736
+ log34.warn(`Thread ${shortId}... deleted, skipping resume`);
71458
71737
  ctx.state.sessionStore.remove(`${state.platformId}:${state.threadId}`);
71459
71738
  return;
71460
71739
  }
71461
71740
  if (ctx.state.sessions.size >= ctx.config.maxSessions) {
71462
- log33.warn(`Max sessions reached, skipping resume for ${shortId}...`);
71741
+ log34.warn(`Max sessions reached, skipping resume for ${shortId}...`);
71463
71742
  return;
71464
71743
  }
71465
71744
  if (!existsSync13(state.workingDir)) {
71466
- log33.warn(`Working directory ${state.workingDir} no longer exists, skipping resume for ${shortId}...`);
71745
+ log34.warn(`Working directory ${state.workingDir} no longer exists, skipping resume for ${shortId}...`);
71467
71746
  ctx.state.sessionStore.remove(`${state.platformId}:${state.threadId}`);
71468
71747
  const resumeFormatter = platform.getFormatter();
71469
71748
  const tempSession = {
@@ -71486,7 +71765,7 @@ Please start a new session.`), { action: "Post resume failure notification" });
71486
71765
  const appendSystemPrompt = await buildAppendSystemPrompt(platform, state.platformId, state.workingDir, state.threadId, state.startedBy, state.sessionAllowedUsers || [state.startedBy], CHAT_PLATFORM_PROMPT, ctx.state.githubEmailsStore, memoryConfig.enabled && memoryConfig.channelLayer ? ctx.state.memoryStore : null, { userAttribution });
71487
71766
  const claudeAccount = ctx.ops.acquireClaudeAccount(state.claudeAccountId, state.threadId);
71488
71767
  if (state.claudeAccountId && !claudeAccount) {
71489
- log33.warn(`Persisted session referenced Claude account "${state.claudeAccountId}" ` + `which is no longer configured — resuming under default env`);
71768
+ log34.warn(`Persisted session referenced Claude account "${state.claudeAccountId}" ` + `which is no longer configured — resuming under default env`);
71490
71769
  }
71491
71770
  const resumeBridgeRef = {};
71492
71771
  const resumeBridge = await createSessionDecisionBridge(resumeBridgeRef);
@@ -71569,7 +71848,7 @@ Please start a new session.`), { action: "Post resume failure notification" });
71569
71848
  worktreePath: detected.worktreePath,
71570
71849
  branch: detected.branch
71571
71850
  };
71572
- log33.info(`Auto-detected worktree info for resumed session: branch=${detected.branch}`);
71851
+ log34.info(`Auto-detected worktree info for resumed session: branch=${detected.branch}`);
71573
71852
  }
71574
71853
  }
71575
71854
  session.messageManager = createMessageManager(session, ctx);
@@ -71628,7 +71907,7 @@ ${sessionFormatter.formatItalic("Reconnected to Claude session. You can continue
71628
71907
  await postResumeCoAuthorOnboarding(session, ctx);
71629
71908
  ctx.ops.persistSession(session);
71630
71909
  } catch (err) {
71631
- log33.error(`Failed to resume session ${shortId}`, err instanceof Error ? err : undefined);
71910
+ log34.error(`Failed to resume session ${shortId}`, err instanceof Error ? err : undefined);
71632
71911
  session.messageManager?.dispose();
71633
71912
  session.decisionBridge?.close();
71634
71913
  session.decisionBridge = undefined;
@@ -71671,28 +71950,28 @@ async function resumePausedSession(threadId, message, files, ctx, username) {
71671
71950
  const persisted = ctx.state.sessionStore.load();
71672
71951
  const state = findPersistedByThreadId(persisted, threadId);
71673
71952
  if (!state) {
71674
- log33.debug(`No persisted session found for ${threadId.substring(0, 8)}...`);
71953
+ log34.debug(`No persisted session found for ${threadId.substring(0, 8)}...`);
71675
71954
  return;
71676
71955
  }
71677
71956
  const shortId = threadId.substring(0, 8);
71678
71957
  const platform = ctx.state.platforms.get(state.platformId);
71679
71958
  if (!platform) {
71680
- log33.warn(`auth.denied.resume: platform '${state.platformId}' not found for ${shortId}...`);
71959
+ log34.warn(`auth.denied.resume: platform '${state.platformId}' not found for ${shortId}...`);
71681
71960
  return;
71682
71961
  }
71683
71962
  const sessionAllowedUsers = new Set(state.sessionAllowedUsers || [state.startedBy].filter(Boolean));
71684
71963
  if (!isAuthorizedForSession({ username, platform, sessionAllowedUsers })) {
71685
- log33.warn(`auth.denied.resume: @${username || "unknown"} not authorized to resume ${shortId}...`);
71964
+ log34.warn(`auth.denied.resume: @${username || "unknown"} not authorized to resume ${shortId}...`);
71686
71965
  return;
71687
71966
  }
71688
- log33.info(`\uD83D\uDD04 Resuming paused session ${shortId}... for new message`);
71967
+ log34.info(`\uD83D\uDD04 Resuming paused session ${shortId}... for new message`);
71689
71968
  await resumeSession(state, ctx);
71690
71969
  const session = ctx.ops.findSessionByThreadId(threadId);
71691
71970
  if (session && session.claude.isRunning() && session.messageManager) {
71692
71971
  session.messageCount++;
71693
71972
  await session.messageManager.handleUserMessage(message, files, username);
71694
71973
  } else {
71695
- log33.warn(`Failed to resume session ${shortId}..., could not send message`);
71974
+ log34.warn(`Failed to resume session ${shortId}..., could not send message`);
71696
71975
  }
71697
71976
  }
71698
71977
  async function handleExit(sessionId, code, ctx, source) {
@@ -71700,7 +71979,7 @@ async function handleExit(sessionId, code, ctx, source) {
71700
71979
  const shortId = sessionId.substring(0, 8);
71701
71980
  sessionLog6(session).debug(`handleExit called code=${code} isShuttingDown=${ctx.state.isShuttingDown}`);
71702
71981
  if (!session) {
71703
- log33.debug(`Session ${shortId}... not found (already cleaned up)`);
71982
+ log34.debug(`Session ${shortId}... not found (already cleaned up)`);
71704
71983
  return;
71705
71984
  }
71706
71985
  if (source && session.claude !== source) {
@@ -71901,37 +72180,338 @@ async function cleanupIdleSessions(timeoutMs, warningMs, ctx) {
71901
72180
  }
71902
72181
  }
71903
72182
 
71904
- // src/operations/monitor/handler.ts
71905
- var log34 = createLogger("monitor");
71906
- var DEFAULT_INTERVAL_MS = 60 * 1000;
72183
+ // src/routines/runner.ts
72184
+ init_logger();
72185
+ var log35 = createLogger("routines");
72186
+ async function fireRoutine(routine, platformId, ctx) {
72187
+ const platforms = ctx.state.platforms;
72188
+ const platform = platforms.get(platformId);
72189
+ if (!platform) {
72190
+ log35.debug(`Routine "${routine.name}": platform ${platformId} not registered — skipping`);
72191
+ return "skipped";
72192
+ }
72193
+ if (!isAuthorizedForSession({ username: routine.createdBy, platform, sessionAllowedUsers: undefined })) {
72194
+ log35.warn(`Routine "${routine.name}": creator @${routine.createdBy} no longer authorized on ${platformId}`);
72195
+ return "unauthorized";
72196
+ }
72197
+ if (ctx.state.sessions.size >= ctx.config.maxSessions) {
72198
+ log35.debug(`Routine "${routine.name}": at MAX_SESSIONS — skipping this tick`);
72199
+ return "skipped";
72200
+ }
72201
+ const formatter = platform.getFormatter();
72202
+ const rootPost = await platform.createPost(`\uD83D\uDD58 ${formatter.formatBold(`Routine: ${routine.name}`)}
72203
+ ` + `${formatter.formatItalic(`${describeSchedule(routine.schedule)} · created by`)} ${formatter.formatUserMention(routine.createdBy)}`);
72204
+ await startSession({
72205
+ prompt: `[Scheduled routine "${routine.name}" — started automatically on its schedule, not by a live user. ` + `Complete the task and post the result in this thread.]
71907
72206
 
71908
- class SessionMonitor {
72207
+ ${routine.prompt}`,
72208
+ skipWorktreePrompt: true
72209
+ }, routine.createdBy, undefined, rootPost.id, platformId, ctx);
72210
+ if (!ctx.state.sessions.has(ctx.ops.getSessionId(platformId, rootPost.id))) {
72211
+ log35.debug(`Routine "${routine.name}": startSession declined to start a session — skipping this tick`);
72212
+ return "skipped";
72213
+ }
72214
+ return "ok";
72215
+ }
72216
+
72217
+ // src/claude/account-pool.ts
72218
+ init_logger();
72219
+
72220
+ // src/claude/usage-probe.ts
72221
+ init_spawn();
72222
+ init_logger();
72223
+ var log36 = createLogger("usage-probe");
72224
+ var DEFAULT_USAGE_PROBE_TIMEOUT_MS = 30000;
72225
+ function parseUsageOutput(text) {
72226
+ if (!text)
72227
+ return null;
72228
+ const sessionMatch = text.match(/Current session:\s*(\d+)%\s*used(?:\s*·\s*resets\s*([^\n]+?))?\s*(?:\n|$)/i);
72229
+ const weekAllMatch = text.match(/Current week(?: \(all models\))?:\s*(\d+)%\s*used(?:\s*·\s*resets\s*([^\n]+?))?\s*(?:\n|$)/i);
72230
+ if (!sessionMatch && !weekAllMatch)
72231
+ return null;
72232
+ const sessionPct = sessionMatch ? clampPct(Number(sessionMatch[1])) : 0;
72233
+ const weekAllModelsPct = weekAllMatch ? clampPct(Number(weekAllMatch[1])) : 0;
72234
+ let weekPerModelPct = null;
72235
+ const perModelRe = /Current week \((?!all models\))[^)]+\):\s*(\d+)%\s*used/gi;
72236
+ for (const m of text.matchAll(perModelRe)) {
72237
+ const pct = clampPct(Number(m[1]));
72238
+ weekPerModelPct = weekPerModelPct === null ? pct : Math.max(weekPerModelPct, pct);
72239
+ }
72240
+ return {
72241
+ sessionPct,
72242
+ weekAllModelsPct,
72243
+ weekPerModelPct,
72244
+ sessionResetsAt: sessionMatch?.[2]?.trim() || null,
72245
+ weekResetsAt: weekAllMatch?.[2]?.trim() || null
72246
+ };
72247
+ }
72248
+ function usageLoadScore(usage) {
72249
+ return Math.max(usage.sessionPct, usage.weekAllModelsPct, usage.weekPerModelPct ?? 0);
72250
+ }
72251
+ function clampPct(n) {
72252
+ if (!Number.isFinite(n))
72253
+ return 0;
72254
+ return Math.max(0, Math.min(100, Math.round(n)));
72255
+ }
72256
+ async function probeAccountUsage(account, opts = {}) {
72257
+ const timeoutMs = opts.timeoutMs ?? DEFAULT_USAGE_PROBE_TIMEOUT_MS;
72258
+ const claudePath = getClaudePath();
72259
+ const env4 = buildClaudeChildEnv(process.env, account);
72260
+ return new Promise((resolve7) => {
72261
+ let settled = false;
72262
+ const finish = (value) => {
72263
+ if (settled)
72264
+ return;
72265
+ settled = true;
72266
+ clearTimeout(timer);
72267
+ resolve7(value);
72268
+ };
72269
+ let child;
72270
+ try {
72271
+ child = crossSpawn(claudePath, ["-p", "/usage", "--output-format", "json"], {
72272
+ env: env4,
72273
+ stdio: ["ignore", "pipe", "pipe"]
72274
+ });
72275
+ } catch (err) {
72276
+ log36.warn(`Failed to spawn /usage probe for "${account.id}": ${err}`);
72277
+ resolve7(null);
72278
+ return;
72279
+ }
72280
+ const timer = setTimeout(() => {
72281
+ log36.warn(`/usage probe for "${account.id}" timed out after ${timeoutMs}ms`);
72282
+ try {
72283
+ child.kill("SIGKILL");
72284
+ } catch {}
72285
+ finish(null);
72286
+ }, timeoutMs);
72287
+ let stdout = "";
72288
+ child.stdout?.on("data", (chunk) => {
72289
+ stdout += chunk.toString();
72290
+ });
72291
+ child.stderr?.on("data", () => {});
72292
+ child.on("error", (err) => {
72293
+ log36.warn(`/usage probe for "${account.id}" errored: ${err}`);
72294
+ finish(null);
72295
+ });
72296
+ child.on("close", () => {
72297
+ const usage = extractUsage(stdout);
72298
+ if (!usage) {
72299
+ log36.debug(`/usage probe for "${account.id}" returned no parseable usage`);
72300
+ }
72301
+ finish(usage);
72302
+ });
72303
+ });
72304
+ }
72305
+ function extractUsage(stdout) {
72306
+ const trimmed = stdout.trim();
72307
+ if (!trimmed)
72308
+ return null;
72309
+ let text = trimmed;
72310
+ try {
72311
+ const parsed = JSON.parse(trimmed);
72312
+ if (typeof parsed.result === "string") {
72313
+ text = parsed.result;
72314
+ }
72315
+ } catch {}
72316
+ return parseUsageOutput(text);
72317
+ }
72318
+
72319
+ // src/claude/account-pool.ts
72320
+ var log37 = createLogger("account-pool");
72321
+ var ACTIVE_SESSION_LOAD_PENALTY = 5;
72322
+ function hashThreadId(threadId) {
72323
+ let h = 2166136261;
72324
+ for (let i = 0;i < threadId.length; i++) {
72325
+ h ^= threadId.charCodeAt(i);
72326
+ h = Math.imul(h, 16777619);
72327
+ }
72328
+ return h >>> 0;
72329
+ }
72330
+
72331
+ class AccountPool {
72332
+ accounts;
72333
+ byId;
72334
+ orderIndex;
72335
+ activeCounts = new Map;
72336
+ coolingUntil = new Map;
72337
+ usage = new Map;
72338
+ rrCursor = 0;
72339
+ constructor(accounts) {
72340
+ this.accounts = (accounts ?? []).filter((acc) => {
72341
+ const hasAuth = !!acc.home || !!acc.apiKey;
72342
+ if (!hasAuth) {
72343
+ log37.warn(`Claude account ${acc.id} has neither home nor apiKey — ignoring`);
72344
+ return false;
72345
+ }
72346
+ if (acc.home && acc.apiKey) {
72347
+ log37.warn(`Claude account ${acc.id} has both home and apiKey set — must choose one; ignoring`);
72348
+ return false;
72349
+ }
72350
+ return true;
72351
+ });
72352
+ this.byId = new Map(this.accounts.map((acc) => [acc.id, acc]));
72353
+ this.orderIndex = new Map(this.accounts.map((acc, i) => [acc.id, i]));
72354
+ for (const acc of this.accounts) {
72355
+ this.activeCounts.set(acc.id, 0);
72356
+ this.usage.set(acc.id, null);
72357
+ }
72358
+ }
72359
+ get isEmpty() {
72360
+ return this.accounts.length === 0;
72361
+ }
72362
+ get size() {
72363
+ return this.accounts.length;
72364
+ }
72365
+ get all() {
72366
+ return this.accounts;
72367
+ }
72368
+ acquire(preferredId, threadId, opts) {
72369
+ if (this.isEmpty)
72370
+ return null;
72371
+ if (preferredId) {
72372
+ const preferred = this.byId.get(preferredId);
72373
+ if (preferred) {
72374
+ this.incrementActive(preferred.id);
72375
+ return preferred;
72376
+ }
72377
+ log37.warn(`Preferred account "${preferredId}" not in pool — falling back to usage balancing`);
72378
+ }
72379
+ const now = Date.now();
72380
+ const n = this.accounts.length;
72381
+ if (threadId && !opts?.balanceByUsage) {
72382
+ const sticky = this.accounts[hashThreadId(threadId) % n];
72383
+ const cooling = this.coolingUntil.get(sticky.id) ?? 0;
72384
+ if (cooling <= now) {
72385
+ this.incrementActive(sticky.id);
72386
+ return sticky;
72387
+ }
72388
+ }
72389
+ const chosen = this.selectLeastLoaded(now);
72390
+ if (!chosen) {
72391
+ log37.warn(`All ${n} accounts are in rate-limit cooldown`);
72392
+ return null;
72393
+ }
72394
+ this.incrementActive(chosen.id);
72395
+ return chosen;
72396
+ }
72397
+ selectLeastLoaded(now) {
72398
+ const n = this.accounts.length;
72399
+ let best = null;
72400
+ let bestScore = Number.POSITIVE_INFINITY;
72401
+ let bestIdx = -1;
72402
+ for (let k = 0;k < n; k++) {
72403
+ const idx = (this.rrCursor + k) % n;
72404
+ const acc = this.accounts[idx];
72405
+ if ((this.coolingUntil.get(acc.id) ?? 0) > now)
72406
+ continue;
72407
+ const score = this.effectiveLoad(acc.id);
72408
+ if (best === null || score < bestScore) {
72409
+ best = acc;
72410
+ bestScore = score;
72411
+ bestIdx = idx;
72412
+ }
72413
+ }
72414
+ if (bestIdx >= 0)
72415
+ this.rrCursor = (bestIdx + 1) % n;
72416
+ return best;
72417
+ }
72418
+ loadScore(accountId) {
72419
+ const u = this.usage.get(accountId);
72420
+ return u ? usageLoadScore(u) : Number.POSITIVE_INFINITY;
72421
+ }
72422
+ effectiveLoad(accountId) {
72423
+ const base = this.loadScore(accountId);
72424
+ const active = this.activeCounts.get(accountId) ?? 0;
72425
+ return base + ACTIVE_SESSION_LOAD_PENALTY * active;
72426
+ }
72427
+ release(accountId) {
72428
+ const current = this.activeCounts.get(accountId);
72429
+ if (current === undefined)
72430
+ return;
72431
+ this.activeCounts.set(accountId, Math.max(0, current - 1));
72432
+ }
72433
+ setUsage(accountId, usage) {
72434
+ if (!this.byId.has(accountId))
72435
+ return;
72436
+ this.usage.set(accountId, usage);
72437
+ if (usage) {
72438
+ log37.debug(`Account "${accountId}" usage: ${usageLoadScore(usage)}% (load score)`);
72439
+ }
72440
+ }
72441
+ markCooling(accountId, untilEpochMs) {
72442
+ if (!this.byId.has(accountId)) {
72443
+ log37.warn(`markCooling called for unknown account "${accountId}"`);
72444
+ return;
72445
+ }
72446
+ const existing = this.coolingUntil.get(accountId) ?? 0;
72447
+ if (untilEpochMs > existing) {
72448
+ this.coolingUntil.set(accountId, untilEpochMs);
72449
+ const minutes = Math.ceil((untilEpochMs - Date.now()) / 60000);
72450
+ log37.info(`Account "${accountId}" cooling for ~${minutes}min`);
72451
+ }
72452
+ }
72453
+ get(accountId) {
72454
+ return this.byId.get(accountId);
72455
+ }
72456
+ status() {
72457
+ const now = Date.now();
72458
+ return this.accounts.map((acc) => {
72459
+ const cooling = this.coolingUntil.get(acc.id) ?? 0;
72460
+ const usage = this.usage.get(acc.id) ?? null;
72461
+ return {
72462
+ id: acc.id,
72463
+ displayName: acc.displayName ?? acc.id,
72464
+ activeSessions: this.activeCounts.get(acc.id) ?? 0,
72465
+ coolingUntil: cooling > now ? cooling : null,
72466
+ usagePercent: usage ? usageLoadScore(usage) : null
72467
+ };
72468
+ });
72469
+ }
72470
+ incrementActive(accountId) {
72471
+ this.activeCounts.set(accountId, (this.activeCounts.get(accountId) ?? 0) + 1);
72472
+ }
72473
+ }
72474
+
72475
+ // src/cleanup/scheduler.ts
72476
+ init_logger();
72477
+ import { existsSync as existsSync14 } from "fs";
72478
+ import { readdir, rm as rm3 } from "fs/promises";
72479
+ import { join as join13 } from "path";
72480
+ init_worktree();
72481
+ var log38 = createLogger("cleanup");
72482
+ var DEFAULT_CLEANUP_INTERVAL_MS = 60 * 60 * 1000;
72483
+ var MAX_WORKTREE_AGE_MS = 24 * 60 * 60 * 1000;
72484
+
72485
+ class CleanupScheduler {
71909
72486
  intervalMs;
71910
- sessionTimeoutMs;
71911
- sessionWarningMs;
71912
- getContext;
71913
- getSessionCount;
71914
- updateStickyMessage;
72487
+ logRetentionDays;
72488
+ threadLogsEnabled;
72489
+ sessionStore;
72490
+ maxWorktreeAgeMs;
72491
+ cleanupWorktrees;
71915
72492
  timer = null;
71916
72493
  isRunning = false;
71917
72494
  constructor(options) {
71918
- this.intervalMs = options.intervalMs ?? DEFAULT_INTERVAL_MS;
71919
- this.sessionTimeoutMs = options.sessionTimeoutMs;
71920
- this.sessionWarningMs = options.sessionWarningMs;
71921
- this.getContext = options.getContext;
71922
- this.getSessionCount = options.getSessionCount;
71923
- this.updateStickyMessage = options.updateStickyMessage;
72495
+ this.intervalMs = options.intervalMs ?? DEFAULT_CLEANUP_INTERVAL_MS;
72496
+ this.logRetentionDays = options.logRetentionDays ?? 30;
72497
+ this.threadLogsEnabled = options.threadLogsEnabled ?? true;
72498
+ this.sessionStore = options.sessionStore;
72499
+ this.maxWorktreeAgeMs = options.maxWorktreeAgeMs ?? MAX_WORKTREE_AGE_MS;
72500
+ this.cleanupWorktrees = options.cleanupWorktrees ?? true;
71924
72501
  }
71925
72502
  start() {
71926
72503
  if (this.isRunning) {
71927
- log34.debug("Session monitor already running");
72504
+ log38.debug("Cleanup scheduler already running");
71928
72505
  return;
71929
72506
  }
71930
72507
  this.isRunning = true;
71931
- log34.debug(`Session monitor started (interval: ${this.intervalMs / 1000}s)`);
72508
+ log38.info(`Cleanup scheduler started (interval: ${Math.round(this.intervalMs / 60000)}min)`);
72509
+ this.runCleanup().catch((err) => {
72510
+ log38.warn(`Initial cleanup failed: ${err}`);
72511
+ });
71932
72512
  this.timer = setInterval(() => {
71933
- this.runCheck().catch((err) => {
71934
- log34.error(`Error during session monitoring: ${err}`);
72513
+ this.runCleanup().catch((err) => {
72514
+ log38.warn(`Periodic cleanup failed: ${err}`);
71935
72515
  });
71936
72516
  }, this.intervalMs);
71937
72517
  }
@@ -71941,20 +72521,141 @@ class SessionMonitor {
71941
72521
  this.timer = null;
71942
72522
  }
71943
72523
  this.isRunning = false;
71944
- log34.debug("Session monitor stopped");
72524
+ log38.debug("Cleanup scheduler stopped");
71945
72525
  }
71946
- async runCheck() {
71947
- await cleanupIdleSessions(this.sessionTimeoutMs, this.sessionWarningMs, this.getContext());
71948
- if (this.getSessionCount() > 0) {
71949
- await this.updateStickyMessage();
72526
+ async runCleanup() {
72527
+ const startTime = Date.now();
72528
+ log38.debug("Running background cleanup...");
72529
+ const stats = {
72530
+ logsDeleted: 0,
72531
+ worktreesCleaned: 0,
72532
+ metadataCleaned: 0,
72533
+ errors: []
72534
+ };
72535
+ const cleanupTasks = [
72536
+ this.cleanupLogs().catch((err) => {
72537
+ stats.errors.push(`Log cleanup: ${err}`);
72538
+ return 0;
72539
+ })
72540
+ ];
72541
+ if (this.cleanupWorktrees) {
72542
+ cleanupTasks.push(this.cleanupOrphanedWorktrees().catch((err) => {
72543
+ stats.errors.push(`Worktree cleanup: ${err}`);
72544
+ return { cleaned: 0, metadata: 0 };
72545
+ }));
72546
+ }
72547
+ const [logStats, worktreeStats = { cleaned: 0, metadata: 0 }] = await Promise.all(cleanupTasks);
72548
+ stats.logsDeleted = logStats;
72549
+ stats.worktreesCleaned = worktreeStats.cleaned;
72550
+ stats.metadataCleaned = worktreeStats.metadata;
72551
+ const elapsed = Date.now() - startTime;
72552
+ const totalCleaned = stats.logsDeleted + stats.worktreesCleaned + stats.metadataCleaned;
72553
+ if (totalCleaned > 0 || stats.errors.length > 0) {
72554
+ log38.info(`Cleanup completed in ${elapsed}ms: ` + `${stats.logsDeleted} logs, ${stats.worktreesCleaned} worktrees, ${stats.metadataCleaned} metadata` + (stats.errors.length > 0 ? ` (${stats.errors.length} errors)` : ""));
72555
+ } else {
72556
+ log38.debug(`Cleanup completed in ${elapsed}ms (nothing to clean)`);
72557
+ }
72558
+ return stats;
72559
+ }
72560
+ async cleanupLogs() {
72561
+ if (!this.threadLogsEnabled) {
72562
+ return 0;
71950
72563
  }
72564
+ return new Promise((resolve7) => {
72565
+ try {
72566
+ const deleted = cleanupOldLogs(this.logRetentionDays);
72567
+ resolve7(deleted);
72568
+ } catch (err) {
72569
+ log38.warn(`Log cleanup error: ${err}`);
72570
+ resolve7(0);
72571
+ }
72572
+ });
72573
+ }
72574
+ async cleanupOrphanedWorktrees() {
72575
+ const worktreesDir = getWorktreesDir();
72576
+ const result = { cleaned: 0, metadata: 0 };
72577
+ if (!existsSync14(worktreesDir)) {
72578
+ log38.debug("No worktrees directory exists, nothing to clean");
72579
+ return result;
72580
+ }
72581
+ const persisted = this.sessionStore.load();
72582
+ const activeWorktrees = new Set;
72583
+ for (const session of persisted.values()) {
72584
+ if (session.worktreeInfo?.worktreePath) {
72585
+ activeWorktrees.add(session.worktreeInfo.worktreePath);
72586
+ }
72587
+ }
72588
+ const now = Date.now();
72589
+ try {
72590
+ const entries = await readdir(worktreesDir, { withFileTypes: true });
72591
+ for (const entry of entries) {
72592
+ if (!entry.isDirectory())
72593
+ continue;
72594
+ const worktreePath = join13(worktreesDir, entry.name);
72595
+ if (activeWorktrees.has(worktreePath)) {
72596
+ log38.debug(`Worktree in use by persisted session, skipping: ${entry.name}`);
72597
+ continue;
72598
+ }
72599
+ const meta = await readWorktreeMetadata(worktreePath);
72600
+ let shouldCleanup = false;
72601
+ let cleanupReason = "";
72602
+ if (meta) {
72603
+ const lastActivity = new Date(meta.lastActivityAt).getTime();
72604
+ const age = now - lastActivity;
72605
+ if (meta.sessionId && age < this.maxWorktreeAgeMs) {
72606
+ log38.debug(`Worktree has active session (${Math.round(age / 60000)}min old), skipping: ${entry.name}`);
72607
+ continue;
72608
+ }
72609
+ const merged = age >= this.maxWorktreeAgeMs ? await isBranchMerged(meta.repoRoot, meta.branch).catch(() => false) : false;
72610
+ if (merged) {
72611
+ shouldCleanup = true;
72612
+ cleanupReason = `branch "${meta.branch}" was merged`;
72613
+ } else if (age >= this.maxWorktreeAgeMs) {
72614
+ shouldCleanup = true;
72615
+ cleanupReason = `inactive for ${Math.round(age / 3600000)}h`;
72616
+ } else {
72617
+ log38.debug(`Worktree recent (${Math.round(age / 60000)}min old), skipping: ${entry.name}`);
72618
+ continue;
72619
+ }
72620
+ } else {
72621
+ shouldCleanup = true;
72622
+ cleanupReason = "no metadata";
72623
+ }
72624
+ if (!shouldCleanup)
72625
+ continue;
72626
+ log38.info(`Cleaning worktree (${cleanupReason}): ${entry.name}`);
72627
+ try {
72628
+ if (meta?.repoRoot) {
72629
+ await removeWorktree(meta.repoRoot, worktreePath);
72630
+ } else {
72631
+ await rm3(worktreePath, { recursive: true, force: true });
72632
+ }
72633
+ result.cleaned++;
72634
+ await removeWorktreeMetadata(worktreePath);
72635
+ result.metadata++;
72636
+ } catch (err) {
72637
+ log38.warn(`Failed to clean orphaned worktree ${entry.name}: ${err}`);
72638
+ try {
72639
+ await rm3(worktreePath, { recursive: true, force: true });
72640
+ result.cleaned++;
72641
+ await removeWorktreeMetadata(worktreePath);
72642
+ result.metadata++;
72643
+ } catch (rmErr) {
72644
+ log38.error(`Failed to force remove worktree ${entry.name}: ${rmErr}`);
72645
+ }
72646
+ }
72647
+ }
72648
+ } catch (err) {
72649
+ log38.warn(`Failed to scan worktrees directory: ${err}`);
72650
+ }
72651
+ return result;
71951
72652
  }
71952
72653
  }
71953
72654
  // src/operations/plugin/handler.ts
71954
72655
  init_spawn();
71955
72656
  init_logger();
71956
- var log35 = createLogger("plugin");
71957
- var sessionLog7 = createSessionLog(log35);
72657
+ var log39 = createLogger("plugin");
72658
+ var sessionLog7 = createSessionLog(log39);
71958
72659
  async function buildPluginRestartCliOptions(session, ctx) {
71959
72660
  const account = session.claudeAccountId ? ctx.ops.getClaudeAccount(session.claudeAccountId) : undefined;
71960
72661
  const memoryConfig = ctx.ops.getPlatformMemoryConfig(session.platformId);
@@ -71996,7 +72697,7 @@ async function runPluginCommand(args, cwd, timeout2 = 60000) {
71996
72697
  });
71997
72698
  proc.on("error", (err) => {
71998
72699
  resolve7({ stdout, stderr, exitCode: 1 });
71999
- log35.error(`Plugin command error: ${err.message}`);
72700
+ log39.error(`Plugin command error: ${err.message}`);
72000
72701
  });
72001
72702
  });
72002
72703
  }
@@ -72170,7 +72871,7 @@ class SessionRegistry {
72170
72871
  // src/session/reaction-router.ts
72171
72872
  init_emoji();
72172
72873
  init_logger();
72173
- var log36 = createLogger("manager");
72874
+ var log40 = createLogger("manager");
72174
72875
  async function handleReaction(deps, platformId, postId, emojiName, username, action) {
72175
72876
  const normalizedEmoji = normalizeEmojiName(emojiName);
72176
72877
  if (action === "added" && isResumeEmoji(normalizedEmoji)) {
@@ -72184,7 +72885,7 @@ async function handleReaction(deps, platformId, postId, emojiName, username, act
72184
72885
  if (session.platformId !== platformId)
72185
72886
  return;
72186
72887
  if (!session.sessionAllowedUsers.has(username) && !session.platform.isUserAllowed(username)) {
72187
- log36.info(`\uD83D\uDEAB rejected reaction from unauthorized user`, {
72888
+ log40.info(`\uD83D\uDEAB rejected reaction from unauthorized user`, {
72188
72889
  event: "reaction.rejected",
72189
72890
  platformId,
72190
72891
  sessionId: session.sessionId,
@@ -72220,7 +72921,7 @@ async function tryResumeFromReaction(deps, platformId, postId, username) {
72220
72921
  return false;
72221
72922
  }
72222
72923
  const shortId = persistedSession.threadId.substring(0, 8);
72223
- log36.info(`\uD83D\uDD04 Resuming session ${shortId}... via emoji reaction by @${username}`);
72924
+ log40.info(`\uD83D\uDD04 Resuming session ${shortId}... via emoji reaction by @${username}`);
72224
72925
  await resumeSession(persistedSession, deps.getContext());
72225
72926
  return true;
72226
72927
  }
@@ -72250,7 +72951,7 @@ async function dispatch(deps, session, postId, emojiName, username, action) {
72250
72951
  }
72251
72952
  if (session.lastError?.postId === postId && isBugReportEmoji(emojiName)) {
72252
72953
  if (session.startedBy === username || session.platform.isUserAllowed(username) || session.sessionAllowedUsers.has(username)) {
72253
- log36.info(`\uD83D\uDC1B @${username} triggered bug report from error reaction`);
72954
+ log40.info(`\uD83D\uDC1B @${username} triggered bug report from error reaction`);
72254
72955
  await reportBug(session, undefined, username, deps.getContext(), session.lastError);
72255
72956
  return;
72256
72957
  }
@@ -72265,7 +72966,7 @@ async function dispatch(deps, session, postId, emojiName, username, action) {
72265
72966
 
72266
72967
  // src/session/manager.ts
72267
72968
  init_logger();
72268
- var log37 = createLogger("manager");
72969
+ var log41 = createLogger("manager");
72269
72970
  var USAGE_PROBE_TIMEOUT_MS = 1e4;
72270
72971
  var USAGE_REFRESH_DEADLINE_MS = 5000;
72271
72972
  var USAGE_CACHE_TTL_MS = 15000;
@@ -72289,6 +72990,8 @@ class SessionManager extends EventEmitter4 {
72289
72990
  sessionStore;
72290
72991
  githubEmailsStore;
72291
72992
  memoryStore;
72993
+ routinesStore;
72994
+ routineScheduler = null;
72292
72995
  sessionMonitor = null;
72293
72996
  backgroundCleanup = null;
72294
72997
  isShuttingDown = false;
@@ -72296,6 +72999,7 @@ class SessionManager extends EventEmitter4 {
72296
72999
  customFooter;
72297
73000
  platformOverhead = new Map;
72298
73001
  platformMemory = new Map;
73002
+ platformRoutines = new Map;
72299
73003
  autoUpdateManager = null;
72300
73004
  accountPool;
72301
73005
  usageRefreshInFlight = null;
@@ -72314,6 +73018,7 @@ class SessionManager extends EventEmitter4 {
72314
73018
  this.sessionStore = new SessionStore(sessionsPath);
72315
73019
  this.githubEmailsStore = new GitHubEmailsStore;
72316
73020
  this.memoryStore = new MemoryStore;
73021
+ this.routinesStore = new RoutinesStore;
72317
73022
  this.registry = new SessionRegistry(this.sessionStore);
72318
73023
  this.accountPool = new AccountPool(claudeAccounts);
72319
73024
  this.sessionMonitor = new SessionMonitor({
@@ -72331,14 +73036,28 @@ class SessionManager extends EventEmitter4 {
72331
73036
  maxWorktreeAgeMs: this.limits.maxWorktreeAgeHours * 60 * 60 * 1000,
72332
73037
  cleanupWorktrees: this.limits.cleanupWorktrees
72333
73038
  });
73039
+ this.routineScheduler = new RoutineScheduler({
73040
+ store: this.routinesStore,
73041
+ listPlatformIds: () => Array.from(this.platforms.keys()),
73042
+ isRoutinesEnabled: (pid) => this.platformRoutines.get(pid) ?? true,
73043
+ fireRoutine: (pid, routine) => fireRoutine(routine, pid, this.getContext()),
73044
+ notifyDisabled: async (pid, routine, reason) => {
73045
+ const platform = this.platforms.get(pid);
73046
+ if (!platform)
73047
+ return;
73048
+ const formatter = platform.getFormatter();
73049
+ await platform.createPost(`\uD83D\uDD58 ${formatter.formatBold(`Routine "${routine.name}" disabled`)} — ${reason}. ` + `Re-enable with ${formatter.formatCode("!routines resume <n>")} once resolved.`).catch(() => {});
73050
+ }
73051
+ });
72334
73052
  }
72335
- addPlatform(platformId, client, overhead, memory) {
73053
+ addPlatform(platformId, client, overhead, memory, routinesEnabled) {
72336
73054
  this.platforms.set(platformId, client);
72337
73055
  this.platformOverhead.set(platformId, {
72338
73056
  sessionHeader: overhead?.sessionHeader ?? DEFAULT_OVERHEAD_VISIBILITY,
72339
73057
  stickyMessage: overhead?.stickyMessage ?? DEFAULT_OVERHEAD_VISIBILITY
72340
73058
  });
72341
73059
  this.platformMemory.set(platformId, memory ?? DEFAULT_MEMORY_CONFIG);
73060
+ this.platformRoutines.set(platformId, routinesEnabled ?? true);
72342
73061
  client.on("message", (post2, user) => this.handleMessage(platformId, post2, user));
72343
73062
  client.on("reaction", (reaction, user) => {
72344
73063
  if (user) {
@@ -72357,12 +73076,13 @@ class SessionManager extends EventEmitter4 {
72357
73076
  markNeedsBump(platformId);
72358
73077
  this.updateStickyMessage();
72359
73078
  });
72360
- log37.info(`\uD83D\uDCE1 Platform "${platformId}" registered`);
73079
+ log41.info(`\uD83D\uDCE1 Platform "${platformId}" registered`);
72361
73080
  }
72362
73081
  removePlatform(platformId) {
72363
73082
  this.platforms.delete(platformId);
72364
73083
  this.platformOverhead.delete(platformId);
72365
73084
  this.platformMemory.delete(platformId);
73085
+ this.platformRoutines.delete(platformId);
72366
73086
  clearHiddenCleanupTracking(platformId);
72367
73087
  }
72368
73088
  setAutoUpdateManager(manager) {
@@ -72376,7 +73096,7 @@ class SessionManager extends EventEmitter4 {
72376
73096
  if (users) {
72377
73097
  users.add(sessionId);
72378
73098
  }
72379
- log37.debug(`Registered session ${sessionId.substring(0, 20)} as worktree user for ${worktreePath}`);
73099
+ log41.debug(`Registered session ${sessionId.substring(0, 20)} as worktree user for ${worktreePath}`);
72380
73100
  }
72381
73101
  unregisterWorktreeUser(worktreePath, sessionId) {
72382
73102
  const users = this.worktreeUsers.get(worktreePath);
@@ -72402,6 +73122,7 @@ class SessionManager extends EventEmitter4 {
72402
73122
  userAttribution: this.userAttribution,
72403
73123
  debug: this.debug,
72404
73124
  maxSessions: this.limits.maxSessions,
73125
+ maxRoutines: this.limits.maxRoutines,
72405
73126
  threadLogsEnabled: this.threadLogsEnabled,
72406
73127
  threadLogsRetentionDays: this.threadLogsRetentionDays,
72407
73128
  permissionTimeoutMs: this.limits.permissionTimeoutSeconds * 1000,
@@ -72414,6 +73135,7 @@ class SessionManager extends EventEmitter4 {
72414
73135
  sessionStore: this.sessionStore,
72415
73136
  githubEmailsStore: this.githubEmailsStore,
72416
73137
  memoryStore: this.memoryStore,
73138
+ routinesStore: this.routinesStore,
72417
73139
  isShuttingDown: this.isShuttingDown
72418
73140
  };
72419
73141
  const ops = {
@@ -72459,7 +73181,9 @@ class SessionManager extends EventEmitter4 {
72459
73181
  sessionHeader: DEFAULT_OVERHEAD_VISIBILITY,
72460
73182
  stickyMessage: DEFAULT_OVERHEAD_VISIBILITY
72461
73183
  },
72462
- getPlatformMemoryConfig: (pid) => this.platformMemory.get(pid) ?? DEFAULT_MEMORY_CONFIG
73184
+ getPlatformMemoryConfig: (pid) => this.platformMemory.get(pid) ?? DEFAULT_MEMORY_CONFIG,
73185
+ isRoutinesEnabled: (pid) => this.platformRoutines.get(pid) ?? true,
73186
+ fireRoutineNow: (pid, routine) => this.fireRoutineNowImpl(pid, routine)
72463
73187
  };
72464
73188
  return createSessionContext(config, state, ops);
72465
73189
  }
@@ -72578,7 +73302,7 @@ class SessionManager extends EventEmitter4 {
72578
73302
  try {
72579
73303
  this.persistSessionUnsafe(session);
72580
73304
  } catch (err) {
72581
- log37.error(`Failed to persist session ${session.sessionId}: ${err}`);
73305
+ log41.error(`Failed to persist session ${session.sessionId}: ${err}`);
72582
73306
  }
72583
73307
  }
72584
73308
  persistSessionUnsafe(session) {
@@ -72694,11 +73418,11 @@ class SessionManager extends EventEmitter4 {
72694
73418
  }
72695
73419
  }
72696
73420
  if (sessionsToKill.length === 0) {
72697
- log37.info(`No active sessions to pause for platform ${platformId}`);
73421
+ log41.info(`No active sessions to pause for platform ${platformId}`);
72698
73422
  await this.updateStickyMessage();
72699
73423
  return;
72700
73424
  }
72701
- log37.info(`⏸️ Pausing ${sessionsToKill.length} session(s) for platform ${platformId}`);
73425
+ log41.info(`⏸️ Pausing ${sessionsToKill.length} session(s) for platform ${platformId}`);
72702
73426
  for (const session of sessionsToKill) {
72703
73427
  try {
72704
73428
  const fmt = session.platform.getFormatter();
@@ -72714,9 +73438,9 @@ class SessionManager extends EventEmitter4 {
72714
73438
  session.claude.kill();
72715
73439
  this.registry.unregister(session.sessionId);
72716
73440
  this.emitSessionRemove(session.sessionId);
72717
- log37.info(`⏸️ Paused session ${session.threadId.substring(0, 8)}`);
73441
+ log41.info(`⏸️ Paused session ${session.threadId.substring(0, 8)}`);
72718
73442
  } catch (err) {
72719
- log37.warn(`Failed to pause session ${session.threadId}: ${err}`);
73443
+ log41.warn(`Failed to pause session ${session.threadId}: ${err}`);
72720
73444
  }
72721
73445
  }
72722
73446
  for (const session of sessionsToKill) {
@@ -72737,17 +73461,17 @@ class SessionManager extends EventEmitter4 {
72737
73461
  sessionsToResume.push(state);
72738
73462
  }
72739
73463
  if (sessionsToResume.length === 0) {
72740
- log37.info(`No paused sessions to resume for platform ${platformId}`);
73464
+ log41.info(`No paused sessions to resume for platform ${platformId}`);
72741
73465
  await this.updateStickyMessage();
72742
73466
  return;
72743
73467
  }
72744
- log37.info(`▶️ Resuming ${sessionsToResume.length} paused session(s) for platform ${platformId}`);
73468
+ log41.info(`▶️ Resuming ${sessionsToResume.length} paused session(s) for platform ${platformId}`);
72745
73469
  for (const state of sessionsToResume) {
72746
73470
  try {
72747
73471
  await resumeSession(state, this.getContext());
72748
- log37.info(`▶️ Resumed session ${state.threadId.substring(0, 8)}`);
73472
+ log41.info(`▶️ Resumed session ${state.threadId.substring(0, 8)}`);
72749
73473
  } catch (err) {
72750
- log37.warn(`Failed to resume session ${state.threadId}: ${err}`);
73474
+ log41.warn(`Failed to resume session ${state.threadId}: ${err}`);
72751
73475
  }
72752
73476
  }
72753
73477
  await this.updateStickyMessage();
@@ -72782,17 +73506,18 @@ class SessionManager extends EventEmitter4 {
72782
73506
  initialize(this.sessionStore);
72783
73507
  this.sessionMonitor?.start();
72784
73508
  this.backgroundCleanup?.start();
73509
+ this.routineScheduler?.start();
72785
73510
  const sessionTimeoutMs = this.limits.sessionTimeoutMinutes * 60 * 1000;
72786
73511
  const staleIds = this.sessionStore.cleanStale(sessionTimeoutMs * 2);
72787
73512
  if (staleIds.length > 0) {
72788
- log37.info(`\uD83E\uDDF9 Soft-deleted ${staleIds.length} stale session(s) (kept for history)`);
73513
+ log41.info(`\uD83E\uDDF9 Soft-deleted ${staleIds.length} stale session(s) (kept for history)`);
72789
73514
  }
72790
73515
  const removedCount = this.sessionStore.cleanHistory();
72791
73516
  if (removedCount > 0) {
72792
- log37.info(`\uD83D\uDDD1️ Permanently removed ${removedCount} old session(s) from history`);
73517
+ log41.info(`\uD83D\uDDD1️ Permanently removed ${removedCount} old session(s) from history`);
72793
73518
  }
72794
73519
  const persisted = this.sessionStore.load();
72795
- log37.info(`\uD83D\uDCC2 Loaded ${persisted.size} session(s) from persistence`);
73520
+ log41.info(`\uD83D\uDCC2 Loaded ${persisted.size} session(s) from persistence`);
72796
73521
  const excludePostIdsByPlatform = new Map;
72797
73522
  for (const session of persisted.values()) {
72798
73523
  const platformId = session.platformId;
@@ -72812,10 +73537,10 @@ class SessionManager extends EventEmitter4 {
72812
73537
  const excludePostIds = excludePostIdsByPlatform.get(platform.platformId);
72813
73538
  platform.getBotUser().then((botUser) => {
72814
73539
  cleanupOldStickyMessages(platform, botUser.id, true, excludePostIds).catch((err) => {
72815
- log37.warn(`Failed to cleanup old sticky messages for ${platform.platformId}: ${err}`);
73540
+ log41.warn(`Failed to cleanup old sticky messages for ${platform.platformId}: ${err}`);
72816
73541
  });
72817
73542
  }).catch((err) => {
72818
- log37.warn(`Failed to get bot user for cleanup on ${platform.platformId}: ${err}`);
73543
+ log41.warn(`Failed to get bot user for cleanup on ${platform.platformId}: ${err}`);
72819
73544
  });
72820
73545
  }
72821
73546
  if (persisted.size > 0) {
@@ -72829,10 +73554,10 @@ class SessionManager extends EventEmitter4 {
72829
73554
  }
72830
73555
  }
72831
73556
  if (pausedToSkip.length > 0) {
72832
- log37.info(`⏸️ ${pausedToSkip.length} session(s) remain paused (waiting for user message)`);
73557
+ log41.info(`⏸️ ${pausedToSkip.length} session(s) remain paused (waiting for user message)`);
72833
73558
  }
72834
73559
  if (activeToResume.length > 0) {
72835
- log37.info(`\uD83D\uDD04 Attempting to resume ${activeToResume.length} active session(s)...`);
73560
+ log41.info(`\uD83D\uDD04 Attempting to resume ${activeToResume.length} active session(s)...`);
72836
73561
  for (const state of activeToResume) {
72837
73562
  await resumeSession(state, this.getContext());
72838
73563
  }
@@ -72951,6 +73676,23 @@ class SessionManager extends EventEmitter4 {
72951
73676
  return;
72952
73677
  await forgetMemory(session, selector, username, this.getContext());
72953
73678
  }
73679
+ async createRoutine(threadId, request, username) {
73680
+ const session = this.findSessionByThreadId(threadId);
73681
+ if (!session)
73682
+ return;
73683
+ await createRoutine(session, request, username, this.getContext());
73684
+ }
73685
+ async manageRoutines(threadId, args, username) {
73686
+ const session = this.findSessionByThreadId(threadId);
73687
+ if (!session)
73688
+ return;
73689
+ await manageRoutines(session, args, username, this.getContext());
73690
+ }
73691
+ async fireRoutineNowImpl(platformId, routine) {
73692
+ if (!this.routineScheduler)
73693
+ return "skipped";
73694
+ return this.routineScheduler.fire(platformId, routine, new Date, false);
73695
+ }
72954
73696
  async setRespondOnlyWhenMentioned(threadId, username, arg) {
72955
73697
  const session = this.findSessionByThreadId(threadId);
72956
73698
  if (!session)
@@ -73293,7 +74035,7 @@ Mention me to start a session in this worktree.`, threadId);
73293
74035
  const message = messageBuilder(formatter);
73294
74036
  await post(session, "info", message);
73295
74037
  } catch (err) {
73296
- log37.warn(`Failed to broadcast to session ${session.threadId}: ${err}`);
74038
+ log41.warn(`Failed to broadcast to session ${session.threadId}: ${err}`);
73297
74039
  }
73298
74040
  }
73299
74041
  }
@@ -73312,7 +74054,7 @@ Mention me to start a session in this worktree.`, threadId);
73312
74054
  session.messageManager?.setPendingUpdatePrompt({ postId: post2.id });
73313
74055
  this.registerPost(post2.id, session.threadId);
73314
74056
  } catch (err) {
73315
- log37.warn(`Failed to post ask message to ${threadId}: ${err}`);
74057
+ log41.warn(`Failed to post ask message to ${threadId}: ${err}`);
73316
74058
  }
73317
74059
  }
73318
74060
  }
@@ -73320,6 +74062,7 @@ Mention me to start a session in this worktree.`, threadId);
73320
74062
  this.isShuttingDown = true;
73321
74063
  this.sessionMonitor?.stop();
73322
74064
  this.backgroundCleanup?.stop();
74065
+ this.routineScheduler?.stop();
73323
74066
  if (message) {
73324
74067
  for (const session of this.registry.getAll()) {
73325
74068
  try {
@@ -80906,29 +81649,29 @@ function SessionLog({ logs, maxLines = 20 }) {
80906
81649
  return /* @__PURE__ */ jsx_dev_runtime4.jsxDEV(Box_default, {
80907
81650
  flexDirection: "column",
80908
81651
  flexShrink: 0,
80909
- children: displayLogs.map((log38) => /* @__PURE__ */ jsx_dev_runtime4.jsxDEV(Box_default, {
81652
+ children: displayLogs.map((log42) => /* @__PURE__ */ jsx_dev_runtime4.jsxDEV(Box_default, {
80910
81653
  flexShrink: 0,
80911
81654
  children: [
80912
81655
  /* @__PURE__ */ jsx_dev_runtime4.jsxDEV(Text, {
80913
- color: getColorForLevel(log38.level),
81656
+ color: getColorForLevel(log42.level),
80914
81657
  dimColor: true,
80915
81658
  wrap: "truncate",
80916
81659
  children: [
80917
81660
  "[",
80918
- padComponent(log38.component),
81661
+ padComponent(log42.component),
80919
81662
  "]"
80920
81663
  ]
80921
81664
  }, undefined, true, undefined, this),
80922
81665
  /* @__PURE__ */ jsx_dev_runtime4.jsxDEV(Text, {
80923
- color: getColorForLevel(log38.level),
81666
+ color: getColorForLevel(log42.level),
80924
81667
  wrap: "truncate",
80925
81668
  children: [
80926
81669
  " ",
80927
- log38.message
81670
+ log42.message
80928
81671
  ]
80929
81672
  }, undefined, true, undefined, this)
80930
81673
  ]
80931
- }, log38.id, true, undefined, this))
81674
+ }, log42.id, true, undefined, this))
80932
81675
  }, undefined, false, undefined, this);
80933
81676
  }
80934
81677
  // src/ui/components/Footer.tsx
@@ -81452,7 +82195,7 @@ function LogPanel({ logs, maxLines = 10, focused = false }) {
81452
82195
  const scrollRef = import_react59.default.useRef(null);
81453
82196
  const { stdout } = use_stdout_default();
81454
82197
  const isDebug = process.env.DEBUG === "1";
81455
- const displayLogs = logs.filter((log38) => isDebug || log38.level !== "debug");
82198
+ const displayLogs = logs.filter((log42) => isDebug || log42.level !== "debug");
81456
82199
  const visibleLogs = displayLogs.slice(-Math.max(maxLines * 3, 100));
81457
82200
  import_react59.default.useEffect(() => {
81458
82201
  const handleResize = () => scrollRef.current?.remeasure();
@@ -81492,25 +82235,25 @@ function LogPanel({ logs, maxLines = 10, focused = false }) {
81492
82235
  overflow: "hidden",
81493
82236
  children: /* @__PURE__ */ jsx_dev_runtime6.jsxDEV(ScrollView, {
81494
82237
  ref: scrollRef,
81495
- children: visibleLogs.map((log38) => /* @__PURE__ */ jsx_dev_runtime6.jsxDEV(Box_default, {
82238
+ children: visibleLogs.map((log42) => /* @__PURE__ */ jsx_dev_runtime6.jsxDEV(Box_default, {
81496
82239
  children: [
81497
82240
  /* @__PURE__ */ jsx_dev_runtime6.jsxDEV(Text, {
81498
82241
  dimColor: true,
81499
82242
  children: [
81500
82243
  "[",
81501
- padComponent2(log38.component),
82244
+ padComponent2(log42.component),
81502
82245
  "]"
81503
82246
  ]
81504
82247
  }, undefined, true, undefined, this),
81505
82248
  /* @__PURE__ */ jsx_dev_runtime6.jsxDEV(Text, {
81506
- color: getLevelColor(log38.level),
82249
+ color: getLevelColor(log42.level),
81507
82250
  children: [
81508
82251
  " ",
81509
- log38.message
82252
+ log42.message
81510
82253
  ]
81511
82254
  }, undefined, true, undefined, this)
81512
82255
  ]
81513
- }, log38.id, true, undefined, this))
82256
+ }, log42.id, true, undefined, this))
81514
82257
  }, undefined, false, undefined, this)
81515
82258
  }, undefined, false, undefined, this);
81516
82259
  }
@@ -82027,10 +82770,10 @@ function useAppState(initialConfig) {
82027
82770
  });
82028
82771
  }, []);
82029
82772
  const getLogsForSession = import_react60.useCallback((sessionId) => {
82030
- return state.logs.filter((log38) => log38.sessionId === sessionId);
82773
+ return state.logs.filter((log42) => log42.sessionId === sessionId);
82031
82774
  }, [state.logs]);
82032
82775
  const getGlobalLogs = import_react60.useCallback(() => {
82033
- return state.logs.filter((log38) => !log38.sessionId);
82776
+ return state.logs.filter((log42) => !log42.sessionId);
82034
82777
  }, [state.logs]);
82035
82778
  const togglePlatformEnabled = import_react60.useCallback((platformId) => {
82036
82779
  let newEnabled = false;
@@ -83045,7 +83788,7 @@ import { EventEmitter as EventEmitter9 } from "events";
83045
83788
  // src/auto-update/checker.ts
83046
83789
  init_logger();
83047
83790
  import { EventEmitter as EventEmitter7 } from "events";
83048
- var log38 = createLogger("checker");
83791
+ var log42 = createLogger("checker");
83049
83792
  var PACKAGE_NAME = "claude-threads";
83050
83793
  function compareVersions(a, b) {
83051
83794
  const partsA = a.replace(/^v/, "").split(".").map(Number);
@@ -83068,13 +83811,13 @@ async function fetchLatestVersion() {
83068
83811
  }
83069
83812
  });
83070
83813
  if (!response.ok) {
83071
- log38.warn(`Failed to fetch latest version: HTTP ${response.status}`);
83814
+ log42.warn(`Failed to fetch latest version: HTTP ${response.status}`);
83072
83815
  return null;
83073
83816
  }
83074
83817
  const data = await response.json();
83075
83818
  return data.version ?? null;
83076
83819
  } catch (err) {
83077
- log38.warn(`Failed to fetch latest version: ${err}`);
83820
+ log42.warn(`Failed to fetch latest version: ${err}`);
83078
83821
  return null;
83079
83822
  }
83080
83823
  }
@@ -83091,38 +83834,38 @@ class UpdateChecker extends EventEmitter7 {
83091
83834
  }
83092
83835
  start() {
83093
83836
  if (!this.config.enabled) {
83094
- log38.debug("Auto-update disabled, not starting checker");
83837
+ log42.debug("Auto-update disabled, not starting checker");
83095
83838
  return;
83096
83839
  }
83097
83840
  setTimeout(() => {
83098
83841
  this.check().catch((err) => {
83099
- log38.warn(`Initial update check failed: ${err}`);
83842
+ log42.warn(`Initial update check failed: ${err}`);
83100
83843
  });
83101
83844
  }, 5000);
83102
83845
  const intervalMs = this.config.checkIntervalMinutes * 60 * 1000;
83103
83846
  this.checkInterval = setInterval(() => {
83104
83847
  this.check().catch((err) => {
83105
- log38.warn(`Periodic update check failed: ${err}`);
83848
+ log42.warn(`Periodic update check failed: ${err}`);
83106
83849
  });
83107
83850
  }, intervalMs);
83108
- log38.info(`\uD83D\uDD04 Update checker started (every ${this.config.checkIntervalMinutes} minutes)`);
83851
+ log42.info(`\uD83D\uDD04 Update checker started (every ${this.config.checkIntervalMinutes} minutes)`);
83109
83852
  }
83110
83853
  stop() {
83111
83854
  if (this.checkInterval) {
83112
83855
  clearInterval(this.checkInterval);
83113
83856
  this.checkInterval = null;
83114
83857
  }
83115
- log38.debug("Update checker stopped");
83858
+ log42.debug("Update checker stopped");
83116
83859
  }
83117
83860
  async check() {
83118
83861
  if (this.isChecking) {
83119
- log38.debug("Check already in progress, skipping");
83862
+ log42.debug("Check already in progress, skipping");
83120
83863
  return this.lastUpdateInfo;
83121
83864
  }
83122
83865
  this.isChecking = true;
83123
83866
  this.emit("check:start");
83124
83867
  try {
83125
- log38.debug("Checking for updates...");
83868
+ log42.debug("Checking for updates...");
83126
83869
  const latestVersion2 = await fetchLatestVersion();
83127
83870
  if (!latestVersion2) {
83128
83871
  this.emit("check:complete", false);
@@ -83139,18 +83882,18 @@ class UpdateChecker extends EventEmitter7 {
83139
83882
  detectedAt: new Date
83140
83883
  };
83141
83884
  if (!this.lastUpdateInfo || this.lastUpdateInfo.latestVersion !== latestVersion2) {
83142
- log38.info(`\uD83C\uDD95 Update available: v${currentVersion} → v${latestVersion2}`);
83885
+ log42.info(`\uD83C\uDD95 Update available: v${currentVersion} → v${latestVersion2}`);
83143
83886
  this.lastUpdateInfo = updateInfo;
83144
83887
  this.emit("update", updateInfo);
83145
83888
  }
83146
83889
  this.emit("check:complete", true);
83147
83890
  return updateInfo;
83148
83891
  }
83149
- log38.debug(`Up to date (v${currentVersion})`);
83892
+ log42.debug(`Up to date (v${currentVersion})`);
83150
83893
  this.emit("check:complete", false);
83151
83894
  return null;
83152
83895
  } catch (err) {
83153
- log38.warn(`Update check failed: ${err}`);
83896
+ log42.warn(`Update check failed: ${err}`);
83154
83897
  this.emit("check:error", err);
83155
83898
  return null;
83156
83899
  } finally {
@@ -83221,7 +83964,7 @@ function isInScheduledWindow(window2) {
83221
83964
  }
83222
83965
 
83223
83966
  // src/auto-update/scheduler.ts
83224
- var log39 = createLogger("scheduler");
83967
+ var log43 = createLogger("scheduler");
83225
83968
 
83226
83969
  class UpdateScheduler extends EventEmitter8 {
83227
83970
  config;
@@ -83245,7 +83988,7 @@ class UpdateScheduler extends EventEmitter8 {
83245
83988
  scheduleUpdate(updateInfo) {
83246
83989
  this.pendingUpdate = updateInfo;
83247
83990
  if (this.config.autoRestartMode === "immediate") {
83248
- log39.info("Immediate mode: triggering update now");
83991
+ log43.info("Immediate mode: triggering update now");
83249
83992
  this.emit("ready", updateInfo);
83250
83993
  return;
83251
83994
  }
@@ -83258,19 +84001,19 @@ class UpdateScheduler extends EventEmitter8 {
83258
84001
  this.scheduledRestartAt = null;
83259
84002
  this.askApprovals.clear();
83260
84003
  this.askStartTime = null;
83261
- log39.debug("Update schedule cancelled");
84004
+ log43.debug("Update schedule cancelled");
83262
84005
  }
83263
84006
  deferUpdate(minutes) {
83264
84007
  const deferUntil = new Date(Date.now() + minutes * 60 * 1000);
83265
84008
  this.scheduledRestartAt = null;
83266
84009
  this.idleStartTime = null;
83267
84010
  this.emit("deferred", deferUntil);
83268
- log39.info(`Update deferred until ${deferUntil.toLocaleTimeString()}`);
84011
+ log43.info(`Update deferred until ${deferUntil.toLocaleTimeString()}`);
83269
84012
  return deferUntil;
83270
84013
  }
83271
84014
  recordAskResponse(threadId, approved) {
83272
84015
  this.askApprovals.set(threadId, approved);
83273
- log39.debug(`Thread ${threadId.substring(0, 8)} ${approved ? "approved" : "denied"} update`);
84016
+ log43.debug(`Thread ${threadId.substring(0, 8)} ${approved ? "approved" : "denied"} update`);
83274
84017
  this.checkAskCondition();
83275
84018
  }
83276
84019
  getScheduledRestartAt() {
@@ -83291,7 +84034,7 @@ class UpdateScheduler extends EventEmitter8 {
83291
84034
  return;
83292
84035
  this.checkCondition();
83293
84036
  this.checkTimer = setInterval(() => this.checkCondition(), 1e4);
83294
- log39.debug(`Started checking for ${this.config.autoRestartMode} condition`);
84037
+ log43.debug(`Started checking for ${this.config.autoRestartMode} condition`);
83295
84038
  }
83296
84039
  stopChecking() {
83297
84040
  if (this.checkTimer) {
@@ -83322,17 +84065,17 @@ class UpdateScheduler extends EventEmitter8 {
83322
84065
  if (activity.activeSessionCount === 0) {
83323
84066
  if (!this.idleStartTime) {
83324
84067
  this.idleStartTime = new Date;
83325
- log39.debug("No active sessions, starting idle timer");
84068
+ log43.debug("No active sessions, starting idle timer");
83326
84069
  }
83327
84070
  const idleMs = Date.now() - this.idleStartTime.getTime();
83328
84071
  const requiredMs = this.config.idleTimeoutMinutes * 60 * 1000;
83329
84072
  if (idleMs >= requiredMs) {
83330
- log39.info(`Idle for ${this.config.idleTimeoutMinutes} minutes, triggering update`);
84073
+ log43.info(`Idle for ${this.config.idleTimeoutMinutes} minutes, triggering update`);
83331
84074
  this.triggerCountdown();
83332
84075
  }
83333
84076
  } else {
83334
84077
  if (this.idleStartTime) {
83335
- log39.debug("Sessions became active, resetting idle timer");
84078
+ log43.debug("Sessions became active, resetting idle timer");
83336
84079
  this.idleStartTime = null;
83337
84080
  }
83338
84081
  }
@@ -83343,7 +84086,7 @@ class UpdateScheduler extends EventEmitter8 {
83343
84086
  const quietMs = Date.now() - activity.lastActivityAt.getTime();
83344
84087
  const requiredMs = this.config.quietTimeoutMinutes * 60 * 1000;
83345
84088
  if (quietMs >= requiredMs && !activity.anySessionBusy) {
83346
- log39.info(`Sessions quiet for ${this.config.quietTimeoutMinutes} minutes, triggering update`);
84089
+ log43.info(`Sessions quiet for ${this.config.quietTimeoutMinutes} minutes, triggering update`);
83347
84090
  this.triggerCountdown();
83348
84091
  }
83349
84092
  } else if (activity.activeSessionCount === 0) {
@@ -83353,7 +84096,7 @@ class UpdateScheduler extends EventEmitter8 {
83353
84096
  const idleMs = Date.now() - this.idleStartTime.getTime();
83354
84097
  const requiredMs = this.config.quietTimeoutMinutes * 60 * 1000;
83355
84098
  if (idleMs >= requiredMs) {
83356
- log39.info("No sessions and quiet timeout reached, triggering update");
84099
+ log43.info("No sessions and quiet timeout reached, triggering update");
83357
84100
  this.triggerCountdown();
83358
84101
  }
83359
84102
  }
@@ -83364,13 +84107,13 @@ class UpdateScheduler extends EventEmitter8 {
83364
84107
  }
83365
84108
  const activity = this.getSessionActivity();
83366
84109
  if (activity.activeSessionCount === 0) {
83367
- log39.info("Within scheduled window and no active sessions, triggering update");
84110
+ log43.info("Within scheduled window and no active sessions, triggering update");
83368
84111
  this.triggerCountdown();
83369
84112
  } else if (activity.lastActivityAt) {
83370
84113
  const quietMs = Date.now() - activity.lastActivityAt.getTime();
83371
84114
  const requiredMs = this.config.idleTimeoutMinutes * 60 * 1000;
83372
84115
  if (quietMs >= requiredMs && !activity.anySessionBusy) {
83373
- log39.info("Within scheduled window and sessions quiet, triggering update");
84116
+ log43.info("Within scheduled window and sessions quiet, triggering update");
83374
84117
  this.triggerCountdown();
83375
84118
  }
83376
84119
  }
@@ -83378,14 +84121,14 @@ class UpdateScheduler extends EventEmitter8 {
83378
84121
  checkAskCondition() {
83379
84122
  const threadIds = this.getActiveThreadIds();
83380
84123
  if (threadIds.length === 0) {
83381
- log39.info("No active threads, proceeding with update");
84124
+ log43.info("No active threads, proceeding with update");
83382
84125
  this.triggerCountdown();
83383
84126
  return;
83384
84127
  }
83385
84128
  if (!this.askStartTime && this.pendingUpdate) {
83386
84129
  this.askStartTime = new Date;
83387
84130
  this.postAskMessage(threadIds, this.pendingUpdate.latestVersion).catch((err) => {
83388
- log39.warn(`Failed to post ask message: ${err}`);
84131
+ log43.warn(`Failed to post ask message: ${err}`);
83389
84132
  });
83390
84133
  return;
83391
84134
  }
@@ -83398,12 +84141,12 @@ class UpdateScheduler extends EventEmitter8 {
83398
84141
  denials++;
83399
84142
  }
83400
84143
  if (approvals > threadIds.length / 2) {
83401
- log39.info(`Majority approved (${approvals}/${threadIds.length}), triggering update`);
84144
+ log43.info(`Majority approved (${approvals}/${threadIds.length}), triggering update`);
83402
84145
  this.triggerCountdown();
83403
84146
  return;
83404
84147
  }
83405
84148
  if (denials > threadIds.length / 2) {
83406
- log39.info(`Majority denied (${denials}/${threadIds.length}), deferring update`);
84149
+ log43.info(`Majority denied (${denials}/${threadIds.length}), deferring update`);
83407
84150
  this.deferUpdate(60);
83408
84151
  return;
83409
84152
  }
@@ -83411,7 +84154,7 @@ class UpdateScheduler extends EventEmitter8 {
83411
84154
  const elapsedMs = Date.now() - this.askStartTime.getTime();
83412
84155
  const timeoutMs = this.config.askTimeoutMinutes * 60 * 1000;
83413
84156
  if (elapsedMs >= timeoutMs) {
83414
- log39.info(`Ask timeout reached (${this.config.askTimeoutMinutes} min), triggering update`);
84157
+ log43.info(`Ask timeout reached (${this.config.askTimeoutMinutes} min), triggering update`);
83415
84158
  this.triggerCountdown();
83416
84159
  }
83417
84160
  }
@@ -83431,7 +84174,7 @@ class UpdateScheduler extends EventEmitter8 {
83431
84174
  this.emit("ready", this.pendingUpdate);
83432
84175
  }
83433
84176
  }, 1000);
83434
- log39.info("Update countdown started (60 seconds)");
84177
+ log43.info("Update countdown started (60 seconds)");
83435
84178
  }
83436
84179
  stopCountdown() {
83437
84180
  if (this.countdownTimer) {
@@ -83444,27 +84187,27 @@ class UpdateScheduler extends EventEmitter8 {
83444
84187
  // src/auto-update/installer.ts
83445
84188
  init_logger();
83446
84189
  import { spawn as spawn4, spawnSync } from "child_process";
83447
- import { existsSync as existsSync15, readFileSync as readFileSync11, writeFileSync as writeFileSync8, mkdirSync as mkdirSync6 } from "fs";
84190
+ import { existsSync as existsSync16, readFileSync as readFileSync12, writeFileSync as writeFileSync9, mkdirSync as mkdirSync7 } from "fs";
83448
84191
  import { dirname as dirname9, resolve as resolve7 } from "path";
83449
- import { homedir as homedir7 } from "os";
83450
- var log40 = createLogger("installer");
84192
+ import { homedir as homedir8 } from "os";
84193
+ var log44 = createLogger("installer");
83451
84194
  function detectPackageManager() {
83452
84195
  const npmCmd = process.platform === "win32" ? "npm.cmd" : "npm";
83453
84196
  const originalInstaller = detectOriginalInstaller();
83454
84197
  if (originalInstaller) {
83455
- log40.debug(`Detected original installer: ${originalInstaller}`);
84198
+ log44.debug(`Detected original installer: ${originalInstaller}`);
83456
84199
  if (originalInstaller === "bun") {
83457
84200
  const bunCheck2 = spawnSync("bun", ["--version"], { stdio: "ignore" });
83458
84201
  if (bunCheck2.status === 0) {
83459
84202
  return { cmd: "bun", isBun: true };
83460
84203
  }
83461
- log40.warn("Originally installed with bun, but bun not found. Falling back to npm.");
84204
+ log44.warn("Originally installed with bun, but bun not found. Falling back to npm.");
83462
84205
  } else {
83463
84206
  const npmCheck2 = spawnSync(npmCmd, ["--version"], { stdio: "ignore" });
83464
84207
  if (npmCheck2.status === 0) {
83465
84208
  return { cmd: npmCmd, isBun: false };
83466
84209
  }
83467
- log40.warn("Originally installed with npm, but npm not found. Falling back to bun.");
84210
+ log44.warn("Originally installed with npm, but npm not found. Falling back to bun.");
83468
84211
  }
83469
84212
  }
83470
84213
  const bunCheck = spawnSync("bun", ["--version"], { stdio: "ignore" });
@@ -83486,7 +84229,7 @@ function normalizePath(p) {
83486
84229
  function detectOriginalInstaller() {
83487
84230
  try {
83488
84231
  const scriptPath = normalizePath(process.argv[1] || "");
83489
- const bunGlobalDir = normalizePath(process.env.BUN_INSTALL || resolve7(homedir7(), ".bun"));
84232
+ const bunGlobalDir = normalizePath(process.env.BUN_INSTALL || resolve7(homedir8(), ".bun"));
83490
84233
  if (scriptPath.startsWith(bunGlobalDir)) {
83491
84234
  return "bun";
83492
84235
  }
@@ -83506,38 +84249,38 @@ function detectOriginalInstaller() {
83506
84249
  return null;
83507
84250
  }
83508
84251
  }
83509
- var STATE_PATH = resolve7(homedir7(), ".config", "claude-threads", UPDATE_STATE_FILENAME);
84252
+ var STATE_PATH = resolve7(homedir8(), ".config", "claude-threads", UPDATE_STATE_FILENAME);
83510
84253
  var PACKAGE_NAME2 = "claude-threads";
83511
84254
  function loadUpdateState() {
83512
84255
  try {
83513
- if (existsSync15(STATE_PATH)) {
83514
- const content = readFileSync11(STATE_PATH, "utf-8");
84256
+ if (existsSync16(STATE_PATH)) {
84257
+ const content = readFileSync12(STATE_PATH, "utf-8");
83515
84258
  return JSON.parse(content);
83516
84259
  }
83517
84260
  } catch (err) {
83518
- log40.warn(`Failed to load update state: ${err}`);
84261
+ log44.warn(`Failed to load update state: ${err}`);
83519
84262
  }
83520
84263
  return {};
83521
84264
  }
83522
84265
  function saveUpdateState(state) {
83523
84266
  try {
83524
84267
  const dir = dirname9(STATE_PATH);
83525
- if (!existsSync15(dir)) {
83526
- mkdirSync6(dir, { recursive: true });
84268
+ if (!existsSync16(dir)) {
84269
+ mkdirSync7(dir, { recursive: true });
83527
84270
  }
83528
- writeFileSync8(STATE_PATH, JSON.stringify(state, null, 2), "utf-8");
83529
- log40.debug("Update state saved");
84271
+ writeFileSync9(STATE_PATH, JSON.stringify(state, null, 2), "utf-8");
84272
+ log44.debug("Update state saved");
83530
84273
  } catch (err) {
83531
- log40.warn(`Failed to save update state: ${err}`);
84274
+ log44.warn(`Failed to save update state: ${err}`);
83532
84275
  }
83533
84276
  }
83534
84277
  function clearUpdateState() {
83535
84278
  try {
83536
- if (existsSync15(STATE_PATH)) {
83537
- writeFileSync8(STATE_PATH, "{}", "utf-8");
84279
+ if (existsSync16(STATE_PATH)) {
84280
+ writeFileSync9(STATE_PATH, "{}", "utf-8");
83538
84281
  }
83539
84282
  } catch (err) {
83540
- log40.warn(`Failed to clear update state: ${err}`);
84283
+ log44.warn(`Failed to clear update state: ${err}`);
83541
84284
  }
83542
84285
  }
83543
84286
  function checkJustUpdated() {
@@ -83569,11 +84312,11 @@ function clearRuntimeSettings() {
83569
84312
  }
83570
84313
  }
83571
84314
  async function installVersion(version) {
83572
- log40.info(`\uD83D\uDCE6 Installing ${PACKAGE_NAME2}@${version}...`);
84315
+ log44.info(`\uD83D\uDCE6 Installing ${PACKAGE_NAME2}@${version}...`);
83573
84316
  const pm = detectPackageManager();
83574
84317
  if (!pm) {
83575
84318
  const error = "Neither bun nor npm found in PATH. Cannot install update.";
83576
- log40.error(`❌ ${error}`);
84319
+ log44.error(`❌ ${error}`);
83577
84320
  return { success: false, error };
83578
84321
  }
83579
84322
  saveUpdateState({
@@ -83585,7 +84328,7 @@ async function installVersion(version) {
83585
84328
  return new Promise((resolve8) => {
83586
84329
  const { cmd, isBun: isBun3 } = pm;
83587
84330
  const args = ["install", "-g", `${PACKAGE_NAME2}@${version}`];
83588
- log40.debug(`Using ${isBun3 ? "bun" : "npm"} for installation`);
84331
+ log44.debug(`Using ${isBun3 ? "bun" : "npm"} for installation`);
83589
84332
  const child = spawn4(cmd, args, {
83590
84333
  stdio: ["ignore", "pipe", "pipe"],
83591
84334
  env: {
@@ -83603,7 +84346,7 @@ async function installVersion(version) {
83603
84346
  });
83604
84347
  child.on("close", (code) => {
83605
84348
  if (code === 0) {
83606
- log40.info(`✅ Successfully installed ${PACKAGE_NAME2}@${version}`);
84349
+ log44.info(`✅ Successfully installed ${PACKAGE_NAME2}@${version}`);
83607
84350
  saveUpdateState({
83608
84351
  previousVersion: VERSION,
83609
84352
  targetVersion: version,
@@ -83613,20 +84356,20 @@ async function installVersion(version) {
83613
84356
  resolve8({ success: true });
83614
84357
  } else {
83615
84358
  const errorMsg = stderr || stdout || `Exit code: ${code}`;
83616
- log40.error(`❌ Installation failed: ${errorMsg}`);
84359
+ log44.error(`❌ Installation failed: ${errorMsg}`);
83617
84360
  clearUpdateState();
83618
84361
  resolve8({ success: false, error: errorMsg });
83619
84362
  }
83620
84363
  });
83621
84364
  child.on("error", (err) => {
83622
- log40.error(`❌ Failed to spawn npm: ${err}`);
84365
+ log44.error(`❌ Failed to spawn npm: ${err}`);
83623
84366
  clearUpdateState();
83624
84367
  resolve8({ success: false, error: err.message });
83625
84368
  });
83626
84369
  setTimeout(() => {
83627
84370
  if (child.exitCode === null) {
83628
84371
  child.kill();
83629
- log40.error("❌ Installation timed out");
84372
+ log44.error("❌ Installation timed out");
83630
84373
  clearUpdateState();
83631
84374
  resolve8({ success: false, error: "Installation timed out" });
83632
84375
  }
@@ -83670,9 +84413,9 @@ class UpdateInstaller {
83670
84413
  // src/auto-update/respawn.ts
83671
84414
  init_logger();
83672
84415
  import { spawn as spawn5 } from "child_process";
83673
- import { existsSync as existsSync16, statSync as statSync4 } from "fs";
83674
- import { delimiter, join as join13 } from "path";
83675
- var log41 = createLogger("respawn");
84416
+ import { existsSync as existsSync17, statSync as statSync4 } from "fs";
84417
+ import { delimiter, join as join14 } from "path";
84418
+ var log45 = createLogger("respawn");
83676
84419
  function decideRespawn(env5 = process.env, isTTY = !!process.stdout.isTTY) {
83677
84420
  if (env5.CLAUDE_THREADS_BIN) {
83678
84421
  return { kind: "exit-for-supervisor", supervisor: "claude-threads-daemon" };
@@ -83691,22 +84434,22 @@ function decideRespawn(env5 = process.env, isTTY = !!process.stdout.isTTY) {
83691
84434
  }
83692
84435
  return { kind: "self-respawn" };
83693
84436
  }
83694
- function resolveClaudeThreadsBin(_env = process.env, _existsSync = existsSync16, _isFileExecutable = isFileExecutable) {
84437
+ function resolveClaudeThreadsBin(_env = process.env, _existsSync = existsSync17, _isFileExecutable = isFileExecutable) {
83695
84438
  const isWin2 = process.platform === "win32";
83696
84439
  const names = isWin2 ? ["claude-threads.cmd", "claude-threads.exe", "claude-threads.bat"] : ["claude-threads"];
83697
84440
  const path10 = _env.PATH || _env.Path || "";
83698
84441
  const dirs = path10.split(delimiter).filter(Boolean);
83699
84442
  const home = _env.HOME || _env.USERPROFILE;
83700
- const bunRoot = _env.BUN_INSTALL || (home ? join13(home, ".bun") : null);
84443
+ const bunRoot = _env.BUN_INSTALL || (home ? join14(home, ".bun") : null);
83701
84444
  if (bunRoot) {
83702
- const bunBin = join13(bunRoot, "bin");
84445
+ const bunBin = join14(bunRoot, "bin");
83703
84446
  if (!dirs.includes(bunBin)) {
83704
84447
  dirs.push(bunBin);
83705
84448
  }
83706
84449
  }
83707
84450
  for (const dir of dirs) {
83708
84451
  for (const name of names) {
83709
- const candidate = join13(dir, name);
84452
+ const candidate = join14(dir, name);
83710
84453
  if (_existsSync(candidate) && _isFileExecutable(candidate)) {
83711
84454
  return candidate;
83712
84455
  }
@@ -83728,7 +84471,7 @@ function isFileExecutable(path10) {
83728
84471
  }
83729
84472
  function spawnReplacement(argv = process.argv.slice(2), binPath = resolveClaudeThreadsBin()) {
83730
84473
  if (!binPath) {
83731
- log41.error("Could not resolve claude-threads on PATH; self-respawn aborted");
84474
+ log45.error("Could not resolve claude-threads on PATH; self-respawn aborted");
83732
84475
  return false;
83733
84476
  }
83734
84477
  if (process.stdin.isTTY && typeof process.stdin.setRawMode === "function") {
@@ -83749,23 +84492,23 @@ function spawnReplacement(argv = process.argv.slice(2), binPath = resolveClaudeT
83749
84492
  shell: useShell
83750
84493
  });
83751
84494
  } catch (err) {
83752
- log41.error(`spawn() threw: ${err instanceof Error ? err.message : String(err)}`);
84495
+ log45.error(`spawn() threw: ${err instanceof Error ? err.message : String(err)}`);
83753
84496
  return false;
83754
84497
  }
83755
84498
  child.once("error", (err) => {
83756
- log41.error(`Replacement process error: ${err.message}`);
84499
+ log45.error(`Replacement process error: ${err.message}`);
83757
84500
  });
83758
84501
  if (child.pid === undefined) {
83759
- log41.error("Spawn returned no pid (binary likely not executable)");
84502
+ log45.error("Spawn returned no pid (binary likely not executable)");
83760
84503
  return false;
83761
84504
  }
83762
84505
  child.unref();
83763
- log41.info(`Spawned replacement pid=${child.pid} from ${binPath}`);
84506
+ log45.info(`Spawned replacement pid=${child.pid} from ${binPath}`);
83764
84507
  return true;
83765
84508
  }
83766
84509
 
83767
84510
  // src/auto-update/manager.ts
83768
- var log42 = createLogger("updater");
84511
+ var log46 = createLogger("updater");
83769
84512
 
83770
84513
  class AutoUpdateManager extends EventEmitter9 {
83771
84514
  config;
@@ -83788,23 +84531,23 @@ class AutoUpdateManager extends EventEmitter9 {
83788
84531
  }
83789
84532
  start() {
83790
84533
  if (!this.config.enabled) {
83791
- log42.info("Auto-update is disabled");
84534
+ log46.info("Auto-update is disabled");
83792
84535
  return;
83793
84536
  }
83794
84537
  const updateResult = this.installer.checkJustUpdated();
83795
84538
  if (updateResult) {
83796
- log42.info(`\uD83C\uDF89 Updated from v${updateResult.previousVersion} to v${updateResult.currentVersion}`);
84539
+ log46.info(`\uD83C\uDF89 Updated from v${updateResult.previousVersion} to v${updateResult.currentVersion}`);
83797
84540
  this.callbacks.broadcastUpdate((fmt) => `\uD83C\uDF89 ${fmt.formatBold("Bot updated")} from v${updateResult.previousVersion} to v${updateResult.currentVersion}`).catch((err) => {
83798
- log42.warn(`Failed to broadcast update notification: ${err}`);
84541
+ log46.warn(`Failed to broadcast update notification: ${err}`);
83799
84542
  });
83800
84543
  }
83801
84544
  this.checker.start();
83802
- log42.info(`\uD83D\uDD04 Auto-update manager started (mode: ${this.config.autoRestartMode})`);
84545
+ log46.info(`\uD83D\uDD04 Auto-update manager started (mode: ${this.config.autoRestartMode})`);
83803
84546
  }
83804
84547
  stop() {
83805
84548
  this.checker.stop();
83806
84549
  this.scheduler.stop();
83807
- log42.debug("Auto-update manager stopped");
84550
+ log46.debug("Auto-update manager stopped");
83808
84551
  }
83809
84552
  getState() {
83810
84553
  return { ...this.state };
@@ -83818,10 +84561,10 @@ class AutoUpdateManager extends EventEmitter9 {
83818
84561
  async forceUpdate() {
83819
84562
  const updateInfo = this.state.updateInfo || await this.checker.check();
83820
84563
  if (!updateInfo) {
83821
- log42.info("No update available");
84564
+ log46.info("No update available");
83822
84565
  return;
83823
84566
  }
83824
- log42.info("Forcing immediate update");
84567
+ log46.info("Forcing immediate update");
83825
84568
  await this.performUpdate(updateInfo);
83826
84569
  }
83827
84570
  deferUpdate(minutes = 60) {
@@ -83887,11 +84630,11 @@ class AutoUpdateManager extends EventEmitter9 {
83887
84630
  await this.callbacks.prepareForRestart();
83888
84631
  } catch (err) {
83889
84632
  const reason = err instanceof Error ? err.message : String(err);
83890
- log42.error(`prepareForRestart failed: ${reason}`);
84633
+ log46.error(`prepareForRestart failed: ${reason}`);
83891
84634
  await this.callbacks.broadcastUpdate((fmt) => `⚠️ ${fmt.formatBold("Restart aborted")}: shutdown sequence failed (${reason}). Sessions may be in an inconsistent state; please run ${fmt.formatCode("claude-threads")} manually.`).catch(() => {});
83892
84635
  process.exit(1);
83893
84636
  }
83894
- log42.info(`\uD83D\uDD04 Restarting for update to v${updateInfo.latestVersion}`);
84637
+ log46.info(`\uD83D\uDD04 Restarting for update to v${updateInfo.latestVersion}`);
83895
84638
  process.stdout.write("\x1B[2J\x1B[H");
83896
84639
  process.stdout.write("\x1B[?25h");
83897
84640
  if (decision.kind === "self-respawn") {
@@ -83900,14 +84643,14 @@ class AutoUpdateManager extends EventEmitter9 {
83900
84643
  if (ok) {
83901
84644
  process.exit(0);
83902
84645
  }
83903
- log42.error("Self-respawn launch failed after binary resolution succeeded");
84646
+ log46.error("Self-respawn launch failed after binary resolution succeeded");
83904
84647
  await this.callbacks.broadcastUpdate((fmt) => `⚠️ ${fmt.formatBold("Auto-restart failed")} after install: please run ${fmt.formatCode("claude-threads")} to bring the bot back. Sessions are persisted and will resume.`).catch(() => {});
83905
84648
  } else {
83906
- log42.error("claude-threads not found on PATH; manual restart required");
84649
+ log46.error("claude-threads not found on PATH; manual restart required");
83907
84650
  }
83908
84651
  process.exit(0);
83909
84652
  }
83910
- log42.debug(`Restart handled by supervisor: ${decision.supervisor}`);
84653
+ log46.debug(`Restart handled by supervisor: ${decision.supervisor}`);
83911
84654
  process.exit(RESTART_EXIT_CODE);
83912
84655
  } else {
83913
84656
  const errorMsg = result.error ?? "Unknown error";
@@ -84291,7 +85034,7 @@ async function startWithoutDaemon() {
84291
85034
  session.addPlatform(platformConfig.id, client, {
84292
85035
  sessionHeader: resolveOverheadVisibility(platformConfig.sessionHeader, `platforms[${platformConfig.id}].sessionHeader`),
84293
85036
  stickyMessage: resolveOverheadVisibility(platformConfig.stickyMessage, `platforms[${platformConfig.id}].stickyMessage`)
84294
- }, resolveMemoryConfig(platformConfig.memory, `platforms[${platformConfig.id}].memory`));
85037
+ }, resolveMemoryConfig(platformConfig.memory, `platforms[${platformConfig.id}].memory`), resolveRoutinesEnabled(platformConfig.routines, `platforms[${platformConfig.id}].routines`));
84295
85038
  wirePlatformEvents(platformConfig.id, client, session, ui);
84296
85039
  }
84297
85040
  const enabledPlatforms = Array.from(platforms.entries()).filter(([id]) => platformEnabledState.get(id) ?? true);