claude-threads 1.25.1 → 1.26.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +11 -0
- package/README.md +5 -0
- package/dist/index.js +1760 -1017
- package/dist/mcp/mcp-server.js +290 -19
- 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,13 +58742,13 @@ ${avoidCommands.map((c) => `- \`!${c.command}\` - ${c.reason}`).join(`
|
|
|
58781
58742
|
`.trim();
|
|
58782
58743
|
}
|
|
58783
58744
|
// src/session/lifecycle.ts
|
|
58784
|
-
import { randomUUID as
|
|
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
|
|
58751
|
+
var log16 = createLogger("keepalive");
|
|
58791
58752
|
function keepAliveSpawnSpec(platform, parentPid) {
|
|
58792
58753
|
switch (platform) {
|
|
58793
58754
|
case "darwin":
|
|
@@ -58843,7 +58804,7 @@ class KeepAliveManager {
|
|
|
58843
58804
|
if (!enabled && this.keepAliveProcess) {
|
|
58844
58805
|
this.stopKeepAlive();
|
|
58845
58806
|
}
|
|
58846
|
-
|
|
58807
|
+
log16.debug(`Keep-alive ${enabled ? "enabled" : "disabled"}`);
|
|
58847
58808
|
}
|
|
58848
58809
|
isEnabled() {
|
|
58849
58810
|
return this.enabled;
|
|
@@ -58853,7 +58814,7 @@ class KeepAliveManager {
|
|
|
58853
58814
|
}
|
|
58854
58815
|
sessionStarted() {
|
|
58855
58816
|
this.activeSessionCount++;
|
|
58856
|
-
|
|
58817
|
+
log16.debug(`Session started (${this.activeSessionCount} active)`);
|
|
58857
58818
|
if (this.activeSessionCount === 1) {
|
|
58858
58819
|
this.startKeepAlive();
|
|
58859
58820
|
}
|
|
@@ -58862,7 +58823,7 @@ class KeepAliveManager {
|
|
|
58862
58823
|
if (this.activeSessionCount > 0) {
|
|
58863
58824
|
this.activeSessionCount--;
|
|
58864
58825
|
}
|
|
58865
|
-
|
|
58826
|
+
log16.debug(`Session ended (${this.activeSessionCount} active)`);
|
|
58866
58827
|
if (this.activeSessionCount === 0) {
|
|
58867
58828
|
this.stopKeepAlive();
|
|
58868
58829
|
}
|
|
@@ -58876,11 +58837,11 @@ class KeepAliveManager {
|
|
|
58876
58837
|
}
|
|
58877
58838
|
startKeepAlive() {
|
|
58878
58839
|
if (!this.enabled) {
|
|
58879
|
-
|
|
58840
|
+
log16.debug("Keep-alive disabled, skipping");
|
|
58880
58841
|
return;
|
|
58881
58842
|
}
|
|
58882
58843
|
if (this.keepAliveProcess) {
|
|
58883
|
-
|
|
58844
|
+
log16.debug("Keep-alive already running");
|
|
58884
58845
|
return;
|
|
58885
58846
|
}
|
|
58886
58847
|
switch (this.platform) {
|
|
@@ -58894,12 +58855,12 @@ class KeepAliveManager {
|
|
|
58894
58855
|
this.startWindowsKeepAlive();
|
|
58895
58856
|
break;
|
|
58896
58857
|
default:
|
|
58897
|
-
|
|
58858
|
+
log16.warn(`Keep-alive not supported on ${this.platform}`);
|
|
58898
58859
|
}
|
|
58899
58860
|
}
|
|
58900
58861
|
stopKeepAlive() {
|
|
58901
58862
|
if (this.keepAliveProcess) {
|
|
58902
|
-
|
|
58863
|
+
log16.debug("Stopping keep-alive");
|
|
58903
58864
|
this.keepAliveProcess.kill();
|
|
58904
58865
|
this.keepAliveProcess = null;
|
|
58905
58866
|
}
|
|
@@ -58914,18 +58875,18 @@ class KeepAliveManager {
|
|
|
58914
58875
|
detached: false
|
|
58915
58876
|
});
|
|
58916
58877
|
this.keepAliveProcess.on("error", (err) => {
|
|
58917
|
-
|
|
58878
|
+
log16.error(`Failed to start caffeinate: ${err.message}`);
|
|
58918
58879
|
this.keepAliveProcess = null;
|
|
58919
58880
|
});
|
|
58920
58881
|
this.keepAliveProcess.on("exit", (code) => {
|
|
58921
58882
|
if (code !== null && code !== 0 && this.activeSessionCount > 0) {
|
|
58922
|
-
|
|
58883
|
+
log16.debug(`caffeinate exited with code ${code}`);
|
|
58923
58884
|
}
|
|
58924
58885
|
this.keepAliveProcess = null;
|
|
58925
58886
|
});
|
|
58926
|
-
|
|
58887
|
+
log16.info("Sleep prevention active (caffeinate)");
|
|
58927
58888
|
} catch (err) {
|
|
58928
|
-
|
|
58889
|
+
log16.error(`Failed to start caffeinate: ${err}`);
|
|
58929
58890
|
}
|
|
58930
58891
|
}
|
|
58931
58892
|
startLinuxKeepAlive() {
|
|
@@ -58938,19 +58899,19 @@ class KeepAliveManager {
|
|
|
58938
58899
|
detached: false
|
|
58939
58900
|
});
|
|
58940
58901
|
this.keepAliveProcess.on("error", (err) => {
|
|
58941
|
-
|
|
58902
|
+
log16.debug(`systemd-inhibit not available: ${err.message}`);
|
|
58942
58903
|
this.keepAliveProcess = null;
|
|
58943
58904
|
this.startLinuxKeepAliveFallback();
|
|
58944
58905
|
});
|
|
58945
58906
|
this.keepAliveProcess.on("exit", (code) => {
|
|
58946
58907
|
if (code !== null && code !== 0 && this.activeSessionCount > 0) {
|
|
58947
|
-
|
|
58908
|
+
log16.debug(`systemd-inhibit exited with code ${code}`);
|
|
58948
58909
|
}
|
|
58949
58910
|
this.keepAliveProcess = null;
|
|
58950
58911
|
});
|
|
58951
|
-
|
|
58912
|
+
log16.info("Sleep prevention active (systemd-inhibit)");
|
|
58952
58913
|
} catch (err) {
|
|
58953
|
-
|
|
58914
|
+
log16.debug(`Failed to start systemd-inhibit: ${err}`);
|
|
58954
58915
|
this.startLinuxKeepAliveFallback();
|
|
58955
58916
|
}
|
|
58956
58917
|
}
|
|
@@ -58961,15 +58922,15 @@ class KeepAliveManager {
|
|
|
58961
58922
|
detached: false
|
|
58962
58923
|
});
|
|
58963
58924
|
this.keepAliveProcess.on("error", (err) => {
|
|
58964
|
-
|
|
58925
|
+
log16.warn(`Linux keep-alive fallback not available: ${err.message}`);
|
|
58965
58926
|
this.keepAliveProcess = null;
|
|
58966
58927
|
});
|
|
58967
58928
|
this.keepAliveProcess.on("exit", () => {
|
|
58968
58929
|
this.keepAliveProcess = null;
|
|
58969
58930
|
});
|
|
58970
|
-
|
|
58931
|
+
log16.info("Sleep prevention active (xdg-screensaver)");
|
|
58971
58932
|
} catch (err) {
|
|
58972
|
-
|
|
58933
|
+
log16.warn(`Linux keep-alive not available: ${err}`);
|
|
58973
58934
|
}
|
|
58974
58935
|
}
|
|
58975
58936
|
startWindowsKeepAlive() {
|
|
@@ -58981,18 +58942,18 @@ class KeepAliveManager {
|
|
|
58981
58942
|
windowsHide: true
|
|
58982
58943
|
});
|
|
58983
58944
|
this.keepAliveProcess.on("error", (err) => {
|
|
58984
|
-
|
|
58945
|
+
log16.warn(`Windows keep-alive not available: ${err.message}`);
|
|
58985
58946
|
this.keepAliveProcess = null;
|
|
58986
58947
|
});
|
|
58987
58948
|
this.keepAliveProcess.on("exit", (code) => {
|
|
58988
58949
|
if (code !== null && code !== 0 && this.activeSessionCount > 0) {
|
|
58989
|
-
|
|
58950
|
+
log16.debug(`PowerShell keep-alive exited with code ${code}`);
|
|
58990
58951
|
}
|
|
58991
58952
|
this.keepAliveProcess = null;
|
|
58992
58953
|
});
|
|
58993
|
-
|
|
58954
|
+
log16.info("Sleep prevention active (SetThreadExecutionState)");
|
|
58994
58955
|
} catch (err) {
|
|
58995
|
-
|
|
58956
|
+
log16.warn(`Windows keep-alive not available: ${err}`);
|
|
58996
58957
|
}
|
|
58997
58958
|
}
|
|
58998
58959
|
}
|
|
@@ -59000,7 +58961,7 @@ var keepAlive = new KeepAliveManager;
|
|
|
59000
58961
|
|
|
59001
58962
|
// src/utils/error-handler/index.ts
|
|
59002
58963
|
init_logger();
|
|
59003
|
-
var
|
|
58964
|
+
var log17 = createLogger("error");
|
|
59004
58965
|
|
|
59005
58966
|
class SessionError extends Error {
|
|
59006
58967
|
sessionId;
|
|
@@ -59026,19 +58987,19 @@ async function handleError(error, context, severity = "recoverable") {
|
|
|
59026
58987
|
const sessionPart = sessionId ? ` (${formatShortId(sessionId)})` : "";
|
|
59027
58988
|
const logMessage = `${context.action}${sessionPart}: ${message}`;
|
|
59028
58989
|
if (severity === "recoverable") {
|
|
59029
|
-
|
|
58990
|
+
log17.warn(logMessage);
|
|
59030
58991
|
} else {
|
|
59031
|
-
|
|
58992
|
+
log17.error(logMessage, error instanceof Error ? error : undefined);
|
|
59032
58993
|
}
|
|
59033
58994
|
if (context.details) {
|
|
59034
|
-
|
|
58995
|
+
log17.debugJson("Error details", context.details);
|
|
59035
58996
|
}
|
|
59036
58997
|
if (context.notifyUser && context.session) {
|
|
59037
58998
|
try {
|
|
59038
58999
|
const fmt = context.session.platform.getFormatter();
|
|
59039
59000
|
await context.session.platform.createPost(`⚠️ ${fmt.formatBold("Error")}: ${context.action} failed - ${message}`, context.session.threadId);
|
|
59040
59001
|
} catch (notifyError) {
|
|
59041
|
-
|
|
59002
|
+
log17.warn(`Could not notify user: ${notifyError}`);
|
|
59042
59003
|
}
|
|
59043
59004
|
}
|
|
59044
59005
|
if (severity === "session-fatal" || severity === "system-fatal") {
|
|
@@ -59065,7 +59026,7 @@ async function logAndNotify(error, context) {
|
|
|
59065
59026
|
}
|
|
59066
59027
|
function logSilentError(context, error) {
|
|
59067
59028
|
const message = error instanceof Error ? error.message : String(error);
|
|
59068
|
-
|
|
59029
|
+
log17.debug(`[${context}] Silently caught: ${message}`);
|
|
59069
59030
|
}
|
|
59070
59031
|
|
|
59071
59032
|
// src/session/lifecycle.ts
|
|
@@ -59085,8 +59046,8 @@ function createSessionLog(baseLog) {
|
|
|
59085
59046
|
init_logger();
|
|
59086
59047
|
init_emoji();
|
|
59087
59048
|
init_worktree();
|
|
59088
|
-
var
|
|
59089
|
-
var sessionLog = createSessionLog(
|
|
59049
|
+
var log18 = createLogger("helpers");
|
|
59050
|
+
var sessionLog = createSessionLog(log18);
|
|
59090
59051
|
var POST_TYPES = {
|
|
59091
59052
|
info: "",
|
|
59092
59053
|
success: "✅",
|
|
@@ -59172,10 +59133,10 @@ function updateLastMessage(session, post2) {
|
|
|
59172
59133
|
|
|
59173
59134
|
// src/operations/streaming/handler.ts
|
|
59174
59135
|
init_logger();
|
|
59175
|
-
import { lstat, mkdir as mkdir2, mkdtemp, rm as
|
|
59136
|
+
import { lstat, mkdir as mkdir2, mkdtemp, rm as rm2, writeFile as writeFile2 } from "fs/promises";
|
|
59176
59137
|
import { tmpdir as tmpdir3 } from "os";
|
|
59177
59138
|
import { join as join11 } from "path";
|
|
59178
|
-
var
|
|
59139
|
+
var log19 = createLogger("streaming");
|
|
59179
59140
|
var UPLOAD_ROOT_DIR = "claude-threads-uploads";
|
|
59180
59141
|
function safeIdSegment2(id) {
|
|
59181
59142
|
return id.replace(/[^A-Za-z0-9._-]/g, "_");
|
|
@@ -59188,9 +59149,9 @@ async function cleanupSessionUploads(platformId, threadId) {
|
|
|
59188
59149
|
return;
|
|
59189
59150
|
const dir = getSessionUploadDir(platformId, threadId);
|
|
59190
59151
|
try {
|
|
59191
|
-
await
|
|
59152
|
+
await rm2(dir, { recursive: true, force: true });
|
|
59192
59153
|
} catch (err) {
|
|
59193
|
-
|
|
59154
|
+
log19.debug(`Upload cleanup for ${platformId}:${threadId} failed (ignored): ${err}`);
|
|
59194
59155
|
}
|
|
59195
59156
|
}
|
|
59196
59157
|
function sanitizeForPrompt(value) {
|
|
@@ -59211,7 +59172,7 @@ async function saveFilesToUploadDir(platform, uploadDir, files, debug = false) {
|
|
|
59211
59172
|
for (const file of files) {
|
|
59212
59173
|
skipped.push({ name: file.name, reason: "Refusing to write under symlinked upload directory" });
|
|
59213
59174
|
}
|
|
59214
|
-
|
|
59175
|
+
log19.error(`Upload dir is a symlink, refusing all writes: ${uploadDir}`);
|
|
59215
59176
|
return { saved, skipped };
|
|
59216
59177
|
}
|
|
59217
59178
|
const messageDir = await mkdtemp(join11(uploadDir, `${Date.now().toString(36)}-`));
|
|
@@ -59229,11 +59190,11 @@ async function saveFilesToUploadDir(platform, uploadDir, files, debug = false) {
|
|
|
59229
59190
|
size: buffer.length
|
|
59230
59191
|
});
|
|
59231
59192
|
if (debug) {
|
|
59232
|
-
|
|
59193
|
+
log19.debug(`Saved ${file.name} → ${absolutePath} (${formatBytes(buffer.length)})`);
|
|
59233
59194
|
}
|
|
59234
59195
|
} catch (err) {
|
|
59235
59196
|
const message = err instanceof Error ? err.message : String(err);
|
|
59236
|
-
|
|
59197
|
+
log19.error(`Failed to save uploaded file ${file.name}: ${message}`);
|
|
59237
59198
|
skipped.push({
|
|
59238
59199
|
name: file.name,
|
|
59239
59200
|
reason: `Download failed: ${message}`
|
|
@@ -59309,7 +59270,7 @@ function buildRestartCliOptions(session, ctx) {
|
|
|
59309
59270
|
}
|
|
59310
59271
|
|
|
59311
59272
|
// src/operations/commands/handler.ts
|
|
59312
|
-
import { randomUUID as
|
|
59273
|
+
import { randomUUID as randomUUID5 } from "crypto";
|
|
59313
59274
|
import { resolve as resolve6 } from "path";
|
|
59314
59275
|
import { existsSync as existsSync12, statSync as statSync3 } from "fs";
|
|
59315
59276
|
|
|
@@ -59656,9 +59617,9 @@ node_default(Temp.purgeSyncAll);
|
|
|
59656
59617
|
var temp_default = Temp;
|
|
59657
59618
|
|
|
59658
59619
|
// node_modules/atomically/dist/index.js
|
|
59659
|
-
function
|
|
59620
|
+
function writeFileSync7(filePath, data, options = DEFAULT_WRITE_OPTIONS) {
|
|
59660
59621
|
if (isString(options))
|
|
59661
|
-
return
|
|
59622
|
+
return writeFileSync7(filePath, data, { encoding: options });
|
|
59662
59623
|
const timeout = options.timeout ?? DEFAULT_TIMEOUT_SYNC;
|
|
59663
59624
|
const retryOptions = { timeout };
|
|
59664
59625
|
let tempDisposer = null;
|
|
@@ -59986,7 +59947,7 @@ class Configstore {
|
|
|
59986
59947
|
}
|
|
59987
59948
|
if (error.name === "SyntaxError") {
|
|
59988
59949
|
if (this._clearInvalidConfig) {
|
|
59989
|
-
|
|
59950
|
+
writeFileSync7(this._path, "", writeFileOptions);
|
|
59990
59951
|
return {};
|
|
59991
59952
|
}
|
|
59992
59953
|
throw error;
|
|
@@ -59998,7 +59959,7 @@ class Configstore {
|
|
|
59998
59959
|
set all(value) {
|
|
59999
59960
|
try {
|
|
60000
59961
|
import_graceful_fs.default.mkdirSync(path5.dirname(this._path), mkdirOptions);
|
|
60001
|
-
|
|
59962
|
+
writeFileSync7(this._path, JSON.stringify(value, undefined, "\t"), writeFileOptions);
|
|
60002
59963
|
} catch (error) {
|
|
60003
59964
|
handlePermissionError(error);
|
|
60004
59965
|
}
|
|
@@ -62726,7 +62687,7 @@ init_emoji();
|
|
|
62726
62687
|
|
|
62727
62688
|
// src/operations/bug-report/handler.ts
|
|
62728
62689
|
import { execSync as execSync2 } from "child_process";
|
|
62729
|
-
import { writeFileSync as
|
|
62690
|
+
import { writeFileSync as writeFileSync8, unlinkSync as unlinkSync3 } from "fs";
|
|
62730
62691
|
import { tmpdir as tmpdir4 } from "os";
|
|
62731
62692
|
import { join as join12 } from "path";
|
|
62732
62693
|
|
|
@@ -63310,7 +63271,7 @@ async function createGitHubIssue(title, body, workingDir) {
|
|
|
63310
63271
|
}
|
|
63311
63272
|
const bodyFile = join12(tmpdir4(), `bug-body-${Date.now()}.md`);
|
|
63312
63273
|
try {
|
|
63313
|
-
|
|
63274
|
+
writeFileSync8(bodyFile, body, "utf-8");
|
|
63314
63275
|
const cmd = `gh issue create --repo "${GITHUB_REPO}" --title "${escapeShell(title)}" --body-file "${bodyFile}"`;
|
|
63315
63276
|
const result = execSync2(cmd, {
|
|
63316
63277
|
cwd: workingDir,
|
|
@@ -66967,7 +66928,8 @@ class PromptExecutor extends BaseExecutor {
|
|
|
66967
66928
|
return {
|
|
66968
66929
|
pendingContextPrompt: null,
|
|
66969
66930
|
pendingExistingWorktreePrompt: null,
|
|
66970
|
-
pendingUpdatePrompt: null
|
|
66931
|
+
pendingUpdatePrompt: null,
|
|
66932
|
+
pendingRoutinePrompt: null
|
|
66971
66933
|
};
|
|
66972
66934
|
}
|
|
66973
66935
|
getInitialState() {
|
|
@@ -66977,14 +66939,16 @@ class PromptExecutor extends BaseExecutor {
|
|
|
66977
66939
|
return {
|
|
66978
66940
|
pendingContextPrompt: this.state.pendingContextPrompt ? { ...this.state.pendingContextPrompt } : null,
|
|
66979
66941
|
pendingExistingWorktreePrompt: this.state.pendingExistingWorktreePrompt ? { ...this.state.pendingExistingWorktreePrompt } : null,
|
|
66980
|
-
pendingUpdatePrompt: this.state.pendingUpdatePrompt ? { ...this.state.pendingUpdatePrompt } : null
|
|
66942
|
+
pendingUpdatePrompt: this.state.pendingUpdatePrompt ? { ...this.state.pendingUpdatePrompt } : null,
|
|
66943
|
+
pendingRoutinePrompt: this.state.pendingRoutinePrompt ? { ...this.state.pendingRoutinePrompt } : null
|
|
66981
66944
|
};
|
|
66982
66945
|
}
|
|
66983
66946
|
hydrateState(persisted) {
|
|
66984
66947
|
this.state = {
|
|
66985
66948
|
pendingContextPrompt: persisted.pendingContextPrompt ?? null,
|
|
66986
66949
|
pendingExistingWorktreePrompt: persisted.pendingExistingWorktreePrompt ?? null,
|
|
66987
|
-
pendingUpdatePrompt: persisted.pendingUpdatePrompt ?? null
|
|
66950
|
+
pendingUpdatePrompt: persisted.pendingUpdatePrompt ?? null,
|
|
66951
|
+
pendingRoutinePrompt: null
|
|
66988
66952
|
};
|
|
66989
66953
|
}
|
|
66990
66954
|
setPendingContextPrompt(prompt) {
|
|
@@ -67114,6 +67078,30 @@ class PromptExecutor extends BaseExecutor {
|
|
|
67114
67078
|
}
|
|
67115
67079
|
return true;
|
|
67116
67080
|
}
|
|
67081
|
+
setPendingRoutinePrompt(prompt) {
|
|
67082
|
+
this.state.pendingRoutinePrompt = prompt;
|
|
67083
|
+
}
|
|
67084
|
+
hasPendingRoutinePrompt() {
|
|
67085
|
+
return this.state.pendingRoutinePrompt !== null;
|
|
67086
|
+
}
|
|
67087
|
+
async handleRoutinePromptResponse(postId, approved, username, ctx) {
|
|
67088
|
+
if (!this.state.pendingRoutinePrompt)
|
|
67089
|
+
return false;
|
|
67090
|
+
if (this.state.pendingRoutinePrompt.postId !== postId)
|
|
67091
|
+
return false;
|
|
67092
|
+
const { parsed, requestedBy } = this.state.pendingRoutinePrompt;
|
|
67093
|
+
const statusMessage = approved ? `✅ ${ctx.formatter.formatBold(`Routine "${parsed.name}" confirmed`)} by ${ctx.formatter.formatUserMention(username)} — saving...` : `❌ ${ctx.formatter.formatBold(`Routine "${parsed.name}" discarded`)} by ${ctx.formatter.formatUserMention(username)}`;
|
|
67094
|
+
try {
|
|
67095
|
+
await ctx.platform.updatePost(postId, statusMessage);
|
|
67096
|
+
} catch (err) {
|
|
67097
|
+
ctx.logger.debug(`Failed to update routine prompt post: ${err}`);
|
|
67098
|
+
}
|
|
67099
|
+
this.state.pendingRoutinePrompt = null;
|
|
67100
|
+
if (this.events) {
|
|
67101
|
+
this.events.emit("routine-prompt:complete", { approved, parsed, requestedBy, postId });
|
|
67102
|
+
}
|
|
67103
|
+
return true;
|
|
67104
|
+
}
|
|
67117
67105
|
async handleReaction(postId, emoji, user, action, ctx) {
|
|
67118
67106
|
ctx.logger.debug(`PromptExecutor.handleReaction: postId=${postId.substring(0, 8)}, emoji=${emoji}, user=${user}, action=${action}`);
|
|
67119
67107
|
if (action !== "added") {
|
|
@@ -67174,6 +67162,18 @@ class PromptExecutor extends BaseExecutor {
|
|
|
67174
67162
|
ctx.logger.debug(`PromptExecutor: emoji ${emoji} not valid for update prompt, ignoring`);
|
|
67175
67163
|
return false;
|
|
67176
67164
|
}
|
|
67165
|
+
if (this.state.pendingRoutinePrompt?.postId === postId) {
|
|
67166
|
+
if (isApprovalEmoji(emoji)) {
|
|
67167
|
+
ctx.logger.debug(`Routine prompt reaction from @${user}: approve`);
|
|
67168
|
+
return this.handleRoutinePromptResponse(postId, true, user, ctx);
|
|
67169
|
+
}
|
|
67170
|
+
if (isDenialEmoji(emoji)) {
|
|
67171
|
+
ctx.logger.debug(`Routine prompt reaction from @${user}: discard`);
|
|
67172
|
+
return this.handleRoutinePromptResponse(postId, false, user, ctx);
|
|
67173
|
+
}
|
|
67174
|
+
ctx.logger.debug(`PromptExecutor: emoji ${emoji} not valid for routine prompt, ignoring`);
|
|
67175
|
+
return false;
|
|
67176
|
+
}
|
|
67177
67177
|
ctx.logger.debug(`PromptExecutor: no pending prompt state matches postId=${postId.substring(0, 8)}`);
|
|
67178
67178
|
return false;
|
|
67179
67179
|
}
|
|
@@ -67268,7 +67268,7 @@ class BugReportExecutor extends BaseExecutor {
|
|
|
67268
67268
|
// src/operations/executors/worktree-prompt.ts
|
|
67269
67269
|
init_emoji();
|
|
67270
67270
|
init_logger();
|
|
67271
|
-
var
|
|
67271
|
+
var log20 = createLogger("wt-prompt");
|
|
67272
67272
|
// src/operations/message-manager.ts
|
|
67273
67273
|
init_logger();
|
|
67274
67274
|
|
|
@@ -67348,7 +67348,7 @@ function formatRelativeTime(date) {
|
|
|
67348
67348
|
return `${diffMin} min ago`;
|
|
67349
67349
|
}
|
|
67350
67350
|
// src/operations/message-manager.ts
|
|
67351
|
-
var
|
|
67351
|
+
var log21 = createLogger("msg-mgr");
|
|
67352
67352
|
|
|
67353
67353
|
class MessageManager {
|
|
67354
67354
|
platform;
|
|
@@ -67441,7 +67441,7 @@ class MessageManager {
|
|
|
67441
67441
|
});
|
|
67442
67442
|
}
|
|
67443
67443
|
async handleEvent(event) {
|
|
67444
|
-
const logger =
|
|
67444
|
+
const logger = log21.forSession(this.sessionId);
|
|
67445
67445
|
const transformCtx = {
|
|
67446
67446
|
sessionId: this.sessionId,
|
|
67447
67447
|
formatter: this.platform.getFormatter(),
|
|
@@ -67495,7 +67495,7 @@ class MessageManager {
|
|
|
67495
67495
|
}
|
|
67496
67496
|
}
|
|
67497
67497
|
async executeOperation(op) {
|
|
67498
|
-
const logger =
|
|
67498
|
+
const logger = log21.forSession(this.sessionId);
|
|
67499
67499
|
const ctx = this.getExecutorContext();
|
|
67500
67500
|
try {
|
|
67501
67501
|
if (isContentOp(op)) {
|
|
@@ -67563,7 +67563,7 @@ class MessageManager {
|
|
|
67563
67563
|
threadId: this.threadId,
|
|
67564
67564
|
platform: this.platform,
|
|
67565
67565
|
formatter: this.platform.getFormatter(),
|
|
67566
|
-
logger:
|
|
67566
|
+
logger: log21.forSession(this.sessionId),
|
|
67567
67567
|
postTracker: this.postTracker,
|
|
67568
67568
|
contentBreaker: this.contentBreaker,
|
|
67569
67569
|
threadLogger: this.session.threadLogger,
|
|
@@ -67682,6 +67682,9 @@ class MessageManager {
|
|
|
67682
67682
|
clearPendingUpdatePrompt() {
|
|
67683
67683
|
this.promptExecutor.clearPendingUpdatePrompt();
|
|
67684
67684
|
}
|
|
67685
|
+
setPendingRoutinePrompt(prompt) {
|
|
67686
|
+
this.promptExecutor.setPendingRoutinePrompt(prompt);
|
|
67687
|
+
}
|
|
67685
67688
|
setPendingBugReport(report) {
|
|
67686
67689
|
this.bugReportExecutor.setPendingBugReport(report);
|
|
67687
67690
|
}
|
|
@@ -67780,13 +67783,13 @@ class MessageManager {
|
|
|
67780
67783
|
return this.systemExecutor.postSuccess(message, this.getExecutorContext());
|
|
67781
67784
|
}
|
|
67782
67785
|
async prepareForUserMessage() {
|
|
67783
|
-
const logger =
|
|
67786
|
+
const logger = log21.forSession(this.sessionId);
|
|
67784
67787
|
logger.debug("Preparing for new user message");
|
|
67785
67788
|
await this.closeCurrentPost();
|
|
67786
67789
|
await this.bumpTaskList();
|
|
67787
67790
|
}
|
|
67788
67791
|
async handleUserMessage(message, files, username, displayName) {
|
|
67789
|
-
const logger =
|
|
67792
|
+
const logger = log21.forSession(this.sessionId);
|
|
67790
67793
|
if (!this.session.claude.isRunning()) {
|
|
67791
67794
|
logger.debug("Claude not running, ignoring user message");
|
|
67792
67795
|
return false;
|
|
@@ -67829,7 +67832,7 @@ class MessageManager {
|
|
|
67829
67832
|
];
|
|
67830
67833
|
}
|
|
67831
67834
|
async handleReaction(postId, emoji, user, action) {
|
|
67832
|
-
const logger =
|
|
67835
|
+
const logger = log21.forSession(this.sessionId);
|
|
67833
67836
|
const ctx = this.getExecutorContext();
|
|
67834
67837
|
logger.debug(`Routing reaction: postId=${postId}, emoji=${emoji}, user=${user}, action=${action}`);
|
|
67835
67838
|
for (const { name, executor } of this.reactionDispatchList()) {
|
|
@@ -67927,7 +67930,7 @@ class MessageManager {
|
|
|
67927
67930
|
}
|
|
67928
67931
|
// src/operations/sticky-message/handler.ts
|
|
67929
67932
|
init_logger();
|
|
67930
|
-
var
|
|
67933
|
+
var log22 = createLogger("sticky");
|
|
67931
67934
|
var botStartedAt = new Date;
|
|
67932
67935
|
function getPendingPrompts(session) {
|
|
67933
67936
|
const prompts2 = [];
|
|
@@ -68002,21 +68005,21 @@ function initialize(store) {
|
|
|
68002
68005
|
stickyPostIds.set(platformId, postId);
|
|
68003
68006
|
}
|
|
68004
68007
|
if (persistedIds.size > 0) {
|
|
68005
|
-
|
|
68008
|
+
log22.info(`\uD83D\uDCCC Restored ${persistedIds.size} sticky post ID(s) from persistence`);
|
|
68006
68009
|
}
|
|
68007
68010
|
}
|
|
68008
68011
|
function setPlatformPaused(platformId, paused) {
|
|
68009
68012
|
if (paused) {
|
|
68010
68013
|
pausedPlatforms.set(platformId, true);
|
|
68011
|
-
|
|
68014
|
+
log22.debug(`Platform ${platformId} marked as paused`);
|
|
68012
68015
|
} else {
|
|
68013
68016
|
pausedPlatforms.delete(platformId);
|
|
68014
|
-
|
|
68017
|
+
log22.debug(`Platform ${platformId} marked as active`);
|
|
68015
68018
|
}
|
|
68016
68019
|
}
|
|
68017
68020
|
function setShuttingDown(shuttingDown) {
|
|
68018
68021
|
isShuttingDown = shuttingDown;
|
|
68019
|
-
|
|
68022
|
+
log22.debug(`Bot shutdown state: ${shuttingDown}`);
|
|
68020
68023
|
}
|
|
68021
68024
|
function getTaskContent(session) {
|
|
68022
68025
|
const taskState = session.messageManager?.getTaskListState();
|
|
@@ -68349,12 +68352,12 @@ async function validateLastMessageIds(platform, sessions) {
|
|
|
68349
68352
|
try {
|
|
68350
68353
|
const post2 = await platform.getPost(lastMessageId);
|
|
68351
68354
|
if (!post2) {
|
|
68352
|
-
|
|
68355
|
+
log22.debug(`lastMessageId ${lastMessageId.substring(0, 8)} for session ${session.sessionId} was deleted, clearing`);
|
|
68353
68356
|
session.lastMessageId = undefined;
|
|
68354
68357
|
session.lastMessageTs = undefined;
|
|
68355
68358
|
}
|
|
68356
68359
|
} catch (err) {
|
|
68357
|
-
|
|
68360
|
+
log22.debug(`Failed to validate lastMessageId for session ${session.sessionId}, clearing: ${err}`);
|
|
68358
68361
|
session.lastMessageId = undefined;
|
|
68359
68362
|
session.lastMessageTs = undefined;
|
|
68360
68363
|
}
|
|
@@ -68371,7 +68374,7 @@ async function updateStickyMessageImpl(platform, sessions, config) {
|
|
|
68371
68374
|
hiddenCleanupDone.add(platform.platformId);
|
|
68372
68375
|
const existing = stickyPostIds.get(platform.platformId);
|
|
68373
68376
|
if (existing) {
|
|
68374
|
-
|
|
68377
|
+
log22.info(`sticky[${platform.platformId}] hidden mode: removing leftover ${formatShortId(existing)}`);
|
|
68375
68378
|
try {
|
|
68376
68379
|
await platform.unpinPost(existing);
|
|
68377
68380
|
} catch {}
|
|
@@ -68391,63 +68394,63 @@ async function updateStickyMessageImpl(platform, sessions, config) {
|
|
|
68391
68394
|
return;
|
|
68392
68395
|
}
|
|
68393
68396
|
const platformSessions = [...sessions.values()].filter((s) => s.platformId === platform.platformId);
|
|
68394
|
-
|
|
68397
|
+
log22.debug(`updateStickyMessage for ${platform.platformId}, ${platformSessions.length} sessions`);
|
|
68395
68398
|
for (const s of platformSessions) {
|
|
68396
|
-
|
|
68399
|
+
log22.debug(` - ${s.sessionId}: title="${s.sessionTitle}" firstPrompt="${s.firstPrompt?.substring(0, 30)}..."`);
|
|
68397
68400
|
}
|
|
68398
68401
|
await validateLastMessageIds(platform, platformSessions);
|
|
68399
68402
|
const formatter = platform.getFormatter();
|
|
68400
68403
|
const content = await buildStickyMessage(sessions, platform.platformId, config, formatter, (threadId) => platform.getThreadLink(threadId));
|
|
68401
68404
|
const existingPostId = stickyPostIds.get(platform.platformId);
|
|
68402
68405
|
const shouldBump = needsBump.get(platform.platformId) ?? false;
|
|
68403
|
-
|
|
68406
|
+
log22.debug(`existingPostId: ${existingPostId || "(none)"}, needsBump: ${shouldBump}`);
|
|
68404
68407
|
try {
|
|
68405
68408
|
if (existingPostId && !shouldBump) {
|
|
68406
|
-
|
|
68409
|
+
log22.debug(`Updating existing post in place...`);
|
|
68407
68410
|
try {
|
|
68408
68411
|
await platform.updatePost(existingPostId, content);
|
|
68409
68412
|
try {
|
|
68410
68413
|
await platform.pinPost(existingPostId);
|
|
68411
|
-
|
|
68414
|
+
log22.debug(`Re-pinned post`);
|
|
68412
68415
|
} catch (pinErr) {
|
|
68413
|
-
|
|
68416
|
+
log22.debug(`Re-pin failed (might already be pinned): ${pinErr}`);
|
|
68414
68417
|
}
|
|
68415
|
-
|
|
68418
|
+
log22.debug(`Updated successfully`);
|
|
68416
68419
|
return;
|
|
68417
68420
|
} catch (err) {
|
|
68418
|
-
|
|
68421
|
+
log22.debug(`Update failed, will create new: ${err}`);
|
|
68419
68422
|
}
|
|
68420
68423
|
}
|
|
68421
68424
|
needsBump.set(platform.platformId, false);
|
|
68422
68425
|
if (existingPostId) {
|
|
68423
|
-
|
|
68426
|
+
log22.debug(`Unpinning and deleting existing post ${existingPostId.substring(0, 8)}...`);
|
|
68424
68427
|
try {
|
|
68425
68428
|
await platform.unpinPost(existingPostId);
|
|
68426
|
-
|
|
68429
|
+
log22.debug(`Unpinned successfully`);
|
|
68427
68430
|
} catch (err) {
|
|
68428
|
-
|
|
68431
|
+
log22.debug(`Unpin failed (probably already unpinned): ${err}`);
|
|
68429
68432
|
}
|
|
68430
68433
|
try {
|
|
68431
68434
|
await platform.deletePost(existingPostId);
|
|
68432
|
-
|
|
68435
|
+
log22.debug(`Deleted successfully`);
|
|
68433
68436
|
} catch (err) {
|
|
68434
|
-
|
|
68437
|
+
log22.debug(`Delete failed (probably already deleted): ${err}`);
|
|
68435
68438
|
}
|
|
68436
68439
|
stickyPostIds.delete(platform.platformId);
|
|
68437
68440
|
}
|
|
68438
|
-
|
|
68441
|
+
log22.debug(`Creating new post...`);
|
|
68439
68442
|
const post2 = await platform.createPost(content);
|
|
68440
68443
|
stickyPostIds.set(platform.platformId, post2.id);
|
|
68441
68444
|
try {
|
|
68442
68445
|
await platform.pinPost(post2.id);
|
|
68443
|
-
|
|
68446
|
+
log22.debug(`Pinned post successfully`);
|
|
68444
68447
|
} catch (err) {
|
|
68445
|
-
|
|
68448
|
+
log22.debug(`Failed to pin post: ${err}`);
|
|
68446
68449
|
}
|
|
68447
68450
|
if (sessionStore) {
|
|
68448
68451
|
sessionStore.saveStickyPostId(platform.platformId, post2.id);
|
|
68449
68452
|
}
|
|
68450
|
-
|
|
68453
|
+
log22.info(`\uD83D\uDCCC Created sticky message for ${platform.platformId}: ${formatShortId(post2.id)}`);
|
|
68451
68454
|
const excludePostIds = new Set;
|
|
68452
68455
|
if (sessionStore) {
|
|
68453
68456
|
for (const session of sessionStore.load().values()) {
|
|
@@ -68463,10 +68466,10 @@ async function updateStickyMessageImpl(platform, sessions, config) {
|
|
|
68463
68466
|
}
|
|
68464
68467
|
const botUser = await platform.getBotUser();
|
|
68465
68468
|
cleanupOldStickyMessages(platform, botUser.id, false, excludePostIds).catch((err) => {
|
|
68466
|
-
|
|
68469
|
+
log22.debug(`Background cleanup failed: ${err}`);
|
|
68467
68470
|
});
|
|
68468
68471
|
} catch (err) {
|
|
68469
|
-
|
|
68472
|
+
log22.error(`Failed to update sticky message for ${platform.platformId}`, err instanceof Error ? err : undefined);
|
|
68470
68473
|
}
|
|
68471
68474
|
}
|
|
68472
68475
|
async function updateAllStickyMessages(platforms, sessions, config, overheadByPlatform) {
|
|
@@ -68494,7 +68497,7 @@ async function cleanupOldStickyMessages(platform, botUserId, forceRun = false, e
|
|
|
68494
68497
|
if (!forceRun) {
|
|
68495
68498
|
const lastRun = lastCleanupTime.get(platformId) || 0;
|
|
68496
68499
|
if (now - lastRun < CLEANUP_THROTTLE_MS) {
|
|
68497
|
-
|
|
68500
|
+
log22.debug(`Cleanup throttled for ${platformId} (last run ${Math.round((now - lastRun) / 1000)}s ago)`);
|
|
68498
68501
|
return;
|
|
68499
68502
|
}
|
|
68500
68503
|
}
|
|
@@ -68504,37 +68507,37 @@ async function cleanupOldStickyMessages(platform, botUserId, forceRun = false, e
|
|
|
68504
68507
|
const pinnedPostIds = await platform.getPinnedPosts();
|
|
68505
68508
|
const recentPinnedIds = pinnedPostIds.filter((id) => id !== currentStickyId && !excludePostIds?.has(id) && isRecentPost(id));
|
|
68506
68509
|
if (recentPinnedIds.length === 0) {
|
|
68507
|
-
|
|
68510
|
+
log22.debug(`No recent pinned posts to check (${pinnedPostIds.length} total, current: ${currentStickyId?.substring(0, 8) || "(none)"})`);
|
|
68508
68511
|
return;
|
|
68509
68512
|
}
|
|
68510
|
-
|
|
68513
|
+
log22.debug(`Checking ${recentPinnedIds.length} recent pinned posts (of ${pinnedPostIds.length} total)`);
|
|
68511
68514
|
for (const postId of recentPinnedIds) {
|
|
68512
68515
|
try {
|
|
68513
68516
|
const post2 = await platform.getPost(postId);
|
|
68514
68517
|
if (!post2)
|
|
68515
68518
|
continue;
|
|
68516
68519
|
if (post2.userId === botUserId) {
|
|
68517
|
-
|
|
68520
|
+
log22.debug(`Cleaning up old sticky: ${postId.substring(0, 8)}...`);
|
|
68518
68521
|
try {
|
|
68519
68522
|
await platform.unpinPost(postId);
|
|
68520
68523
|
await platform.deletePost(postId);
|
|
68521
|
-
|
|
68524
|
+
log22.info(`\uD83E\uDDF9 Cleaned up old sticky message: ${postId.substring(0, 8)}...`);
|
|
68522
68525
|
} catch (err) {
|
|
68523
|
-
|
|
68526
|
+
log22.debug(`Failed to cleanup ${postId}: ${err}`);
|
|
68524
68527
|
}
|
|
68525
68528
|
}
|
|
68526
68529
|
} catch (err) {
|
|
68527
|
-
|
|
68530
|
+
log22.debug(`Could not check post ${postId}: ${err}`);
|
|
68528
68531
|
}
|
|
68529
68532
|
}
|
|
68530
68533
|
} catch (err) {
|
|
68531
|
-
|
|
68534
|
+
log22.error(`Failed to cleanup old sticky messages`, err instanceof Error ? err : undefined);
|
|
68532
68535
|
}
|
|
68533
68536
|
}
|
|
68534
68537
|
// src/claude/quick-query.ts
|
|
68535
68538
|
init_spawn();
|
|
68536
68539
|
init_logger();
|
|
68537
|
-
var
|
|
68540
|
+
var log23 = createLogger("query");
|
|
68538
68541
|
async function quickQuery(options) {
|
|
68539
68542
|
const {
|
|
68540
68543
|
prompt,
|
|
@@ -68549,7 +68552,7 @@ async function quickQuery(options) {
|
|
|
68549
68552
|
if (systemPrompt) {
|
|
68550
68553
|
args.push("--system-prompt", systemPrompt);
|
|
68551
68554
|
}
|
|
68552
|
-
|
|
68555
|
+
log23.debug(`Quick query: model=${model}, timeout=${timeout2}ms, prompt="${prompt.substring(0, 50)}..."`);
|
|
68553
68556
|
return new Promise((resolve6) => {
|
|
68554
68557
|
let stdout = "";
|
|
68555
68558
|
let stderr = "";
|
|
@@ -68563,7 +68566,7 @@ async function quickQuery(options) {
|
|
|
68563
68566
|
if (!resolved) {
|
|
68564
68567
|
resolved = true;
|
|
68565
68568
|
proc.kill("SIGTERM");
|
|
68566
|
-
|
|
68569
|
+
log23.debug(`Quick query timed out after ${timeout2}ms`);
|
|
68567
68570
|
resolve6({
|
|
68568
68571
|
success: false,
|
|
68569
68572
|
error: "timeout",
|
|
@@ -68581,7 +68584,7 @@ async function quickQuery(options) {
|
|
|
68581
68584
|
if (!resolved) {
|
|
68582
68585
|
resolved = true;
|
|
68583
68586
|
clearTimeout(timeoutId);
|
|
68584
|
-
|
|
68587
|
+
log23.debug(`Quick query error: ${err.message}`);
|
|
68585
68588
|
resolve6({
|
|
68586
68589
|
success: false,
|
|
68587
68590
|
error: err.message,
|
|
@@ -68595,14 +68598,14 @@ async function quickQuery(options) {
|
|
|
68595
68598
|
clearTimeout(timeoutId);
|
|
68596
68599
|
const durationMs = Date.now() - startTime;
|
|
68597
68600
|
if (code === 0 && stdout.trim()) {
|
|
68598
|
-
|
|
68601
|
+
log23.debug(`Quick query success: ${durationMs}ms, ${stdout.length} chars`);
|
|
68599
68602
|
resolve6({
|
|
68600
68603
|
success: true,
|
|
68601
68604
|
response: stdout.trim(),
|
|
68602
68605
|
durationMs
|
|
68603
68606
|
});
|
|
68604
68607
|
} else {
|
|
68605
|
-
|
|
68608
|
+
log23.debug(`Quick query failed: code=${code}, stderr=${stderr.substring(0, 100)}`);
|
|
68606
68609
|
resolve6({
|
|
68607
68610
|
success: false,
|
|
68608
68611
|
error: stderr || `exit code ${code}`,
|
|
@@ -68621,7 +68624,7 @@ init_logger();
|
|
|
68621
68624
|
import { exec as exec3 } from "child_process";
|
|
68622
68625
|
import { promisify as promisify3 } from "util";
|
|
68623
68626
|
var execAsync2 = promisify3(exec3);
|
|
68624
|
-
var
|
|
68627
|
+
var log24 = createLogger("branch");
|
|
68625
68628
|
var SUGGESTION_TIMEOUT = 15000;
|
|
68626
68629
|
var MAX_SUGGESTIONS = 3;
|
|
68627
68630
|
async function getCurrentBranch3(workingDir) {
|
|
@@ -68670,7 +68673,7 @@ function parseBranchSuggestions(response) {
|
|
|
68670
68673
|
return lines.slice(0, MAX_SUGGESTIONS);
|
|
68671
68674
|
}
|
|
68672
68675
|
async function suggestBranchNames(workingDir, userMessage) {
|
|
68673
|
-
|
|
68676
|
+
log24.debug(`Suggesting branch names for: "${userMessage.substring(0, 50)}..."`);
|
|
68674
68677
|
try {
|
|
68675
68678
|
const [currentBranch, recentCommits] = await Promise.all([
|
|
68676
68679
|
getCurrentBranch3(workingDir),
|
|
@@ -68684,24 +68687,24 @@ async function suggestBranchNames(workingDir, userMessage) {
|
|
|
68684
68687
|
workingDir
|
|
68685
68688
|
});
|
|
68686
68689
|
if (!result.success || !result.response) {
|
|
68687
|
-
|
|
68690
|
+
log24.debug(`Branch suggestion failed: ${result.error || "no response"}`);
|
|
68688
68691
|
return [];
|
|
68689
68692
|
}
|
|
68690
68693
|
const suggestions = parseBranchSuggestions(result.response);
|
|
68691
|
-
|
|
68694
|
+
log24.debug(`Got ${suggestions.length} branch suggestions: ${suggestions.join(", ")}`);
|
|
68692
68695
|
return suggestions;
|
|
68693
68696
|
} catch (err) {
|
|
68694
|
-
|
|
68697
|
+
log24.debug(`Branch suggestion error: ${err}`);
|
|
68695
68698
|
return [];
|
|
68696
68699
|
}
|
|
68697
68700
|
}
|
|
68698
68701
|
|
|
68699
68702
|
// src/operations/worktree/handler.ts
|
|
68700
68703
|
init_worktree();
|
|
68701
|
-
import { randomUUID as
|
|
68704
|
+
import { randomUUID as randomUUID4 } from "crypto";
|
|
68702
68705
|
init_logger();
|
|
68703
|
-
var
|
|
68704
|
-
var sessionLog2 = createSessionLog(
|
|
68706
|
+
var log25 = createLogger("worktree");
|
|
68707
|
+
var sessionLog2 = createSessionLog(log25);
|
|
68705
68708
|
function parseWorktreeError(error) {
|
|
68706
68709
|
const message = error instanceof Error ? error.message : String(error);
|
|
68707
68710
|
const lowerMessage = message.toLowerCase();
|
|
@@ -68945,7 +68948,7 @@ async function createAndSwitchToWorktree(session, branch, username, options) {
|
|
|
68945
68948
|
transitionTo(session, "restarting");
|
|
68946
68949
|
await session.claude.kill();
|
|
68947
68950
|
await options.flush(session);
|
|
68948
|
-
const newSessionId =
|
|
68951
|
+
const newSessionId = randomUUID4();
|
|
68949
68952
|
session.claudeSessionId = newSessionId;
|
|
68950
68953
|
const needsTitlePrompt = !session.sessionTitle;
|
|
68951
68954
|
const memoryConfig = options.getPlatformMemoryConfig(session.platformId);
|
|
@@ -69044,7 +69047,7 @@ ${fmt.formatItalic("Claude Code restarted in the worktree")}`);
|
|
|
69044
69047
|
transitionTo(session, "restarting");
|
|
69045
69048
|
await session.claude.kill();
|
|
69046
69049
|
await options.flush(session);
|
|
69047
|
-
const newSessionId =
|
|
69050
|
+
const newSessionId = randomUUID4();
|
|
69048
69051
|
session.claudeSessionId = newSessionId;
|
|
69049
69052
|
const needsTitlePrompt = !session.sessionTitle;
|
|
69050
69053
|
const memoryConfig = options.getPlatformMemoryConfig(session.platformId);
|
|
@@ -69283,8 +69286,8 @@ async function cleanupWorktreeCommand(session, username, hasOtherSessionsUsingWo
|
|
|
69283
69286
|
}
|
|
69284
69287
|
// src/operations/events/handler.ts
|
|
69285
69288
|
init_logger();
|
|
69286
|
-
var
|
|
69287
|
-
var sessionLog3 = createSessionLog(
|
|
69289
|
+
var log26 = createLogger("events");
|
|
69290
|
+
var sessionLog3 = createSessionLog(log26);
|
|
69288
69291
|
function detectAndExecuteClaudeCommands(text, session, ctx) {
|
|
69289
69292
|
const parsed = parseClaudeCommand(text);
|
|
69290
69293
|
if (parsed && isClaudeAllowedCommand(parsed.command)) {
|
|
@@ -69417,7 +69420,11 @@ function handleEventPostProcessing(session, event, ctx, mainHandling) {
|
|
|
69417
69420
|
ctx.ops.emitSessionUpdate(session.sessionId, { status: getSessionStatus(session) });
|
|
69418
69421
|
updateUsageStats(session, event, ctx);
|
|
69419
69422
|
if (mainHandling) {
|
|
69420
|
-
mainHandling.catch(() => {}).then(() =>
|
|
69423
|
+
mainHandling.catch(() => {}).then(() => {
|
|
69424
|
+
if (!ctx.state.sessions.has(session.sessionId))
|
|
69425
|
+
return;
|
|
69426
|
+
ctx.ops.persistSession(session);
|
|
69427
|
+
});
|
|
69421
69428
|
} else {
|
|
69422
69429
|
ctx.ops.persistSession(session);
|
|
69423
69430
|
}
|
|
@@ -69593,6 +69600,56 @@ function updateUsageFromStatusLine(session) {
|
|
|
69593
69600
|
sessionLog3(session).debug(`Updated from status line: context ${contextTokens}/${session.usageStats.contextWindowSize} (${contextPct}%)`);
|
|
69594
69601
|
}
|
|
69595
69602
|
}
|
|
69603
|
+
// src/operations/monitor/handler.ts
|
|
69604
|
+
init_logger();
|
|
69605
|
+
var log27 = createLogger("monitor");
|
|
69606
|
+
var DEFAULT_INTERVAL_MS2 = 60 * 1000;
|
|
69607
|
+
|
|
69608
|
+
class SessionMonitor {
|
|
69609
|
+
intervalMs;
|
|
69610
|
+
sessionTimeoutMs;
|
|
69611
|
+
sessionWarningMs;
|
|
69612
|
+
getContext;
|
|
69613
|
+
getSessionCount;
|
|
69614
|
+
updateStickyMessage;
|
|
69615
|
+
timer = null;
|
|
69616
|
+
isRunning = false;
|
|
69617
|
+
constructor(options) {
|
|
69618
|
+
this.intervalMs = options.intervalMs ?? DEFAULT_INTERVAL_MS2;
|
|
69619
|
+
this.sessionTimeoutMs = options.sessionTimeoutMs;
|
|
69620
|
+
this.sessionWarningMs = options.sessionWarningMs;
|
|
69621
|
+
this.getContext = options.getContext;
|
|
69622
|
+
this.getSessionCount = options.getSessionCount;
|
|
69623
|
+
this.updateStickyMessage = options.updateStickyMessage;
|
|
69624
|
+
}
|
|
69625
|
+
start() {
|
|
69626
|
+
if (this.isRunning) {
|
|
69627
|
+
log27.debug("Session monitor already running");
|
|
69628
|
+
return;
|
|
69629
|
+
}
|
|
69630
|
+
this.isRunning = true;
|
|
69631
|
+
log27.debug(`Session monitor started (interval: ${this.intervalMs / 1000}s)`);
|
|
69632
|
+
this.timer = setInterval(() => {
|
|
69633
|
+
this.runCheck().catch((err) => {
|
|
69634
|
+
log27.error(`Error during session monitoring: ${err}`);
|
|
69635
|
+
});
|
|
69636
|
+
}, this.intervalMs);
|
|
69637
|
+
}
|
|
69638
|
+
stop() {
|
|
69639
|
+
if (this.timer) {
|
|
69640
|
+
clearInterval(this.timer);
|
|
69641
|
+
this.timer = null;
|
|
69642
|
+
}
|
|
69643
|
+
this.isRunning = false;
|
|
69644
|
+
log27.debug("Session monitor stopped");
|
|
69645
|
+
}
|
|
69646
|
+
async runCheck() {
|
|
69647
|
+
await cleanupIdleSessions(this.sessionTimeoutMs, this.sessionWarningMs, this.getContext());
|
|
69648
|
+
if (this.getSessionCount() > 0) {
|
|
69649
|
+
await this.updateStickyMessage();
|
|
69650
|
+
}
|
|
69651
|
+
}
|
|
69652
|
+
}
|
|
69596
69653
|
// src/operations/session-context/types.ts
|
|
69597
69654
|
function createSessionContext(config, state, ops) {
|
|
69598
69655
|
return {
|
|
@@ -69994,9 +70051,90 @@ async function suggestSessionMetadata(context) {
|
|
|
69994
70051
|
return null;
|
|
69995
70052
|
}
|
|
69996
70053
|
}
|
|
70054
|
+
// src/routines/parser.ts
|
|
70055
|
+
init_logger();
|
|
70056
|
+
var log31 = createLogger("routines");
|
|
70057
|
+
var PARSE_TIMEOUT_MS = 15000;
|
|
70058
|
+
function hostTimezone() {
|
|
70059
|
+
return Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC";
|
|
70060
|
+
}
|
|
70061
|
+
function buildParsePrompt(request, defaultTimezone) {
|
|
70062
|
+
return `Parse this scheduled-routine request from a chat user into JSON.
|
|
70063
|
+
|
|
70064
|
+
Request: ${request}
|
|
70065
|
+
|
|
70066
|
+
Output ONLY a JSON object, no other text, with exactly these fields:
|
|
70067
|
+
- "name": short descriptive name for the routine (max 6 words)
|
|
70068
|
+
- "prompt": the task to perform on each run, as an instruction (everything that is not the schedule)
|
|
70069
|
+
- "preset": one of "hourly", "daily", "weekdays", "weekly" (the closest match; sub-hourly is not supported — if the user asked for more often than hourly, use "hourly")
|
|
70070
|
+
- "time": "HH:MM" 24-hour (omit for hourly)
|
|
70071
|
+
- "weekday": 1-7 where 1=Monday (only for weekly)
|
|
70072
|
+
- "timezone": IANA timezone ONLY if the user named one (e.g. "9am Pacific" -> "America/Los_Angeles"); otherwise omit and ${defaultTimezone} will be used
|
|
70073
|
+
|
|
70074
|
+
If the request is not actually asking for a recurring schedule, output exactly: {"error": "reason"}`;
|
|
70075
|
+
}
|
|
70076
|
+
function extractJsonObject(output) {
|
|
70077
|
+
const start = output.indexOf("{");
|
|
70078
|
+
const end = output.lastIndexOf("}");
|
|
70079
|
+
if (start < 0 || end <= start)
|
|
70080
|
+
return;
|
|
70081
|
+
try {
|
|
70082
|
+
const parsed = JSON.parse(output.slice(start, end + 1));
|
|
70083
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : undefined;
|
|
70084
|
+
} catch {
|
|
70085
|
+
return;
|
|
70086
|
+
}
|
|
70087
|
+
}
|
|
70088
|
+
function validateParsedRoutine(raw, defaultTimezone) {
|
|
70089
|
+
if (typeof raw.error === "string" && raw.error) {
|
|
70090
|
+
return { ok: false, error: raw.error };
|
|
70091
|
+
}
|
|
70092
|
+
const name = typeof raw.name === "string" ? raw.name.trim() : "";
|
|
70093
|
+
const prompt = typeof raw.prompt === "string" ? raw.prompt.trim() : "";
|
|
70094
|
+
const preset = raw.preset;
|
|
70095
|
+
if (!name)
|
|
70096
|
+
return { ok: false, error: "could not derive a routine name" };
|
|
70097
|
+
if (!prompt)
|
|
70098
|
+
return { ok: false, error: "could not tell what the routine should do" };
|
|
70099
|
+
if (!SCHEDULE_PRESETS.includes(preset)) {
|
|
70100
|
+
return { ok: false, error: `could not map the schedule to ${SCHEDULE_PRESETS.join("/")}` };
|
|
70101
|
+
}
|
|
70102
|
+
const timezoneDefaulted = !(typeof raw.timezone === "string" && raw.timezone);
|
|
70103
|
+
const timezone = timezoneDefaulted ? defaultTimezone : raw.timezone;
|
|
70104
|
+
if (!isValidTimezone(timezone)) {
|
|
70105
|
+
return { ok: false, error: `unknown timezone "${String(raw.timezone)}"` };
|
|
70106
|
+
}
|
|
70107
|
+
const schedule = {
|
|
70108
|
+
preset,
|
|
70109
|
+
timezone,
|
|
70110
|
+
...preset !== "hourly" && typeof raw.time === "string" ? { time: raw.time } : {},
|
|
70111
|
+
...preset === "weekly" && typeof raw.weekday === "number" ? { weekday: raw.weekday } : {}
|
|
70112
|
+
};
|
|
70113
|
+
const scheduleError = validateSchedule(schedule);
|
|
70114
|
+
if (scheduleError)
|
|
70115
|
+
return { ok: false, error: scheduleError };
|
|
70116
|
+
return { ok: true, parsed: { name, prompt, schedule }, timezoneDefaulted };
|
|
70117
|
+
}
|
|
70118
|
+
async function parseRoutineRequest(request, defaultTimezone = hostTimezone()) {
|
|
70119
|
+
const result = await quickQuery({
|
|
70120
|
+
prompt: buildParsePrompt(request, defaultTimezone),
|
|
70121
|
+
model: "haiku",
|
|
70122
|
+
timeout: PARSE_TIMEOUT_MS
|
|
70123
|
+
});
|
|
70124
|
+
if (!result.success || !result.response) {
|
|
70125
|
+
log31.debug(`Routine parse quickQuery failed: ${result.error ?? "no response"}`);
|
|
70126
|
+
return { ok: false, error: "could not reach the parsing model — try again in a moment" };
|
|
70127
|
+
}
|
|
70128
|
+
const raw = extractJsonObject(result.response);
|
|
70129
|
+
if (!raw) {
|
|
70130
|
+
return { ok: false, error: 'could not understand the schedule — try e.g. "every weekday at 9:00, <task>"' };
|
|
70131
|
+
}
|
|
70132
|
+
return validateParsedRoutine(raw, defaultTimezone);
|
|
70133
|
+
}
|
|
70134
|
+
|
|
69997
70135
|
// src/operations/commands/handler.ts
|
|
69998
|
-
var
|
|
69999
|
-
var sessionLog5 = createSessionLog(
|
|
70136
|
+
var log32 = createLogger("commands");
|
|
70137
|
+
var sessionLog5 = createSessionLog(log32);
|
|
70000
70138
|
function sessionAccountOption(session, ctx) {
|
|
70001
70139
|
if (!session.claudeAccountId)
|
|
70002
70140
|
return;
|
|
@@ -70173,7 +70311,7 @@ async function changeDirectory(session, newDir, username, ctx) {
|
|
|
70173
70311
|
sessionLog5(session).debug(`Stored work summary for context preservation`);
|
|
70174
70312
|
}
|
|
70175
70313
|
session.workingDir = absoluteDir;
|
|
70176
|
-
const newSessionId =
|
|
70314
|
+
const newSessionId = randomUUID5();
|
|
70177
70315
|
session.claudeSessionId = newSessionId;
|
|
70178
70316
|
const memoryConfig = ctx.ops.getPlatformMemoryConfig(session.platformId);
|
|
70179
70317
|
const appendSystemPrompt = await buildAppendSystemPrompt(session.platform, session.platformId, absoluteDir, session.threadId, session.startedBy, session.sessionAllowedUsers, CHAT_PLATFORM_PROMPT, ctx.state.githubEmailsStore, memoryConfig.enabled && memoryConfig.channelLayer ? ctx.state.memoryStore : null, { userAttribution: session.userAttribution });
|
|
@@ -70463,6 +70601,125 @@ ${list}${more}`);
|
|
|
70463
70601
|
await post(session, "warning", `\uD83E\uDDE0 No matching entry. Use ${formatter.formatCode("!memory")} to list entries, then ${formatter.formatCode("!memory forget <number>")}.`);
|
|
70464
70602
|
}
|
|
70465
70603
|
}
|
|
70604
|
+
async function requireRoutinesEnabled(session, ctx) {
|
|
70605
|
+
if (ctx.ops.isRoutinesEnabled(session.platformId))
|
|
70606
|
+
return true;
|
|
70607
|
+
await post(session, "info", `\uD83D\uDD58 Routines are disabled for this platform (see the \`routines\` option in config.yaml).`);
|
|
70608
|
+
return false;
|
|
70609
|
+
}
|
|
70610
|
+
async function createRoutine(session, request, username, ctx, parse = parseRoutineRequest) {
|
|
70611
|
+
if (!await requireRoutinesEnabled(session, ctx))
|
|
70612
|
+
return;
|
|
70613
|
+
if (!await requireSessionOwner(session, username, "create routines"))
|
|
70614
|
+
return;
|
|
70615
|
+
const formatter = session.platform.getFormatter();
|
|
70616
|
+
const trimmed = request.trim();
|
|
70617
|
+
if (!trimmed) {
|
|
70618
|
+
await post(session, "warning", `Usage: ${formatter.formatCode("!routine every weekday at 9:00, <task>")}`);
|
|
70619
|
+
return;
|
|
70620
|
+
}
|
|
70621
|
+
await post(session, "info", `\uD83D\uDD58 Parsing the schedule...`);
|
|
70622
|
+
const result = await parse(trimmed, hostTimezone());
|
|
70623
|
+
if (!result.ok) {
|
|
70624
|
+
await post(session, "warning", `\uD83D\uDD58 Could not create a routine: ${result.error}`);
|
|
70625
|
+
sessionLog5(session).warn(`\uD83D\uDD58 Routine parse failed for @${username}: ${result.error}`);
|
|
70626
|
+
return;
|
|
70627
|
+
}
|
|
70628
|
+
const { parsed, timezoneDefaulted } = result;
|
|
70629
|
+
const tzNote = timezoneDefaulted ? `
|
|
70630
|
+
${formatter.formatItalic(`Timezone defaulted to the bot host's ${parsed.schedule.timezone} — name one explicitly ("9am Pacific") to override.`)}` : "";
|
|
70631
|
+
const confirmPost = await postInteractiveAndRegister(session, `\uD83D\uDD58 ${formatter.formatBold(`Create routine "${parsed.name}"?`)}
|
|
70632
|
+
` + `${formatter.formatBold("Schedule:")} ${describeSchedule(parsed.schedule)}
|
|
70633
|
+
` + `${formatter.formatBold("Task:")} ${parsed.prompt}${tzNote}
|
|
70634
|
+
|
|
70635
|
+
` + `${formatter.formatItalic("Each run starts a full Claude session in a new thread. React \uD83D\uDC4D to save or \uD83D\uDC4E to discard.")}`, ["+1", "-1"], (postId, threadId) => ctx.ops.registerPost(postId, threadId));
|
|
70636
|
+
session.messageManager?.setPendingRoutinePrompt({
|
|
70637
|
+
postId: confirmPost.id,
|
|
70638
|
+
parsed,
|
|
70639
|
+
requestedBy: username
|
|
70640
|
+
});
|
|
70641
|
+
sessionLog5(session).info(`\uD83D\uDD58 Routine proposal posted for @${username}: "${parsed.name}"`);
|
|
70642
|
+
}
|
|
70643
|
+
function routineByIndex(ctx, platformId, arg) {
|
|
70644
|
+
if (!/^\d+$/.test(arg))
|
|
70645
|
+
return;
|
|
70646
|
+
const list = ctx.state.routinesStore.list(platformId);
|
|
70647
|
+
return list[parseInt(arg, 10) - 1];
|
|
70648
|
+
}
|
|
70649
|
+
async function manageRoutines(session, args, username, ctx) {
|
|
70650
|
+
if (!await requireRoutinesEnabled(session, ctx))
|
|
70651
|
+
return;
|
|
70652
|
+
const formatter = session.platform.getFormatter();
|
|
70653
|
+
const platformId = session.platformId;
|
|
70654
|
+
const trimmed = args?.trim();
|
|
70655
|
+
if (!trimmed) {
|
|
70656
|
+
const routines = ctx.state.routinesStore.list(platformId);
|
|
70657
|
+
if (routines.length === 0) {
|
|
70658
|
+
await post(session, "info", `\uD83D\uDD58 No routines yet. Create one with ${formatter.formatCode("!routine every weekday at 9:00, <task>")}.`);
|
|
70659
|
+
return;
|
|
70660
|
+
}
|
|
70661
|
+
const lines = routines.map((r, i) => {
|
|
70662
|
+
const status = r.enabled ? "" : " — ⏸️ paused";
|
|
70663
|
+
const last = r.lastRunAt ? ` · last run ${r.lastRunAt.slice(0, 16).replace("T", " ")}Z (${r.lastRunStatus})` : "";
|
|
70664
|
+
return `${i + 1}. ${formatter.formatBold(r.name)} — ${describeSchedule(r.schedule)} · by ${formatter.formatCode("@" + r.createdBy)}${status}${last}`;
|
|
70665
|
+
});
|
|
70666
|
+
await post(session, "info", `\uD83D\uDD58 ${formatter.formatBold(`Routines (${routines.length})`)} — each run starts a full Claude session in a new thread:
|
|
70667
|
+
|
|
70668
|
+
` + `${lines.join(`
|
|
70669
|
+
`)}
|
|
70670
|
+
|
|
70671
|
+
` + `${formatter.formatItalic(`Manage with ${"`!routines pause|resume|delete|run <n>`"}.`)}`);
|
|
70672
|
+
session.threadLogger?.logCommand("routines", "list", username);
|
|
70673
|
+
return;
|
|
70674
|
+
}
|
|
70675
|
+
const match = trimmed.match(/^(pause|resume|delete|run)\s+(\d+)$/i);
|
|
70676
|
+
if (!match) {
|
|
70677
|
+
await post(session, "warning", `\uD83D\uDD58 Usage: ${formatter.formatCode("!routines")} or ${formatter.formatCode("!routines pause|resume|delete|run <n>")}`);
|
|
70678
|
+
return;
|
|
70679
|
+
}
|
|
70680
|
+
const [, action, indexArg] = match;
|
|
70681
|
+
const routine = routineByIndex(ctx, platformId, indexArg);
|
|
70682
|
+
if (!routine) {
|
|
70683
|
+
await post(session, "warning", `\uD83D\uDD58 No routine ${indexArg}. See ${formatter.formatCode("!routines")}.`);
|
|
70684
|
+
return;
|
|
70685
|
+
}
|
|
70686
|
+
const lowered = action.toLowerCase();
|
|
70687
|
+
if (lowered !== "run" && !await requireSessionOwner(session, username, "manage routines")) {
|
|
70688
|
+
return;
|
|
70689
|
+
}
|
|
70690
|
+
if (lowered === "run" && !session.platform.isUserAllowed(username)) {
|
|
70691
|
+
await post(session, "warning", `\uD83D\uDD58 Only platform-allowed users can run routines (${formatter.formatCode("@" + username)} is invited to this session only).`);
|
|
70692
|
+
return;
|
|
70693
|
+
}
|
|
70694
|
+
switch (lowered) {
|
|
70695
|
+
case "pause":
|
|
70696
|
+
await ctx.state.routinesStore.update(platformId, routine.id, { enabled: false });
|
|
70697
|
+
await post(session, "success", `⏸️ Routine ${formatter.formatBold(routine.name)} paused.`);
|
|
70698
|
+
break;
|
|
70699
|
+
case "resume":
|
|
70700
|
+
await ctx.state.routinesStore.update(platformId, routine.id, { enabled: true, consecutiveFailures: 0 });
|
|
70701
|
+
await post(session, "success", `▶️ Routine ${formatter.formatBold(routine.name)} resumed.`);
|
|
70702
|
+
break;
|
|
70703
|
+
case "delete":
|
|
70704
|
+
await ctx.state.routinesStore.remove(platformId, routine.id);
|
|
70705
|
+
await post(session, "success", `\uD83D\uDDD1️ Routine ${formatter.formatBold(routine.name)} deleted.`);
|
|
70706
|
+
break;
|
|
70707
|
+
case "run": {
|
|
70708
|
+
await post(session, "info", `\uD83D\uDD58 Running ${formatter.formatBold(routine.name)} now — it will post in a new thread.`);
|
|
70709
|
+
const status = await ctx.ops.fireRoutineNow(platformId, routine);
|
|
70710
|
+
if (status === "skipped") {
|
|
70711
|
+
await post(session, "warning", `\uD83D\uDD58 Could not run now (session limit reached or platform busy) — try again shortly.`);
|
|
70712
|
+
} else if (status === "unauthorized") {
|
|
70713
|
+
await post(session, "warning", `\uD83D\uDD58 The routine's creator ${formatter.formatCode("@" + routine.createdBy)} is no longer authorized — routine disabled.`);
|
|
70714
|
+
} else if (status === "failed") {
|
|
70715
|
+
await post(session, "warning", `\uD83D\uDD58 The run failed to start — check the bot logs.`);
|
|
70716
|
+
}
|
|
70717
|
+
break;
|
|
70718
|
+
}
|
|
70719
|
+
}
|
|
70720
|
+
sessionLog5(session).info(`\uD83D\uDD58 @${username}: !routines ${lowered} ${indexArg} ("${routine.name}")`);
|
|
70721
|
+
session.threadLogger?.logCommand("routines", `${lowered} ${indexArg}`, username);
|
|
70722
|
+
}
|
|
70466
70723
|
async function setSessionPermissionMode(session, username, mode, ctx) {
|
|
70467
70724
|
if (!await requireSessionOwner(session, username, "change permissions")) {
|
|
70468
70725
|
return;
|
|
@@ -70763,7 +71020,7 @@ init_worktree();
|
|
|
70763
71020
|
|
|
70764
71021
|
// src/memory/distiller.ts
|
|
70765
71022
|
init_logger();
|
|
70766
|
-
var
|
|
71023
|
+
var log33 = createLogger("memory");
|
|
70767
71024
|
var MIN_THREAD_MESSAGES = 4;
|
|
70768
71025
|
var DISTILL_MESSAGE_LIMIT = 30;
|
|
70769
71026
|
var MESSAGE_CHAR_CAP = 500;
|
|
@@ -70810,10 +71067,10 @@ function scheduleDistillation(session, ctx, reason) {
|
|
|
70810
71067
|
const store = ctx.state.memoryStore;
|
|
70811
71068
|
distillThread(store, platformId, threadId, platform).then((added) => {
|
|
70812
71069
|
if (added > 0) {
|
|
70813
|
-
|
|
71070
|
+
log33.debug(`Distilled ${added} memory entries from ${platformId}:${threadId} (${reason})`);
|
|
70814
71071
|
}
|
|
70815
71072
|
}).catch((err) => {
|
|
70816
|
-
|
|
71073
|
+
log33.debug(`Distillation failed for ${platformId}:${threadId}: ${err.message}`);
|
|
70817
71074
|
});
|
|
70818
71075
|
}
|
|
70819
71076
|
async function distillThread(store, platformId, threadId, platform) {
|
|
@@ -70840,8 +71097,8 @@ async function distillThread(store, platformId, threadId, platform) {
|
|
|
70840
71097
|
}
|
|
70841
71098
|
|
|
70842
71099
|
// src/session/lifecycle.ts
|
|
70843
|
-
var
|
|
70844
|
-
var sessionLog6 = createSessionLog(
|
|
71100
|
+
var log34 = createLogger("lifecycle");
|
|
71101
|
+
var sessionLog6 = createSessionLog(log34);
|
|
70845
71102
|
function mutableSessions(ctx) {
|
|
70846
71103
|
return ctx.state.sessions;
|
|
70847
71104
|
}
|
|
@@ -70945,7 +71202,7 @@ async function createSessionDecisionBridge(ref) {
|
|
|
70945
71202
|
return messageManager.handleBridgeRequest(request, signal);
|
|
70946
71203
|
});
|
|
70947
71204
|
} catch (err) {
|
|
70948
|
-
|
|
71205
|
+
log34.warn(`Decision bridge unavailable — falling back to legacy MCP prompts: ${err}`);
|
|
70949
71206
|
return null;
|
|
70950
71207
|
}
|
|
70951
71208
|
}
|
|
@@ -71013,6 +71270,28 @@ function createMessageManager(session, ctx) {
|
|
|
71013
71270
|
sessionLog6(session).info(`@${fromUser} invited to session by @${approvedBy}`);
|
|
71014
71271
|
}
|
|
71015
71272
|
});
|
|
71273
|
+
messageManager.events.on("routine-prompt:complete", async ({ approved, parsed, requestedBy, postId }) => {
|
|
71274
|
+
session.threadLogger?.logCommand("routine", approved ? "created" : "discarded", requestedBy);
|
|
71275
|
+
if (!approved) {
|
|
71276
|
+
sessionLog6(session).info(`\uD83D\uDD58 Routine "${parsed.name}" discarded before saving`);
|
|
71277
|
+
return;
|
|
71278
|
+
}
|
|
71279
|
+
let result;
|
|
71280
|
+
try {
|
|
71281
|
+
result = await ctx.state.routinesStore.add(session.platformId, { name: parsed.name, prompt: parsed.prompt, schedule: parsed.schedule, createdBy: requestedBy }, ctx.config.maxRoutines);
|
|
71282
|
+
} catch (err) {
|
|
71283
|
+
result = { ok: false, error: `could not write the routines file (${err.message})` };
|
|
71284
|
+
}
|
|
71285
|
+
const formatter = session.platform.getFormatter();
|
|
71286
|
+
if (result.ok) {
|
|
71287
|
+
const position = ctx.state.routinesStore.list(session.platformId).length;
|
|
71288
|
+
await withErrorHandling(() => session.platform.updatePost(postId, `✅ ${formatter.formatBold(`Routine ${position}: ${result.routine.name}`)} saved — it will post its runs as new threads in this channel. ` + `${formatter.formatItalic(`Manage with ${"`!routines`"}. Each run starts a full Claude session.`)}`), { action: "Update routine confirmation post", session });
|
|
71289
|
+
sessionLog6(session).info(`\uD83D\uDD58 Routine "${result.routine.name}" saved by @${requestedBy}`);
|
|
71290
|
+
} else {
|
|
71291
|
+
await withErrorHandling(() => session.platform.updatePost(postId, `⚠️ Could not save routine: ${result.error}`), { action: "Update routine confirmation post", session });
|
|
71292
|
+
sessionLog6(session).warn(`\uD83D\uDD58 Routine save failed: ${result.error}`);
|
|
71293
|
+
}
|
|
71294
|
+
});
|
|
71016
71295
|
messageManager.events.on("context-prompt:complete", async ({ selection, queuedPrompt, queuedByUsername, queuedFiles: _queuedFiles, threadMessageCount: _threadMessageCount }) => {
|
|
71017
71296
|
const userTurn = formatUserTurn(queuedPrompt, queuedByUsername, shouldAttribute(session.userAttribution, session.sessionAllowedUsers.size));
|
|
71018
71297
|
let messageToSend = userTurn;
|
|
@@ -71210,7 +71489,7 @@ function resumeSessionHeaderMode(persisted, platformConfigured) {
|
|
|
71210
71489
|
function resolveSessionHeaderMode(configured, replyToPostId, platformId) {
|
|
71211
71490
|
const mode = configured ?? DEFAULT_OVERHEAD_VISIBILITY;
|
|
71212
71491
|
if (mode === "hidden" && !replyToPostId) {
|
|
71213
|
-
|
|
71492
|
+
log34.error(`sessionHeader: hidden requires a replyToPostId for ${platformId}; ` + `downgrading this session to 'minimal' so the header post is still short.`);
|
|
71214
71493
|
return "minimal";
|
|
71215
71494
|
}
|
|
71216
71495
|
return mode;
|
|
@@ -71229,7 +71508,7 @@ async function startSession(options, username, displayName, replyToPostId, platf
|
|
|
71229
71508
|
throw new Error(`Platform '${platformId}' not found. Call addPlatform() first.`);
|
|
71230
71509
|
}
|
|
71231
71510
|
if (!isAuthorizedForSession({ username, platform, sessionAllowedUsers: undefined })) {
|
|
71232
|
-
|
|
71511
|
+
log34.warn(`auth.denied.startSession: @${username || "unknown"} not authorized to start session in ${threadId.substring(0, 8)}...`);
|
|
71233
71512
|
return;
|
|
71234
71513
|
}
|
|
71235
71514
|
const activeOrPending = ctx.state.sessions.size + pendingStartsCount;
|
|
@@ -71258,7 +71537,7 @@ async function startSession(options, username, displayName, replyToPostId, platf
|
|
|
71258
71537
|
const actualThreadId = replyToPostId || (startPost ? startPost.id : "");
|
|
71259
71538
|
const sessionId = ctx.ops.getSessionId(platformId, actualThreadId);
|
|
71260
71539
|
platform.sendTyping(actualThreadId);
|
|
71261
|
-
const claudeSessionId =
|
|
71540
|
+
const claudeSessionId = randomUUID6();
|
|
71262
71541
|
let workingDir = ctx.config.workingDir;
|
|
71263
71542
|
let permissionMode = ctx.config.permissionMode;
|
|
71264
71543
|
let forceInteractivePermissions = false;
|
|
@@ -71290,17 +71569,17 @@ async function startSession(options, username, displayName, replyToPostId, platf
|
|
|
71290
71569
|
return;
|
|
71291
71570
|
}
|
|
71292
71571
|
workingDir = resolvedDir;
|
|
71293
|
-
|
|
71572
|
+
log34.info(`Starting session in directory: ${workingDir} (from !cd command)`);
|
|
71294
71573
|
}
|
|
71295
71574
|
if (initialOptions?.permissionMode) {
|
|
71296
71575
|
permissionMode = initialOptions.permissionMode;
|
|
71297
71576
|
forceInteractivePermissions = permissionMode === "default";
|
|
71298
71577
|
sessionPermissionModeOverride = permissionMode;
|
|
71299
|
-
|
|
71578
|
+
log34.info(`Starting session with permission mode "${permissionMode}" (from !permissions command)`);
|
|
71300
71579
|
} else if (initialOptions?.forceInteractivePermissions) {
|
|
71301
71580
|
forceInteractivePermissions = true;
|
|
71302
71581
|
permissionMode = "default";
|
|
71303
|
-
|
|
71582
|
+
log34.info(`Starting session with interactive permissions (from !permissions command)`);
|
|
71304
71583
|
}
|
|
71305
71584
|
const userAttribution = ctx.config.userAttribution ?? true;
|
|
71306
71585
|
const memoryConfig = ctx.ops.getPlatformMemoryConfig(platformId);
|
|
@@ -71311,7 +71590,7 @@ async function startSession(options, username, displayName, replyToPostId, platf
|
|
|
71311
71590
|
balanceByUsage: true
|
|
71312
71591
|
});
|
|
71313
71592
|
if (claudeAccount) {
|
|
71314
|
-
|
|
71593
|
+
log34.info(`Session ${sessionId.substring(0, 20)} reserved Claude account "${claudeAccount.id}"`);
|
|
71315
71594
|
}
|
|
71316
71595
|
const bridgeSessionRef = {};
|
|
71317
71596
|
const decisionBridge = await createSessionDecisionBridge(bridgeSessionRef);
|
|
@@ -71442,28 +71721,28 @@ async function resumeSession(state, ctx) {
|
|
|
71442
71721
|
!state.claudeSessionId && "claudeSessionId",
|
|
71443
71722
|
!state.workingDir && "workingDir"
|
|
71444
71723
|
].filter(Boolean).join(", ");
|
|
71445
|
-
|
|
71724
|
+
log34.warn(`Skipping session with missing required fields: ${missing}`);
|
|
71446
71725
|
return;
|
|
71447
71726
|
}
|
|
71448
71727
|
const shortId = state.threadId.substring(0, 8);
|
|
71449
71728
|
const platforms = ctx.state.platforms;
|
|
71450
71729
|
const platform = platforms.get(state.platformId);
|
|
71451
71730
|
if (!platform) {
|
|
71452
|
-
|
|
71731
|
+
log34.warn(`Platform ${state.platformId} not registered, skipping resume for ${shortId}...`);
|
|
71453
71732
|
return;
|
|
71454
71733
|
}
|
|
71455
71734
|
const threadPost = await platform.getPost(state.threadId);
|
|
71456
71735
|
if (!threadPost) {
|
|
71457
|
-
|
|
71736
|
+
log34.warn(`Thread ${shortId}... deleted, skipping resume`);
|
|
71458
71737
|
ctx.state.sessionStore.remove(`${state.platformId}:${state.threadId}`);
|
|
71459
71738
|
return;
|
|
71460
71739
|
}
|
|
71461
71740
|
if (ctx.state.sessions.size >= ctx.config.maxSessions) {
|
|
71462
|
-
|
|
71741
|
+
log34.warn(`Max sessions reached, skipping resume for ${shortId}...`);
|
|
71463
71742
|
return;
|
|
71464
71743
|
}
|
|
71465
71744
|
if (!existsSync13(state.workingDir)) {
|
|
71466
|
-
|
|
71745
|
+
log34.warn(`Working directory ${state.workingDir} no longer exists, skipping resume for ${shortId}...`);
|
|
71467
71746
|
ctx.state.sessionStore.remove(`${state.platformId}:${state.threadId}`);
|
|
71468
71747
|
const resumeFormatter = platform.getFormatter();
|
|
71469
71748
|
const tempSession = {
|
|
@@ -71486,7 +71765,7 @@ Please start a new session.`), { action: "Post resume failure notification" });
|
|
|
71486
71765
|
const appendSystemPrompt = await buildAppendSystemPrompt(platform, state.platformId, state.workingDir, state.threadId, state.startedBy, state.sessionAllowedUsers || [state.startedBy], CHAT_PLATFORM_PROMPT, ctx.state.githubEmailsStore, memoryConfig.enabled && memoryConfig.channelLayer ? ctx.state.memoryStore : null, { userAttribution });
|
|
71487
71766
|
const claudeAccount = ctx.ops.acquireClaudeAccount(state.claudeAccountId, state.threadId);
|
|
71488
71767
|
if (state.claudeAccountId && !claudeAccount) {
|
|
71489
|
-
|
|
71768
|
+
log34.warn(`Persisted session referenced Claude account "${state.claudeAccountId}" ` + `which is no longer configured — resuming under default env`);
|
|
71490
71769
|
}
|
|
71491
71770
|
const resumeBridgeRef = {};
|
|
71492
71771
|
const resumeBridge = await createSessionDecisionBridge(resumeBridgeRef);
|
|
@@ -71569,7 +71848,7 @@ Please start a new session.`), { action: "Post resume failure notification" });
|
|
|
71569
71848
|
worktreePath: detected.worktreePath,
|
|
71570
71849
|
branch: detected.branch
|
|
71571
71850
|
};
|
|
71572
|
-
|
|
71851
|
+
log34.info(`Auto-detected worktree info for resumed session: branch=${detected.branch}`);
|
|
71573
71852
|
}
|
|
71574
71853
|
}
|
|
71575
71854
|
session.messageManager = createMessageManager(session, ctx);
|
|
@@ -71628,7 +71907,7 @@ ${sessionFormatter.formatItalic("Reconnected to Claude session. You can continue
|
|
|
71628
71907
|
await postResumeCoAuthorOnboarding(session, ctx);
|
|
71629
71908
|
ctx.ops.persistSession(session);
|
|
71630
71909
|
} catch (err) {
|
|
71631
|
-
|
|
71910
|
+
log34.error(`Failed to resume session ${shortId}`, err instanceof Error ? err : undefined);
|
|
71632
71911
|
session.messageManager?.dispose();
|
|
71633
71912
|
session.decisionBridge?.close();
|
|
71634
71913
|
session.decisionBridge = undefined;
|
|
@@ -71671,28 +71950,28 @@ async function resumePausedSession(threadId, message, files, ctx, username) {
|
|
|
71671
71950
|
const persisted = ctx.state.sessionStore.load();
|
|
71672
71951
|
const state = findPersistedByThreadId(persisted, threadId);
|
|
71673
71952
|
if (!state) {
|
|
71674
|
-
|
|
71953
|
+
log34.debug(`No persisted session found for ${threadId.substring(0, 8)}...`);
|
|
71675
71954
|
return;
|
|
71676
71955
|
}
|
|
71677
71956
|
const shortId = threadId.substring(0, 8);
|
|
71678
71957
|
const platform = ctx.state.platforms.get(state.platformId);
|
|
71679
71958
|
if (!platform) {
|
|
71680
|
-
|
|
71959
|
+
log34.warn(`auth.denied.resume: platform '${state.platformId}' not found for ${shortId}...`);
|
|
71681
71960
|
return;
|
|
71682
71961
|
}
|
|
71683
71962
|
const sessionAllowedUsers = new Set(state.sessionAllowedUsers || [state.startedBy].filter(Boolean));
|
|
71684
71963
|
if (!isAuthorizedForSession({ username, platform, sessionAllowedUsers })) {
|
|
71685
|
-
|
|
71964
|
+
log34.warn(`auth.denied.resume: @${username || "unknown"} not authorized to resume ${shortId}...`);
|
|
71686
71965
|
return;
|
|
71687
71966
|
}
|
|
71688
|
-
|
|
71967
|
+
log34.info(`\uD83D\uDD04 Resuming paused session ${shortId}... for new message`);
|
|
71689
71968
|
await resumeSession(state, ctx);
|
|
71690
71969
|
const session = ctx.ops.findSessionByThreadId(threadId);
|
|
71691
71970
|
if (session && session.claude.isRunning() && session.messageManager) {
|
|
71692
71971
|
session.messageCount++;
|
|
71693
71972
|
await session.messageManager.handleUserMessage(message, files, username);
|
|
71694
71973
|
} else {
|
|
71695
|
-
|
|
71974
|
+
log34.warn(`Failed to resume session ${shortId}..., could not send message`);
|
|
71696
71975
|
}
|
|
71697
71976
|
}
|
|
71698
71977
|
async function handleExit(sessionId, code, ctx, source) {
|
|
@@ -71700,7 +71979,7 @@ async function handleExit(sessionId, code, ctx, source) {
|
|
|
71700
71979
|
const shortId = sessionId.substring(0, 8);
|
|
71701
71980
|
sessionLog6(session).debug(`handleExit called code=${code} isShuttingDown=${ctx.state.isShuttingDown}`);
|
|
71702
71981
|
if (!session) {
|
|
71703
|
-
|
|
71982
|
+
log34.debug(`Session ${shortId}... not found (already cleaned up)`);
|
|
71704
71983
|
return;
|
|
71705
71984
|
}
|
|
71706
71985
|
if (source && session.claude !== source) {
|
|
@@ -71901,37 +72180,338 @@ async function cleanupIdleSessions(timeoutMs, warningMs, ctx) {
|
|
|
71901
72180
|
}
|
|
71902
72181
|
}
|
|
71903
72182
|
|
|
71904
|
-
// src/
|
|
71905
|
-
|
|
71906
|
-
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.]
|
|
71907
72206
|
|
|
71908
|
-
|
|
72207
|
+
${routine.prompt}`,
|
|
72208
|
+
skipWorktreePrompt: true
|
|
72209
|
+
}, routine.createdBy, undefined, rootPost.id, platformId, ctx);
|
|
72210
|
+
if (!ctx.state.sessions.has(ctx.ops.getSessionId(platformId, rootPost.id))) {
|
|
72211
|
+
log35.debug(`Routine "${routine.name}": startSession declined to start a session — skipping this tick`);
|
|
72212
|
+
return "skipped";
|
|
72213
|
+
}
|
|
72214
|
+
return "ok";
|
|
72215
|
+
}
|
|
72216
|
+
|
|
72217
|
+
// src/claude/account-pool.ts
|
|
72218
|
+
init_logger();
|
|
72219
|
+
|
|
72220
|
+
// src/claude/usage-probe.ts
|
|
72221
|
+
init_spawn();
|
|
72222
|
+
init_logger();
|
|
72223
|
+
var log36 = createLogger("usage-probe");
|
|
72224
|
+
var DEFAULT_USAGE_PROBE_TIMEOUT_MS = 30000;
|
|
72225
|
+
function parseUsageOutput(text) {
|
|
72226
|
+
if (!text)
|
|
72227
|
+
return null;
|
|
72228
|
+
const sessionMatch = text.match(/Current session:\s*(\d+)%\s*used(?:\s*·\s*resets\s*([^\n]+?))?\s*(?:\n|$)/i);
|
|
72229
|
+
const weekAllMatch = text.match(/Current week(?: \(all models\))?:\s*(\d+)%\s*used(?:\s*·\s*resets\s*([^\n]+?))?\s*(?:\n|$)/i);
|
|
72230
|
+
if (!sessionMatch && !weekAllMatch)
|
|
72231
|
+
return null;
|
|
72232
|
+
const sessionPct = sessionMatch ? clampPct(Number(sessionMatch[1])) : 0;
|
|
72233
|
+
const weekAllModelsPct = weekAllMatch ? clampPct(Number(weekAllMatch[1])) : 0;
|
|
72234
|
+
let weekPerModelPct = null;
|
|
72235
|
+
const perModelRe = /Current week \((?!all models\))[^)]+\):\s*(\d+)%\s*used/gi;
|
|
72236
|
+
for (const m of text.matchAll(perModelRe)) {
|
|
72237
|
+
const pct = clampPct(Number(m[1]));
|
|
72238
|
+
weekPerModelPct = weekPerModelPct === null ? pct : Math.max(weekPerModelPct, pct);
|
|
72239
|
+
}
|
|
72240
|
+
return {
|
|
72241
|
+
sessionPct,
|
|
72242
|
+
weekAllModelsPct,
|
|
72243
|
+
weekPerModelPct,
|
|
72244
|
+
sessionResetsAt: sessionMatch?.[2]?.trim() || null,
|
|
72245
|
+
weekResetsAt: weekAllMatch?.[2]?.trim() || null
|
|
72246
|
+
};
|
|
72247
|
+
}
|
|
72248
|
+
function usageLoadScore(usage) {
|
|
72249
|
+
return Math.max(usage.sessionPct, usage.weekAllModelsPct, usage.weekPerModelPct ?? 0);
|
|
72250
|
+
}
|
|
72251
|
+
function clampPct(n) {
|
|
72252
|
+
if (!Number.isFinite(n))
|
|
72253
|
+
return 0;
|
|
72254
|
+
return Math.max(0, Math.min(100, Math.round(n)));
|
|
72255
|
+
}
|
|
72256
|
+
async function probeAccountUsage(account, opts = {}) {
|
|
72257
|
+
const timeoutMs = opts.timeoutMs ?? DEFAULT_USAGE_PROBE_TIMEOUT_MS;
|
|
72258
|
+
const claudePath = getClaudePath();
|
|
72259
|
+
const env4 = buildClaudeChildEnv(process.env, account);
|
|
72260
|
+
return new Promise((resolve7) => {
|
|
72261
|
+
let settled = false;
|
|
72262
|
+
const finish = (value) => {
|
|
72263
|
+
if (settled)
|
|
72264
|
+
return;
|
|
72265
|
+
settled = true;
|
|
72266
|
+
clearTimeout(timer);
|
|
72267
|
+
resolve7(value);
|
|
72268
|
+
};
|
|
72269
|
+
let child;
|
|
72270
|
+
try {
|
|
72271
|
+
child = crossSpawn(claudePath, ["-p", "/usage", "--output-format", "json"], {
|
|
72272
|
+
env: env4,
|
|
72273
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
72274
|
+
});
|
|
72275
|
+
} catch (err) {
|
|
72276
|
+
log36.warn(`Failed to spawn /usage probe for "${account.id}": ${err}`);
|
|
72277
|
+
resolve7(null);
|
|
72278
|
+
return;
|
|
72279
|
+
}
|
|
72280
|
+
const timer = setTimeout(() => {
|
|
72281
|
+
log36.warn(`/usage probe for "${account.id}" timed out after ${timeoutMs}ms`);
|
|
72282
|
+
try {
|
|
72283
|
+
child.kill("SIGKILL");
|
|
72284
|
+
} catch {}
|
|
72285
|
+
finish(null);
|
|
72286
|
+
}, timeoutMs);
|
|
72287
|
+
let stdout = "";
|
|
72288
|
+
child.stdout?.on("data", (chunk) => {
|
|
72289
|
+
stdout += chunk.toString();
|
|
72290
|
+
});
|
|
72291
|
+
child.stderr?.on("data", () => {});
|
|
72292
|
+
child.on("error", (err) => {
|
|
72293
|
+
log36.warn(`/usage probe for "${account.id}" errored: ${err}`);
|
|
72294
|
+
finish(null);
|
|
72295
|
+
});
|
|
72296
|
+
child.on("close", () => {
|
|
72297
|
+
const usage = extractUsage(stdout);
|
|
72298
|
+
if (!usage) {
|
|
72299
|
+
log36.debug(`/usage probe for "${account.id}" returned no parseable usage`);
|
|
72300
|
+
}
|
|
72301
|
+
finish(usage);
|
|
72302
|
+
});
|
|
72303
|
+
});
|
|
72304
|
+
}
|
|
72305
|
+
function extractUsage(stdout) {
|
|
72306
|
+
const trimmed = stdout.trim();
|
|
72307
|
+
if (!trimmed)
|
|
72308
|
+
return null;
|
|
72309
|
+
let text = trimmed;
|
|
72310
|
+
try {
|
|
72311
|
+
const parsed = JSON.parse(trimmed);
|
|
72312
|
+
if (typeof parsed.result === "string") {
|
|
72313
|
+
text = parsed.result;
|
|
72314
|
+
}
|
|
72315
|
+
} catch {}
|
|
72316
|
+
return parseUsageOutput(text);
|
|
72317
|
+
}
|
|
72318
|
+
|
|
72319
|
+
// src/claude/account-pool.ts
|
|
72320
|
+
var log37 = createLogger("account-pool");
|
|
72321
|
+
var ACTIVE_SESSION_LOAD_PENALTY = 5;
|
|
72322
|
+
function hashThreadId(threadId) {
|
|
72323
|
+
let h = 2166136261;
|
|
72324
|
+
for (let i = 0;i < threadId.length; i++) {
|
|
72325
|
+
h ^= threadId.charCodeAt(i);
|
|
72326
|
+
h = Math.imul(h, 16777619);
|
|
72327
|
+
}
|
|
72328
|
+
return h >>> 0;
|
|
72329
|
+
}
|
|
72330
|
+
|
|
72331
|
+
class AccountPool {
|
|
72332
|
+
accounts;
|
|
72333
|
+
byId;
|
|
72334
|
+
orderIndex;
|
|
72335
|
+
activeCounts = new Map;
|
|
72336
|
+
coolingUntil = new Map;
|
|
72337
|
+
usage = new Map;
|
|
72338
|
+
rrCursor = 0;
|
|
72339
|
+
constructor(accounts) {
|
|
72340
|
+
this.accounts = (accounts ?? []).filter((acc) => {
|
|
72341
|
+
const hasAuth = !!acc.home || !!acc.apiKey;
|
|
72342
|
+
if (!hasAuth) {
|
|
72343
|
+
log37.warn(`Claude account ${acc.id} has neither home nor apiKey — ignoring`);
|
|
72344
|
+
return false;
|
|
72345
|
+
}
|
|
72346
|
+
if (acc.home && acc.apiKey) {
|
|
72347
|
+
log37.warn(`Claude account ${acc.id} has both home and apiKey set — must choose one; ignoring`);
|
|
72348
|
+
return false;
|
|
72349
|
+
}
|
|
72350
|
+
return true;
|
|
72351
|
+
});
|
|
72352
|
+
this.byId = new Map(this.accounts.map((acc) => [acc.id, acc]));
|
|
72353
|
+
this.orderIndex = new Map(this.accounts.map((acc, i) => [acc.id, i]));
|
|
72354
|
+
for (const acc of this.accounts) {
|
|
72355
|
+
this.activeCounts.set(acc.id, 0);
|
|
72356
|
+
this.usage.set(acc.id, null);
|
|
72357
|
+
}
|
|
72358
|
+
}
|
|
72359
|
+
get isEmpty() {
|
|
72360
|
+
return this.accounts.length === 0;
|
|
72361
|
+
}
|
|
72362
|
+
get size() {
|
|
72363
|
+
return this.accounts.length;
|
|
72364
|
+
}
|
|
72365
|
+
get all() {
|
|
72366
|
+
return this.accounts;
|
|
72367
|
+
}
|
|
72368
|
+
acquire(preferredId, threadId, opts) {
|
|
72369
|
+
if (this.isEmpty)
|
|
72370
|
+
return null;
|
|
72371
|
+
if (preferredId) {
|
|
72372
|
+
const preferred = this.byId.get(preferredId);
|
|
72373
|
+
if (preferred) {
|
|
72374
|
+
this.incrementActive(preferred.id);
|
|
72375
|
+
return preferred;
|
|
72376
|
+
}
|
|
72377
|
+
log37.warn(`Preferred account "${preferredId}" not in pool — falling back to usage balancing`);
|
|
72378
|
+
}
|
|
72379
|
+
const now = Date.now();
|
|
72380
|
+
const n = this.accounts.length;
|
|
72381
|
+
if (threadId && !opts?.balanceByUsage) {
|
|
72382
|
+
const sticky = this.accounts[hashThreadId(threadId) % n];
|
|
72383
|
+
const cooling = this.coolingUntil.get(sticky.id) ?? 0;
|
|
72384
|
+
if (cooling <= now) {
|
|
72385
|
+
this.incrementActive(sticky.id);
|
|
72386
|
+
return sticky;
|
|
72387
|
+
}
|
|
72388
|
+
}
|
|
72389
|
+
const chosen = this.selectLeastLoaded(now);
|
|
72390
|
+
if (!chosen) {
|
|
72391
|
+
log37.warn(`All ${n} accounts are in rate-limit cooldown`);
|
|
72392
|
+
return null;
|
|
72393
|
+
}
|
|
72394
|
+
this.incrementActive(chosen.id);
|
|
72395
|
+
return chosen;
|
|
72396
|
+
}
|
|
72397
|
+
selectLeastLoaded(now) {
|
|
72398
|
+
const n = this.accounts.length;
|
|
72399
|
+
let best = null;
|
|
72400
|
+
let bestScore = Number.POSITIVE_INFINITY;
|
|
72401
|
+
let bestIdx = -1;
|
|
72402
|
+
for (let k = 0;k < n; k++) {
|
|
72403
|
+
const idx = (this.rrCursor + k) % n;
|
|
72404
|
+
const acc = this.accounts[idx];
|
|
72405
|
+
if ((this.coolingUntil.get(acc.id) ?? 0) > now)
|
|
72406
|
+
continue;
|
|
72407
|
+
const score = this.effectiveLoad(acc.id);
|
|
72408
|
+
if (best === null || score < bestScore) {
|
|
72409
|
+
best = acc;
|
|
72410
|
+
bestScore = score;
|
|
72411
|
+
bestIdx = idx;
|
|
72412
|
+
}
|
|
72413
|
+
}
|
|
72414
|
+
if (bestIdx >= 0)
|
|
72415
|
+
this.rrCursor = (bestIdx + 1) % n;
|
|
72416
|
+
return best;
|
|
72417
|
+
}
|
|
72418
|
+
loadScore(accountId) {
|
|
72419
|
+
const u = this.usage.get(accountId);
|
|
72420
|
+
return u ? usageLoadScore(u) : Number.POSITIVE_INFINITY;
|
|
72421
|
+
}
|
|
72422
|
+
effectiveLoad(accountId) {
|
|
72423
|
+
const base = this.loadScore(accountId);
|
|
72424
|
+
const active = this.activeCounts.get(accountId) ?? 0;
|
|
72425
|
+
return base + ACTIVE_SESSION_LOAD_PENALTY * active;
|
|
72426
|
+
}
|
|
72427
|
+
release(accountId) {
|
|
72428
|
+
const current = this.activeCounts.get(accountId);
|
|
72429
|
+
if (current === undefined)
|
|
72430
|
+
return;
|
|
72431
|
+
this.activeCounts.set(accountId, Math.max(0, current - 1));
|
|
72432
|
+
}
|
|
72433
|
+
setUsage(accountId, usage) {
|
|
72434
|
+
if (!this.byId.has(accountId))
|
|
72435
|
+
return;
|
|
72436
|
+
this.usage.set(accountId, usage);
|
|
72437
|
+
if (usage) {
|
|
72438
|
+
log37.debug(`Account "${accountId}" usage: ${usageLoadScore(usage)}% (load score)`);
|
|
72439
|
+
}
|
|
72440
|
+
}
|
|
72441
|
+
markCooling(accountId, untilEpochMs) {
|
|
72442
|
+
if (!this.byId.has(accountId)) {
|
|
72443
|
+
log37.warn(`markCooling called for unknown account "${accountId}"`);
|
|
72444
|
+
return;
|
|
72445
|
+
}
|
|
72446
|
+
const existing = this.coolingUntil.get(accountId) ?? 0;
|
|
72447
|
+
if (untilEpochMs > existing) {
|
|
72448
|
+
this.coolingUntil.set(accountId, untilEpochMs);
|
|
72449
|
+
const minutes = Math.ceil((untilEpochMs - Date.now()) / 60000);
|
|
72450
|
+
log37.info(`Account "${accountId}" cooling for ~${minutes}min`);
|
|
72451
|
+
}
|
|
72452
|
+
}
|
|
72453
|
+
get(accountId) {
|
|
72454
|
+
return this.byId.get(accountId);
|
|
72455
|
+
}
|
|
72456
|
+
status() {
|
|
72457
|
+
const now = Date.now();
|
|
72458
|
+
return this.accounts.map((acc) => {
|
|
72459
|
+
const cooling = this.coolingUntil.get(acc.id) ?? 0;
|
|
72460
|
+
const usage = this.usage.get(acc.id) ?? null;
|
|
72461
|
+
return {
|
|
72462
|
+
id: acc.id,
|
|
72463
|
+
displayName: acc.displayName ?? acc.id,
|
|
72464
|
+
activeSessions: this.activeCounts.get(acc.id) ?? 0,
|
|
72465
|
+
coolingUntil: cooling > now ? cooling : null,
|
|
72466
|
+
usagePercent: usage ? usageLoadScore(usage) : null
|
|
72467
|
+
};
|
|
72468
|
+
});
|
|
72469
|
+
}
|
|
72470
|
+
incrementActive(accountId) {
|
|
72471
|
+
this.activeCounts.set(accountId, (this.activeCounts.get(accountId) ?? 0) + 1);
|
|
72472
|
+
}
|
|
72473
|
+
}
|
|
72474
|
+
|
|
72475
|
+
// src/cleanup/scheduler.ts
|
|
72476
|
+
init_logger();
|
|
72477
|
+
import { existsSync as existsSync14 } from "fs";
|
|
72478
|
+
import { readdir, rm as rm3 } from "fs/promises";
|
|
72479
|
+
import { join as join13 } from "path";
|
|
72480
|
+
init_worktree();
|
|
72481
|
+
var log38 = createLogger("cleanup");
|
|
72482
|
+
var DEFAULT_CLEANUP_INTERVAL_MS = 60 * 60 * 1000;
|
|
72483
|
+
var MAX_WORKTREE_AGE_MS = 24 * 60 * 60 * 1000;
|
|
72484
|
+
|
|
72485
|
+
class CleanupScheduler {
|
|
71909
72486
|
intervalMs;
|
|
71910
|
-
|
|
71911
|
-
|
|
71912
|
-
|
|
71913
|
-
|
|
71914
|
-
|
|
72487
|
+
logRetentionDays;
|
|
72488
|
+
threadLogsEnabled;
|
|
72489
|
+
sessionStore;
|
|
72490
|
+
maxWorktreeAgeMs;
|
|
72491
|
+
cleanupWorktrees;
|
|
71915
72492
|
timer = null;
|
|
71916
72493
|
isRunning = false;
|
|
71917
72494
|
constructor(options) {
|
|
71918
|
-
this.intervalMs = options.intervalMs ??
|
|
71919
|
-
this.
|
|
71920
|
-
this.
|
|
71921
|
-
this.
|
|
71922
|
-
this.
|
|
71923
|
-
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;
|
|
71924
72501
|
}
|
|
71925
72502
|
start() {
|
|
71926
72503
|
if (this.isRunning) {
|
|
71927
|
-
|
|
72504
|
+
log38.debug("Cleanup scheduler already running");
|
|
71928
72505
|
return;
|
|
71929
72506
|
}
|
|
71930
72507
|
this.isRunning = true;
|
|
71931
|
-
|
|
72508
|
+
log38.info(`Cleanup scheduler started (interval: ${Math.round(this.intervalMs / 60000)}min)`);
|
|
72509
|
+
this.runCleanup().catch((err) => {
|
|
72510
|
+
log38.warn(`Initial cleanup failed: ${err}`);
|
|
72511
|
+
});
|
|
71932
72512
|
this.timer = setInterval(() => {
|
|
71933
|
-
this.
|
|
71934
|
-
|
|
72513
|
+
this.runCleanup().catch((err) => {
|
|
72514
|
+
log38.warn(`Periodic cleanup failed: ${err}`);
|
|
71935
72515
|
});
|
|
71936
72516
|
}, this.intervalMs);
|
|
71937
72517
|
}
|
|
@@ -71941,20 +72521,141 @@ class SessionMonitor {
|
|
|
71941
72521
|
this.timer = null;
|
|
71942
72522
|
}
|
|
71943
72523
|
this.isRunning = false;
|
|
71944
|
-
|
|
72524
|
+
log38.debug("Cleanup scheduler stopped");
|
|
71945
72525
|
}
|
|
71946
|
-
async
|
|
71947
|
-
|
|
71948
|
-
|
|
71949
|
-
|
|
72526
|
+
async runCleanup() {
|
|
72527
|
+
const startTime = Date.now();
|
|
72528
|
+
log38.debug("Running background cleanup...");
|
|
72529
|
+
const stats = {
|
|
72530
|
+
logsDeleted: 0,
|
|
72531
|
+
worktreesCleaned: 0,
|
|
72532
|
+
metadataCleaned: 0,
|
|
72533
|
+
errors: []
|
|
72534
|
+
};
|
|
72535
|
+
const cleanupTasks = [
|
|
72536
|
+
this.cleanupLogs().catch((err) => {
|
|
72537
|
+
stats.errors.push(`Log cleanup: ${err}`);
|
|
72538
|
+
return 0;
|
|
72539
|
+
})
|
|
72540
|
+
];
|
|
72541
|
+
if (this.cleanupWorktrees) {
|
|
72542
|
+
cleanupTasks.push(this.cleanupOrphanedWorktrees().catch((err) => {
|
|
72543
|
+
stats.errors.push(`Worktree cleanup: ${err}`);
|
|
72544
|
+
return { cleaned: 0, metadata: 0 };
|
|
72545
|
+
}));
|
|
72546
|
+
}
|
|
72547
|
+
const [logStats, worktreeStats = { cleaned: 0, metadata: 0 }] = await Promise.all(cleanupTasks);
|
|
72548
|
+
stats.logsDeleted = logStats;
|
|
72549
|
+
stats.worktreesCleaned = worktreeStats.cleaned;
|
|
72550
|
+
stats.metadataCleaned = worktreeStats.metadata;
|
|
72551
|
+
const elapsed = Date.now() - startTime;
|
|
72552
|
+
const totalCleaned = stats.logsDeleted + stats.worktreesCleaned + stats.metadataCleaned;
|
|
72553
|
+
if (totalCleaned > 0 || stats.errors.length > 0) {
|
|
72554
|
+
log38.info(`Cleanup completed in ${elapsed}ms: ` + `${stats.logsDeleted} logs, ${stats.worktreesCleaned} worktrees, ${stats.metadataCleaned} metadata` + (stats.errors.length > 0 ? ` (${stats.errors.length} errors)` : ""));
|
|
72555
|
+
} else {
|
|
72556
|
+
log38.debug(`Cleanup completed in ${elapsed}ms (nothing to clean)`);
|
|
72557
|
+
}
|
|
72558
|
+
return stats;
|
|
72559
|
+
}
|
|
72560
|
+
async cleanupLogs() {
|
|
72561
|
+
if (!this.threadLogsEnabled) {
|
|
72562
|
+
return 0;
|
|
71950
72563
|
}
|
|
72564
|
+
return new Promise((resolve7) => {
|
|
72565
|
+
try {
|
|
72566
|
+
const deleted = cleanupOldLogs(this.logRetentionDays);
|
|
72567
|
+
resolve7(deleted);
|
|
72568
|
+
} catch (err) {
|
|
72569
|
+
log38.warn(`Log cleanup error: ${err}`);
|
|
72570
|
+
resolve7(0);
|
|
72571
|
+
}
|
|
72572
|
+
});
|
|
72573
|
+
}
|
|
72574
|
+
async cleanupOrphanedWorktrees() {
|
|
72575
|
+
const worktreesDir = getWorktreesDir();
|
|
72576
|
+
const result = { cleaned: 0, metadata: 0 };
|
|
72577
|
+
if (!existsSync14(worktreesDir)) {
|
|
72578
|
+
log38.debug("No worktrees directory exists, nothing to clean");
|
|
72579
|
+
return result;
|
|
72580
|
+
}
|
|
72581
|
+
const persisted = this.sessionStore.load();
|
|
72582
|
+
const activeWorktrees = new Set;
|
|
72583
|
+
for (const session of persisted.values()) {
|
|
72584
|
+
if (session.worktreeInfo?.worktreePath) {
|
|
72585
|
+
activeWorktrees.add(session.worktreeInfo.worktreePath);
|
|
72586
|
+
}
|
|
72587
|
+
}
|
|
72588
|
+
const now = Date.now();
|
|
72589
|
+
try {
|
|
72590
|
+
const entries = await readdir(worktreesDir, { withFileTypes: true });
|
|
72591
|
+
for (const entry of entries) {
|
|
72592
|
+
if (!entry.isDirectory())
|
|
72593
|
+
continue;
|
|
72594
|
+
const worktreePath = join13(worktreesDir, entry.name);
|
|
72595
|
+
if (activeWorktrees.has(worktreePath)) {
|
|
72596
|
+
log38.debug(`Worktree in use by persisted session, skipping: ${entry.name}`);
|
|
72597
|
+
continue;
|
|
72598
|
+
}
|
|
72599
|
+
const meta = await readWorktreeMetadata(worktreePath);
|
|
72600
|
+
let shouldCleanup = false;
|
|
72601
|
+
let cleanupReason = "";
|
|
72602
|
+
if (meta) {
|
|
72603
|
+
const lastActivity = new Date(meta.lastActivityAt).getTime();
|
|
72604
|
+
const age = now - lastActivity;
|
|
72605
|
+
if (meta.sessionId && age < this.maxWorktreeAgeMs) {
|
|
72606
|
+
log38.debug(`Worktree has active session (${Math.round(age / 60000)}min old), skipping: ${entry.name}`);
|
|
72607
|
+
continue;
|
|
72608
|
+
}
|
|
72609
|
+
const merged = age >= this.maxWorktreeAgeMs ? await isBranchMerged(meta.repoRoot, meta.branch).catch(() => false) : false;
|
|
72610
|
+
if (merged) {
|
|
72611
|
+
shouldCleanup = true;
|
|
72612
|
+
cleanupReason = `branch "${meta.branch}" was merged`;
|
|
72613
|
+
} else if (age >= this.maxWorktreeAgeMs) {
|
|
72614
|
+
shouldCleanup = true;
|
|
72615
|
+
cleanupReason = `inactive for ${Math.round(age / 3600000)}h`;
|
|
72616
|
+
} else {
|
|
72617
|
+
log38.debug(`Worktree recent (${Math.round(age / 60000)}min old), skipping: ${entry.name}`);
|
|
72618
|
+
continue;
|
|
72619
|
+
}
|
|
72620
|
+
} else {
|
|
72621
|
+
shouldCleanup = true;
|
|
72622
|
+
cleanupReason = "no metadata";
|
|
72623
|
+
}
|
|
72624
|
+
if (!shouldCleanup)
|
|
72625
|
+
continue;
|
|
72626
|
+
log38.info(`Cleaning worktree (${cleanupReason}): ${entry.name}`);
|
|
72627
|
+
try {
|
|
72628
|
+
if (meta?.repoRoot) {
|
|
72629
|
+
await removeWorktree(meta.repoRoot, worktreePath);
|
|
72630
|
+
} else {
|
|
72631
|
+
await rm3(worktreePath, { recursive: true, force: true });
|
|
72632
|
+
}
|
|
72633
|
+
result.cleaned++;
|
|
72634
|
+
await removeWorktreeMetadata(worktreePath);
|
|
72635
|
+
result.metadata++;
|
|
72636
|
+
} catch (err) {
|
|
72637
|
+
log38.warn(`Failed to clean orphaned worktree ${entry.name}: ${err}`);
|
|
72638
|
+
try {
|
|
72639
|
+
await rm3(worktreePath, { recursive: true, force: true });
|
|
72640
|
+
result.cleaned++;
|
|
72641
|
+
await removeWorktreeMetadata(worktreePath);
|
|
72642
|
+
result.metadata++;
|
|
72643
|
+
} catch (rmErr) {
|
|
72644
|
+
log38.error(`Failed to force remove worktree ${entry.name}: ${rmErr}`);
|
|
72645
|
+
}
|
|
72646
|
+
}
|
|
72647
|
+
}
|
|
72648
|
+
} catch (err) {
|
|
72649
|
+
log38.warn(`Failed to scan worktrees directory: ${err}`);
|
|
72650
|
+
}
|
|
72651
|
+
return result;
|
|
71951
72652
|
}
|
|
71952
72653
|
}
|
|
71953
72654
|
// src/operations/plugin/handler.ts
|
|
71954
72655
|
init_spawn();
|
|
71955
72656
|
init_logger();
|
|
71956
|
-
var
|
|
71957
|
-
var sessionLog7 = createSessionLog(
|
|
72657
|
+
var log39 = createLogger("plugin");
|
|
72658
|
+
var sessionLog7 = createSessionLog(log39);
|
|
71958
72659
|
async function buildPluginRestartCliOptions(session, ctx) {
|
|
71959
72660
|
const account = session.claudeAccountId ? ctx.ops.getClaudeAccount(session.claudeAccountId) : undefined;
|
|
71960
72661
|
const memoryConfig = ctx.ops.getPlatformMemoryConfig(session.platformId);
|
|
@@ -71996,7 +72697,7 @@ async function runPluginCommand(args, cwd, timeout2 = 60000) {
|
|
|
71996
72697
|
});
|
|
71997
72698
|
proc.on("error", (err) => {
|
|
71998
72699
|
resolve7({ stdout, stderr, exitCode: 1 });
|
|
71999
|
-
|
|
72700
|
+
log39.error(`Plugin command error: ${err.message}`);
|
|
72000
72701
|
});
|
|
72001
72702
|
});
|
|
72002
72703
|
}
|
|
@@ -72170,7 +72871,7 @@ class SessionRegistry {
|
|
|
72170
72871
|
// src/session/reaction-router.ts
|
|
72171
72872
|
init_emoji();
|
|
72172
72873
|
init_logger();
|
|
72173
|
-
var
|
|
72874
|
+
var log40 = createLogger("manager");
|
|
72174
72875
|
async function handleReaction(deps, platformId, postId, emojiName, username, action) {
|
|
72175
72876
|
const normalizedEmoji = normalizeEmojiName(emojiName);
|
|
72176
72877
|
if (action === "added" && isResumeEmoji(normalizedEmoji)) {
|
|
@@ -72184,7 +72885,7 @@ async function handleReaction(deps, platformId, postId, emojiName, username, act
|
|
|
72184
72885
|
if (session.platformId !== platformId)
|
|
72185
72886
|
return;
|
|
72186
72887
|
if (!session.sessionAllowedUsers.has(username) && !session.platform.isUserAllowed(username)) {
|
|
72187
|
-
|
|
72888
|
+
log40.info(`\uD83D\uDEAB rejected reaction from unauthorized user`, {
|
|
72188
72889
|
event: "reaction.rejected",
|
|
72189
72890
|
platformId,
|
|
72190
72891
|
sessionId: session.sessionId,
|
|
@@ -72220,7 +72921,7 @@ async function tryResumeFromReaction(deps, platformId, postId, username) {
|
|
|
72220
72921
|
return false;
|
|
72221
72922
|
}
|
|
72222
72923
|
const shortId = persistedSession.threadId.substring(0, 8);
|
|
72223
|
-
|
|
72924
|
+
log40.info(`\uD83D\uDD04 Resuming session ${shortId}... via emoji reaction by @${username}`);
|
|
72224
72925
|
await resumeSession(persistedSession, deps.getContext());
|
|
72225
72926
|
return true;
|
|
72226
72927
|
}
|
|
@@ -72250,7 +72951,7 @@ async function dispatch(deps, session, postId, emojiName, username, action) {
|
|
|
72250
72951
|
}
|
|
72251
72952
|
if (session.lastError?.postId === postId && isBugReportEmoji(emojiName)) {
|
|
72252
72953
|
if (session.startedBy === username || session.platform.isUserAllowed(username) || session.sessionAllowedUsers.has(username)) {
|
|
72253
|
-
|
|
72954
|
+
log40.info(`\uD83D\uDC1B @${username} triggered bug report from error reaction`);
|
|
72254
72955
|
await reportBug(session, undefined, username, deps.getContext(), session.lastError);
|
|
72255
72956
|
return;
|
|
72256
72957
|
}
|
|
@@ -72265,7 +72966,7 @@ async function dispatch(deps, session, postId, emojiName, username, action) {
|
|
|
72265
72966
|
|
|
72266
72967
|
// src/session/manager.ts
|
|
72267
72968
|
init_logger();
|
|
72268
|
-
var
|
|
72969
|
+
var log41 = createLogger("manager");
|
|
72269
72970
|
var USAGE_PROBE_TIMEOUT_MS = 1e4;
|
|
72270
72971
|
var USAGE_REFRESH_DEADLINE_MS = 5000;
|
|
72271
72972
|
var USAGE_CACHE_TTL_MS = 15000;
|
|
@@ -72289,6 +72990,8 @@ class SessionManager extends EventEmitter4 {
|
|
|
72289
72990
|
sessionStore;
|
|
72290
72991
|
githubEmailsStore;
|
|
72291
72992
|
memoryStore;
|
|
72993
|
+
routinesStore;
|
|
72994
|
+
routineScheduler = null;
|
|
72292
72995
|
sessionMonitor = null;
|
|
72293
72996
|
backgroundCleanup = null;
|
|
72294
72997
|
isShuttingDown = false;
|
|
@@ -72296,6 +72999,7 @@ class SessionManager extends EventEmitter4 {
|
|
|
72296
72999
|
customFooter;
|
|
72297
73000
|
platformOverhead = new Map;
|
|
72298
73001
|
platformMemory = new Map;
|
|
73002
|
+
platformRoutines = new Map;
|
|
72299
73003
|
autoUpdateManager = null;
|
|
72300
73004
|
accountPool;
|
|
72301
73005
|
usageRefreshInFlight = null;
|
|
@@ -72314,6 +73018,7 @@ class SessionManager extends EventEmitter4 {
|
|
|
72314
73018
|
this.sessionStore = new SessionStore(sessionsPath);
|
|
72315
73019
|
this.githubEmailsStore = new GitHubEmailsStore;
|
|
72316
73020
|
this.memoryStore = new MemoryStore;
|
|
73021
|
+
this.routinesStore = new RoutinesStore;
|
|
72317
73022
|
this.registry = new SessionRegistry(this.sessionStore);
|
|
72318
73023
|
this.accountPool = new AccountPool(claudeAccounts);
|
|
72319
73024
|
this.sessionMonitor = new SessionMonitor({
|
|
@@ -72331,14 +73036,28 @@ class SessionManager extends EventEmitter4 {
|
|
|
72331
73036
|
maxWorktreeAgeMs: this.limits.maxWorktreeAgeHours * 60 * 60 * 1000,
|
|
72332
73037
|
cleanupWorktrees: this.limits.cleanupWorktrees
|
|
72333
73038
|
});
|
|
73039
|
+
this.routineScheduler = new RoutineScheduler({
|
|
73040
|
+
store: this.routinesStore,
|
|
73041
|
+
listPlatformIds: () => Array.from(this.platforms.keys()),
|
|
73042
|
+
isRoutinesEnabled: (pid) => this.platformRoutines.get(pid) ?? true,
|
|
73043
|
+
fireRoutine: (pid, routine) => fireRoutine(routine, pid, this.getContext()),
|
|
73044
|
+
notifyDisabled: async (pid, routine, reason) => {
|
|
73045
|
+
const platform = this.platforms.get(pid);
|
|
73046
|
+
if (!platform)
|
|
73047
|
+
return;
|
|
73048
|
+
const formatter = platform.getFormatter();
|
|
73049
|
+
await platform.createPost(`\uD83D\uDD58 ${formatter.formatBold(`Routine "${routine.name}" disabled`)} — ${reason}. ` + `Re-enable with ${formatter.formatCode("!routines resume <n>")} once resolved.`).catch(() => {});
|
|
73050
|
+
}
|
|
73051
|
+
});
|
|
72334
73052
|
}
|
|
72335
|
-
addPlatform(platformId, client, overhead, memory) {
|
|
73053
|
+
addPlatform(platformId, client, overhead, memory, routinesEnabled) {
|
|
72336
73054
|
this.platforms.set(platformId, client);
|
|
72337
73055
|
this.platformOverhead.set(platformId, {
|
|
72338
73056
|
sessionHeader: overhead?.sessionHeader ?? DEFAULT_OVERHEAD_VISIBILITY,
|
|
72339
73057
|
stickyMessage: overhead?.stickyMessage ?? DEFAULT_OVERHEAD_VISIBILITY
|
|
72340
73058
|
});
|
|
72341
73059
|
this.platformMemory.set(platformId, memory ?? DEFAULT_MEMORY_CONFIG);
|
|
73060
|
+
this.platformRoutines.set(platformId, routinesEnabled ?? true);
|
|
72342
73061
|
client.on("message", (post2, user) => this.handleMessage(platformId, post2, user));
|
|
72343
73062
|
client.on("reaction", (reaction, user) => {
|
|
72344
73063
|
if (user) {
|
|
@@ -72357,12 +73076,13 @@ class SessionManager extends EventEmitter4 {
|
|
|
72357
73076
|
markNeedsBump(platformId);
|
|
72358
73077
|
this.updateStickyMessage();
|
|
72359
73078
|
});
|
|
72360
|
-
|
|
73079
|
+
log41.info(`\uD83D\uDCE1 Platform "${platformId}" registered`);
|
|
72361
73080
|
}
|
|
72362
73081
|
removePlatform(platformId) {
|
|
72363
73082
|
this.platforms.delete(platformId);
|
|
72364
73083
|
this.platformOverhead.delete(platformId);
|
|
72365
73084
|
this.platformMemory.delete(platformId);
|
|
73085
|
+
this.platformRoutines.delete(platformId);
|
|
72366
73086
|
clearHiddenCleanupTracking(platformId);
|
|
72367
73087
|
}
|
|
72368
73088
|
setAutoUpdateManager(manager) {
|
|
@@ -72376,7 +73096,7 @@ class SessionManager extends EventEmitter4 {
|
|
|
72376
73096
|
if (users) {
|
|
72377
73097
|
users.add(sessionId);
|
|
72378
73098
|
}
|
|
72379
|
-
|
|
73099
|
+
log41.debug(`Registered session ${sessionId.substring(0, 20)} as worktree user for ${worktreePath}`);
|
|
72380
73100
|
}
|
|
72381
73101
|
unregisterWorktreeUser(worktreePath, sessionId) {
|
|
72382
73102
|
const users = this.worktreeUsers.get(worktreePath);
|
|
@@ -72402,6 +73122,7 @@ class SessionManager extends EventEmitter4 {
|
|
|
72402
73122
|
userAttribution: this.userAttribution,
|
|
72403
73123
|
debug: this.debug,
|
|
72404
73124
|
maxSessions: this.limits.maxSessions,
|
|
73125
|
+
maxRoutines: this.limits.maxRoutines,
|
|
72405
73126
|
threadLogsEnabled: this.threadLogsEnabled,
|
|
72406
73127
|
threadLogsRetentionDays: this.threadLogsRetentionDays,
|
|
72407
73128
|
permissionTimeoutMs: this.limits.permissionTimeoutSeconds * 1000,
|
|
@@ -72414,6 +73135,7 @@ class SessionManager extends EventEmitter4 {
|
|
|
72414
73135
|
sessionStore: this.sessionStore,
|
|
72415
73136
|
githubEmailsStore: this.githubEmailsStore,
|
|
72416
73137
|
memoryStore: this.memoryStore,
|
|
73138
|
+
routinesStore: this.routinesStore,
|
|
72417
73139
|
isShuttingDown: this.isShuttingDown
|
|
72418
73140
|
};
|
|
72419
73141
|
const ops = {
|
|
@@ -72459,7 +73181,9 @@ class SessionManager extends EventEmitter4 {
|
|
|
72459
73181
|
sessionHeader: DEFAULT_OVERHEAD_VISIBILITY,
|
|
72460
73182
|
stickyMessage: DEFAULT_OVERHEAD_VISIBILITY
|
|
72461
73183
|
},
|
|
72462
|
-
getPlatformMemoryConfig: (pid) => this.platformMemory.get(pid) ?? DEFAULT_MEMORY_CONFIG
|
|
73184
|
+
getPlatformMemoryConfig: (pid) => this.platformMemory.get(pid) ?? DEFAULT_MEMORY_CONFIG,
|
|
73185
|
+
isRoutinesEnabled: (pid) => this.platformRoutines.get(pid) ?? true,
|
|
73186
|
+
fireRoutineNow: (pid, routine) => this.fireRoutineNowImpl(pid, routine)
|
|
72463
73187
|
};
|
|
72464
73188
|
return createSessionContext(config, state, ops);
|
|
72465
73189
|
}
|
|
@@ -72578,7 +73302,7 @@ class SessionManager extends EventEmitter4 {
|
|
|
72578
73302
|
try {
|
|
72579
73303
|
this.persistSessionUnsafe(session);
|
|
72580
73304
|
} catch (err) {
|
|
72581
|
-
|
|
73305
|
+
log41.error(`Failed to persist session ${session.sessionId}: ${err}`);
|
|
72582
73306
|
}
|
|
72583
73307
|
}
|
|
72584
73308
|
persistSessionUnsafe(session) {
|
|
@@ -72694,11 +73418,11 @@ class SessionManager extends EventEmitter4 {
|
|
|
72694
73418
|
}
|
|
72695
73419
|
}
|
|
72696
73420
|
if (sessionsToKill.length === 0) {
|
|
72697
|
-
|
|
73421
|
+
log41.info(`No active sessions to pause for platform ${platformId}`);
|
|
72698
73422
|
await this.updateStickyMessage();
|
|
72699
73423
|
return;
|
|
72700
73424
|
}
|
|
72701
|
-
|
|
73425
|
+
log41.info(`⏸️ Pausing ${sessionsToKill.length} session(s) for platform ${platformId}`);
|
|
72702
73426
|
for (const session of sessionsToKill) {
|
|
72703
73427
|
try {
|
|
72704
73428
|
const fmt = session.platform.getFormatter();
|
|
@@ -72714,9 +73438,9 @@ class SessionManager extends EventEmitter4 {
|
|
|
72714
73438
|
session.claude.kill();
|
|
72715
73439
|
this.registry.unregister(session.sessionId);
|
|
72716
73440
|
this.emitSessionRemove(session.sessionId);
|
|
72717
|
-
|
|
73441
|
+
log41.info(`⏸️ Paused session ${session.threadId.substring(0, 8)}`);
|
|
72718
73442
|
} catch (err) {
|
|
72719
|
-
|
|
73443
|
+
log41.warn(`Failed to pause session ${session.threadId}: ${err}`);
|
|
72720
73444
|
}
|
|
72721
73445
|
}
|
|
72722
73446
|
for (const session of sessionsToKill) {
|
|
@@ -72737,17 +73461,17 @@ class SessionManager extends EventEmitter4 {
|
|
|
72737
73461
|
sessionsToResume.push(state);
|
|
72738
73462
|
}
|
|
72739
73463
|
if (sessionsToResume.length === 0) {
|
|
72740
|
-
|
|
73464
|
+
log41.info(`No paused sessions to resume for platform ${platformId}`);
|
|
72741
73465
|
await this.updateStickyMessage();
|
|
72742
73466
|
return;
|
|
72743
73467
|
}
|
|
72744
|
-
|
|
73468
|
+
log41.info(`▶️ Resuming ${sessionsToResume.length} paused session(s) for platform ${platformId}`);
|
|
72745
73469
|
for (const state of sessionsToResume) {
|
|
72746
73470
|
try {
|
|
72747
73471
|
await resumeSession(state, this.getContext());
|
|
72748
|
-
|
|
73472
|
+
log41.info(`▶️ Resumed session ${state.threadId.substring(0, 8)}`);
|
|
72749
73473
|
} catch (err) {
|
|
72750
|
-
|
|
73474
|
+
log41.warn(`Failed to resume session ${state.threadId}: ${err}`);
|
|
72751
73475
|
}
|
|
72752
73476
|
}
|
|
72753
73477
|
await this.updateStickyMessage();
|
|
@@ -72782,17 +73506,18 @@ class SessionManager extends EventEmitter4 {
|
|
|
72782
73506
|
initialize(this.sessionStore);
|
|
72783
73507
|
this.sessionMonitor?.start();
|
|
72784
73508
|
this.backgroundCleanup?.start();
|
|
73509
|
+
this.routineScheduler?.start();
|
|
72785
73510
|
const sessionTimeoutMs = this.limits.sessionTimeoutMinutes * 60 * 1000;
|
|
72786
73511
|
const staleIds = this.sessionStore.cleanStale(sessionTimeoutMs * 2);
|
|
72787
73512
|
if (staleIds.length > 0) {
|
|
72788
|
-
|
|
73513
|
+
log41.info(`\uD83E\uDDF9 Soft-deleted ${staleIds.length} stale session(s) (kept for history)`);
|
|
72789
73514
|
}
|
|
72790
73515
|
const removedCount = this.sessionStore.cleanHistory();
|
|
72791
73516
|
if (removedCount > 0) {
|
|
72792
|
-
|
|
73517
|
+
log41.info(`\uD83D\uDDD1️ Permanently removed ${removedCount} old session(s) from history`);
|
|
72793
73518
|
}
|
|
72794
73519
|
const persisted = this.sessionStore.load();
|
|
72795
|
-
|
|
73520
|
+
log41.info(`\uD83D\uDCC2 Loaded ${persisted.size} session(s) from persistence`);
|
|
72796
73521
|
const excludePostIdsByPlatform = new Map;
|
|
72797
73522
|
for (const session of persisted.values()) {
|
|
72798
73523
|
const platformId = session.platformId;
|
|
@@ -72812,10 +73537,10 @@ class SessionManager extends EventEmitter4 {
|
|
|
72812
73537
|
const excludePostIds = excludePostIdsByPlatform.get(platform.platformId);
|
|
72813
73538
|
platform.getBotUser().then((botUser) => {
|
|
72814
73539
|
cleanupOldStickyMessages(platform, botUser.id, true, excludePostIds).catch((err) => {
|
|
72815
|
-
|
|
73540
|
+
log41.warn(`Failed to cleanup old sticky messages for ${platform.platformId}: ${err}`);
|
|
72816
73541
|
});
|
|
72817
73542
|
}).catch((err) => {
|
|
72818
|
-
|
|
73543
|
+
log41.warn(`Failed to get bot user for cleanup on ${platform.platformId}: ${err}`);
|
|
72819
73544
|
});
|
|
72820
73545
|
}
|
|
72821
73546
|
if (persisted.size > 0) {
|
|
@@ -72829,10 +73554,10 @@ class SessionManager extends EventEmitter4 {
|
|
|
72829
73554
|
}
|
|
72830
73555
|
}
|
|
72831
73556
|
if (pausedToSkip.length > 0) {
|
|
72832
|
-
|
|
73557
|
+
log41.info(`⏸️ ${pausedToSkip.length} session(s) remain paused (waiting for user message)`);
|
|
72833
73558
|
}
|
|
72834
73559
|
if (activeToResume.length > 0) {
|
|
72835
|
-
|
|
73560
|
+
log41.info(`\uD83D\uDD04 Attempting to resume ${activeToResume.length} active session(s)...`);
|
|
72836
73561
|
for (const state of activeToResume) {
|
|
72837
73562
|
await resumeSession(state, this.getContext());
|
|
72838
73563
|
}
|
|
@@ -72951,6 +73676,23 @@ class SessionManager extends EventEmitter4 {
|
|
|
72951
73676
|
return;
|
|
72952
73677
|
await forgetMemory(session, selector, username, this.getContext());
|
|
72953
73678
|
}
|
|
73679
|
+
async createRoutine(threadId, request, username) {
|
|
73680
|
+
const session = this.findSessionByThreadId(threadId);
|
|
73681
|
+
if (!session)
|
|
73682
|
+
return;
|
|
73683
|
+
await createRoutine(session, request, username, this.getContext());
|
|
73684
|
+
}
|
|
73685
|
+
async manageRoutines(threadId, args, username) {
|
|
73686
|
+
const session = this.findSessionByThreadId(threadId);
|
|
73687
|
+
if (!session)
|
|
73688
|
+
return;
|
|
73689
|
+
await manageRoutines(session, args, username, this.getContext());
|
|
73690
|
+
}
|
|
73691
|
+
async fireRoutineNowImpl(platformId, routine) {
|
|
73692
|
+
if (!this.routineScheduler)
|
|
73693
|
+
return "skipped";
|
|
73694
|
+
return this.routineScheduler.fire(platformId, routine, new Date, false);
|
|
73695
|
+
}
|
|
72954
73696
|
async setRespondOnlyWhenMentioned(threadId, username, arg) {
|
|
72955
73697
|
const session = this.findSessionByThreadId(threadId);
|
|
72956
73698
|
if (!session)
|
|
@@ -73293,7 +74035,7 @@ Mention me to start a session in this worktree.`, threadId);
|
|
|
73293
74035
|
const message = messageBuilder(formatter);
|
|
73294
74036
|
await post(session, "info", message);
|
|
73295
74037
|
} catch (err) {
|
|
73296
|
-
|
|
74038
|
+
log41.warn(`Failed to broadcast to session ${session.threadId}: ${err}`);
|
|
73297
74039
|
}
|
|
73298
74040
|
}
|
|
73299
74041
|
}
|
|
@@ -73312,7 +74054,7 @@ Mention me to start a session in this worktree.`, threadId);
|
|
|
73312
74054
|
session.messageManager?.setPendingUpdatePrompt({ postId: post2.id });
|
|
73313
74055
|
this.registerPost(post2.id, session.threadId);
|
|
73314
74056
|
} catch (err) {
|
|
73315
|
-
|
|
74057
|
+
log41.warn(`Failed to post ask message to ${threadId}: ${err}`);
|
|
73316
74058
|
}
|
|
73317
74059
|
}
|
|
73318
74060
|
}
|
|
@@ -73320,6 +74062,7 @@ Mention me to start a session in this worktree.`, threadId);
|
|
|
73320
74062
|
this.isShuttingDown = true;
|
|
73321
74063
|
this.sessionMonitor?.stop();
|
|
73322
74064
|
this.backgroundCleanup?.stop();
|
|
74065
|
+
this.routineScheduler?.stop();
|
|
73323
74066
|
if (message) {
|
|
73324
74067
|
for (const session of this.registry.getAll()) {
|
|
73325
74068
|
try {
|
|
@@ -80906,29 +81649,29 @@ function SessionLog({ logs, maxLines = 20 }) {
|
|
|
80906
81649
|
return /* @__PURE__ */ jsx_dev_runtime4.jsxDEV(Box_default, {
|
|
80907
81650
|
flexDirection: "column",
|
|
80908
81651
|
flexShrink: 0,
|
|
80909
|
-
children: displayLogs.map((
|
|
81652
|
+
children: displayLogs.map((log42) => /* @__PURE__ */ jsx_dev_runtime4.jsxDEV(Box_default, {
|
|
80910
81653
|
flexShrink: 0,
|
|
80911
81654
|
children: [
|
|
80912
81655
|
/* @__PURE__ */ jsx_dev_runtime4.jsxDEV(Text, {
|
|
80913
|
-
color: getColorForLevel(
|
|
81656
|
+
color: getColorForLevel(log42.level),
|
|
80914
81657
|
dimColor: true,
|
|
80915
81658
|
wrap: "truncate",
|
|
80916
81659
|
children: [
|
|
80917
81660
|
"[",
|
|
80918
|
-
padComponent(
|
|
81661
|
+
padComponent(log42.component),
|
|
80919
81662
|
"]"
|
|
80920
81663
|
]
|
|
80921
81664
|
}, undefined, true, undefined, this),
|
|
80922
81665
|
/* @__PURE__ */ jsx_dev_runtime4.jsxDEV(Text, {
|
|
80923
|
-
color: getColorForLevel(
|
|
81666
|
+
color: getColorForLevel(log42.level),
|
|
80924
81667
|
wrap: "truncate",
|
|
80925
81668
|
children: [
|
|
80926
81669
|
" ",
|
|
80927
|
-
|
|
81670
|
+
log42.message
|
|
80928
81671
|
]
|
|
80929
81672
|
}, undefined, true, undefined, this)
|
|
80930
81673
|
]
|
|
80931
|
-
},
|
|
81674
|
+
}, log42.id, true, undefined, this))
|
|
80932
81675
|
}, undefined, false, undefined, this);
|
|
80933
81676
|
}
|
|
80934
81677
|
// src/ui/components/Footer.tsx
|
|
@@ -81452,7 +82195,7 @@ function LogPanel({ logs, maxLines = 10, focused = false }) {
|
|
|
81452
82195
|
const scrollRef = import_react59.default.useRef(null);
|
|
81453
82196
|
const { stdout } = use_stdout_default();
|
|
81454
82197
|
const isDebug = process.env.DEBUG === "1";
|
|
81455
|
-
const displayLogs = logs.filter((
|
|
82198
|
+
const displayLogs = logs.filter((log42) => isDebug || log42.level !== "debug");
|
|
81456
82199
|
const visibleLogs = displayLogs.slice(-Math.max(maxLines * 3, 100));
|
|
81457
82200
|
import_react59.default.useEffect(() => {
|
|
81458
82201
|
const handleResize = () => scrollRef.current?.remeasure();
|
|
@@ -81492,25 +82235,25 @@ function LogPanel({ logs, maxLines = 10, focused = false }) {
|
|
|
81492
82235
|
overflow: "hidden",
|
|
81493
82236
|
children: /* @__PURE__ */ jsx_dev_runtime6.jsxDEV(ScrollView, {
|
|
81494
82237
|
ref: scrollRef,
|
|
81495
|
-
children: visibleLogs.map((
|
|
82238
|
+
children: visibleLogs.map((log42) => /* @__PURE__ */ jsx_dev_runtime6.jsxDEV(Box_default, {
|
|
81496
82239
|
children: [
|
|
81497
82240
|
/* @__PURE__ */ jsx_dev_runtime6.jsxDEV(Text, {
|
|
81498
82241
|
dimColor: true,
|
|
81499
82242
|
children: [
|
|
81500
82243
|
"[",
|
|
81501
|
-
padComponent2(
|
|
82244
|
+
padComponent2(log42.component),
|
|
81502
82245
|
"]"
|
|
81503
82246
|
]
|
|
81504
82247
|
}, undefined, true, undefined, this),
|
|
81505
82248
|
/* @__PURE__ */ jsx_dev_runtime6.jsxDEV(Text, {
|
|
81506
|
-
color: getLevelColor(
|
|
82249
|
+
color: getLevelColor(log42.level),
|
|
81507
82250
|
children: [
|
|
81508
82251
|
" ",
|
|
81509
|
-
|
|
82252
|
+
log42.message
|
|
81510
82253
|
]
|
|
81511
82254
|
}, undefined, true, undefined, this)
|
|
81512
82255
|
]
|
|
81513
|
-
},
|
|
82256
|
+
}, log42.id, true, undefined, this))
|
|
81514
82257
|
}, undefined, false, undefined, this)
|
|
81515
82258
|
}, undefined, false, undefined, this);
|
|
81516
82259
|
}
|
|
@@ -82027,10 +82770,10 @@ function useAppState(initialConfig) {
|
|
|
82027
82770
|
});
|
|
82028
82771
|
}, []);
|
|
82029
82772
|
const getLogsForSession = import_react60.useCallback((sessionId) => {
|
|
82030
|
-
return state.logs.filter((
|
|
82773
|
+
return state.logs.filter((log42) => log42.sessionId === sessionId);
|
|
82031
82774
|
}, [state.logs]);
|
|
82032
82775
|
const getGlobalLogs = import_react60.useCallback(() => {
|
|
82033
|
-
return state.logs.filter((
|
|
82776
|
+
return state.logs.filter((log42) => !log42.sessionId);
|
|
82034
82777
|
}, [state.logs]);
|
|
82035
82778
|
const togglePlatformEnabled = import_react60.useCallback((platformId) => {
|
|
82036
82779
|
let newEnabled = false;
|
|
@@ -83045,7 +83788,7 @@ import { EventEmitter as EventEmitter9 } from "events";
|
|
|
83045
83788
|
// src/auto-update/checker.ts
|
|
83046
83789
|
init_logger();
|
|
83047
83790
|
import { EventEmitter as EventEmitter7 } from "events";
|
|
83048
|
-
var
|
|
83791
|
+
var log42 = createLogger("checker");
|
|
83049
83792
|
var PACKAGE_NAME = "claude-threads";
|
|
83050
83793
|
function compareVersions(a, b) {
|
|
83051
83794
|
const partsA = a.replace(/^v/, "").split(".").map(Number);
|
|
@@ -83068,13 +83811,13 @@ async function fetchLatestVersion() {
|
|
|
83068
83811
|
}
|
|
83069
83812
|
});
|
|
83070
83813
|
if (!response.ok) {
|
|
83071
|
-
|
|
83814
|
+
log42.warn(`Failed to fetch latest version: HTTP ${response.status}`);
|
|
83072
83815
|
return null;
|
|
83073
83816
|
}
|
|
83074
83817
|
const data = await response.json();
|
|
83075
83818
|
return data.version ?? null;
|
|
83076
83819
|
} catch (err) {
|
|
83077
|
-
|
|
83820
|
+
log42.warn(`Failed to fetch latest version: ${err}`);
|
|
83078
83821
|
return null;
|
|
83079
83822
|
}
|
|
83080
83823
|
}
|
|
@@ -83091,38 +83834,38 @@ class UpdateChecker extends EventEmitter7 {
|
|
|
83091
83834
|
}
|
|
83092
83835
|
start() {
|
|
83093
83836
|
if (!this.config.enabled) {
|
|
83094
|
-
|
|
83837
|
+
log42.debug("Auto-update disabled, not starting checker");
|
|
83095
83838
|
return;
|
|
83096
83839
|
}
|
|
83097
83840
|
setTimeout(() => {
|
|
83098
83841
|
this.check().catch((err) => {
|
|
83099
|
-
|
|
83842
|
+
log42.warn(`Initial update check failed: ${err}`);
|
|
83100
83843
|
});
|
|
83101
83844
|
}, 5000);
|
|
83102
83845
|
const intervalMs = this.config.checkIntervalMinutes * 60 * 1000;
|
|
83103
83846
|
this.checkInterval = setInterval(() => {
|
|
83104
83847
|
this.check().catch((err) => {
|
|
83105
|
-
|
|
83848
|
+
log42.warn(`Periodic update check failed: ${err}`);
|
|
83106
83849
|
});
|
|
83107
83850
|
}, intervalMs);
|
|
83108
|
-
|
|
83851
|
+
log42.info(`\uD83D\uDD04 Update checker started (every ${this.config.checkIntervalMinutes} minutes)`);
|
|
83109
83852
|
}
|
|
83110
83853
|
stop() {
|
|
83111
83854
|
if (this.checkInterval) {
|
|
83112
83855
|
clearInterval(this.checkInterval);
|
|
83113
83856
|
this.checkInterval = null;
|
|
83114
83857
|
}
|
|
83115
|
-
|
|
83858
|
+
log42.debug("Update checker stopped");
|
|
83116
83859
|
}
|
|
83117
83860
|
async check() {
|
|
83118
83861
|
if (this.isChecking) {
|
|
83119
|
-
|
|
83862
|
+
log42.debug("Check already in progress, skipping");
|
|
83120
83863
|
return this.lastUpdateInfo;
|
|
83121
83864
|
}
|
|
83122
83865
|
this.isChecking = true;
|
|
83123
83866
|
this.emit("check:start");
|
|
83124
83867
|
try {
|
|
83125
|
-
|
|
83868
|
+
log42.debug("Checking for updates...");
|
|
83126
83869
|
const latestVersion2 = await fetchLatestVersion();
|
|
83127
83870
|
if (!latestVersion2) {
|
|
83128
83871
|
this.emit("check:complete", false);
|
|
@@ -83139,18 +83882,18 @@ class UpdateChecker extends EventEmitter7 {
|
|
|
83139
83882
|
detectedAt: new Date
|
|
83140
83883
|
};
|
|
83141
83884
|
if (!this.lastUpdateInfo || this.lastUpdateInfo.latestVersion !== latestVersion2) {
|
|
83142
|
-
|
|
83885
|
+
log42.info(`\uD83C\uDD95 Update available: v${currentVersion} → v${latestVersion2}`);
|
|
83143
83886
|
this.lastUpdateInfo = updateInfo;
|
|
83144
83887
|
this.emit("update", updateInfo);
|
|
83145
83888
|
}
|
|
83146
83889
|
this.emit("check:complete", true);
|
|
83147
83890
|
return updateInfo;
|
|
83148
83891
|
}
|
|
83149
|
-
|
|
83892
|
+
log42.debug(`Up to date (v${currentVersion})`);
|
|
83150
83893
|
this.emit("check:complete", false);
|
|
83151
83894
|
return null;
|
|
83152
83895
|
} catch (err) {
|
|
83153
|
-
|
|
83896
|
+
log42.warn(`Update check failed: ${err}`);
|
|
83154
83897
|
this.emit("check:error", err);
|
|
83155
83898
|
return null;
|
|
83156
83899
|
} finally {
|
|
@@ -83221,7 +83964,7 @@ function isInScheduledWindow(window2) {
|
|
|
83221
83964
|
}
|
|
83222
83965
|
|
|
83223
83966
|
// src/auto-update/scheduler.ts
|
|
83224
|
-
var
|
|
83967
|
+
var log43 = createLogger("scheduler");
|
|
83225
83968
|
|
|
83226
83969
|
class UpdateScheduler extends EventEmitter8 {
|
|
83227
83970
|
config;
|
|
@@ -83245,7 +83988,7 @@ class UpdateScheduler extends EventEmitter8 {
|
|
|
83245
83988
|
scheduleUpdate(updateInfo) {
|
|
83246
83989
|
this.pendingUpdate = updateInfo;
|
|
83247
83990
|
if (this.config.autoRestartMode === "immediate") {
|
|
83248
|
-
|
|
83991
|
+
log43.info("Immediate mode: triggering update now");
|
|
83249
83992
|
this.emit("ready", updateInfo);
|
|
83250
83993
|
return;
|
|
83251
83994
|
}
|
|
@@ -83258,19 +84001,19 @@ class UpdateScheduler extends EventEmitter8 {
|
|
|
83258
84001
|
this.scheduledRestartAt = null;
|
|
83259
84002
|
this.askApprovals.clear();
|
|
83260
84003
|
this.askStartTime = null;
|
|
83261
|
-
|
|
84004
|
+
log43.debug("Update schedule cancelled");
|
|
83262
84005
|
}
|
|
83263
84006
|
deferUpdate(minutes) {
|
|
83264
84007
|
const deferUntil = new Date(Date.now() + minutes * 60 * 1000);
|
|
83265
84008
|
this.scheduledRestartAt = null;
|
|
83266
84009
|
this.idleStartTime = null;
|
|
83267
84010
|
this.emit("deferred", deferUntil);
|
|
83268
|
-
|
|
84011
|
+
log43.info(`Update deferred until ${deferUntil.toLocaleTimeString()}`);
|
|
83269
84012
|
return deferUntil;
|
|
83270
84013
|
}
|
|
83271
84014
|
recordAskResponse(threadId, approved) {
|
|
83272
84015
|
this.askApprovals.set(threadId, approved);
|
|
83273
|
-
|
|
84016
|
+
log43.debug(`Thread ${threadId.substring(0, 8)} ${approved ? "approved" : "denied"} update`);
|
|
83274
84017
|
this.checkAskCondition();
|
|
83275
84018
|
}
|
|
83276
84019
|
getScheduledRestartAt() {
|
|
@@ -83291,7 +84034,7 @@ class UpdateScheduler extends EventEmitter8 {
|
|
|
83291
84034
|
return;
|
|
83292
84035
|
this.checkCondition();
|
|
83293
84036
|
this.checkTimer = setInterval(() => this.checkCondition(), 1e4);
|
|
83294
|
-
|
|
84037
|
+
log43.debug(`Started checking for ${this.config.autoRestartMode} condition`);
|
|
83295
84038
|
}
|
|
83296
84039
|
stopChecking() {
|
|
83297
84040
|
if (this.checkTimer) {
|
|
@@ -83322,17 +84065,17 @@ class UpdateScheduler extends EventEmitter8 {
|
|
|
83322
84065
|
if (activity.activeSessionCount === 0) {
|
|
83323
84066
|
if (!this.idleStartTime) {
|
|
83324
84067
|
this.idleStartTime = new Date;
|
|
83325
|
-
|
|
84068
|
+
log43.debug("No active sessions, starting idle timer");
|
|
83326
84069
|
}
|
|
83327
84070
|
const idleMs = Date.now() - this.idleStartTime.getTime();
|
|
83328
84071
|
const requiredMs = this.config.idleTimeoutMinutes * 60 * 1000;
|
|
83329
84072
|
if (idleMs >= requiredMs) {
|
|
83330
|
-
|
|
84073
|
+
log43.info(`Idle for ${this.config.idleTimeoutMinutes} minutes, triggering update`);
|
|
83331
84074
|
this.triggerCountdown();
|
|
83332
84075
|
}
|
|
83333
84076
|
} else {
|
|
83334
84077
|
if (this.idleStartTime) {
|
|
83335
|
-
|
|
84078
|
+
log43.debug("Sessions became active, resetting idle timer");
|
|
83336
84079
|
this.idleStartTime = null;
|
|
83337
84080
|
}
|
|
83338
84081
|
}
|
|
@@ -83343,7 +84086,7 @@ class UpdateScheduler extends EventEmitter8 {
|
|
|
83343
84086
|
const quietMs = Date.now() - activity.lastActivityAt.getTime();
|
|
83344
84087
|
const requiredMs = this.config.quietTimeoutMinutes * 60 * 1000;
|
|
83345
84088
|
if (quietMs >= requiredMs && !activity.anySessionBusy) {
|
|
83346
|
-
|
|
84089
|
+
log43.info(`Sessions quiet for ${this.config.quietTimeoutMinutes} minutes, triggering update`);
|
|
83347
84090
|
this.triggerCountdown();
|
|
83348
84091
|
}
|
|
83349
84092
|
} else if (activity.activeSessionCount === 0) {
|
|
@@ -83353,7 +84096,7 @@ class UpdateScheduler extends EventEmitter8 {
|
|
|
83353
84096
|
const idleMs = Date.now() - this.idleStartTime.getTime();
|
|
83354
84097
|
const requiredMs = this.config.quietTimeoutMinutes * 60 * 1000;
|
|
83355
84098
|
if (idleMs >= requiredMs) {
|
|
83356
|
-
|
|
84099
|
+
log43.info("No sessions and quiet timeout reached, triggering update");
|
|
83357
84100
|
this.triggerCountdown();
|
|
83358
84101
|
}
|
|
83359
84102
|
}
|
|
@@ -83364,13 +84107,13 @@ class UpdateScheduler extends EventEmitter8 {
|
|
|
83364
84107
|
}
|
|
83365
84108
|
const activity = this.getSessionActivity();
|
|
83366
84109
|
if (activity.activeSessionCount === 0) {
|
|
83367
|
-
|
|
84110
|
+
log43.info("Within scheduled window and no active sessions, triggering update");
|
|
83368
84111
|
this.triggerCountdown();
|
|
83369
84112
|
} else if (activity.lastActivityAt) {
|
|
83370
84113
|
const quietMs = Date.now() - activity.lastActivityAt.getTime();
|
|
83371
84114
|
const requiredMs = this.config.idleTimeoutMinutes * 60 * 1000;
|
|
83372
84115
|
if (quietMs >= requiredMs && !activity.anySessionBusy) {
|
|
83373
|
-
|
|
84116
|
+
log43.info("Within scheduled window and sessions quiet, triggering update");
|
|
83374
84117
|
this.triggerCountdown();
|
|
83375
84118
|
}
|
|
83376
84119
|
}
|
|
@@ -83378,14 +84121,14 @@ class UpdateScheduler extends EventEmitter8 {
|
|
|
83378
84121
|
checkAskCondition() {
|
|
83379
84122
|
const threadIds = this.getActiveThreadIds();
|
|
83380
84123
|
if (threadIds.length === 0) {
|
|
83381
|
-
|
|
84124
|
+
log43.info("No active threads, proceeding with update");
|
|
83382
84125
|
this.triggerCountdown();
|
|
83383
84126
|
return;
|
|
83384
84127
|
}
|
|
83385
84128
|
if (!this.askStartTime && this.pendingUpdate) {
|
|
83386
84129
|
this.askStartTime = new Date;
|
|
83387
84130
|
this.postAskMessage(threadIds, this.pendingUpdate.latestVersion).catch((err) => {
|
|
83388
|
-
|
|
84131
|
+
log43.warn(`Failed to post ask message: ${err}`);
|
|
83389
84132
|
});
|
|
83390
84133
|
return;
|
|
83391
84134
|
}
|
|
@@ -83398,12 +84141,12 @@ class UpdateScheduler extends EventEmitter8 {
|
|
|
83398
84141
|
denials++;
|
|
83399
84142
|
}
|
|
83400
84143
|
if (approvals > threadIds.length / 2) {
|
|
83401
|
-
|
|
84144
|
+
log43.info(`Majority approved (${approvals}/${threadIds.length}), triggering update`);
|
|
83402
84145
|
this.triggerCountdown();
|
|
83403
84146
|
return;
|
|
83404
84147
|
}
|
|
83405
84148
|
if (denials > threadIds.length / 2) {
|
|
83406
|
-
|
|
84149
|
+
log43.info(`Majority denied (${denials}/${threadIds.length}), deferring update`);
|
|
83407
84150
|
this.deferUpdate(60);
|
|
83408
84151
|
return;
|
|
83409
84152
|
}
|
|
@@ -83411,7 +84154,7 @@ class UpdateScheduler extends EventEmitter8 {
|
|
|
83411
84154
|
const elapsedMs = Date.now() - this.askStartTime.getTime();
|
|
83412
84155
|
const timeoutMs = this.config.askTimeoutMinutes * 60 * 1000;
|
|
83413
84156
|
if (elapsedMs >= timeoutMs) {
|
|
83414
|
-
|
|
84157
|
+
log43.info(`Ask timeout reached (${this.config.askTimeoutMinutes} min), triggering update`);
|
|
83415
84158
|
this.triggerCountdown();
|
|
83416
84159
|
}
|
|
83417
84160
|
}
|
|
@@ -83431,7 +84174,7 @@ class UpdateScheduler extends EventEmitter8 {
|
|
|
83431
84174
|
this.emit("ready", this.pendingUpdate);
|
|
83432
84175
|
}
|
|
83433
84176
|
}, 1000);
|
|
83434
|
-
|
|
84177
|
+
log43.info("Update countdown started (60 seconds)");
|
|
83435
84178
|
}
|
|
83436
84179
|
stopCountdown() {
|
|
83437
84180
|
if (this.countdownTimer) {
|
|
@@ -83444,27 +84187,27 @@ class UpdateScheduler extends EventEmitter8 {
|
|
|
83444
84187
|
// src/auto-update/installer.ts
|
|
83445
84188
|
init_logger();
|
|
83446
84189
|
import { spawn as spawn4, spawnSync } from "child_process";
|
|
83447
|
-
import { existsSync as
|
|
84190
|
+
import { existsSync as existsSync16, readFileSync as readFileSync12, writeFileSync as writeFileSync9, mkdirSync as mkdirSync7 } from "fs";
|
|
83448
84191
|
import { dirname as dirname9, resolve as resolve7 } from "path";
|
|
83449
|
-
import { homedir as
|
|
83450
|
-
var
|
|
84192
|
+
import { homedir as homedir8 } from "os";
|
|
84193
|
+
var log44 = createLogger("installer");
|
|
83451
84194
|
function detectPackageManager() {
|
|
83452
84195
|
const npmCmd = process.platform === "win32" ? "npm.cmd" : "npm";
|
|
83453
84196
|
const originalInstaller = detectOriginalInstaller();
|
|
83454
84197
|
if (originalInstaller) {
|
|
83455
|
-
|
|
84198
|
+
log44.debug(`Detected original installer: ${originalInstaller}`);
|
|
83456
84199
|
if (originalInstaller === "bun") {
|
|
83457
84200
|
const bunCheck2 = spawnSync("bun", ["--version"], { stdio: "ignore" });
|
|
83458
84201
|
if (bunCheck2.status === 0) {
|
|
83459
84202
|
return { cmd: "bun", isBun: true };
|
|
83460
84203
|
}
|
|
83461
|
-
|
|
84204
|
+
log44.warn("Originally installed with bun, but bun not found. Falling back to npm.");
|
|
83462
84205
|
} else {
|
|
83463
84206
|
const npmCheck2 = spawnSync(npmCmd, ["--version"], { stdio: "ignore" });
|
|
83464
84207
|
if (npmCheck2.status === 0) {
|
|
83465
84208
|
return { cmd: npmCmd, isBun: false };
|
|
83466
84209
|
}
|
|
83467
|
-
|
|
84210
|
+
log44.warn("Originally installed with npm, but npm not found. Falling back to bun.");
|
|
83468
84211
|
}
|
|
83469
84212
|
}
|
|
83470
84213
|
const bunCheck = spawnSync("bun", ["--version"], { stdio: "ignore" });
|
|
@@ -83486,7 +84229,7 @@ function normalizePath(p) {
|
|
|
83486
84229
|
function detectOriginalInstaller() {
|
|
83487
84230
|
try {
|
|
83488
84231
|
const scriptPath = normalizePath(process.argv[1] || "");
|
|
83489
|
-
const bunGlobalDir = normalizePath(process.env.BUN_INSTALL || resolve7(
|
|
84232
|
+
const bunGlobalDir = normalizePath(process.env.BUN_INSTALL || resolve7(homedir8(), ".bun"));
|
|
83490
84233
|
if (scriptPath.startsWith(bunGlobalDir)) {
|
|
83491
84234
|
return "bun";
|
|
83492
84235
|
}
|
|
@@ -83506,38 +84249,38 @@ function detectOriginalInstaller() {
|
|
|
83506
84249
|
return null;
|
|
83507
84250
|
}
|
|
83508
84251
|
}
|
|
83509
|
-
var STATE_PATH = resolve7(
|
|
84252
|
+
var STATE_PATH = resolve7(homedir8(), ".config", "claude-threads", UPDATE_STATE_FILENAME);
|
|
83510
84253
|
var PACKAGE_NAME2 = "claude-threads";
|
|
83511
84254
|
function loadUpdateState() {
|
|
83512
84255
|
try {
|
|
83513
|
-
if (
|
|
83514
|
-
const content =
|
|
84256
|
+
if (existsSync16(STATE_PATH)) {
|
|
84257
|
+
const content = readFileSync12(STATE_PATH, "utf-8");
|
|
83515
84258
|
return JSON.parse(content);
|
|
83516
84259
|
}
|
|
83517
84260
|
} catch (err) {
|
|
83518
|
-
|
|
84261
|
+
log44.warn(`Failed to load update state: ${err}`);
|
|
83519
84262
|
}
|
|
83520
84263
|
return {};
|
|
83521
84264
|
}
|
|
83522
84265
|
function saveUpdateState(state) {
|
|
83523
84266
|
try {
|
|
83524
84267
|
const dir = dirname9(STATE_PATH);
|
|
83525
|
-
if (!
|
|
83526
|
-
|
|
84268
|
+
if (!existsSync16(dir)) {
|
|
84269
|
+
mkdirSync7(dir, { recursive: true });
|
|
83527
84270
|
}
|
|
83528
|
-
|
|
83529
|
-
|
|
84271
|
+
writeFileSync9(STATE_PATH, JSON.stringify(state, null, 2), "utf-8");
|
|
84272
|
+
log44.debug("Update state saved");
|
|
83530
84273
|
} catch (err) {
|
|
83531
|
-
|
|
84274
|
+
log44.warn(`Failed to save update state: ${err}`);
|
|
83532
84275
|
}
|
|
83533
84276
|
}
|
|
83534
84277
|
function clearUpdateState() {
|
|
83535
84278
|
try {
|
|
83536
|
-
if (
|
|
83537
|
-
|
|
84279
|
+
if (existsSync16(STATE_PATH)) {
|
|
84280
|
+
writeFileSync9(STATE_PATH, "{}", "utf-8");
|
|
83538
84281
|
}
|
|
83539
84282
|
} catch (err) {
|
|
83540
|
-
|
|
84283
|
+
log44.warn(`Failed to clear update state: ${err}`);
|
|
83541
84284
|
}
|
|
83542
84285
|
}
|
|
83543
84286
|
function checkJustUpdated() {
|
|
@@ -83569,11 +84312,11 @@ function clearRuntimeSettings() {
|
|
|
83569
84312
|
}
|
|
83570
84313
|
}
|
|
83571
84314
|
async function installVersion(version) {
|
|
83572
|
-
|
|
84315
|
+
log44.info(`\uD83D\uDCE6 Installing ${PACKAGE_NAME2}@${version}...`);
|
|
83573
84316
|
const pm = detectPackageManager();
|
|
83574
84317
|
if (!pm) {
|
|
83575
84318
|
const error = "Neither bun nor npm found in PATH. Cannot install update.";
|
|
83576
|
-
|
|
84319
|
+
log44.error(`❌ ${error}`);
|
|
83577
84320
|
return { success: false, error };
|
|
83578
84321
|
}
|
|
83579
84322
|
saveUpdateState({
|
|
@@ -83585,7 +84328,7 @@ async function installVersion(version) {
|
|
|
83585
84328
|
return new Promise((resolve8) => {
|
|
83586
84329
|
const { cmd, isBun: isBun3 } = pm;
|
|
83587
84330
|
const args = ["install", "-g", `${PACKAGE_NAME2}@${version}`];
|
|
83588
|
-
|
|
84331
|
+
log44.debug(`Using ${isBun3 ? "bun" : "npm"} for installation`);
|
|
83589
84332
|
const child = spawn4(cmd, args, {
|
|
83590
84333
|
stdio: ["ignore", "pipe", "pipe"],
|
|
83591
84334
|
env: {
|
|
@@ -83603,7 +84346,7 @@ async function installVersion(version) {
|
|
|
83603
84346
|
});
|
|
83604
84347
|
child.on("close", (code) => {
|
|
83605
84348
|
if (code === 0) {
|
|
83606
|
-
|
|
84349
|
+
log44.info(`✅ Successfully installed ${PACKAGE_NAME2}@${version}`);
|
|
83607
84350
|
saveUpdateState({
|
|
83608
84351
|
previousVersion: VERSION,
|
|
83609
84352
|
targetVersion: version,
|
|
@@ -83613,20 +84356,20 @@ async function installVersion(version) {
|
|
|
83613
84356
|
resolve8({ success: true });
|
|
83614
84357
|
} else {
|
|
83615
84358
|
const errorMsg = stderr || stdout || `Exit code: ${code}`;
|
|
83616
|
-
|
|
84359
|
+
log44.error(`❌ Installation failed: ${errorMsg}`);
|
|
83617
84360
|
clearUpdateState();
|
|
83618
84361
|
resolve8({ success: false, error: errorMsg });
|
|
83619
84362
|
}
|
|
83620
84363
|
});
|
|
83621
84364
|
child.on("error", (err) => {
|
|
83622
|
-
|
|
84365
|
+
log44.error(`❌ Failed to spawn npm: ${err}`);
|
|
83623
84366
|
clearUpdateState();
|
|
83624
84367
|
resolve8({ success: false, error: err.message });
|
|
83625
84368
|
});
|
|
83626
84369
|
setTimeout(() => {
|
|
83627
84370
|
if (child.exitCode === null) {
|
|
83628
84371
|
child.kill();
|
|
83629
|
-
|
|
84372
|
+
log44.error("❌ Installation timed out");
|
|
83630
84373
|
clearUpdateState();
|
|
83631
84374
|
resolve8({ success: false, error: "Installation timed out" });
|
|
83632
84375
|
}
|
|
@@ -83670,9 +84413,9 @@ class UpdateInstaller {
|
|
|
83670
84413
|
// src/auto-update/respawn.ts
|
|
83671
84414
|
init_logger();
|
|
83672
84415
|
import { spawn as spawn5 } from "child_process";
|
|
83673
|
-
import { existsSync as
|
|
83674
|
-
import { delimiter, join as
|
|
83675
|
-
var
|
|
84416
|
+
import { existsSync as existsSync17, statSync as statSync4 } from "fs";
|
|
84417
|
+
import { delimiter, join as join14 } from "path";
|
|
84418
|
+
var log45 = createLogger("respawn");
|
|
83676
84419
|
function decideRespawn(env5 = process.env, isTTY = !!process.stdout.isTTY) {
|
|
83677
84420
|
if (env5.CLAUDE_THREADS_BIN) {
|
|
83678
84421
|
return { kind: "exit-for-supervisor", supervisor: "claude-threads-daemon" };
|
|
@@ -83691,22 +84434,22 @@ function decideRespawn(env5 = process.env, isTTY = !!process.stdout.isTTY) {
|
|
|
83691
84434
|
}
|
|
83692
84435
|
return { kind: "self-respawn" };
|
|
83693
84436
|
}
|
|
83694
|
-
function resolveClaudeThreadsBin(_env = process.env, _existsSync =
|
|
84437
|
+
function resolveClaudeThreadsBin(_env = process.env, _existsSync = existsSync17, _isFileExecutable = isFileExecutable) {
|
|
83695
84438
|
const isWin2 = process.platform === "win32";
|
|
83696
84439
|
const names = isWin2 ? ["claude-threads.cmd", "claude-threads.exe", "claude-threads.bat"] : ["claude-threads"];
|
|
83697
84440
|
const path10 = _env.PATH || _env.Path || "";
|
|
83698
84441
|
const dirs = path10.split(delimiter).filter(Boolean);
|
|
83699
84442
|
const home = _env.HOME || _env.USERPROFILE;
|
|
83700
|
-
const bunRoot = _env.BUN_INSTALL || (home ?
|
|
84443
|
+
const bunRoot = _env.BUN_INSTALL || (home ? join14(home, ".bun") : null);
|
|
83701
84444
|
if (bunRoot) {
|
|
83702
|
-
const bunBin =
|
|
84445
|
+
const bunBin = join14(bunRoot, "bin");
|
|
83703
84446
|
if (!dirs.includes(bunBin)) {
|
|
83704
84447
|
dirs.push(bunBin);
|
|
83705
84448
|
}
|
|
83706
84449
|
}
|
|
83707
84450
|
for (const dir of dirs) {
|
|
83708
84451
|
for (const name of names) {
|
|
83709
|
-
const candidate =
|
|
84452
|
+
const candidate = join14(dir, name);
|
|
83710
84453
|
if (_existsSync(candidate) && _isFileExecutable(candidate)) {
|
|
83711
84454
|
return candidate;
|
|
83712
84455
|
}
|
|
@@ -83728,7 +84471,7 @@ function isFileExecutable(path10) {
|
|
|
83728
84471
|
}
|
|
83729
84472
|
function spawnReplacement(argv = process.argv.slice(2), binPath = resolveClaudeThreadsBin()) {
|
|
83730
84473
|
if (!binPath) {
|
|
83731
|
-
|
|
84474
|
+
log45.error("Could not resolve claude-threads on PATH; self-respawn aborted");
|
|
83732
84475
|
return false;
|
|
83733
84476
|
}
|
|
83734
84477
|
if (process.stdin.isTTY && typeof process.stdin.setRawMode === "function") {
|
|
@@ -83749,23 +84492,23 @@ function spawnReplacement(argv = process.argv.slice(2), binPath = resolveClaudeT
|
|
|
83749
84492
|
shell: useShell
|
|
83750
84493
|
});
|
|
83751
84494
|
} catch (err) {
|
|
83752
|
-
|
|
84495
|
+
log45.error(`spawn() threw: ${err instanceof Error ? err.message : String(err)}`);
|
|
83753
84496
|
return false;
|
|
83754
84497
|
}
|
|
83755
84498
|
child.once("error", (err) => {
|
|
83756
|
-
|
|
84499
|
+
log45.error(`Replacement process error: ${err.message}`);
|
|
83757
84500
|
});
|
|
83758
84501
|
if (child.pid === undefined) {
|
|
83759
|
-
|
|
84502
|
+
log45.error("Spawn returned no pid (binary likely not executable)");
|
|
83760
84503
|
return false;
|
|
83761
84504
|
}
|
|
83762
84505
|
child.unref();
|
|
83763
|
-
|
|
84506
|
+
log45.info(`Spawned replacement pid=${child.pid} from ${binPath}`);
|
|
83764
84507
|
return true;
|
|
83765
84508
|
}
|
|
83766
84509
|
|
|
83767
84510
|
// src/auto-update/manager.ts
|
|
83768
|
-
var
|
|
84511
|
+
var log46 = createLogger("updater");
|
|
83769
84512
|
|
|
83770
84513
|
class AutoUpdateManager extends EventEmitter9 {
|
|
83771
84514
|
config;
|
|
@@ -83788,23 +84531,23 @@ class AutoUpdateManager extends EventEmitter9 {
|
|
|
83788
84531
|
}
|
|
83789
84532
|
start() {
|
|
83790
84533
|
if (!this.config.enabled) {
|
|
83791
|
-
|
|
84534
|
+
log46.info("Auto-update is disabled");
|
|
83792
84535
|
return;
|
|
83793
84536
|
}
|
|
83794
84537
|
const updateResult = this.installer.checkJustUpdated();
|
|
83795
84538
|
if (updateResult) {
|
|
83796
|
-
|
|
84539
|
+
log46.info(`\uD83C\uDF89 Updated from v${updateResult.previousVersion} to v${updateResult.currentVersion}`);
|
|
83797
84540
|
this.callbacks.broadcastUpdate((fmt) => `\uD83C\uDF89 ${fmt.formatBold("Bot updated")} from v${updateResult.previousVersion} to v${updateResult.currentVersion}`).catch((err) => {
|
|
83798
|
-
|
|
84541
|
+
log46.warn(`Failed to broadcast update notification: ${err}`);
|
|
83799
84542
|
});
|
|
83800
84543
|
}
|
|
83801
84544
|
this.checker.start();
|
|
83802
|
-
|
|
84545
|
+
log46.info(`\uD83D\uDD04 Auto-update manager started (mode: ${this.config.autoRestartMode})`);
|
|
83803
84546
|
}
|
|
83804
84547
|
stop() {
|
|
83805
84548
|
this.checker.stop();
|
|
83806
84549
|
this.scheduler.stop();
|
|
83807
|
-
|
|
84550
|
+
log46.debug("Auto-update manager stopped");
|
|
83808
84551
|
}
|
|
83809
84552
|
getState() {
|
|
83810
84553
|
return { ...this.state };
|
|
@@ -83818,10 +84561,10 @@ class AutoUpdateManager extends EventEmitter9 {
|
|
|
83818
84561
|
async forceUpdate() {
|
|
83819
84562
|
const updateInfo = this.state.updateInfo || await this.checker.check();
|
|
83820
84563
|
if (!updateInfo) {
|
|
83821
|
-
|
|
84564
|
+
log46.info("No update available");
|
|
83822
84565
|
return;
|
|
83823
84566
|
}
|
|
83824
|
-
|
|
84567
|
+
log46.info("Forcing immediate update");
|
|
83825
84568
|
await this.performUpdate(updateInfo);
|
|
83826
84569
|
}
|
|
83827
84570
|
deferUpdate(minutes = 60) {
|
|
@@ -83887,11 +84630,11 @@ class AutoUpdateManager extends EventEmitter9 {
|
|
|
83887
84630
|
await this.callbacks.prepareForRestart();
|
|
83888
84631
|
} catch (err) {
|
|
83889
84632
|
const reason = err instanceof Error ? err.message : String(err);
|
|
83890
|
-
|
|
84633
|
+
log46.error(`prepareForRestart failed: ${reason}`);
|
|
83891
84634
|
await this.callbacks.broadcastUpdate((fmt) => `⚠️ ${fmt.formatBold("Restart aborted")}: shutdown sequence failed (${reason}). Sessions may be in an inconsistent state; please run ${fmt.formatCode("claude-threads")} manually.`).catch(() => {});
|
|
83892
84635
|
process.exit(1);
|
|
83893
84636
|
}
|
|
83894
|
-
|
|
84637
|
+
log46.info(`\uD83D\uDD04 Restarting for update to v${updateInfo.latestVersion}`);
|
|
83895
84638
|
process.stdout.write("\x1B[2J\x1B[H");
|
|
83896
84639
|
process.stdout.write("\x1B[?25h");
|
|
83897
84640
|
if (decision.kind === "self-respawn") {
|
|
@@ -83900,14 +84643,14 @@ class AutoUpdateManager extends EventEmitter9 {
|
|
|
83900
84643
|
if (ok) {
|
|
83901
84644
|
process.exit(0);
|
|
83902
84645
|
}
|
|
83903
|
-
|
|
84646
|
+
log46.error("Self-respawn launch failed after binary resolution succeeded");
|
|
83904
84647
|
await this.callbacks.broadcastUpdate((fmt) => `⚠️ ${fmt.formatBold("Auto-restart failed")} after install: please run ${fmt.formatCode("claude-threads")} to bring the bot back. Sessions are persisted and will resume.`).catch(() => {});
|
|
83905
84648
|
} else {
|
|
83906
|
-
|
|
84649
|
+
log46.error("claude-threads not found on PATH; manual restart required");
|
|
83907
84650
|
}
|
|
83908
84651
|
process.exit(0);
|
|
83909
84652
|
}
|
|
83910
|
-
|
|
84653
|
+
log46.debug(`Restart handled by supervisor: ${decision.supervisor}`);
|
|
83911
84654
|
process.exit(RESTART_EXIT_CODE);
|
|
83912
84655
|
} else {
|
|
83913
84656
|
const errorMsg = result.error ?? "Unknown error";
|
|
@@ -84291,7 +85034,7 @@ async function startWithoutDaemon() {
|
|
|
84291
85034
|
session.addPlatform(platformConfig.id, client, {
|
|
84292
85035
|
sessionHeader: resolveOverheadVisibility(platformConfig.sessionHeader, `platforms[${platformConfig.id}].sessionHeader`),
|
|
84293
85036
|
stickyMessage: resolveOverheadVisibility(platformConfig.stickyMessage, `platforms[${platformConfig.id}].stickyMessage`)
|
|
84294
|
-
}, resolveMemoryConfig(platformConfig.memory, `platforms[${platformConfig.id}].memory`));
|
|
85037
|
+
}, resolveMemoryConfig(platformConfig.memory, `platforms[${platformConfig.id}].memory`), resolveRoutinesEnabled(platformConfig.routines, `platforms[${platformConfig.id}].routines`));
|
|
84295
85038
|
wirePlatformEvents(platformConfig.id, client, session, ui);
|
|
84296
85039
|
}
|
|
84297
85040
|
const enabledPlatforms = Array.from(platforms.entries()).filter(([id]) => platformEnabledState.get(id) ?? true);
|