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/CHANGELOG.md +16 -0
- package/README.md +5 -0
- package/dist/index.js +1814 -1046
- package/dist/mcp/mcp-server.js +344 -48
- package/docs/CONFIGURATION.md +59 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -12857,9 +12857,9 @@ GFS4: `);
|
|
|
12857
12857
|
}
|
|
12858
12858
|
}
|
|
12859
12859
|
var fs$readdir = fs3.readdir;
|
|
12860
|
-
fs3.readdir =
|
|
12860
|
+
fs3.readdir = readdir;
|
|
12861
12861
|
var noReaddirOptionVersions = /^v[0-5]\./;
|
|
12862
|
-
function
|
|
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 =
|
|
14813
|
+
fs5.readdir = readdir;
|
|
14814
14814
|
var noReaddirOptionVersions = /^v[0-5]\./;
|
|
14815
|
-
function
|
|
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
|
|
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,
|
|
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/
|
|
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/
|
|
56185
|
-
|
|
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
|
|
56194
|
-
import { tmpdir } from "os";
|
|
56195
|
-
import { join as
|
|
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
|
|
56855
|
+
var log13 = createLogger("claude");
|
|
56287
56856
|
function cleanupBrowserBridgeSockets() {
|
|
56288
56857
|
try {
|
|
56289
|
-
const tempDir =
|
|
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 =
|
|
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
|
-
|
|
56867
|
+
log13.debug(`Removed stale browser bridge socket: ${file}`);
|
|
56299
56868
|
}
|
|
56300
56869
|
} catch {}
|
|
56301
56870
|
}
|
|
56302
56871
|
}
|
|
56303
56872
|
} catch (err) {
|
|
56304
|
-
|
|
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 ??
|
|
56363
|
-
const path2 =
|
|
56364
|
-
|
|
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 (
|
|
56465
|
-
const data =
|
|
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 (
|
|
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 =
|
|
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 (
|
|
57352
|
+
if (existsSync9(bundledPath)) {
|
|
56784
57353
|
return bundledPath;
|
|
56785
57354
|
}
|
|
56786
57355
|
const sourceLayoutPath = resolve4(__dirname4, "..", "mcp", "mcp-server.js");
|
|
56787
|
-
if (
|
|
57356
|
+
if (existsSync9(sourceLayoutPath)) {
|
|
56788
57357
|
return sourceLayoutPath;
|
|
56789
57358
|
}
|
|
56790
57359
|
const tsPath = resolve4(__dirname4, "..", "mcp", "mcp-server.ts");
|
|
56791
|
-
if (
|
|
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 (
|
|
57369
|
+
if (existsSync9(bundledPath)) {
|
|
56801
57370
|
return bundledPath;
|
|
56802
57371
|
}
|
|
56803
57372
|
const sourceLayoutPath = resolve4(__dirname4, "..", "statusline", "writer.js");
|
|
56804
|
-
if (
|
|
57373
|
+
if (existsSync9(sourceLayoutPath)) {
|
|
56805
57374
|
return sourceLayoutPath;
|
|
56806
57375
|
}
|
|
56807
57376
|
const tsPath = resolve4(__dirname4, "..", "statusline", "writer.ts");
|
|
56808
|
-
if (
|
|
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
|
|
57078
|
-
import { homedir as
|
|
57079
|
-
import { join as
|
|
57080
|
-
var
|
|
57081
|
-
var LOGS_BASE_DIR =
|
|
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 =
|
|
57410
|
+
this.logPath = join10(LOGS_BASE_DIR, platformId, `${claudeSessionId}.jsonl`);
|
|
57102
57411
|
if (this.enabled) {
|
|
57103
57412
|
const dir = dirname7(this.logPath);
|
|
57104
|
-
if (!
|
|
57105
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 = !
|
|
57548
|
+
const isNewFile = !existsSync10(this.logPath);
|
|
57240
57549
|
appendFileSync(this.logPath, lines, { encoding: "utf8", mode: 384 });
|
|
57241
57550
|
if (isNewFile) {
|
|
57242
|
-
|
|
57551
|
+
chmodSync6(this.logPath, 384);
|
|
57243
57552
|
}
|
|
57244
57553
|
this.buffer = [];
|
|
57245
57554
|
} catch (err) {
|
|
57246
|
-
|
|
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 (!
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
57606
|
+
log14.debug(`Deleted old log file: ${filePath}`);
|
|
57298
57607
|
}
|
|
57299
57608
|
} catch (err) {
|
|
57300
|
-
|
|
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
|
-
|
|
57616
|
+
log14.debug(`Removed empty platform log directory: ${platformDir}`);
|
|
57308
57617
|
}
|
|
57309
57618
|
} catch {}
|
|
57310
57619
|
}
|
|
57311
57620
|
if (deletedCount > 0) {
|
|
57312
|
-
|
|
57621
|
+
log14.info(`Cleaned up ${deletedCount} old log file(s)`);
|
|
57313
57622
|
}
|
|
57314
57623
|
} catch (err) {
|
|
57315
|
-
|
|
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
|
|
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
|
-
|
|
57325
|
-
if (!
|
|
57326
|
-
|
|
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 =
|
|
57639
|
+
const content = readFileSync9(logPath, "utf8");
|
|
57331
57640
|
const lines = content.trim().split(`
|
|
57332
57641
|
`);
|
|
57333
|
-
|
|
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
|
-
|
|
57652
|
+
log14.debug(`Parsed ${entries.length} log entries`);
|
|
57344
57653
|
return entries;
|
|
57345
57654
|
} catch (err) {
|
|
57346
|
-
|
|
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
|
|
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 =
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
58840
|
+
log16.debug("Keep-alive disabled, skipping");
|
|
58839
58841
|
return;
|
|
58840
58842
|
}
|
|
58841
58843
|
if (this.keepAliveProcess) {
|
|
58842
|
-
|
|
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
|
-
|
|
58858
|
+
log16.warn(`Keep-alive not supported on ${this.platform}`);
|
|
58857
58859
|
}
|
|
58858
58860
|
}
|
|
58859
58861
|
stopKeepAlive() {
|
|
58860
58862
|
if (this.keepAliveProcess) {
|
|
58861
|
-
|
|
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
|
-
|
|
58869
|
-
|
|
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
|
-
|
|
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
|
-
|
|
58883
|
+
log16.debug(`caffeinate exited with code ${code}`);
|
|
58879
58884
|
}
|
|
58880
58885
|
this.keepAliveProcess = null;
|
|
58881
58886
|
});
|
|
58882
|
-
|
|
58887
|
+
log16.info("Sleep prevention active (caffeinate)");
|
|
58883
58888
|
} catch (err) {
|
|
58884
|
-
|
|
58889
|
+
log16.error(`Failed to start caffeinate: ${err}`);
|
|
58885
58890
|
}
|
|
58886
58891
|
}
|
|
58887
58892
|
startLinuxKeepAlive() {
|
|
58888
58893
|
try {
|
|
58889
|
-
|
|
58890
|
-
|
|
58891
|
-
|
|
58892
|
-
|
|
58893
|
-
|
|
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
|
-
|
|
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
|
-
|
|
58908
|
+
log16.debug(`systemd-inhibit exited with code ${code}`);
|
|
58907
58909
|
}
|
|
58908
58910
|
this.keepAliveProcess = null;
|
|
58909
58911
|
});
|
|
58910
|
-
|
|
58912
|
+
log16.info("Sleep prevention active (systemd-inhibit)");
|
|
58911
58913
|
} catch (err) {
|
|
58912
|
-
|
|
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
|
-
|
|
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
|
-
|
|
58931
|
+
log16.info("Sleep prevention active (xdg-screensaver)");
|
|
58933
58932
|
} catch (err) {
|
|
58934
|
-
|
|
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
|
-
|
|
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
|
-
|
|
58950
|
+
log16.debug(`PowerShell keep-alive exited with code ${code}`);
|
|
58965
58951
|
}
|
|
58966
58952
|
this.keepAliveProcess = null;
|
|
58967
58953
|
});
|
|
58968
|
-
|
|
58954
|
+
log16.info("Sleep prevention active (SetThreadExecutionState)");
|
|
58969
58955
|
} catch (err) {
|
|
58970
|
-
|
|
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
|
|
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
|
-
|
|
58990
|
+
log17.warn(logMessage);
|
|
59005
58991
|
} else {
|
|
59006
|
-
|
|
58992
|
+
log17.error(logMessage, error instanceof Error ? error : undefined);
|
|
59007
58993
|
}
|
|
59008
58994
|
if (context.details) {
|
|
59009
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
59064
|
-
var sessionLog = createSessionLog(
|
|
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
|
|
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
|
|
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
|
|
59152
|
+
await rm2(dir, { recursive: true, force: true });
|
|
59167
59153
|
} catch (err) {
|
|
59168
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
59620
|
+
function writeFileSync7(filePath, data, options = DEFAULT_WRITE_OPTIONS) {
|
|
59635
59621
|
if (isString(options))
|
|
59636
|
-
return
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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 =
|
|
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 =
|
|
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:
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
68014
|
+
log22.debug(`Platform ${platformId} marked as paused`);
|
|
67987
68015
|
} else {
|
|
67988
68016
|
pausedPlatforms.delete(platformId);
|
|
67989
|
-
|
|
68017
|
+
log22.debug(`Platform ${platformId} marked as active`);
|
|
67990
68018
|
}
|
|
67991
68019
|
}
|
|
67992
68020
|
function setShuttingDown(shuttingDown) {
|
|
67993
68021
|
isShuttingDown = shuttingDown;
|
|
67994
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
68397
|
+
log22.debug(`updateStickyMessage for ${platform.platformId}, ${platformSessions.length} sessions`);
|
|
68370
68398
|
for (const s of platformSessions) {
|
|
68371
|
-
|
|
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
|
-
|
|
68406
|
+
log22.debug(`existingPostId: ${existingPostId || "(none)"}, needsBump: ${shouldBump}`);
|
|
68379
68407
|
try {
|
|
68380
68408
|
if (existingPostId && !shouldBump) {
|
|
68381
|
-
|
|
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
|
-
|
|
68414
|
+
log22.debug(`Re-pinned post`);
|
|
68387
68415
|
} catch (pinErr) {
|
|
68388
|
-
|
|
68416
|
+
log22.debug(`Re-pin failed (might already be pinned): ${pinErr}`);
|
|
68389
68417
|
}
|
|
68390
|
-
|
|
68418
|
+
log22.debug(`Updated successfully`);
|
|
68391
68419
|
return;
|
|
68392
68420
|
} catch (err) {
|
|
68393
|
-
|
|
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
|
-
|
|
68426
|
+
log22.debug(`Unpinning and deleting existing post ${existingPostId.substring(0, 8)}...`);
|
|
68399
68427
|
try {
|
|
68400
68428
|
await platform.unpinPost(existingPostId);
|
|
68401
|
-
|
|
68429
|
+
log22.debug(`Unpinned successfully`);
|
|
68402
68430
|
} catch (err) {
|
|
68403
|
-
|
|
68431
|
+
log22.debug(`Unpin failed (probably already unpinned): ${err}`);
|
|
68404
68432
|
}
|
|
68405
68433
|
try {
|
|
68406
68434
|
await platform.deletePost(existingPostId);
|
|
68407
|
-
|
|
68435
|
+
log22.debug(`Deleted successfully`);
|
|
68408
68436
|
} catch (err) {
|
|
68409
|
-
|
|
68437
|
+
log22.debug(`Delete failed (probably already deleted): ${err}`);
|
|
68410
68438
|
}
|
|
68411
68439
|
stickyPostIds.delete(platform.platformId);
|
|
68412
68440
|
}
|
|
68413
|
-
|
|
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
|
-
|
|
68446
|
+
log22.debug(`Pinned post successfully`);
|
|
68419
68447
|
} catch (err) {
|
|
68420
|
-
|
|
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
|
-
|
|
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
|
-
|
|
68469
|
+
log22.debug(`Background cleanup failed: ${err}`);
|
|
68442
68470
|
});
|
|
68443
68471
|
} catch (err) {
|
|
68444
|
-
|
|
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
|
-
|
|
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
|
-
|
|
68510
|
+
log22.debug(`No recent pinned posts to check (${pinnedPostIds.length} total, current: ${currentStickyId?.substring(0, 8) || "(none)"})`);
|
|
68483
68511
|
return;
|
|
68484
68512
|
}
|
|
68485
|
-
|
|
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
|
-
|
|
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
|
-
|
|
68524
|
+
log22.info(`\uD83E\uDDF9 Cleaned up old sticky message: ${postId.substring(0, 8)}...`);
|
|
68497
68525
|
} catch (err) {
|
|
68498
|
-
|
|
68526
|
+
log22.debug(`Failed to cleanup ${postId}: ${err}`);
|
|
68499
68527
|
}
|
|
68500
68528
|
}
|
|
68501
68529
|
} catch (err) {
|
|
68502
|
-
|
|
68530
|
+
log22.debug(`Could not check post ${postId}: ${err}`);
|
|
68503
68531
|
}
|
|
68504
68532
|
}
|
|
68505
68533
|
} catch (err) {
|
|
68506
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
68690
|
+
log24.debug(`Branch suggestion failed: ${result.error || "no response"}`);
|
|
68663
68691
|
return [];
|
|
68664
68692
|
}
|
|
68665
68693
|
const suggestions = parseBranchSuggestions(result.response);
|
|
68666
|
-
|
|
68694
|
+
log24.debug(`Got ${suggestions.length} branch suggestions: ${suggestions.join(", ")}`);
|
|
68667
68695
|
return suggestions;
|
|
68668
68696
|
} catch (err) {
|
|
68669
|
-
|
|
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
|
|
68704
|
+
import { randomUUID as randomUUID4 } from "crypto";
|
|
68677
68705
|
init_logger();
|
|
68678
|
-
var
|
|
68679
|
-
var sessionLog2 = createSessionLog(
|
|
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 =
|
|
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 =
|
|
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
|
|
69262
|
-
var sessionLog3 = createSessionLog(
|
|
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(() =>
|
|
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
|
|
69974
|
-
var sessionLog5 = createSessionLog(
|
|
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 =
|
|
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
|
|
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
|
-
|
|
71070
|
+
log33.debug(`Distilled ${added} memory entries from ${platformId}:${threadId} (${reason})`);
|
|
70789
71071
|
}
|
|
70790
71072
|
}).catch((err) => {
|
|
70791
|
-
|
|
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
|
|
70819
|
-
var sessionLog6 = createSessionLog(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
71741
|
+
log34.warn(`Max sessions reached, skipping resume for ${shortId}...`);
|
|
71438
71742
|
return;
|
|
71439
71743
|
}
|
|
71440
71744
|
if (!existsSync13(state.workingDir)) {
|
|
71441
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
71964
|
+
log34.warn(`auth.denied.resume: @${username || "unknown"} not authorized to resume ${shortId}...`);
|
|
71661
71965
|
return;
|
|
71662
71966
|
}
|
|
71663
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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/
|
|
71880
|
-
|
|
71881
|
-
var
|
|
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
|
-
|
|
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
|
-
|
|
71886
|
-
|
|
71887
|
-
|
|
71888
|
-
|
|
71889
|
-
|
|
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 ??
|
|
71894
|
-
this.
|
|
71895
|
-
this.
|
|
71896
|
-
this.
|
|
71897
|
-
this.
|
|
71898
|
-
this.
|
|
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
|
-
|
|
72504
|
+
log38.debug("Cleanup scheduler already running");
|
|
71903
72505
|
return;
|
|
71904
72506
|
}
|
|
71905
72507
|
this.isRunning = true;
|
|
71906
|
-
|
|
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.
|
|
71909
|
-
|
|
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
|
-
|
|
72524
|
+
log38.debug("Cleanup scheduler stopped");
|
|
71920
72525
|
}
|
|
71921
|
-
async
|
|
71922
|
-
|
|
71923
|
-
|
|
71924
|
-
|
|
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
|
|
71932
|
-
var sessionLog7 = createSessionLog(
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
73421
|
+
log41.info(`No active sessions to pause for platform ${platformId}`);
|
|
72673
73422
|
await this.updateStickyMessage();
|
|
72674
73423
|
return;
|
|
72675
73424
|
}
|
|
72676
|
-
|
|
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
|
-
|
|
73441
|
+
log41.info(`⏸️ Paused session ${session.threadId.substring(0, 8)}`);
|
|
72693
73442
|
} catch (err) {
|
|
72694
|
-
|
|
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
|
-
|
|
73464
|
+
log41.info(`No paused sessions to resume for platform ${platformId}`);
|
|
72716
73465
|
await this.updateStickyMessage();
|
|
72717
73466
|
return;
|
|
72718
73467
|
}
|
|
72719
|
-
|
|
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
|
-
|
|
73472
|
+
log41.info(`▶️ Resumed session ${state.threadId.substring(0, 8)}`);
|
|
72724
73473
|
} catch (err) {
|
|
72725
|
-
|
|
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
|
-
|
|
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
|
-
|
|
73517
|
+
log41.info(`\uD83D\uDDD1️ Permanently removed ${removedCount} old session(s) from history`);
|
|
72768
73518
|
}
|
|
72769
73519
|
const persisted = this.sessionStore.load();
|
|
72770
|
-
|
|
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
|
-
|
|
73540
|
+
log41.warn(`Failed to cleanup old sticky messages for ${platform.platformId}: ${err}`);
|
|
72791
73541
|
});
|
|
72792
73542
|
}).catch((err) => {
|
|
72793
|
-
|
|
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
|
-
|
|
73557
|
+
log41.info(`⏸️ ${pausedToSkip.length} session(s) remain paused (waiting for user message)`);
|
|
72808
73558
|
}
|
|
72809
73559
|
if (activeToResume.length > 0) {
|
|
72810
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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((
|
|
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(
|
|
81656
|
+
color: getColorForLevel(log42.level),
|
|
80889
81657
|
dimColor: true,
|
|
80890
81658
|
wrap: "truncate",
|
|
80891
81659
|
children: [
|
|
80892
81660
|
"[",
|
|
80893
|
-
padComponent(
|
|
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(
|
|
81666
|
+
color: getColorForLevel(log42.level),
|
|
80899
81667
|
wrap: "truncate",
|
|
80900
81668
|
children: [
|
|
80901
81669
|
" ",
|
|
80902
|
-
|
|
81670
|
+
log42.message
|
|
80903
81671
|
]
|
|
80904
81672
|
}, undefined, true, undefined, this)
|
|
80905
81673
|
]
|
|
80906
|
-
},
|
|
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((
|
|
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((
|
|
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(
|
|
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(
|
|
82249
|
+
color: getLevelColor(log42.level),
|
|
81482
82250
|
children: [
|
|
81483
82251
|
" ",
|
|
81484
|
-
|
|
82252
|
+
log42.message
|
|
81485
82253
|
]
|
|
81486
82254
|
}, undefined, true, undefined, this)
|
|
81487
82255
|
]
|
|
81488
|
-
},
|
|
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((
|
|
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((
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
83837
|
+
log42.debug("Auto-update disabled, not starting checker");
|
|
83070
83838
|
return;
|
|
83071
83839
|
}
|
|
83072
83840
|
setTimeout(() => {
|
|
83073
83841
|
this.check().catch((err) => {
|
|
83074
|
-
|
|
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
|
-
|
|
83848
|
+
log42.warn(`Periodic update check failed: ${err}`);
|
|
83081
83849
|
});
|
|
83082
83850
|
}, intervalMs);
|
|
83083
|
-
|
|
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
|
-
|
|
83858
|
+
log42.debug("Update checker stopped");
|
|
83091
83859
|
}
|
|
83092
83860
|
async check() {
|
|
83093
83861
|
if (this.isChecking) {
|
|
83094
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
83892
|
+
log42.debug(`Up to date (v${currentVersion})`);
|
|
83125
83893
|
this.emit("check:complete", false);
|
|
83126
83894
|
return null;
|
|
83127
83895
|
} catch (err) {
|
|
83128
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
83425
|
-
var
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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 (
|
|
83489
|
-
const content =
|
|
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
|
-
|
|
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 (!
|
|
83501
|
-
|
|
84268
|
+
if (!existsSync16(dir)) {
|
|
84269
|
+
mkdirSync7(dir, { recursive: true });
|
|
83502
84270
|
}
|
|
83503
|
-
|
|
83504
|
-
|
|
84271
|
+
writeFileSync9(STATE_PATH, JSON.stringify(state, null, 2), "utf-8");
|
|
84272
|
+
log44.debug("Update state saved");
|
|
83505
84273
|
} catch (err) {
|
|
83506
|
-
|
|
84274
|
+
log44.warn(`Failed to save update state: ${err}`);
|
|
83507
84275
|
}
|
|
83508
84276
|
}
|
|
83509
84277
|
function clearUpdateState() {
|
|
83510
84278
|
try {
|
|
83511
|
-
if (
|
|
83512
|
-
|
|
84279
|
+
if (existsSync16(STATE_PATH)) {
|
|
84280
|
+
writeFileSync9(STATE_PATH, "{}", "utf-8");
|
|
83513
84281
|
}
|
|
83514
84282
|
} catch (err) {
|
|
83515
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
83649
|
-
import { delimiter, join as
|
|
83650
|
-
var
|
|
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 =
|
|
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 ?
|
|
84443
|
+
const bunRoot = _env.BUN_INSTALL || (home ? join14(home, ".bun") : null);
|
|
83676
84444
|
if (bunRoot) {
|
|
83677
|
-
const bunBin =
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
84499
|
+
log45.error(`Replacement process error: ${err.message}`);
|
|
83732
84500
|
});
|
|
83733
84501
|
if (child.pid === undefined) {
|
|
83734
|
-
|
|
84502
|
+
log45.error("Spawn returned no pid (binary likely not executable)");
|
|
83735
84503
|
return false;
|
|
83736
84504
|
}
|
|
83737
84505
|
child.unref();
|
|
83738
|
-
|
|
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
|
|
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
|
-
|
|
84534
|
+
log46.info("Auto-update is disabled");
|
|
83767
84535
|
return;
|
|
83768
84536
|
}
|
|
83769
84537
|
const updateResult = this.installer.checkJustUpdated();
|
|
83770
84538
|
if (updateResult) {
|
|
83771
|
-
|
|
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
|
-
|
|
84541
|
+
log46.warn(`Failed to broadcast update notification: ${err}`);
|
|
83774
84542
|
});
|
|
83775
84543
|
}
|
|
83776
84544
|
this.checker.start();
|
|
83777
|
-
|
|
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
|
-
|
|
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
|
-
|
|
84564
|
+
log46.info("No update available");
|
|
83797
84565
|
return;
|
|
83798
84566
|
}
|
|
83799
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
84649
|
+
log46.error("claude-threads not found on PATH; manual restart required");
|
|
83882
84650
|
}
|
|
83883
84651
|
process.exit(0);
|
|
83884
84652
|
}
|
|
83885
|
-
|
|
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);
|