claude-threads 1.25.0 → 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,14 +58742,55 @@ ${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");
58791
-
58751
+ var log16 = createLogger("keepalive");
58752
+ function keepAliveSpawnSpec(platform, parentPid) {
58753
+ switch (platform) {
58754
+ case "darwin":
58755
+ return {
58756
+ command: "caffeinate",
58757
+ args: ["-s", "-i", "-w", String(parentPid)],
58758
+ stdio: "ignore"
58759
+ };
58760
+ case "linux":
58761
+ return {
58762
+ command: "systemd-inhibit",
58763
+ args: [
58764
+ "--what=sleep:idle:handle-lid-switch",
58765
+ "--why=Claude Code session active",
58766
+ "--mode=block",
58767
+ "cat"
58768
+ ],
58769
+ stdio: ["pipe", "ignore", "ignore"]
58770
+ };
58771
+ default:
58772
+ return null;
58773
+ }
58774
+ }
58775
+ function linuxFallbackScript(parentPid) {
58776
+ return `while kill -0 ${parentPid} 2>/dev/null; do xdg-screensaver reset 2>/dev/null || true; sleep 60; done`;
58777
+ }
58778
+ function windowsScript(parentPid) {
58779
+ return `
58780
+ Add-Type -TypeDefinition @"
58781
+ using System;
58782
+ using System.Runtime.InteropServices;
58783
+ public class PowerState {
58784
+ [DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
58785
+ public static extern uint SetThreadExecutionState(uint esFlags);
58786
+ }
58787
+ "@
58788
+ # ES_CONTINUOUS | ES_SYSTEM_REQUIRED
58789
+ [PowerState]::SetThreadExecutionState(0x80000001) | Out-Null
58790
+ # Keep running until killed or the parent process exits
58791
+ while (Get-Process -Id ${parentPid} -ErrorAction SilentlyContinue) { Start-Sleep -Seconds 60 }
58792
+ `;
58793
+ }
58792
58794
  class KeepAliveManager {
58793
58795
  activeSessionCount = 0;
58794
58796
  keepAliveProcess = null;
@@ -58802,7 +58804,7 @@ class KeepAliveManager {
58802
58804
  if (!enabled && this.keepAliveProcess) {
58803
58805
  this.stopKeepAlive();
58804
58806
  }
58805
- log17.debug(`Keep-alive ${enabled ? "enabled" : "disabled"}`);
58807
+ log16.debug(`Keep-alive ${enabled ? "enabled" : "disabled"}`);
58806
58808
  }
58807
58809
  isEnabled() {
58808
58810
  return this.enabled;
@@ -58812,7 +58814,7 @@ class KeepAliveManager {
58812
58814
  }
58813
58815
  sessionStarted() {
58814
58816
  this.activeSessionCount++;
58815
- log17.debug(`Session started (${this.activeSessionCount} active)`);
58817
+ log16.debug(`Session started (${this.activeSessionCount} active)`);
58816
58818
  if (this.activeSessionCount === 1) {
58817
58819
  this.startKeepAlive();
58818
58820
  }
@@ -58821,7 +58823,7 @@ class KeepAliveManager {
58821
58823
  if (this.activeSessionCount > 0) {
58822
58824
  this.activeSessionCount--;
58823
58825
  }
58824
- log17.debug(`Session ended (${this.activeSessionCount} active)`);
58826
+ log16.debug(`Session ended (${this.activeSessionCount} active)`);
58825
58827
  if (this.activeSessionCount === 0) {
58826
58828
  this.stopKeepAlive();
58827
58829
  }
@@ -58835,11 +58837,11 @@ class KeepAliveManager {
58835
58837
  }
58836
58838
  startKeepAlive() {
58837
58839
  if (!this.enabled) {
58838
- log17.debug("Keep-alive disabled, skipping");
58840
+ log16.debug("Keep-alive disabled, skipping");
58839
58841
  return;
58840
58842
  }
58841
58843
  if (this.keepAliveProcess) {
58842
- log17.debug("Keep-alive already running");
58844
+ log16.debug("Keep-alive already running");
58843
58845
  return;
58844
58846
  }
58845
58847
  switch (this.platform) {
@@ -58853,121 +58855,105 @@ class KeepAliveManager {
58853
58855
  this.startWindowsKeepAlive();
58854
58856
  break;
58855
58857
  default:
58856
- log17.warn(`Keep-alive not supported on ${this.platform}`);
58858
+ log16.warn(`Keep-alive not supported on ${this.platform}`);
58857
58859
  }
58858
58860
  }
58859
58861
  stopKeepAlive() {
58860
58862
  if (this.keepAliveProcess) {
58861
- log17.debug("Stopping keep-alive");
58863
+ log16.debug("Stopping keep-alive");
58862
58864
  this.keepAliveProcess.kill();
58863
58865
  this.keepAliveProcess = null;
58864
58866
  }
58865
58867
  }
58866
58868
  startMacOSKeepAlive() {
58867
58869
  try {
58868
- this.keepAliveProcess = spawn2("caffeinate", ["-s", "-i"], {
58869
- stdio: "ignore",
58870
+ const spec = keepAliveSpawnSpec("darwin", process.pid);
58871
+ if (!spec)
58872
+ return;
58873
+ this.keepAliveProcess = spawn2(spec.command, spec.args, {
58874
+ stdio: spec.stdio,
58870
58875
  detached: false
58871
58876
  });
58872
58877
  this.keepAliveProcess.on("error", (err) => {
58873
- log17.error(`Failed to start caffeinate: ${err.message}`);
58878
+ log16.error(`Failed to start caffeinate: ${err.message}`);
58874
58879
  this.keepAliveProcess = null;
58875
58880
  });
58876
58881
  this.keepAliveProcess.on("exit", (code) => {
58877
58882
  if (code !== null && code !== 0 && this.activeSessionCount > 0) {
58878
- log17.debug(`caffeinate exited with code ${code}`);
58883
+ log16.debug(`caffeinate exited with code ${code}`);
58879
58884
  }
58880
58885
  this.keepAliveProcess = null;
58881
58886
  });
58882
- log17.info("Sleep prevention active (caffeinate)");
58887
+ log16.info("Sleep prevention active (caffeinate)");
58883
58888
  } catch (err) {
58884
- log17.error(`Failed to start caffeinate: ${err}`);
58889
+ log16.error(`Failed to start caffeinate: ${err}`);
58885
58890
  }
58886
58891
  }
58887
58892
  startLinuxKeepAlive() {
58888
58893
  try {
58889
- this.keepAliveProcess = spawn2("systemd-inhibit", [
58890
- "--what=sleep:idle:handle-lid-switch",
58891
- "--why=Claude Code session active",
58892
- "--mode=block",
58893
- "sleep",
58894
- "infinity"
58895
- ], {
58896
- stdio: "ignore",
58894
+ const spec = keepAliveSpawnSpec("linux", process.pid);
58895
+ if (!spec)
58896
+ return;
58897
+ this.keepAliveProcess = spawn2(spec.command, spec.args, {
58898
+ stdio: spec.stdio,
58897
58899
  detached: false
58898
58900
  });
58899
58901
  this.keepAliveProcess.on("error", (err) => {
58900
- log17.debug(`systemd-inhibit not available: ${err.message}`);
58902
+ log16.debug(`systemd-inhibit not available: ${err.message}`);
58901
58903
  this.keepAliveProcess = null;
58902
58904
  this.startLinuxKeepAliveFallback();
58903
58905
  });
58904
58906
  this.keepAliveProcess.on("exit", (code) => {
58905
58907
  if (code !== null && code !== 0 && this.activeSessionCount > 0) {
58906
- log17.debug(`systemd-inhibit exited with code ${code}`);
58908
+ log16.debug(`systemd-inhibit exited with code ${code}`);
58907
58909
  }
58908
58910
  this.keepAliveProcess = null;
58909
58911
  });
58910
- log17.info("Sleep prevention active (systemd-inhibit)");
58912
+ log16.info("Sleep prevention active (systemd-inhibit)");
58911
58913
  } catch (err) {
58912
- log17.debug(`Failed to start systemd-inhibit: ${err}`);
58914
+ log16.debug(`Failed to start systemd-inhibit: ${err}`);
58913
58915
  this.startLinuxKeepAliveFallback();
58914
58916
  }
58915
58917
  }
58916
58918
  startLinuxKeepAliveFallback() {
58917
58919
  try {
58918
- this.keepAliveProcess = spawn2("bash", [
58919
- "-c",
58920
- `while true; do xdg-screensaver reset 2>/dev/null || true; sleep 60; done`
58921
- ], {
58920
+ this.keepAliveProcess = spawn2("bash", ["-c", linuxFallbackScript(process.pid)], {
58922
58921
  stdio: "ignore",
58923
58922
  detached: false
58924
58923
  });
58925
58924
  this.keepAliveProcess.on("error", (err) => {
58926
- log17.warn(`Linux keep-alive fallback not available: ${err.message}`);
58925
+ log16.warn(`Linux keep-alive fallback not available: ${err.message}`);
58927
58926
  this.keepAliveProcess = null;
58928
58927
  });
58929
58928
  this.keepAliveProcess.on("exit", () => {
58930
58929
  this.keepAliveProcess = null;
58931
58930
  });
58932
- log17.info("Sleep prevention active (xdg-screensaver)");
58931
+ log16.info("Sleep prevention active (xdg-screensaver)");
58933
58932
  } catch (err) {
58934
- log17.warn(`Linux keep-alive not available: ${err}`);
58933
+ log16.warn(`Linux keep-alive not available: ${err}`);
58935
58934
  }
58936
58935
  }
58937
58936
  startWindowsKeepAlive() {
58938
58937
  try {
58939
- const script = `
58940
- Add-Type -TypeDefinition @"
58941
- using System;
58942
- using System.Runtime.InteropServices;
58943
- public class PowerState {
58944
- [DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
58945
- public static extern uint SetThreadExecutionState(uint esFlags);
58946
- }
58947
- "@
58948
- # ES_CONTINUOUS | ES_SYSTEM_REQUIRED
58949
- [PowerState]::SetThreadExecutionState(0x80000001) | Out-Null
58950
- # Keep running until killed
58951
- while ($true) { Start-Sleep -Seconds 60 }
58952
- `;
58938
+ const script = windowsScript(process.pid);
58953
58939
  this.keepAliveProcess = spawn2("powershell", ["-NoProfile", "-Command", script], {
58954
58940
  stdio: "ignore",
58955
58941
  detached: false,
58956
58942
  windowsHide: true
58957
58943
  });
58958
58944
  this.keepAliveProcess.on("error", (err) => {
58959
- log17.warn(`Windows keep-alive not available: ${err.message}`);
58945
+ log16.warn(`Windows keep-alive not available: ${err.message}`);
58960
58946
  this.keepAliveProcess = null;
58961
58947
  });
58962
58948
  this.keepAliveProcess.on("exit", (code) => {
58963
58949
  if (code !== null && code !== 0 && this.activeSessionCount > 0) {
58964
- log17.debug(`PowerShell keep-alive exited with code ${code}`);
58950
+ log16.debug(`PowerShell keep-alive exited with code ${code}`);
58965
58951
  }
58966
58952
  this.keepAliveProcess = null;
58967
58953
  });
58968
- log17.info("Sleep prevention active (SetThreadExecutionState)");
58954
+ log16.info("Sleep prevention active (SetThreadExecutionState)");
58969
58955
  } catch (err) {
58970
- log17.warn(`Windows keep-alive not available: ${err}`);
58956
+ log16.warn(`Windows keep-alive not available: ${err}`);
58971
58957
  }
58972
58958
  }
58973
58959
  }
@@ -58975,7 +58961,7 @@ var keepAlive = new KeepAliveManager;
58975
58961
 
58976
58962
  // src/utils/error-handler/index.ts
58977
58963
  init_logger();
58978
- var log18 = createLogger("error");
58964
+ var log17 = createLogger("error");
58979
58965
 
58980
58966
  class SessionError extends Error {
58981
58967
  sessionId;
@@ -59001,19 +58987,19 @@ async function handleError(error, context, severity = "recoverable") {
59001
58987
  const sessionPart = sessionId ? ` (${formatShortId(sessionId)})` : "";
59002
58988
  const logMessage = `${context.action}${sessionPart}: ${message}`;
59003
58989
  if (severity === "recoverable") {
59004
- log18.warn(logMessage);
58990
+ log17.warn(logMessage);
59005
58991
  } else {
59006
- log18.error(logMessage, error instanceof Error ? error : undefined);
58992
+ log17.error(logMessage, error instanceof Error ? error : undefined);
59007
58993
  }
59008
58994
  if (context.details) {
59009
- log18.debugJson("Error details", context.details);
58995
+ log17.debugJson("Error details", context.details);
59010
58996
  }
59011
58997
  if (context.notifyUser && context.session) {
59012
58998
  try {
59013
58999
  const fmt = context.session.platform.getFormatter();
59014
59000
  await context.session.platform.createPost(`⚠️ ${fmt.formatBold("Error")}: ${context.action} failed - ${message}`, context.session.threadId);
59015
59001
  } catch (notifyError) {
59016
- log18.warn(`Could not notify user: ${notifyError}`);
59002
+ log17.warn(`Could not notify user: ${notifyError}`);
59017
59003
  }
59018
59004
  }
59019
59005
  if (severity === "session-fatal" || severity === "system-fatal") {
@@ -59040,7 +59026,7 @@ async function logAndNotify(error, context) {
59040
59026
  }
59041
59027
  function logSilentError(context, error) {
59042
59028
  const message = error instanceof Error ? error.message : String(error);
59043
- log18.debug(`[${context}] Silently caught: ${message}`);
59029
+ log17.debug(`[${context}] Silently caught: ${message}`);
59044
59030
  }
59045
59031
 
59046
59032
  // src/session/lifecycle.ts
@@ -59060,8 +59046,8 @@ function createSessionLog(baseLog) {
59060
59046
  init_logger();
59061
59047
  init_emoji();
59062
59048
  init_worktree();
59063
- var log19 = createLogger("helpers");
59064
- var sessionLog = createSessionLog(log19);
59049
+ var log18 = createLogger("helpers");
59050
+ var sessionLog = createSessionLog(log18);
59065
59051
  var POST_TYPES = {
59066
59052
  info: "",
59067
59053
  success: "✅",
@@ -59147,10 +59133,10 @@ function updateLastMessage(session, post2) {
59147
59133
 
59148
59134
  // src/operations/streaming/handler.ts
59149
59135
  init_logger();
59150
- 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";
59151
59137
  import { tmpdir as tmpdir3 } from "os";
59152
59138
  import { join as join11 } from "path";
59153
- var log20 = createLogger("streaming");
59139
+ var log19 = createLogger("streaming");
59154
59140
  var UPLOAD_ROOT_DIR = "claude-threads-uploads";
59155
59141
  function safeIdSegment2(id) {
59156
59142
  return id.replace(/[^A-Za-z0-9._-]/g, "_");
@@ -59163,9 +59149,9 @@ async function cleanupSessionUploads(platformId, threadId) {
59163
59149
  return;
59164
59150
  const dir = getSessionUploadDir(platformId, threadId);
59165
59151
  try {
59166
- await rm3(dir, { recursive: true, force: true });
59152
+ await rm2(dir, { recursive: true, force: true });
59167
59153
  } catch (err) {
59168
- log20.debug(`Upload cleanup for ${platformId}:${threadId} failed (ignored): ${err}`);
59154
+ log19.debug(`Upload cleanup for ${platformId}:${threadId} failed (ignored): ${err}`);
59169
59155
  }
59170
59156
  }
59171
59157
  function sanitizeForPrompt(value) {
@@ -59186,7 +59172,7 @@ async function saveFilesToUploadDir(platform, uploadDir, files, debug = false) {
59186
59172
  for (const file of files) {
59187
59173
  skipped.push({ name: file.name, reason: "Refusing to write under symlinked upload directory" });
59188
59174
  }
59189
- log20.error(`Upload dir is a symlink, refusing all writes: ${uploadDir}`);
59175
+ log19.error(`Upload dir is a symlink, refusing all writes: ${uploadDir}`);
59190
59176
  return { saved, skipped };
59191
59177
  }
59192
59178
  const messageDir = await mkdtemp(join11(uploadDir, `${Date.now().toString(36)}-`));
@@ -59204,11 +59190,11 @@ async function saveFilesToUploadDir(platform, uploadDir, files, debug = false) {
59204
59190
  size: buffer.length
59205
59191
  });
59206
59192
  if (debug) {
59207
- log20.debug(`Saved ${file.name} → ${absolutePath} (${formatBytes(buffer.length)})`);
59193
+ log19.debug(`Saved ${file.name} → ${absolutePath} (${formatBytes(buffer.length)})`);
59208
59194
  }
59209
59195
  } catch (err) {
59210
59196
  const message = err instanceof Error ? err.message : String(err);
59211
- log20.error(`Failed to save uploaded file ${file.name}: ${message}`);
59197
+ log19.error(`Failed to save uploaded file ${file.name}: ${message}`);
59212
59198
  skipped.push({
59213
59199
  name: file.name,
59214
59200
  reason: `Download failed: ${message}`
@@ -59284,7 +59270,7 @@ function buildRestartCliOptions(session, ctx) {
59284
59270
  }
59285
59271
 
59286
59272
  // src/operations/commands/handler.ts
59287
- import { randomUUID as randomUUID4 } from "crypto";
59273
+ import { randomUUID as randomUUID5 } from "crypto";
59288
59274
  import { resolve as resolve6 } from "path";
59289
59275
  import { existsSync as existsSync12, statSync as statSync3 } from "fs";
59290
59276
 
@@ -59631,9 +59617,9 @@ node_default(Temp.purgeSyncAll);
59631
59617
  var temp_default = Temp;
59632
59618
 
59633
59619
  // node_modules/atomically/dist/index.js
59634
- function writeFileSync6(filePath, data, options = DEFAULT_WRITE_OPTIONS) {
59620
+ function writeFileSync7(filePath, data, options = DEFAULT_WRITE_OPTIONS) {
59635
59621
  if (isString(options))
59636
- return writeFileSync6(filePath, data, { encoding: options });
59622
+ return writeFileSync7(filePath, data, { encoding: options });
59637
59623
  const timeout = options.timeout ?? DEFAULT_TIMEOUT_SYNC;
59638
59624
  const retryOptions = { timeout };
59639
59625
  let tempDisposer = null;
@@ -59961,7 +59947,7 @@ class Configstore {
59961
59947
  }
59962
59948
  if (error.name === "SyntaxError") {
59963
59949
  if (this._clearInvalidConfig) {
59964
- writeFileSync6(this._path, "", writeFileOptions);
59950
+ writeFileSync7(this._path, "", writeFileOptions);
59965
59951
  return {};
59966
59952
  }
59967
59953
  throw error;
@@ -59973,7 +59959,7 @@ class Configstore {
59973
59959
  set all(value) {
59974
59960
  try {
59975
59961
  import_graceful_fs.default.mkdirSync(path5.dirname(this._path), mkdirOptions);
59976
- writeFileSync6(this._path, JSON.stringify(value, undefined, "\t"), writeFileOptions);
59962
+ writeFileSync7(this._path, JSON.stringify(value, undefined, "\t"), writeFileOptions);
59977
59963
  } catch (error) {
59978
59964
  handlePermissionError(error);
59979
59965
  }
@@ -62701,7 +62687,7 @@ init_emoji();
62701
62687
 
62702
62688
  // src/operations/bug-report/handler.ts
62703
62689
  import { execSync as execSync2 } from "child_process";
62704
- import { writeFileSync as writeFileSync7, unlinkSync as unlinkSync3 } from "fs";
62690
+ import { writeFileSync as writeFileSync8, unlinkSync as unlinkSync3 } from "fs";
62705
62691
  import { tmpdir as tmpdir4 } from "os";
62706
62692
  import { join as join12 } from "path";
62707
62693
 
@@ -63285,7 +63271,7 @@ async function createGitHubIssue(title, body, workingDir) {
63285
63271
  }
63286
63272
  const bodyFile = join12(tmpdir4(), `bug-body-${Date.now()}.md`);
63287
63273
  try {
63288
- writeFileSync7(bodyFile, body, "utf-8");
63274
+ writeFileSync8(bodyFile, body, "utf-8");
63289
63275
  const cmd = `gh issue create --repo "${GITHUB_REPO}" --title "${escapeShell(title)}" --body-file "${bodyFile}"`;
63290
63276
  const result = execSync2(cmd, {
63291
63277
  cwd: workingDir,
@@ -66942,7 +66928,8 @@ class PromptExecutor extends BaseExecutor {
66942
66928
  return {
66943
66929
  pendingContextPrompt: null,
66944
66930
  pendingExistingWorktreePrompt: null,
66945
- pendingUpdatePrompt: null
66931
+ pendingUpdatePrompt: null,
66932
+ pendingRoutinePrompt: null
66946
66933
  };
66947
66934
  }
66948
66935
  getInitialState() {
@@ -66952,14 +66939,16 @@ class PromptExecutor extends BaseExecutor {
66952
66939
  return {
66953
66940
  pendingContextPrompt: this.state.pendingContextPrompt ? { ...this.state.pendingContextPrompt } : null,
66954
66941
  pendingExistingWorktreePrompt: this.state.pendingExistingWorktreePrompt ? { ...this.state.pendingExistingWorktreePrompt } : null,
66955
- 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
66956
66944
  };
66957
66945
  }
66958
66946
  hydrateState(persisted) {
66959
66947
  this.state = {
66960
66948
  pendingContextPrompt: persisted.pendingContextPrompt ?? null,
66961
66949
  pendingExistingWorktreePrompt: persisted.pendingExistingWorktreePrompt ?? null,
66962
- pendingUpdatePrompt: persisted.pendingUpdatePrompt ?? null
66950
+ pendingUpdatePrompt: persisted.pendingUpdatePrompt ?? null,
66951
+ pendingRoutinePrompt: null
66963
66952
  };
66964
66953
  }
66965
66954
  setPendingContextPrompt(prompt) {
@@ -67089,6 +67078,30 @@ class PromptExecutor extends BaseExecutor {
67089
67078
  }
67090
67079
  return true;
67091
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
+ }
67092
67105
  async handleReaction(postId, emoji, user, action, ctx) {
67093
67106
  ctx.logger.debug(`PromptExecutor.handleReaction: postId=${postId.substring(0, 8)}, emoji=${emoji}, user=${user}, action=${action}`);
67094
67107
  if (action !== "added") {
@@ -67149,6 +67162,18 @@ class PromptExecutor extends BaseExecutor {
67149
67162
  ctx.logger.debug(`PromptExecutor: emoji ${emoji} not valid for update prompt, ignoring`);
67150
67163
  return false;
67151
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
+ }
67152
67177
  ctx.logger.debug(`PromptExecutor: no pending prompt state matches postId=${postId.substring(0, 8)}`);
67153
67178
  return false;
67154
67179
  }
@@ -67243,7 +67268,7 @@ class BugReportExecutor extends BaseExecutor {
67243
67268
  // src/operations/executors/worktree-prompt.ts
67244
67269
  init_emoji();
67245
67270
  init_logger();
67246
- var log21 = createLogger("wt-prompt");
67271
+ var log20 = createLogger("wt-prompt");
67247
67272
  // src/operations/message-manager.ts
67248
67273
  init_logger();
67249
67274
 
@@ -67323,7 +67348,7 @@ function formatRelativeTime(date) {
67323
67348
  return `${diffMin} min ago`;
67324
67349
  }
67325
67350
  // src/operations/message-manager.ts
67326
- var log22 = createLogger("msg-mgr");
67351
+ var log21 = createLogger("msg-mgr");
67327
67352
 
67328
67353
  class MessageManager {
67329
67354
  platform;
@@ -67416,7 +67441,7 @@ class MessageManager {
67416
67441
  });
67417
67442
  }
67418
67443
  async handleEvent(event) {
67419
- const logger = log22.forSession(this.sessionId);
67444
+ const logger = log21.forSession(this.sessionId);
67420
67445
  const transformCtx = {
67421
67446
  sessionId: this.sessionId,
67422
67447
  formatter: this.platform.getFormatter(),
@@ -67470,7 +67495,7 @@ class MessageManager {
67470
67495
  }
67471
67496
  }
67472
67497
  async executeOperation(op) {
67473
- const logger = log22.forSession(this.sessionId);
67498
+ const logger = log21.forSession(this.sessionId);
67474
67499
  const ctx = this.getExecutorContext();
67475
67500
  try {
67476
67501
  if (isContentOp(op)) {
@@ -67538,7 +67563,7 @@ class MessageManager {
67538
67563
  threadId: this.threadId,
67539
67564
  platform: this.platform,
67540
67565
  formatter: this.platform.getFormatter(),
67541
- logger: log22.forSession(this.sessionId),
67566
+ logger: log21.forSession(this.sessionId),
67542
67567
  postTracker: this.postTracker,
67543
67568
  contentBreaker: this.contentBreaker,
67544
67569
  threadLogger: this.session.threadLogger,
@@ -67657,6 +67682,9 @@ class MessageManager {
67657
67682
  clearPendingUpdatePrompt() {
67658
67683
  this.promptExecutor.clearPendingUpdatePrompt();
67659
67684
  }
67685
+ setPendingRoutinePrompt(prompt) {
67686
+ this.promptExecutor.setPendingRoutinePrompt(prompt);
67687
+ }
67660
67688
  setPendingBugReport(report) {
67661
67689
  this.bugReportExecutor.setPendingBugReport(report);
67662
67690
  }
@@ -67755,13 +67783,13 @@ class MessageManager {
67755
67783
  return this.systemExecutor.postSuccess(message, this.getExecutorContext());
67756
67784
  }
67757
67785
  async prepareForUserMessage() {
67758
- const logger = log22.forSession(this.sessionId);
67786
+ const logger = log21.forSession(this.sessionId);
67759
67787
  logger.debug("Preparing for new user message");
67760
67788
  await this.closeCurrentPost();
67761
67789
  await this.bumpTaskList();
67762
67790
  }
67763
67791
  async handleUserMessage(message, files, username, displayName) {
67764
- const logger = log22.forSession(this.sessionId);
67792
+ const logger = log21.forSession(this.sessionId);
67765
67793
  if (!this.session.claude.isRunning()) {
67766
67794
  logger.debug("Claude not running, ignoring user message");
67767
67795
  return false;
@@ -67804,7 +67832,7 @@ class MessageManager {
67804
67832
  ];
67805
67833
  }
67806
67834
  async handleReaction(postId, emoji, user, action) {
67807
- const logger = log22.forSession(this.sessionId);
67835
+ const logger = log21.forSession(this.sessionId);
67808
67836
  const ctx = this.getExecutorContext();
67809
67837
  logger.debug(`Routing reaction: postId=${postId}, emoji=${emoji}, user=${user}, action=${action}`);
67810
67838
  for (const { name, executor } of this.reactionDispatchList()) {
@@ -67902,7 +67930,7 @@ class MessageManager {
67902
67930
  }
67903
67931
  // src/operations/sticky-message/handler.ts
67904
67932
  init_logger();
67905
- var log23 = createLogger("sticky");
67933
+ var log22 = createLogger("sticky");
67906
67934
  var botStartedAt = new Date;
67907
67935
  function getPendingPrompts(session) {
67908
67936
  const prompts2 = [];
@@ -67977,21 +68005,21 @@ function initialize(store) {
67977
68005
  stickyPostIds.set(platformId, postId);
67978
68006
  }
67979
68007
  if (persistedIds.size > 0) {
67980
- 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`);
67981
68009
  }
67982
68010
  }
67983
68011
  function setPlatformPaused(platformId, paused) {
67984
68012
  if (paused) {
67985
68013
  pausedPlatforms.set(platformId, true);
67986
- log23.debug(`Platform ${platformId} marked as paused`);
68014
+ log22.debug(`Platform ${platformId} marked as paused`);
67987
68015
  } else {
67988
68016
  pausedPlatforms.delete(platformId);
67989
- log23.debug(`Platform ${platformId} marked as active`);
68017
+ log22.debug(`Platform ${platformId} marked as active`);
67990
68018
  }
67991
68019
  }
67992
68020
  function setShuttingDown(shuttingDown) {
67993
68021
  isShuttingDown = shuttingDown;
67994
- log23.debug(`Bot shutdown state: ${shuttingDown}`);
68022
+ log22.debug(`Bot shutdown state: ${shuttingDown}`);
67995
68023
  }
67996
68024
  function getTaskContent(session) {
67997
68025
  const taskState = session.messageManager?.getTaskListState();
@@ -68324,12 +68352,12 @@ async function validateLastMessageIds(platform, sessions) {
68324
68352
  try {
68325
68353
  const post2 = await platform.getPost(lastMessageId);
68326
68354
  if (!post2) {
68327
- 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`);
68328
68356
  session.lastMessageId = undefined;
68329
68357
  session.lastMessageTs = undefined;
68330
68358
  }
68331
68359
  } catch (err) {
68332
- 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}`);
68333
68361
  session.lastMessageId = undefined;
68334
68362
  session.lastMessageTs = undefined;
68335
68363
  }
@@ -68346,7 +68374,7 @@ async function updateStickyMessageImpl(platform, sessions, config) {
68346
68374
  hiddenCleanupDone.add(platform.platformId);
68347
68375
  const existing = stickyPostIds.get(platform.platformId);
68348
68376
  if (existing) {
68349
- log23.info(`sticky[${platform.platformId}] hidden mode: removing leftover ${formatShortId(existing)}`);
68377
+ log22.info(`sticky[${platform.platformId}] hidden mode: removing leftover ${formatShortId(existing)}`);
68350
68378
  try {
68351
68379
  await platform.unpinPost(existing);
68352
68380
  } catch {}
@@ -68366,63 +68394,63 @@ async function updateStickyMessageImpl(platform, sessions, config) {
68366
68394
  return;
68367
68395
  }
68368
68396
  const platformSessions = [...sessions.values()].filter((s) => s.platformId === platform.platformId);
68369
- log23.debug(`updateStickyMessage for ${platform.platformId}, ${platformSessions.length} sessions`);
68397
+ log22.debug(`updateStickyMessage for ${platform.platformId}, ${platformSessions.length} sessions`);
68370
68398
  for (const s of platformSessions) {
68371
- 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)}..."`);
68372
68400
  }
68373
68401
  await validateLastMessageIds(platform, platformSessions);
68374
68402
  const formatter = platform.getFormatter();
68375
68403
  const content = await buildStickyMessage(sessions, platform.platformId, config, formatter, (threadId) => platform.getThreadLink(threadId));
68376
68404
  const existingPostId = stickyPostIds.get(platform.platformId);
68377
68405
  const shouldBump = needsBump.get(platform.platformId) ?? false;
68378
- log23.debug(`existingPostId: ${existingPostId || "(none)"}, needsBump: ${shouldBump}`);
68406
+ log22.debug(`existingPostId: ${existingPostId || "(none)"}, needsBump: ${shouldBump}`);
68379
68407
  try {
68380
68408
  if (existingPostId && !shouldBump) {
68381
- log23.debug(`Updating existing post in place...`);
68409
+ log22.debug(`Updating existing post in place...`);
68382
68410
  try {
68383
68411
  await platform.updatePost(existingPostId, content);
68384
68412
  try {
68385
68413
  await platform.pinPost(existingPostId);
68386
- log23.debug(`Re-pinned post`);
68414
+ log22.debug(`Re-pinned post`);
68387
68415
  } catch (pinErr) {
68388
- log23.debug(`Re-pin failed (might already be pinned): ${pinErr}`);
68416
+ log22.debug(`Re-pin failed (might already be pinned): ${pinErr}`);
68389
68417
  }
68390
- log23.debug(`Updated successfully`);
68418
+ log22.debug(`Updated successfully`);
68391
68419
  return;
68392
68420
  } catch (err) {
68393
- log23.debug(`Update failed, will create new: ${err}`);
68421
+ log22.debug(`Update failed, will create new: ${err}`);
68394
68422
  }
68395
68423
  }
68396
68424
  needsBump.set(platform.platformId, false);
68397
68425
  if (existingPostId) {
68398
- log23.debug(`Unpinning and deleting existing post ${existingPostId.substring(0, 8)}...`);
68426
+ log22.debug(`Unpinning and deleting existing post ${existingPostId.substring(0, 8)}...`);
68399
68427
  try {
68400
68428
  await platform.unpinPost(existingPostId);
68401
- log23.debug(`Unpinned successfully`);
68429
+ log22.debug(`Unpinned successfully`);
68402
68430
  } catch (err) {
68403
- log23.debug(`Unpin failed (probably already unpinned): ${err}`);
68431
+ log22.debug(`Unpin failed (probably already unpinned): ${err}`);
68404
68432
  }
68405
68433
  try {
68406
68434
  await platform.deletePost(existingPostId);
68407
- log23.debug(`Deleted successfully`);
68435
+ log22.debug(`Deleted successfully`);
68408
68436
  } catch (err) {
68409
- log23.debug(`Delete failed (probably already deleted): ${err}`);
68437
+ log22.debug(`Delete failed (probably already deleted): ${err}`);
68410
68438
  }
68411
68439
  stickyPostIds.delete(platform.platformId);
68412
68440
  }
68413
- log23.debug(`Creating new post...`);
68441
+ log22.debug(`Creating new post...`);
68414
68442
  const post2 = await platform.createPost(content);
68415
68443
  stickyPostIds.set(platform.platformId, post2.id);
68416
68444
  try {
68417
68445
  await platform.pinPost(post2.id);
68418
- log23.debug(`Pinned post successfully`);
68446
+ log22.debug(`Pinned post successfully`);
68419
68447
  } catch (err) {
68420
- log23.debug(`Failed to pin post: ${err}`);
68448
+ log22.debug(`Failed to pin post: ${err}`);
68421
68449
  }
68422
68450
  if (sessionStore) {
68423
68451
  sessionStore.saveStickyPostId(platform.platformId, post2.id);
68424
68452
  }
68425
- 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)}`);
68426
68454
  const excludePostIds = new Set;
68427
68455
  if (sessionStore) {
68428
68456
  for (const session of sessionStore.load().values()) {
@@ -68438,10 +68466,10 @@ async function updateStickyMessageImpl(platform, sessions, config) {
68438
68466
  }
68439
68467
  const botUser = await platform.getBotUser();
68440
68468
  cleanupOldStickyMessages(platform, botUser.id, false, excludePostIds).catch((err) => {
68441
- log23.debug(`Background cleanup failed: ${err}`);
68469
+ log22.debug(`Background cleanup failed: ${err}`);
68442
68470
  });
68443
68471
  } catch (err) {
68444
- 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);
68445
68473
  }
68446
68474
  }
68447
68475
  async function updateAllStickyMessages(platforms, sessions, config, overheadByPlatform) {
@@ -68469,7 +68497,7 @@ async function cleanupOldStickyMessages(platform, botUserId, forceRun = false, e
68469
68497
  if (!forceRun) {
68470
68498
  const lastRun = lastCleanupTime.get(platformId) || 0;
68471
68499
  if (now - lastRun < CLEANUP_THROTTLE_MS) {
68472
- 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)`);
68473
68501
  return;
68474
68502
  }
68475
68503
  }
@@ -68479,37 +68507,37 @@ async function cleanupOldStickyMessages(platform, botUserId, forceRun = false, e
68479
68507
  const pinnedPostIds = await platform.getPinnedPosts();
68480
68508
  const recentPinnedIds = pinnedPostIds.filter((id) => id !== currentStickyId && !excludePostIds?.has(id) && isRecentPost(id));
68481
68509
  if (recentPinnedIds.length === 0) {
68482
- 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)"})`);
68483
68511
  return;
68484
68512
  }
68485
- 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)`);
68486
68514
  for (const postId of recentPinnedIds) {
68487
68515
  try {
68488
68516
  const post2 = await platform.getPost(postId);
68489
68517
  if (!post2)
68490
68518
  continue;
68491
68519
  if (post2.userId === botUserId) {
68492
- log23.debug(`Cleaning up old sticky: ${postId.substring(0, 8)}...`);
68520
+ log22.debug(`Cleaning up old sticky: ${postId.substring(0, 8)}...`);
68493
68521
  try {
68494
68522
  await platform.unpinPost(postId);
68495
68523
  await platform.deletePost(postId);
68496
- 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)}...`);
68497
68525
  } catch (err) {
68498
- log23.debug(`Failed to cleanup ${postId}: ${err}`);
68526
+ log22.debug(`Failed to cleanup ${postId}: ${err}`);
68499
68527
  }
68500
68528
  }
68501
68529
  } catch (err) {
68502
- log23.debug(`Could not check post ${postId}: ${err}`);
68530
+ log22.debug(`Could not check post ${postId}: ${err}`);
68503
68531
  }
68504
68532
  }
68505
68533
  } catch (err) {
68506
- 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);
68507
68535
  }
68508
68536
  }
68509
68537
  // src/claude/quick-query.ts
68510
68538
  init_spawn();
68511
68539
  init_logger();
68512
- var log24 = createLogger("query");
68540
+ var log23 = createLogger("query");
68513
68541
  async function quickQuery(options) {
68514
68542
  const {
68515
68543
  prompt,
@@ -68524,7 +68552,7 @@ async function quickQuery(options) {
68524
68552
  if (systemPrompt) {
68525
68553
  args.push("--system-prompt", systemPrompt);
68526
68554
  }
68527
- 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)}..."`);
68528
68556
  return new Promise((resolve6) => {
68529
68557
  let stdout = "";
68530
68558
  let stderr = "";
@@ -68538,7 +68566,7 @@ async function quickQuery(options) {
68538
68566
  if (!resolved) {
68539
68567
  resolved = true;
68540
68568
  proc.kill("SIGTERM");
68541
- log24.debug(`Quick query timed out after ${timeout2}ms`);
68569
+ log23.debug(`Quick query timed out after ${timeout2}ms`);
68542
68570
  resolve6({
68543
68571
  success: false,
68544
68572
  error: "timeout",
@@ -68556,7 +68584,7 @@ async function quickQuery(options) {
68556
68584
  if (!resolved) {
68557
68585
  resolved = true;
68558
68586
  clearTimeout(timeoutId);
68559
- log24.debug(`Quick query error: ${err.message}`);
68587
+ log23.debug(`Quick query error: ${err.message}`);
68560
68588
  resolve6({
68561
68589
  success: false,
68562
68590
  error: err.message,
@@ -68570,14 +68598,14 @@ async function quickQuery(options) {
68570
68598
  clearTimeout(timeoutId);
68571
68599
  const durationMs = Date.now() - startTime;
68572
68600
  if (code === 0 && stdout.trim()) {
68573
- log24.debug(`Quick query success: ${durationMs}ms, ${stdout.length} chars`);
68601
+ log23.debug(`Quick query success: ${durationMs}ms, ${stdout.length} chars`);
68574
68602
  resolve6({
68575
68603
  success: true,
68576
68604
  response: stdout.trim(),
68577
68605
  durationMs
68578
68606
  });
68579
68607
  } else {
68580
- 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)}`);
68581
68609
  resolve6({
68582
68610
  success: false,
68583
68611
  error: stderr || `exit code ${code}`,
@@ -68596,7 +68624,7 @@ init_logger();
68596
68624
  import { exec as exec3 } from "child_process";
68597
68625
  import { promisify as promisify3 } from "util";
68598
68626
  var execAsync2 = promisify3(exec3);
68599
- var log25 = createLogger("branch");
68627
+ var log24 = createLogger("branch");
68600
68628
  var SUGGESTION_TIMEOUT = 15000;
68601
68629
  var MAX_SUGGESTIONS = 3;
68602
68630
  async function getCurrentBranch3(workingDir) {
@@ -68645,7 +68673,7 @@ function parseBranchSuggestions(response) {
68645
68673
  return lines.slice(0, MAX_SUGGESTIONS);
68646
68674
  }
68647
68675
  async function suggestBranchNames(workingDir, userMessage) {
68648
- log25.debug(`Suggesting branch names for: "${userMessage.substring(0, 50)}..."`);
68676
+ log24.debug(`Suggesting branch names for: "${userMessage.substring(0, 50)}..."`);
68649
68677
  try {
68650
68678
  const [currentBranch, recentCommits] = await Promise.all([
68651
68679
  getCurrentBranch3(workingDir),
@@ -68659,24 +68687,24 @@ async function suggestBranchNames(workingDir, userMessage) {
68659
68687
  workingDir
68660
68688
  });
68661
68689
  if (!result.success || !result.response) {
68662
- log25.debug(`Branch suggestion failed: ${result.error || "no response"}`);
68690
+ log24.debug(`Branch suggestion failed: ${result.error || "no response"}`);
68663
68691
  return [];
68664
68692
  }
68665
68693
  const suggestions = parseBranchSuggestions(result.response);
68666
- log25.debug(`Got ${suggestions.length} branch suggestions: ${suggestions.join(", ")}`);
68694
+ log24.debug(`Got ${suggestions.length} branch suggestions: ${suggestions.join(", ")}`);
68667
68695
  return suggestions;
68668
68696
  } catch (err) {
68669
- log25.debug(`Branch suggestion error: ${err}`);
68697
+ log24.debug(`Branch suggestion error: ${err}`);
68670
68698
  return [];
68671
68699
  }
68672
68700
  }
68673
68701
 
68674
68702
  // src/operations/worktree/handler.ts
68675
68703
  init_worktree();
68676
- import { randomUUID as randomUUID3 } from "crypto";
68704
+ import { randomUUID as randomUUID4 } from "crypto";
68677
68705
  init_logger();
68678
- var log26 = createLogger("worktree");
68679
- var sessionLog2 = createSessionLog(log26);
68706
+ var log25 = createLogger("worktree");
68707
+ var sessionLog2 = createSessionLog(log25);
68680
68708
  function parseWorktreeError(error) {
68681
68709
  const message = error instanceof Error ? error.message : String(error);
68682
68710
  const lowerMessage = message.toLowerCase();
@@ -68920,7 +68948,7 @@ async function createAndSwitchToWorktree(session, branch, username, options) {
68920
68948
  transitionTo(session, "restarting");
68921
68949
  await session.claude.kill();
68922
68950
  await options.flush(session);
68923
- const newSessionId = randomUUID3();
68951
+ const newSessionId = randomUUID4();
68924
68952
  session.claudeSessionId = newSessionId;
68925
68953
  const needsTitlePrompt = !session.sessionTitle;
68926
68954
  const memoryConfig = options.getPlatformMemoryConfig(session.platformId);
@@ -69019,7 +69047,7 @@ ${fmt.formatItalic("Claude Code restarted in the worktree")}`);
69019
69047
  transitionTo(session, "restarting");
69020
69048
  await session.claude.kill();
69021
69049
  await options.flush(session);
69022
- const newSessionId = randomUUID3();
69050
+ const newSessionId = randomUUID4();
69023
69051
  session.claudeSessionId = newSessionId;
69024
69052
  const needsTitlePrompt = !session.sessionTitle;
69025
69053
  const memoryConfig = options.getPlatformMemoryConfig(session.platformId);
@@ -69258,8 +69286,8 @@ async function cleanupWorktreeCommand(session, username, hasOtherSessionsUsingWo
69258
69286
  }
69259
69287
  // src/operations/events/handler.ts
69260
69288
  init_logger();
69261
- var log27 = createLogger("events");
69262
- var sessionLog3 = createSessionLog(log27);
69289
+ var log26 = createLogger("events");
69290
+ var sessionLog3 = createSessionLog(log26);
69263
69291
  function detectAndExecuteClaudeCommands(text, session, ctx) {
69264
69292
  const parsed = parseClaudeCommand(text);
69265
69293
  if (parsed && isClaudeAllowedCommand(parsed.command)) {
@@ -69392,7 +69420,11 @@ function handleEventPostProcessing(session, event, ctx, mainHandling) {
69392
69420
  ctx.ops.emitSessionUpdate(session.sessionId, { status: getSessionStatus(session) });
69393
69421
  updateUsageStats(session, event, ctx);
69394
69422
  if (mainHandling) {
69395
- 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
+ });
69396
69428
  } else {
69397
69429
  ctx.ops.persistSession(session);
69398
69430
  }
@@ -69568,6 +69600,56 @@ function updateUsageFromStatusLine(session) {
69568
69600
  sessionLog3(session).debug(`Updated from status line: context ${contextTokens}/${session.usageStats.contextWindowSize} (${contextPct}%)`);
69569
69601
  }
69570
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
+ }
69571
69653
  // src/operations/session-context/types.ts
69572
69654
  function createSessionContext(config, state, ops) {
69573
69655
  return {
@@ -69969,9 +70051,90 @@ async function suggestSessionMetadata(context) {
69969
70051
  return null;
69970
70052
  }
69971
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
+
69972
70135
  // src/operations/commands/handler.ts
69973
- var log31 = createLogger("commands");
69974
- var sessionLog5 = createSessionLog(log31);
70136
+ var log32 = createLogger("commands");
70137
+ var sessionLog5 = createSessionLog(log32);
69975
70138
  function sessionAccountOption(session, ctx) {
69976
70139
  if (!session.claudeAccountId)
69977
70140
  return;
@@ -70148,7 +70311,7 @@ async function changeDirectory(session, newDir, username, ctx) {
70148
70311
  sessionLog5(session).debug(`Stored work summary for context preservation`);
70149
70312
  }
70150
70313
  session.workingDir = absoluteDir;
70151
- const newSessionId = randomUUID4();
70314
+ const newSessionId = randomUUID5();
70152
70315
  session.claudeSessionId = newSessionId;
70153
70316
  const memoryConfig = ctx.ops.getPlatformMemoryConfig(session.platformId);
70154
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 });
@@ -70438,6 +70601,125 @@ ${list}${more}`);
70438
70601
  await post(session, "warning", `\uD83E\uDDE0 No matching entry. Use ${formatter.formatCode("!memory")} to list entries, then ${formatter.formatCode("!memory forget <number>")}.`);
70439
70602
  }
70440
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
+ }
70441
70723
  async function setSessionPermissionMode(session, username, mode, ctx) {
70442
70724
  if (!await requireSessionOwner(session, username, "change permissions")) {
70443
70725
  return;
@@ -70738,7 +71020,7 @@ init_worktree();
70738
71020
 
70739
71021
  // src/memory/distiller.ts
70740
71022
  init_logger();
70741
- var log32 = createLogger("memory");
71023
+ var log33 = createLogger("memory");
70742
71024
  var MIN_THREAD_MESSAGES = 4;
70743
71025
  var DISTILL_MESSAGE_LIMIT = 30;
70744
71026
  var MESSAGE_CHAR_CAP = 500;
@@ -70785,10 +71067,10 @@ function scheduleDistillation(session, ctx, reason) {
70785
71067
  const store = ctx.state.memoryStore;
70786
71068
  distillThread(store, platformId, threadId, platform).then((added) => {
70787
71069
  if (added > 0) {
70788
- log32.debug(`Distilled ${added} memory entries from ${platformId}:${threadId} (${reason})`);
71070
+ log33.debug(`Distilled ${added} memory entries from ${platformId}:${threadId} (${reason})`);
70789
71071
  }
70790
71072
  }).catch((err) => {
70791
- log32.debug(`Distillation failed for ${platformId}:${threadId}: ${err.message}`);
71073
+ log33.debug(`Distillation failed for ${platformId}:${threadId}: ${err.message}`);
70792
71074
  });
70793
71075
  }
70794
71076
  async function distillThread(store, platformId, threadId, platform) {
@@ -70815,8 +71097,8 @@ async function distillThread(store, platformId, threadId, platform) {
70815
71097
  }
70816
71098
 
70817
71099
  // src/session/lifecycle.ts
70818
- var log33 = createLogger("lifecycle");
70819
- var sessionLog6 = createSessionLog(log33);
71100
+ var log34 = createLogger("lifecycle");
71101
+ var sessionLog6 = createSessionLog(log34);
70820
71102
  function mutableSessions(ctx) {
70821
71103
  return ctx.state.sessions;
70822
71104
  }
@@ -70920,7 +71202,7 @@ async function createSessionDecisionBridge(ref) {
70920
71202
  return messageManager.handleBridgeRequest(request, signal);
70921
71203
  });
70922
71204
  } catch (err) {
70923
- 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}`);
70924
71206
  return null;
70925
71207
  }
70926
71208
  }
@@ -70988,6 +71270,28 @@ function createMessageManager(session, ctx) {
70988
71270
  sessionLog6(session).info(`@${fromUser} invited to session by @${approvedBy}`);
70989
71271
  }
70990
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
+ });
70991
71295
  messageManager.events.on("context-prompt:complete", async ({ selection, queuedPrompt, queuedByUsername, queuedFiles: _queuedFiles, threadMessageCount: _threadMessageCount }) => {
70992
71296
  const userTurn = formatUserTurn(queuedPrompt, queuedByUsername, shouldAttribute(session.userAttribution, session.sessionAllowedUsers.size));
70993
71297
  let messageToSend = userTurn;
@@ -71185,7 +71489,7 @@ function resumeSessionHeaderMode(persisted, platformConfigured) {
71185
71489
  function resolveSessionHeaderMode(configured, replyToPostId, platformId) {
71186
71490
  const mode = configured ?? DEFAULT_OVERHEAD_VISIBILITY;
71187
71491
  if (mode === "hidden" && !replyToPostId) {
71188
- 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.`);
71189
71493
  return "minimal";
71190
71494
  }
71191
71495
  return mode;
@@ -71204,7 +71508,7 @@ async function startSession(options, username, displayName, replyToPostId, platf
71204
71508
  throw new Error(`Platform '${platformId}' not found. Call addPlatform() first.`);
71205
71509
  }
71206
71510
  if (!isAuthorizedForSession({ username, platform, sessionAllowedUsers: undefined })) {
71207
- 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)}...`);
71208
71512
  return;
71209
71513
  }
71210
71514
  const activeOrPending = ctx.state.sessions.size + pendingStartsCount;
@@ -71233,7 +71537,7 @@ async function startSession(options, username, displayName, replyToPostId, platf
71233
71537
  const actualThreadId = replyToPostId || (startPost ? startPost.id : "");
71234
71538
  const sessionId = ctx.ops.getSessionId(platformId, actualThreadId);
71235
71539
  platform.sendTyping(actualThreadId);
71236
- const claudeSessionId = randomUUID5();
71540
+ const claudeSessionId = randomUUID6();
71237
71541
  let workingDir = ctx.config.workingDir;
71238
71542
  let permissionMode = ctx.config.permissionMode;
71239
71543
  let forceInteractivePermissions = false;
@@ -71265,17 +71569,17 @@ async function startSession(options, username, displayName, replyToPostId, platf
71265
71569
  return;
71266
71570
  }
71267
71571
  workingDir = resolvedDir;
71268
- log33.info(`Starting session in directory: ${workingDir} (from !cd command)`);
71572
+ log34.info(`Starting session in directory: ${workingDir} (from !cd command)`);
71269
71573
  }
71270
71574
  if (initialOptions?.permissionMode) {
71271
71575
  permissionMode = initialOptions.permissionMode;
71272
71576
  forceInteractivePermissions = permissionMode === "default";
71273
71577
  sessionPermissionModeOverride = permissionMode;
71274
- log33.info(`Starting session with permission mode "${permissionMode}" (from !permissions command)`);
71578
+ log34.info(`Starting session with permission mode "${permissionMode}" (from !permissions command)`);
71275
71579
  } else if (initialOptions?.forceInteractivePermissions) {
71276
71580
  forceInteractivePermissions = true;
71277
71581
  permissionMode = "default";
71278
- log33.info(`Starting session with interactive permissions (from !permissions command)`);
71582
+ log34.info(`Starting session with interactive permissions (from !permissions command)`);
71279
71583
  }
71280
71584
  const userAttribution = ctx.config.userAttribution ?? true;
71281
71585
  const memoryConfig = ctx.ops.getPlatformMemoryConfig(platformId);
@@ -71286,7 +71590,7 @@ async function startSession(options, username, displayName, replyToPostId, platf
71286
71590
  balanceByUsage: true
71287
71591
  });
71288
71592
  if (claudeAccount) {
71289
- 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}"`);
71290
71594
  }
71291
71595
  const bridgeSessionRef = {};
71292
71596
  const decisionBridge = await createSessionDecisionBridge(bridgeSessionRef);
@@ -71417,28 +71721,28 @@ async function resumeSession(state, ctx) {
71417
71721
  !state.claudeSessionId && "claudeSessionId",
71418
71722
  !state.workingDir && "workingDir"
71419
71723
  ].filter(Boolean).join(", ");
71420
- log33.warn(`Skipping session with missing required fields: ${missing}`);
71724
+ log34.warn(`Skipping session with missing required fields: ${missing}`);
71421
71725
  return;
71422
71726
  }
71423
71727
  const shortId = state.threadId.substring(0, 8);
71424
71728
  const platforms = ctx.state.platforms;
71425
71729
  const platform = platforms.get(state.platformId);
71426
71730
  if (!platform) {
71427
- log33.warn(`Platform ${state.platformId} not registered, skipping resume for ${shortId}...`);
71731
+ log34.warn(`Platform ${state.platformId} not registered, skipping resume for ${shortId}...`);
71428
71732
  return;
71429
71733
  }
71430
71734
  const threadPost = await platform.getPost(state.threadId);
71431
71735
  if (!threadPost) {
71432
- log33.warn(`Thread ${shortId}... deleted, skipping resume`);
71736
+ log34.warn(`Thread ${shortId}... deleted, skipping resume`);
71433
71737
  ctx.state.sessionStore.remove(`${state.platformId}:${state.threadId}`);
71434
71738
  return;
71435
71739
  }
71436
71740
  if (ctx.state.sessions.size >= ctx.config.maxSessions) {
71437
- log33.warn(`Max sessions reached, skipping resume for ${shortId}...`);
71741
+ log34.warn(`Max sessions reached, skipping resume for ${shortId}...`);
71438
71742
  return;
71439
71743
  }
71440
71744
  if (!existsSync13(state.workingDir)) {
71441
- 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}...`);
71442
71746
  ctx.state.sessionStore.remove(`${state.platformId}:${state.threadId}`);
71443
71747
  const resumeFormatter = platform.getFormatter();
71444
71748
  const tempSession = {
@@ -71461,7 +71765,7 @@ Please start a new session.`), { action: "Post resume failure notification" });
71461
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 });
71462
71766
  const claudeAccount = ctx.ops.acquireClaudeAccount(state.claudeAccountId, state.threadId);
71463
71767
  if (state.claudeAccountId && !claudeAccount) {
71464
- 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`);
71465
71769
  }
71466
71770
  const resumeBridgeRef = {};
71467
71771
  const resumeBridge = await createSessionDecisionBridge(resumeBridgeRef);
@@ -71544,7 +71848,7 @@ Please start a new session.`), { action: "Post resume failure notification" });
71544
71848
  worktreePath: detected.worktreePath,
71545
71849
  branch: detected.branch
71546
71850
  };
71547
- 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}`);
71548
71852
  }
71549
71853
  }
71550
71854
  session.messageManager = createMessageManager(session, ctx);
@@ -71603,7 +71907,7 @@ ${sessionFormatter.formatItalic("Reconnected to Claude session. You can continue
71603
71907
  await postResumeCoAuthorOnboarding(session, ctx);
71604
71908
  ctx.ops.persistSession(session);
71605
71909
  } catch (err) {
71606
- 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);
71607
71911
  session.messageManager?.dispose();
71608
71912
  session.decisionBridge?.close();
71609
71913
  session.decisionBridge = undefined;
@@ -71646,28 +71950,28 @@ async function resumePausedSession(threadId, message, files, ctx, username) {
71646
71950
  const persisted = ctx.state.sessionStore.load();
71647
71951
  const state = findPersistedByThreadId(persisted, threadId);
71648
71952
  if (!state) {
71649
- log33.debug(`No persisted session found for ${threadId.substring(0, 8)}...`);
71953
+ log34.debug(`No persisted session found for ${threadId.substring(0, 8)}...`);
71650
71954
  return;
71651
71955
  }
71652
71956
  const shortId = threadId.substring(0, 8);
71653
71957
  const platform = ctx.state.platforms.get(state.platformId);
71654
71958
  if (!platform) {
71655
- 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}...`);
71656
71960
  return;
71657
71961
  }
71658
71962
  const sessionAllowedUsers = new Set(state.sessionAllowedUsers || [state.startedBy].filter(Boolean));
71659
71963
  if (!isAuthorizedForSession({ username, platform, sessionAllowedUsers })) {
71660
- 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}...`);
71661
71965
  return;
71662
71966
  }
71663
- log33.info(`\uD83D\uDD04 Resuming paused session ${shortId}... for new message`);
71967
+ log34.info(`\uD83D\uDD04 Resuming paused session ${shortId}... for new message`);
71664
71968
  await resumeSession(state, ctx);
71665
71969
  const session = ctx.ops.findSessionByThreadId(threadId);
71666
71970
  if (session && session.claude.isRunning() && session.messageManager) {
71667
71971
  session.messageCount++;
71668
71972
  await session.messageManager.handleUserMessage(message, files, username);
71669
71973
  } else {
71670
- log33.warn(`Failed to resume session ${shortId}..., could not send message`);
71974
+ log34.warn(`Failed to resume session ${shortId}..., could not send message`);
71671
71975
  }
71672
71976
  }
71673
71977
  async function handleExit(sessionId, code, ctx, source) {
@@ -71675,7 +71979,7 @@ async function handleExit(sessionId, code, ctx, source) {
71675
71979
  const shortId = sessionId.substring(0, 8);
71676
71980
  sessionLog6(session).debug(`handleExit called code=${code} isShuttingDown=${ctx.state.isShuttingDown}`);
71677
71981
  if (!session) {
71678
- log33.debug(`Session ${shortId}... not found (already cleaned up)`);
71982
+ log34.debug(`Session ${shortId}... not found (already cleaned up)`);
71679
71983
  return;
71680
71984
  }
71681
71985
  if (source && session.claude !== source) {
@@ -71876,37 +72180,338 @@ async function cleanupIdleSessions(timeoutMs, warningMs, ctx) {
71876
72180
  }
71877
72181
  }
71878
72182
 
71879
- // src/operations/monitor/handler.ts
71880
- var log34 = createLogger("monitor");
71881
- 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.]
71882
72206
 
71883
- 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 {
71884
72486
  intervalMs;
71885
- sessionTimeoutMs;
71886
- sessionWarningMs;
71887
- getContext;
71888
- getSessionCount;
71889
- updateStickyMessage;
72487
+ logRetentionDays;
72488
+ threadLogsEnabled;
72489
+ sessionStore;
72490
+ maxWorktreeAgeMs;
72491
+ cleanupWorktrees;
71890
72492
  timer = null;
71891
72493
  isRunning = false;
71892
72494
  constructor(options) {
71893
- this.intervalMs = options.intervalMs ?? DEFAULT_INTERVAL_MS;
71894
- this.sessionTimeoutMs = options.sessionTimeoutMs;
71895
- this.sessionWarningMs = options.sessionWarningMs;
71896
- this.getContext = options.getContext;
71897
- this.getSessionCount = options.getSessionCount;
71898
- 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;
71899
72501
  }
71900
72502
  start() {
71901
72503
  if (this.isRunning) {
71902
- log34.debug("Session monitor already running");
72504
+ log38.debug("Cleanup scheduler already running");
71903
72505
  return;
71904
72506
  }
71905
72507
  this.isRunning = true;
71906
- 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
+ });
71907
72512
  this.timer = setInterval(() => {
71908
- this.runCheck().catch((err) => {
71909
- log34.error(`Error during session monitoring: ${err}`);
72513
+ this.runCleanup().catch((err) => {
72514
+ log38.warn(`Periodic cleanup failed: ${err}`);
71910
72515
  });
71911
72516
  }, this.intervalMs);
71912
72517
  }
@@ -71916,20 +72521,141 @@ class SessionMonitor {
71916
72521
  this.timer = null;
71917
72522
  }
71918
72523
  this.isRunning = false;
71919
- log34.debug("Session monitor stopped");
72524
+ log38.debug("Cleanup scheduler stopped");
71920
72525
  }
71921
- async runCheck() {
71922
- await cleanupIdleSessions(this.sessionTimeoutMs, this.sessionWarningMs, this.getContext());
71923
- if (this.getSessionCount() > 0) {
71924
- 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;
71925
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;
71926
72652
  }
71927
72653
  }
71928
72654
  // src/operations/plugin/handler.ts
71929
72655
  init_spawn();
71930
72656
  init_logger();
71931
- var log35 = createLogger("plugin");
71932
- var sessionLog7 = createSessionLog(log35);
72657
+ var log39 = createLogger("plugin");
72658
+ var sessionLog7 = createSessionLog(log39);
71933
72659
  async function buildPluginRestartCliOptions(session, ctx) {
71934
72660
  const account = session.claudeAccountId ? ctx.ops.getClaudeAccount(session.claudeAccountId) : undefined;
71935
72661
  const memoryConfig = ctx.ops.getPlatformMemoryConfig(session.platformId);
@@ -71971,7 +72697,7 @@ async function runPluginCommand(args, cwd, timeout2 = 60000) {
71971
72697
  });
71972
72698
  proc.on("error", (err) => {
71973
72699
  resolve7({ stdout, stderr, exitCode: 1 });
71974
- log35.error(`Plugin command error: ${err.message}`);
72700
+ log39.error(`Plugin command error: ${err.message}`);
71975
72701
  });
71976
72702
  });
71977
72703
  }
@@ -72145,7 +72871,7 @@ class SessionRegistry {
72145
72871
  // src/session/reaction-router.ts
72146
72872
  init_emoji();
72147
72873
  init_logger();
72148
- var log36 = createLogger("manager");
72874
+ var log40 = createLogger("manager");
72149
72875
  async function handleReaction(deps, platformId, postId, emojiName, username, action) {
72150
72876
  const normalizedEmoji = normalizeEmojiName(emojiName);
72151
72877
  if (action === "added" && isResumeEmoji(normalizedEmoji)) {
@@ -72159,7 +72885,7 @@ async function handleReaction(deps, platformId, postId, emojiName, username, act
72159
72885
  if (session.platformId !== platformId)
72160
72886
  return;
72161
72887
  if (!session.sessionAllowedUsers.has(username) && !session.platform.isUserAllowed(username)) {
72162
- log36.info(`\uD83D\uDEAB rejected reaction from unauthorized user`, {
72888
+ log40.info(`\uD83D\uDEAB rejected reaction from unauthorized user`, {
72163
72889
  event: "reaction.rejected",
72164
72890
  platformId,
72165
72891
  sessionId: session.sessionId,
@@ -72195,7 +72921,7 @@ async function tryResumeFromReaction(deps, platformId, postId, username) {
72195
72921
  return false;
72196
72922
  }
72197
72923
  const shortId = persistedSession.threadId.substring(0, 8);
72198
- 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}`);
72199
72925
  await resumeSession(persistedSession, deps.getContext());
72200
72926
  return true;
72201
72927
  }
@@ -72225,7 +72951,7 @@ async function dispatch(deps, session, postId, emojiName, username, action) {
72225
72951
  }
72226
72952
  if (session.lastError?.postId === postId && isBugReportEmoji(emojiName)) {
72227
72953
  if (session.startedBy === username || session.platform.isUserAllowed(username) || session.sessionAllowedUsers.has(username)) {
72228
- log36.info(`\uD83D\uDC1B @${username} triggered bug report from error reaction`);
72954
+ log40.info(`\uD83D\uDC1B @${username} triggered bug report from error reaction`);
72229
72955
  await reportBug(session, undefined, username, deps.getContext(), session.lastError);
72230
72956
  return;
72231
72957
  }
@@ -72240,7 +72966,7 @@ async function dispatch(deps, session, postId, emojiName, username, action) {
72240
72966
 
72241
72967
  // src/session/manager.ts
72242
72968
  init_logger();
72243
- var log37 = createLogger("manager");
72969
+ var log41 = createLogger("manager");
72244
72970
  var USAGE_PROBE_TIMEOUT_MS = 1e4;
72245
72971
  var USAGE_REFRESH_DEADLINE_MS = 5000;
72246
72972
  var USAGE_CACHE_TTL_MS = 15000;
@@ -72264,6 +72990,8 @@ class SessionManager extends EventEmitter4 {
72264
72990
  sessionStore;
72265
72991
  githubEmailsStore;
72266
72992
  memoryStore;
72993
+ routinesStore;
72994
+ routineScheduler = null;
72267
72995
  sessionMonitor = null;
72268
72996
  backgroundCleanup = null;
72269
72997
  isShuttingDown = false;
@@ -72271,6 +72999,7 @@ class SessionManager extends EventEmitter4 {
72271
72999
  customFooter;
72272
73000
  platformOverhead = new Map;
72273
73001
  platformMemory = new Map;
73002
+ platformRoutines = new Map;
72274
73003
  autoUpdateManager = null;
72275
73004
  accountPool;
72276
73005
  usageRefreshInFlight = null;
@@ -72289,6 +73018,7 @@ class SessionManager extends EventEmitter4 {
72289
73018
  this.sessionStore = new SessionStore(sessionsPath);
72290
73019
  this.githubEmailsStore = new GitHubEmailsStore;
72291
73020
  this.memoryStore = new MemoryStore;
73021
+ this.routinesStore = new RoutinesStore;
72292
73022
  this.registry = new SessionRegistry(this.sessionStore);
72293
73023
  this.accountPool = new AccountPool(claudeAccounts);
72294
73024
  this.sessionMonitor = new SessionMonitor({
@@ -72306,14 +73036,28 @@ class SessionManager extends EventEmitter4 {
72306
73036
  maxWorktreeAgeMs: this.limits.maxWorktreeAgeHours * 60 * 60 * 1000,
72307
73037
  cleanupWorktrees: this.limits.cleanupWorktrees
72308
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
+ });
72309
73052
  }
72310
- addPlatform(platformId, client, overhead, memory) {
73053
+ addPlatform(platformId, client, overhead, memory, routinesEnabled) {
72311
73054
  this.platforms.set(platformId, client);
72312
73055
  this.platformOverhead.set(platformId, {
72313
73056
  sessionHeader: overhead?.sessionHeader ?? DEFAULT_OVERHEAD_VISIBILITY,
72314
73057
  stickyMessage: overhead?.stickyMessage ?? DEFAULT_OVERHEAD_VISIBILITY
72315
73058
  });
72316
73059
  this.platformMemory.set(platformId, memory ?? DEFAULT_MEMORY_CONFIG);
73060
+ this.platformRoutines.set(platformId, routinesEnabled ?? true);
72317
73061
  client.on("message", (post2, user) => this.handleMessage(platformId, post2, user));
72318
73062
  client.on("reaction", (reaction, user) => {
72319
73063
  if (user) {
@@ -72332,12 +73076,13 @@ class SessionManager extends EventEmitter4 {
72332
73076
  markNeedsBump(platformId);
72333
73077
  this.updateStickyMessage();
72334
73078
  });
72335
- log37.info(`\uD83D\uDCE1 Platform "${platformId}" registered`);
73079
+ log41.info(`\uD83D\uDCE1 Platform "${platformId}" registered`);
72336
73080
  }
72337
73081
  removePlatform(platformId) {
72338
73082
  this.platforms.delete(platformId);
72339
73083
  this.platformOverhead.delete(platformId);
72340
73084
  this.platformMemory.delete(platformId);
73085
+ this.platformRoutines.delete(platformId);
72341
73086
  clearHiddenCleanupTracking(platformId);
72342
73087
  }
72343
73088
  setAutoUpdateManager(manager) {
@@ -72351,7 +73096,7 @@ class SessionManager extends EventEmitter4 {
72351
73096
  if (users) {
72352
73097
  users.add(sessionId);
72353
73098
  }
72354
- 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}`);
72355
73100
  }
72356
73101
  unregisterWorktreeUser(worktreePath, sessionId) {
72357
73102
  const users = this.worktreeUsers.get(worktreePath);
@@ -72377,6 +73122,7 @@ class SessionManager extends EventEmitter4 {
72377
73122
  userAttribution: this.userAttribution,
72378
73123
  debug: this.debug,
72379
73124
  maxSessions: this.limits.maxSessions,
73125
+ maxRoutines: this.limits.maxRoutines,
72380
73126
  threadLogsEnabled: this.threadLogsEnabled,
72381
73127
  threadLogsRetentionDays: this.threadLogsRetentionDays,
72382
73128
  permissionTimeoutMs: this.limits.permissionTimeoutSeconds * 1000,
@@ -72389,6 +73135,7 @@ class SessionManager extends EventEmitter4 {
72389
73135
  sessionStore: this.sessionStore,
72390
73136
  githubEmailsStore: this.githubEmailsStore,
72391
73137
  memoryStore: this.memoryStore,
73138
+ routinesStore: this.routinesStore,
72392
73139
  isShuttingDown: this.isShuttingDown
72393
73140
  };
72394
73141
  const ops = {
@@ -72434,7 +73181,9 @@ class SessionManager extends EventEmitter4 {
72434
73181
  sessionHeader: DEFAULT_OVERHEAD_VISIBILITY,
72435
73182
  stickyMessage: DEFAULT_OVERHEAD_VISIBILITY
72436
73183
  },
72437
- 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)
72438
73187
  };
72439
73188
  return createSessionContext(config, state, ops);
72440
73189
  }
@@ -72553,7 +73302,7 @@ class SessionManager extends EventEmitter4 {
72553
73302
  try {
72554
73303
  this.persistSessionUnsafe(session);
72555
73304
  } catch (err) {
72556
- log37.error(`Failed to persist session ${session.sessionId}: ${err}`);
73305
+ log41.error(`Failed to persist session ${session.sessionId}: ${err}`);
72557
73306
  }
72558
73307
  }
72559
73308
  persistSessionUnsafe(session) {
@@ -72669,11 +73418,11 @@ class SessionManager extends EventEmitter4 {
72669
73418
  }
72670
73419
  }
72671
73420
  if (sessionsToKill.length === 0) {
72672
- log37.info(`No active sessions to pause for platform ${platformId}`);
73421
+ log41.info(`No active sessions to pause for platform ${platformId}`);
72673
73422
  await this.updateStickyMessage();
72674
73423
  return;
72675
73424
  }
72676
- log37.info(`⏸️ Pausing ${sessionsToKill.length} session(s) for platform ${platformId}`);
73425
+ log41.info(`⏸️ Pausing ${sessionsToKill.length} session(s) for platform ${platformId}`);
72677
73426
  for (const session of sessionsToKill) {
72678
73427
  try {
72679
73428
  const fmt = session.platform.getFormatter();
@@ -72689,9 +73438,9 @@ class SessionManager extends EventEmitter4 {
72689
73438
  session.claude.kill();
72690
73439
  this.registry.unregister(session.sessionId);
72691
73440
  this.emitSessionRemove(session.sessionId);
72692
- log37.info(`⏸️ Paused session ${session.threadId.substring(0, 8)}`);
73441
+ log41.info(`⏸️ Paused session ${session.threadId.substring(0, 8)}`);
72693
73442
  } catch (err) {
72694
- log37.warn(`Failed to pause session ${session.threadId}: ${err}`);
73443
+ log41.warn(`Failed to pause session ${session.threadId}: ${err}`);
72695
73444
  }
72696
73445
  }
72697
73446
  for (const session of sessionsToKill) {
@@ -72712,17 +73461,17 @@ class SessionManager extends EventEmitter4 {
72712
73461
  sessionsToResume.push(state);
72713
73462
  }
72714
73463
  if (sessionsToResume.length === 0) {
72715
- log37.info(`No paused sessions to resume for platform ${platformId}`);
73464
+ log41.info(`No paused sessions to resume for platform ${platformId}`);
72716
73465
  await this.updateStickyMessage();
72717
73466
  return;
72718
73467
  }
72719
- log37.info(`▶️ Resuming ${sessionsToResume.length} paused session(s) for platform ${platformId}`);
73468
+ log41.info(`▶️ Resuming ${sessionsToResume.length} paused session(s) for platform ${platformId}`);
72720
73469
  for (const state of sessionsToResume) {
72721
73470
  try {
72722
73471
  await resumeSession(state, this.getContext());
72723
- log37.info(`▶️ Resumed session ${state.threadId.substring(0, 8)}`);
73472
+ log41.info(`▶️ Resumed session ${state.threadId.substring(0, 8)}`);
72724
73473
  } catch (err) {
72725
- log37.warn(`Failed to resume session ${state.threadId}: ${err}`);
73474
+ log41.warn(`Failed to resume session ${state.threadId}: ${err}`);
72726
73475
  }
72727
73476
  }
72728
73477
  await this.updateStickyMessage();
@@ -72757,17 +73506,18 @@ class SessionManager extends EventEmitter4 {
72757
73506
  initialize(this.sessionStore);
72758
73507
  this.sessionMonitor?.start();
72759
73508
  this.backgroundCleanup?.start();
73509
+ this.routineScheduler?.start();
72760
73510
  const sessionTimeoutMs = this.limits.sessionTimeoutMinutes * 60 * 1000;
72761
73511
  const staleIds = this.sessionStore.cleanStale(sessionTimeoutMs * 2);
72762
73512
  if (staleIds.length > 0) {
72763
- 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)`);
72764
73514
  }
72765
73515
  const removedCount = this.sessionStore.cleanHistory();
72766
73516
  if (removedCount > 0) {
72767
- 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`);
72768
73518
  }
72769
73519
  const persisted = this.sessionStore.load();
72770
- log37.info(`\uD83D\uDCC2 Loaded ${persisted.size} session(s) from persistence`);
73520
+ log41.info(`\uD83D\uDCC2 Loaded ${persisted.size} session(s) from persistence`);
72771
73521
  const excludePostIdsByPlatform = new Map;
72772
73522
  for (const session of persisted.values()) {
72773
73523
  const platformId = session.platformId;
@@ -72787,10 +73537,10 @@ class SessionManager extends EventEmitter4 {
72787
73537
  const excludePostIds = excludePostIdsByPlatform.get(platform.platformId);
72788
73538
  platform.getBotUser().then((botUser) => {
72789
73539
  cleanupOldStickyMessages(platform, botUser.id, true, excludePostIds).catch((err) => {
72790
- 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}`);
72791
73541
  });
72792
73542
  }).catch((err) => {
72793
- 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}`);
72794
73544
  });
72795
73545
  }
72796
73546
  if (persisted.size > 0) {
@@ -72804,10 +73554,10 @@ class SessionManager extends EventEmitter4 {
72804
73554
  }
72805
73555
  }
72806
73556
  if (pausedToSkip.length > 0) {
72807
- 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)`);
72808
73558
  }
72809
73559
  if (activeToResume.length > 0) {
72810
- 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)...`);
72811
73561
  for (const state of activeToResume) {
72812
73562
  await resumeSession(state, this.getContext());
72813
73563
  }
@@ -72926,6 +73676,23 @@ class SessionManager extends EventEmitter4 {
72926
73676
  return;
72927
73677
  await forgetMemory(session, selector, username, this.getContext());
72928
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
+ }
72929
73696
  async setRespondOnlyWhenMentioned(threadId, username, arg) {
72930
73697
  const session = this.findSessionByThreadId(threadId);
72931
73698
  if (!session)
@@ -73268,7 +74035,7 @@ Mention me to start a session in this worktree.`, threadId);
73268
74035
  const message = messageBuilder(formatter);
73269
74036
  await post(session, "info", message);
73270
74037
  } catch (err) {
73271
- log37.warn(`Failed to broadcast to session ${session.threadId}: ${err}`);
74038
+ log41.warn(`Failed to broadcast to session ${session.threadId}: ${err}`);
73272
74039
  }
73273
74040
  }
73274
74041
  }
@@ -73287,7 +74054,7 @@ Mention me to start a session in this worktree.`, threadId);
73287
74054
  session.messageManager?.setPendingUpdatePrompt({ postId: post2.id });
73288
74055
  this.registerPost(post2.id, session.threadId);
73289
74056
  } catch (err) {
73290
- log37.warn(`Failed to post ask message to ${threadId}: ${err}`);
74057
+ log41.warn(`Failed to post ask message to ${threadId}: ${err}`);
73291
74058
  }
73292
74059
  }
73293
74060
  }
@@ -73295,6 +74062,7 @@ Mention me to start a session in this worktree.`, threadId);
73295
74062
  this.isShuttingDown = true;
73296
74063
  this.sessionMonitor?.stop();
73297
74064
  this.backgroundCleanup?.stop();
74065
+ this.routineScheduler?.stop();
73298
74066
  if (message) {
73299
74067
  for (const session of this.registry.getAll()) {
73300
74068
  try {
@@ -80881,29 +81649,29 @@ function SessionLog({ logs, maxLines = 20 }) {
80881
81649
  return /* @__PURE__ */ jsx_dev_runtime4.jsxDEV(Box_default, {
80882
81650
  flexDirection: "column",
80883
81651
  flexShrink: 0,
80884
- children: displayLogs.map((log38) => /* @__PURE__ */ jsx_dev_runtime4.jsxDEV(Box_default, {
81652
+ children: displayLogs.map((log42) => /* @__PURE__ */ jsx_dev_runtime4.jsxDEV(Box_default, {
80885
81653
  flexShrink: 0,
80886
81654
  children: [
80887
81655
  /* @__PURE__ */ jsx_dev_runtime4.jsxDEV(Text, {
80888
- color: getColorForLevel(log38.level),
81656
+ color: getColorForLevel(log42.level),
80889
81657
  dimColor: true,
80890
81658
  wrap: "truncate",
80891
81659
  children: [
80892
81660
  "[",
80893
- padComponent(log38.component),
81661
+ padComponent(log42.component),
80894
81662
  "]"
80895
81663
  ]
80896
81664
  }, undefined, true, undefined, this),
80897
81665
  /* @__PURE__ */ jsx_dev_runtime4.jsxDEV(Text, {
80898
- color: getColorForLevel(log38.level),
81666
+ color: getColorForLevel(log42.level),
80899
81667
  wrap: "truncate",
80900
81668
  children: [
80901
81669
  " ",
80902
- log38.message
81670
+ log42.message
80903
81671
  ]
80904
81672
  }, undefined, true, undefined, this)
80905
81673
  ]
80906
- }, log38.id, true, undefined, this))
81674
+ }, log42.id, true, undefined, this))
80907
81675
  }, undefined, false, undefined, this);
80908
81676
  }
80909
81677
  // src/ui/components/Footer.tsx
@@ -81427,7 +82195,7 @@ function LogPanel({ logs, maxLines = 10, focused = false }) {
81427
82195
  const scrollRef = import_react59.default.useRef(null);
81428
82196
  const { stdout } = use_stdout_default();
81429
82197
  const isDebug = process.env.DEBUG === "1";
81430
- const displayLogs = logs.filter((log38) => isDebug || log38.level !== "debug");
82198
+ const displayLogs = logs.filter((log42) => isDebug || log42.level !== "debug");
81431
82199
  const visibleLogs = displayLogs.slice(-Math.max(maxLines * 3, 100));
81432
82200
  import_react59.default.useEffect(() => {
81433
82201
  const handleResize = () => scrollRef.current?.remeasure();
@@ -81467,25 +82235,25 @@ function LogPanel({ logs, maxLines = 10, focused = false }) {
81467
82235
  overflow: "hidden",
81468
82236
  children: /* @__PURE__ */ jsx_dev_runtime6.jsxDEV(ScrollView, {
81469
82237
  ref: scrollRef,
81470
- children: visibleLogs.map((log38) => /* @__PURE__ */ jsx_dev_runtime6.jsxDEV(Box_default, {
82238
+ children: visibleLogs.map((log42) => /* @__PURE__ */ jsx_dev_runtime6.jsxDEV(Box_default, {
81471
82239
  children: [
81472
82240
  /* @__PURE__ */ jsx_dev_runtime6.jsxDEV(Text, {
81473
82241
  dimColor: true,
81474
82242
  children: [
81475
82243
  "[",
81476
- padComponent2(log38.component),
82244
+ padComponent2(log42.component),
81477
82245
  "]"
81478
82246
  ]
81479
82247
  }, undefined, true, undefined, this),
81480
82248
  /* @__PURE__ */ jsx_dev_runtime6.jsxDEV(Text, {
81481
- color: getLevelColor(log38.level),
82249
+ color: getLevelColor(log42.level),
81482
82250
  children: [
81483
82251
  " ",
81484
- log38.message
82252
+ log42.message
81485
82253
  ]
81486
82254
  }, undefined, true, undefined, this)
81487
82255
  ]
81488
- }, log38.id, true, undefined, this))
82256
+ }, log42.id, true, undefined, this))
81489
82257
  }, undefined, false, undefined, this)
81490
82258
  }, undefined, false, undefined, this);
81491
82259
  }
@@ -82002,10 +82770,10 @@ function useAppState(initialConfig) {
82002
82770
  });
82003
82771
  }, []);
82004
82772
  const getLogsForSession = import_react60.useCallback((sessionId) => {
82005
- return state.logs.filter((log38) => log38.sessionId === sessionId);
82773
+ return state.logs.filter((log42) => log42.sessionId === sessionId);
82006
82774
  }, [state.logs]);
82007
82775
  const getGlobalLogs = import_react60.useCallback(() => {
82008
- return state.logs.filter((log38) => !log38.sessionId);
82776
+ return state.logs.filter((log42) => !log42.sessionId);
82009
82777
  }, [state.logs]);
82010
82778
  const togglePlatformEnabled = import_react60.useCallback((platformId) => {
82011
82779
  let newEnabled = false;
@@ -83020,7 +83788,7 @@ import { EventEmitter as EventEmitter9 } from "events";
83020
83788
  // src/auto-update/checker.ts
83021
83789
  init_logger();
83022
83790
  import { EventEmitter as EventEmitter7 } from "events";
83023
- var log38 = createLogger("checker");
83791
+ var log42 = createLogger("checker");
83024
83792
  var PACKAGE_NAME = "claude-threads";
83025
83793
  function compareVersions(a, b) {
83026
83794
  const partsA = a.replace(/^v/, "").split(".").map(Number);
@@ -83043,13 +83811,13 @@ async function fetchLatestVersion() {
83043
83811
  }
83044
83812
  });
83045
83813
  if (!response.ok) {
83046
- log38.warn(`Failed to fetch latest version: HTTP ${response.status}`);
83814
+ log42.warn(`Failed to fetch latest version: HTTP ${response.status}`);
83047
83815
  return null;
83048
83816
  }
83049
83817
  const data = await response.json();
83050
83818
  return data.version ?? null;
83051
83819
  } catch (err) {
83052
- log38.warn(`Failed to fetch latest version: ${err}`);
83820
+ log42.warn(`Failed to fetch latest version: ${err}`);
83053
83821
  return null;
83054
83822
  }
83055
83823
  }
@@ -83066,38 +83834,38 @@ class UpdateChecker extends EventEmitter7 {
83066
83834
  }
83067
83835
  start() {
83068
83836
  if (!this.config.enabled) {
83069
- log38.debug("Auto-update disabled, not starting checker");
83837
+ log42.debug("Auto-update disabled, not starting checker");
83070
83838
  return;
83071
83839
  }
83072
83840
  setTimeout(() => {
83073
83841
  this.check().catch((err) => {
83074
- log38.warn(`Initial update check failed: ${err}`);
83842
+ log42.warn(`Initial update check failed: ${err}`);
83075
83843
  });
83076
83844
  }, 5000);
83077
83845
  const intervalMs = this.config.checkIntervalMinutes * 60 * 1000;
83078
83846
  this.checkInterval = setInterval(() => {
83079
83847
  this.check().catch((err) => {
83080
- log38.warn(`Periodic update check failed: ${err}`);
83848
+ log42.warn(`Periodic update check failed: ${err}`);
83081
83849
  });
83082
83850
  }, intervalMs);
83083
- 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)`);
83084
83852
  }
83085
83853
  stop() {
83086
83854
  if (this.checkInterval) {
83087
83855
  clearInterval(this.checkInterval);
83088
83856
  this.checkInterval = null;
83089
83857
  }
83090
- log38.debug("Update checker stopped");
83858
+ log42.debug("Update checker stopped");
83091
83859
  }
83092
83860
  async check() {
83093
83861
  if (this.isChecking) {
83094
- log38.debug("Check already in progress, skipping");
83862
+ log42.debug("Check already in progress, skipping");
83095
83863
  return this.lastUpdateInfo;
83096
83864
  }
83097
83865
  this.isChecking = true;
83098
83866
  this.emit("check:start");
83099
83867
  try {
83100
- log38.debug("Checking for updates...");
83868
+ log42.debug("Checking for updates...");
83101
83869
  const latestVersion2 = await fetchLatestVersion();
83102
83870
  if (!latestVersion2) {
83103
83871
  this.emit("check:complete", false);
@@ -83114,18 +83882,18 @@ class UpdateChecker extends EventEmitter7 {
83114
83882
  detectedAt: new Date
83115
83883
  };
83116
83884
  if (!this.lastUpdateInfo || this.lastUpdateInfo.latestVersion !== latestVersion2) {
83117
- log38.info(`\uD83C\uDD95 Update available: v${currentVersion} → v${latestVersion2}`);
83885
+ log42.info(`\uD83C\uDD95 Update available: v${currentVersion} → v${latestVersion2}`);
83118
83886
  this.lastUpdateInfo = updateInfo;
83119
83887
  this.emit("update", updateInfo);
83120
83888
  }
83121
83889
  this.emit("check:complete", true);
83122
83890
  return updateInfo;
83123
83891
  }
83124
- log38.debug(`Up to date (v${currentVersion})`);
83892
+ log42.debug(`Up to date (v${currentVersion})`);
83125
83893
  this.emit("check:complete", false);
83126
83894
  return null;
83127
83895
  } catch (err) {
83128
- log38.warn(`Update check failed: ${err}`);
83896
+ log42.warn(`Update check failed: ${err}`);
83129
83897
  this.emit("check:error", err);
83130
83898
  return null;
83131
83899
  } finally {
@@ -83196,7 +83964,7 @@ function isInScheduledWindow(window2) {
83196
83964
  }
83197
83965
 
83198
83966
  // src/auto-update/scheduler.ts
83199
- var log39 = createLogger("scheduler");
83967
+ var log43 = createLogger("scheduler");
83200
83968
 
83201
83969
  class UpdateScheduler extends EventEmitter8 {
83202
83970
  config;
@@ -83220,7 +83988,7 @@ class UpdateScheduler extends EventEmitter8 {
83220
83988
  scheduleUpdate(updateInfo) {
83221
83989
  this.pendingUpdate = updateInfo;
83222
83990
  if (this.config.autoRestartMode === "immediate") {
83223
- log39.info("Immediate mode: triggering update now");
83991
+ log43.info("Immediate mode: triggering update now");
83224
83992
  this.emit("ready", updateInfo);
83225
83993
  return;
83226
83994
  }
@@ -83233,19 +84001,19 @@ class UpdateScheduler extends EventEmitter8 {
83233
84001
  this.scheduledRestartAt = null;
83234
84002
  this.askApprovals.clear();
83235
84003
  this.askStartTime = null;
83236
- log39.debug("Update schedule cancelled");
84004
+ log43.debug("Update schedule cancelled");
83237
84005
  }
83238
84006
  deferUpdate(minutes) {
83239
84007
  const deferUntil = new Date(Date.now() + minutes * 60 * 1000);
83240
84008
  this.scheduledRestartAt = null;
83241
84009
  this.idleStartTime = null;
83242
84010
  this.emit("deferred", deferUntil);
83243
- log39.info(`Update deferred until ${deferUntil.toLocaleTimeString()}`);
84011
+ log43.info(`Update deferred until ${deferUntil.toLocaleTimeString()}`);
83244
84012
  return deferUntil;
83245
84013
  }
83246
84014
  recordAskResponse(threadId, approved) {
83247
84015
  this.askApprovals.set(threadId, approved);
83248
- log39.debug(`Thread ${threadId.substring(0, 8)} ${approved ? "approved" : "denied"} update`);
84016
+ log43.debug(`Thread ${threadId.substring(0, 8)} ${approved ? "approved" : "denied"} update`);
83249
84017
  this.checkAskCondition();
83250
84018
  }
83251
84019
  getScheduledRestartAt() {
@@ -83266,7 +84034,7 @@ class UpdateScheduler extends EventEmitter8 {
83266
84034
  return;
83267
84035
  this.checkCondition();
83268
84036
  this.checkTimer = setInterval(() => this.checkCondition(), 1e4);
83269
- log39.debug(`Started checking for ${this.config.autoRestartMode} condition`);
84037
+ log43.debug(`Started checking for ${this.config.autoRestartMode} condition`);
83270
84038
  }
83271
84039
  stopChecking() {
83272
84040
  if (this.checkTimer) {
@@ -83297,17 +84065,17 @@ class UpdateScheduler extends EventEmitter8 {
83297
84065
  if (activity.activeSessionCount === 0) {
83298
84066
  if (!this.idleStartTime) {
83299
84067
  this.idleStartTime = new Date;
83300
- log39.debug("No active sessions, starting idle timer");
84068
+ log43.debug("No active sessions, starting idle timer");
83301
84069
  }
83302
84070
  const idleMs = Date.now() - this.idleStartTime.getTime();
83303
84071
  const requiredMs = this.config.idleTimeoutMinutes * 60 * 1000;
83304
84072
  if (idleMs >= requiredMs) {
83305
- log39.info(`Idle for ${this.config.idleTimeoutMinutes} minutes, triggering update`);
84073
+ log43.info(`Idle for ${this.config.idleTimeoutMinutes} minutes, triggering update`);
83306
84074
  this.triggerCountdown();
83307
84075
  }
83308
84076
  } else {
83309
84077
  if (this.idleStartTime) {
83310
- log39.debug("Sessions became active, resetting idle timer");
84078
+ log43.debug("Sessions became active, resetting idle timer");
83311
84079
  this.idleStartTime = null;
83312
84080
  }
83313
84081
  }
@@ -83318,7 +84086,7 @@ class UpdateScheduler extends EventEmitter8 {
83318
84086
  const quietMs = Date.now() - activity.lastActivityAt.getTime();
83319
84087
  const requiredMs = this.config.quietTimeoutMinutes * 60 * 1000;
83320
84088
  if (quietMs >= requiredMs && !activity.anySessionBusy) {
83321
- log39.info(`Sessions quiet for ${this.config.quietTimeoutMinutes} minutes, triggering update`);
84089
+ log43.info(`Sessions quiet for ${this.config.quietTimeoutMinutes} minutes, triggering update`);
83322
84090
  this.triggerCountdown();
83323
84091
  }
83324
84092
  } else if (activity.activeSessionCount === 0) {
@@ -83328,7 +84096,7 @@ class UpdateScheduler extends EventEmitter8 {
83328
84096
  const idleMs = Date.now() - this.idleStartTime.getTime();
83329
84097
  const requiredMs = this.config.quietTimeoutMinutes * 60 * 1000;
83330
84098
  if (idleMs >= requiredMs) {
83331
- log39.info("No sessions and quiet timeout reached, triggering update");
84099
+ log43.info("No sessions and quiet timeout reached, triggering update");
83332
84100
  this.triggerCountdown();
83333
84101
  }
83334
84102
  }
@@ -83339,13 +84107,13 @@ class UpdateScheduler extends EventEmitter8 {
83339
84107
  }
83340
84108
  const activity = this.getSessionActivity();
83341
84109
  if (activity.activeSessionCount === 0) {
83342
- log39.info("Within scheduled window and no active sessions, triggering update");
84110
+ log43.info("Within scheduled window and no active sessions, triggering update");
83343
84111
  this.triggerCountdown();
83344
84112
  } else if (activity.lastActivityAt) {
83345
84113
  const quietMs = Date.now() - activity.lastActivityAt.getTime();
83346
84114
  const requiredMs = this.config.idleTimeoutMinutes * 60 * 1000;
83347
84115
  if (quietMs >= requiredMs && !activity.anySessionBusy) {
83348
- log39.info("Within scheduled window and sessions quiet, triggering update");
84116
+ log43.info("Within scheduled window and sessions quiet, triggering update");
83349
84117
  this.triggerCountdown();
83350
84118
  }
83351
84119
  }
@@ -83353,14 +84121,14 @@ class UpdateScheduler extends EventEmitter8 {
83353
84121
  checkAskCondition() {
83354
84122
  const threadIds = this.getActiveThreadIds();
83355
84123
  if (threadIds.length === 0) {
83356
- log39.info("No active threads, proceeding with update");
84124
+ log43.info("No active threads, proceeding with update");
83357
84125
  this.triggerCountdown();
83358
84126
  return;
83359
84127
  }
83360
84128
  if (!this.askStartTime && this.pendingUpdate) {
83361
84129
  this.askStartTime = new Date;
83362
84130
  this.postAskMessage(threadIds, this.pendingUpdate.latestVersion).catch((err) => {
83363
- log39.warn(`Failed to post ask message: ${err}`);
84131
+ log43.warn(`Failed to post ask message: ${err}`);
83364
84132
  });
83365
84133
  return;
83366
84134
  }
@@ -83373,12 +84141,12 @@ class UpdateScheduler extends EventEmitter8 {
83373
84141
  denials++;
83374
84142
  }
83375
84143
  if (approvals > threadIds.length / 2) {
83376
- log39.info(`Majority approved (${approvals}/${threadIds.length}), triggering update`);
84144
+ log43.info(`Majority approved (${approvals}/${threadIds.length}), triggering update`);
83377
84145
  this.triggerCountdown();
83378
84146
  return;
83379
84147
  }
83380
84148
  if (denials > threadIds.length / 2) {
83381
- log39.info(`Majority denied (${denials}/${threadIds.length}), deferring update`);
84149
+ log43.info(`Majority denied (${denials}/${threadIds.length}), deferring update`);
83382
84150
  this.deferUpdate(60);
83383
84151
  return;
83384
84152
  }
@@ -83386,7 +84154,7 @@ class UpdateScheduler extends EventEmitter8 {
83386
84154
  const elapsedMs = Date.now() - this.askStartTime.getTime();
83387
84155
  const timeoutMs = this.config.askTimeoutMinutes * 60 * 1000;
83388
84156
  if (elapsedMs >= timeoutMs) {
83389
- log39.info(`Ask timeout reached (${this.config.askTimeoutMinutes} min), triggering update`);
84157
+ log43.info(`Ask timeout reached (${this.config.askTimeoutMinutes} min), triggering update`);
83390
84158
  this.triggerCountdown();
83391
84159
  }
83392
84160
  }
@@ -83406,7 +84174,7 @@ class UpdateScheduler extends EventEmitter8 {
83406
84174
  this.emit("ready", this.pendingUpdate);
83407
84175
  }
83408
84176
  }, 1000);
83409
- log39.info("Update countdown started (60 seconds)");
84177
+ log43.info("Update countdown started (60 seconds)");
83410
84178
  }
83411
84179
  stopCountdown() {
83412
84180
  if (this.countdownTimer) {
@@ -83419,27 +84187,27 @@ class UpdateScheduler extends EventEmitter8 {
83419
84187
  // src/auto-update/installer.ts
83420
84188
  init_logger();
83421
84189
  import { spawn as spawn4, spawnSync } from "child_process";
83422
- 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";
83423
84191
  import { dirname as dirname9, resolve as resolve7 } from "path";
83424
- import { homedir as homedir7 } from "os";
83425
- var log40 = createLogger("installer");
84192
+ import { homedir as homedir8 } from "os";
84193
+ var log44 = createLogger("installer");
83426
84194
  function detectPackageManager() {
83427
84195
  const npmCmd = process.platform === "win32" ? "npm.cmd" : "npm";
83428
84196
  const originalInstaller = detectOriginalInstaller();
83429
84197
  if (originalInstaller) {
83430
- log40.debug(`Detected original installer: ${originalInstaller}`);
84198
+ log44.debug(`Detected original installer: ${originalInstaller}`);
83431
84199
  if (originalInstaller === "bun") {
83432
84200
  const bunCheck2 = spawnSync("bun", ["--version"], { stdio: "ignore" });
83433
84201
  if (bunCheck2.status === 0) {
83434
84202
  return { cmd: "bun", isBun: true };
83435
84203
  }
83436
- 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.");
83437
84205
  } else {
83438
84206
  const npmCheck2 = spawnSync(npmCmd, ["--version"], { stdio: "ignore" });
83439
84207
  if (npmCheck2.status === 0) {
83440
84208
  return { cmd: npmCmd, isBun: false };
83441
84209
  }
83442
- 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.");
83443
84211
  }
83444
84212
  }
83445
84213
  const bunCheck = spawnSync("bun", ["--version"], { stdio: "ignore" });
@@ -83461,7 +84229,7 @@ function normalizePath(p) {
83461
84229
  function detectOriginalInstaller() {
83462
84230
  try {
83463
84231
  const scriptPath = normalizePath(process.argv[1] || "");
83464
- const bunGlobalDir = normalizePath(process.env.BUN_INSTALL || resolve7(homedir7(), ".bun"));
84232
+ const bunGlobalDir = normalizePath(process.env.BUN_INSTALL || resolve7(homedir8(), ".bun"));
83465
84233
  if (scriptPath.startsWith(bunGlobalDir)) {
83466
84234
  return "bun";
83467
84235
  }
@@ -83481,38 +84249,38 @@ function detectOriginalInstaller() {
83481
84249
  return null;
83482
84250
  }
83483
84251
  }
83484
- var STATE_PATH = resolve7(homedir7(), ".config", "claude-threads", UPDATE_STATE_FILENAME);
84252
+ var STATE_PATH = resolve7(homedir8(), ".config", "claude-threads", UPDATE_STATE_FILENAME);
83485
84253
  var PACKAGE_NAME2 = "claude-threads";
83486
84254
  function loadUpdateState() {
83487
84255
  try {
83488
- if (existsSync15(STATE_PATH)) {
83489
- const content = readFileSync11(STATE_PATH, "utf-8");
84256
+ if (existsSync16(STATE_PATH)) {
84257
+ const content = readFileSync12(STATE_PATH, "utf-8");
83490
84258
  return JSON.parse(content);
83491
84259
  }
83492
84260
  } catch (err) {
83493
- log40.warn(`Failed to load update state: ${err}`);
84261
+ log44.warn(`Failed to load update state: ${err}`);
83494
84262
  }
83495
84263
  return {};
83496
84264
  }
83497
84265
  function saveUpdateState(state) {
83498
84266
  try {
83499
84267
  const dir = dirname9(STATE_PATH);
83500
- if (!existsSync15(dir)) {
83501
- mkdirSync6(dir, { recursive: true });
84268
+ if (!existsSync16(dir)) {
84269
+ mkdirSync7(dir, { recursive: true });
83502
84270
  }
83503
- writeFileSync8(STATE_PATH, JSON.stringify(state, null, 2), "utf-8");
83504
- log40.debug("Update state saved");
84271
+ writeFileSync9(STATE_PATH, JSON.stringify(state, null, 2), "utf-8");
84272
+ log44.debug("Update state saved");
83505
84273
  } catch (err) {
83506
- log40.warn(`Failed to save update state: ${err}`);
84274
+ log44.warn(`Failed to save update state: ${err}`);
83507
84275
  }
83508
84276
  }
83509
84277
  function clearUpdateState() {
83510
84278
  try {
83511
- if (existsSync15(STATE_PATH)) {
83512
- writeFileSync8(STATE_PATH, "{}", "utf-8");
84279
+ if (existsSync16(STATE_PATH)) {
84280
+ writeFileSync9(STATE_PATH, "{}", "utf-8");
83513
84281
  }
83514
84282
  } catch (err) {
83515
- log40.warn(`Failed to clear update state: ${err}`);
84283
+ log44.warn(`Failed to clear update state: ${err}`);
83516
84284
  }
83517
84285
  }
83518
84286
  function checkJustUpdated() {
@@ -83544,11 +84312,11 @@ function clearRuntimeSettings() {
83544
84312
  }
83545
84313
  }
83546
84314
  async function installVersion(version) {
83547
- log40.info(`\uD83D\uDCE6 Installing ${PACKAGE_NAME2}@${version}...`);
84315
+ log44.info(`\uD83D\uDCE6 Installing ${PACKAGE_NAME2}@${version}...`);
83548
84316
  const pm = detectPackageManager();
83549
84317
  if (!pm) {
83550
84318
  const error = "Neither bun nor npm found in PATH. Cannot install update.";
83551
- log40.error(`❌ ${error}`);
84319
+ log44.error(`❌ ${error}`);
83552
84320
  return { success: false, error };
83553
84321
  }
83554
84322
  saveUpdateState({
@@ -83560,7 +84328,7 @@ async function installVersion(version) {
83560
84328
  return new Promise((resolve8) => {
83561
84329
  const { cmd, isBun: isBun3 } = pm;
83562
84330
  const args = ["install", "-g", `${PACKAGE_NAME2}@${version}`];
83563
- log40.debug(`Using ${isBun3 ? "bun" : "npm"} for installation`);
84331
+ log44.debug(`Using ${isBun3 ? "bun" : "npm"} for installation`);
83564
84332
  const child = spawn4(cmd, args, {
83565
84333
  stdio: ["ignore", "pipe", "pipe"],
83566
84334
  env: {
@@ -83578,7 +84346,7 @@ async function installVersion(version) {
83578
84346
  });
83579
84347
  child.on("close", (code) => {
83580
84348
  if (code === 0) {
83581
- log40.info(`✅ Successfully installed ${PACKAGE_NAME2}@${version}`);
84349
+ log44.info(`✅ Successfully installed ${PACKAGE_NAME2}@${version}`);
83582
84350
  saveUpdateState({
83583
84351
  previousVersion: VERSION,
83584
84352
  targetVersion: version,
@@ -83588,20 +84356,20 @@ async function installVersion(version) {
83588
84356
  resolve8({ success: true });
83589
84357
  } else {
83590
84358
  const errorMsg = stderr || stdout || `Exit code: ${code}`;
83591
- log40.error(`❌ Installation failed: ${errorMsg}`);
84359
+ log44.error(`❌ Installation failed: ${errorMsg}`);
83592
84360
  clearUpdateState();
83593
84361
  resolve8({ success: false, error: errorMsg });
83594
84362
  }
83595
84363
  });
83596
84364
  child.on("error", (err) => {
83597
- log40.error(`❌ Failed to spawn npm: ${err}`);
84365
+ log44.error(`❌ Failed to spawn npm: ${err}`);
83598
84366
  clearUpdateState();
83599
84367
  resolve8({ success: false, error: err.message });
83600
84368
  });
83601
84369
  setTimeout(() => {
83602
84370
  if (child.exitCode === null) {
83603
84371
  child.kill();
83604
- log40.error("❌ Installation timed out");
84372
+ log44.error("❌ Installation timed out");
83605
84373
  clearUpdateState();
83606
84374
  resolve8({ success: false, error: "Installation timed out" });
83607
84375
  }
@@ -83645,9 +84413,9 @@ class UpdateInstaller {
83645
84413
  // src/auto-update/respawn.ts
83646
84414
  init_logger();
83647
84415
  import { spawn as spawn5 } from "child_process";
83648
- import { existsSync as existsSync16, statSync as statSync4 } from "fs";
83649
- import { delimiter, join as join13 } from "path";
83650
- 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");
83651
84419
  function decideRespawn(env5 = process.env, isTTY = !!process.stdout.isTTY) {
83652
84420
  if (env5.CLAUDE_THREADS_BIN) {
83653
84421
  return { kind: "exit-for-supervisor", supervisor: "claude-threads-daemon" };
@@ -83666,22 +84434,22 @@ function decideRespawn(env5 = process.env, isTTY = !!process.stdout.isTTY) {
83666
84434
  }
83667
84435
  return { kind: "self-respawn" };
83668
84436
  }
83669
- function resolveClaudeThreadsBin(_env = process.env, _existsSync = existsSync16, _isFileExecutable = isFileExecutable) {
84437
+ function resolveClaudeThreadsBin(_env = process.env, _existsSync = existsSync17, _isFileExecutable = isFileExecutable) {
83670
84438
  const isWin2 = process.platform === "win32";
83671
84439
  const names = isWin2 ? ["claude-threads.cmd", "claude-threads.exe", "claude-threads.bat"] : ["claude-threads"];
83672
84440
  const path10 = _env.PATH || _env.Path || "";
83673
84441
  const dirs = path10.split(delimiter).filter(Boolean);
83674
84442
  const home = _env.HOME || _env.USERPROFILE;
83675
- const bunRoot = _env.BUN_INSTALL || (home ? join13(home, ".bun") : null);
84443
+ const bunRoot = _env.BUN_INSTALL || (home ? join14(home, ".bun") : null);
83676
84444
  if (bunRoot) {
83677
- const bunBin = join13(bunRoot, "bin");
84445
+ const bunBin = join14(bunRoot, "bin");
83678
84446
  if (!dirs.includes(bunBin)) {
83679
84447
  dirs.push(bunBin);
83680
84448
  }
83681
84449
  }
83682
84450
  for (const dir of dirs) {
83683
84451
  for (const name of names) {
83684
- const candidate = join13(dir, name);
84452
+ const candidate = join14(dir, name);
83685
84453
  if (_existsSync(candidate) && _isFileExecutable(candidate)) {
83686
84454
  return candidate;
83687
84455
  }
@@ -83703,7 +84471,7 @@ function isFileExecutable(path10) {
83703
84471
  }
83704
84472
  function spawnReplacement(argv = process.argv.slice(2), binPath = resolveClaudeThreadsBin()) {
83705
84473
  if (!binPath) {
83706
- 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");
83707
84475
  return false;
83708
84476
  }
83709
84477
  if (process.stdin.isTTY && typeof process.stdin.setRawMode === "function") {
@@ -83724,23 +84492,23 @@ function spawnReplacement(argv = process.argv.slice(2), binPath = resolveClaudeT
83724
84492
  shell: useShell
83725
84493
  });
83726
84494
  } catch (err) {
83727
- log41.error(`spawn() threw: ${err instanceof Error ? err.message : String(err)}`);
84495
+ log45.error(`spawn() threw: ${err instanceof Error ? err.message : String(err)}`);
83728
84496
  return false;
83729
84497
  }
83730
84498
  child.once("error", (err) => {
83731
- log41.error(`Replacement process error: ${err.message}`);
84499
+ log45.error(`Replacement process error: ${err.message}`);
83732
84500
  });
83733
84501
  if (child.pid === undefined) {
83734
- log41.error("Spawn returned no pid (binary likely not executable)");
84502
+ log45.error("Spawn returned no pid (binary likely not executable)");
83735
84503
  return false;
83736
84504
  }
83737
84505
  child.unref();
83738
- log41.info(`Spawned replacement pid=${child.pid} from ${binPath}`);
84506
+ log45.info(`Spawned replacement pid=${child.pid} from ${binPath}`);
83739
84507
  return true;
83740
84508
  }
83741
84509
 
83742
84510
  // src/auto-update/manager.ts
83743
- var log42 = createLogger("updater");
84511
+ var log46 = createLogger("updater");
83744
84512
 
83745
84513
  class AutoUpdateManager extends EventEmitter9 {
83746
84514
  config;
@@ -83763,23 +84531,23 @@ class AutoUpdateManager extends EventEmitter9 {
83763
84531
  }
83764
84532
  start() {
83765
84533
  if (!this.config.enabled) {
83766
- log42.info("Auto-update is disabled");
84534
+ log46.info("Auto-update is disabled");
83767
84535
  return;
83768
84536
  }
83769
84537
  const updateResult = this.installer.checkJustUpdated();
83770
84538
  if (updateResult) {
83771
- 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}`);
83772
84540
  this.callbacks.broadcastUpdate((fmt) => `\uD83C\uDF89 ${fmt.formatBold("Bot updated")} from v${updateResult.previousVersion} to v${updateResult.currentVersion}`).catch((err) => {
83773
- log42.warn(`Failed to broadcast update notification: ${err}`);
84541
+ log46.warn(`Failed to broadcast update notification: ${err}`);
83774
84542
  });
83775
84543
  }
83776
84544
  this.checker.start();
83777
- 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})`);
83778
84546
  }
83779
84547
  stop() {
83780
84548
  this.checker.stop();
83781
84549
  this.scheduler.stop();
83782
- log42.debug("Auto-update manager stopped");
84550
+ log46.debug("Auto-update manager stopped");
83783
84551
  }
83784
84552
  getState() {
83785
84553
  return { ...this.state };
@@ -83793,10 +84561,10 @@ class AutoUpdateManager extends EventEmitter9 {
83793
84561
  async forceUpdate() {
83794
84562
  const updateInfo = this.state.updateInfo || await this.checker.check();
83795
84563
  if (!updateInfo) {
83796
- log42.info("No update available");
84564
+ log46.info("No update available");
83797
84565
  return;
83798
84566
  }
83799
- log42.info("Forcing immediate update");
84567
+ log46.info("Forcing immediate update");
83800
84568
  await this.performUpdate(updateInfo);
83801
84569
  }
83802
84570
  deferUpdate(minutes = 60) {
@@ -83862,11 +84630,11 @@ class AutoUpdateManager extends EventEmitter9 {
83862
84630
  await this.callbacks.prepareForRestart();
83863
84631
  } catch (err) {
83864
84632
  const reason = err instanceof Error ? err.message : String(err);
83865
- log42.error(`prepareForRestart failed: ${reason}`);
84633
+ log46.error(`prepareForRestart failed: ${reason}`);
83866
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(() => {});
83867
84635
  process.exit(1);
83868
84636
  }
83869
- log42.info(`\uD83D\uDD04 Restarting for update to v${updateInfo.latestVersion}`);
84637
+ log46.info(`\uD83D\uDD04 Restarting for update to v${updateInfo.latestVersion}`);
83870
84638
  process.stdout.write("\x1B[2J\x1B[H");
83871
84639
  process.stdout.write("\x1B[?25h");
83872
84640
  if (decision.kind === "self-respawn") {
@@ -83875,14 +84643,14 @@ class AutoUpdateManager extends EventEmitter9 {
83875
84643
  if (ok) {
83876
84644
  process.exit(0);
83877
84645
  }
83878
- log42.error("Self-respawn launch failed after binary resolution succeeded");
84646
+ log46.error("Self-respawn launch failed after binary resolution succeeded");
83879
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(() => {});
83880
84648
  } else {
83881
- log42.error("claude-threads not found on PATH; manual restart required");
84649
+ log46.error("claude-threads not found on PATH; manual restart required");
83882
84650
  }
83883
84651
  process.exit(0);
83884
84652
  }
83885
- log42.debug(`Restart handled by supervisor: ${decision.supervisor}`);
84653
+ log46.debug(`Restart handled by supervisor: ${decision.supervisor}`);
83886
84654
  process.exit(RESTART_EXIT_CODE);
83887
84655
  } else {
83888
84656
  const errorMsg = result.error ?? "Unknown error";
@@ -84266,7 +85034,7 @@ async function startWithoutDaemon() {
84266
85034
  session.addPlatform(platformConfig.id, client, {
84267
85035
  sessionHeader: resolveOverheadVisibility(platformConfig.sessionHeader, `platforms[${platformConfig.id}].sessionHeader`),
84268
85036
  stickyMessage: resolveOverheadVisibility(platformConfig.stickyMessage, `platforms[${platformConfig.id}].stickyMessage`)
84269
- }, resolveMemoryConfig(platformConfig.memory, `platforms[${platformConfig.id}].memory`));
85037
+ }, resolveMemoryConfig(platformConfig.memory, `platforms[${platformConfig.id}].memory`), resolveRoutinesEnabled(platformConfig.routines, `platforms[${platformConfig.id}].routines`));
84270
85038
  wirePlatformEvents(platformConfig.id, client, session, ui);
84271
85039
  }
84272
85040
  const enabledPlatforms = Array.from(platforms.entries()).filter(([id]) => platformEnabledState.get(id) ?? true);