switchroom 0.15.8 → 0.15.9
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.
|
@@ -12276,7 +12276,7 @@ function isKnownCheapModel(model) {
|
|
|
12276
12276
|
}
|
|
12277
12277
|
function isCheapCronEnabled(env = process.env) {
|
|
12278
12278
|
const v = (env.SWITCHROOM_CHEAP_CRON ?? "").toLowerCase();
|
|
12279
|
-
return v === "
|
|
12279
|
+
return !(v === "0" || v === "false" || v === "off");
|
|
12280
12280
|
}
|
|
12281
12281
|
function resolveCronModel(model) {
|
|
12282
12282
|
return isKnownCheapModel(model) ? model : DEFAULT_CRON_MODEL;
|
|
@@ -12313,6 +12313,90 @@ function resolveEscalationRouting(input, opts) {
|
|
|
12313
12313
|
return resolveCronRouting({ ...input, kind: "prompt" }, opts);
|
|
12314
12314
|
}
|
|
12315
12315
|
|
|
12316
|
+
// src/scheduler/cron-cadence.ts
|
|
12317
|
+
function csvSmallestGap(field) {
|
|
12318
|
+
if (!field.includes(","))
|
|
12319
|
+
return null;
|
|
12320
|
+
const parts = field.split(",").map((s) => Number(s)).filter((n) => Number.isInteger(n) && n >= 0);
|
|
12321
|
+
if (parts.length < 2)
|
|
12322
|
+
return null;
|
|
12323
|
+
const sorted = [...parts].sort((a, b) => a - b);
|
|
12324
|
+
let smallest = Infinity;
|
|
12325
|
+
for (let i = 1;i < sorted.length; i++) {
|
|
12326
|
+
const gap = sorted[i] - sorted[i - 1];
|
|
12327
|
+
if (gap > 0 && gap < smallest)
|
|
12328
|
+
smallest = gap;
|
|
12329
|
+
}
|
|
12330
|
+
return Number.isFinite(smallest) ? smallest : null;
|
|
12331
|
+
}
|
|
12332
|
+
function estimateCronGapMin(expr) {
|
|
12333
|
+
const fields = expr.trim().split(/\s+/);
|
|
12334
|
+
if (fields.length < 5)
|
|
12335
|
+
return Infinity;
|
|
12336
|
+
const [min, hour] = fields;
|
|
12337
|
+
if (min === "*")
|
|
12338
|
+
return 1;
|
|
12339
|
+
const minStep = min.match(/^\*\/(\d+)$/);
|
|
12340
|
+
if (minStep) {
|
|
12341
|
+
const n = Number(minStep[1]);
|
|
12342
|
+
return n > 0 ? n : Infinity;
|
|
12343
|
+
}
|
|
12344
|
+
const minCsv = csvSmallestGap(min);
|
|
12345
|
+
if (minCsv !== null)
|
|
12346
|
+
return minCsv;
|
|
12347
|
+
if (!/^\d+$/.test(min))
|
|
12348
|
+
return Infinity;
|
|
12349
|
+
if (hour === "*")
|
|
12350
|
+
return 60;
|
|
12351
|
+
const hourStep = hour.match(/^\*\/(\d+)$/);
|
|
12352
|
+
if (hourStep) {
|
|
12353
|
+
const n = Number(hourStep[1]);
|
|
12354
|
+
return n > 0 ? n * 60 : Infinity;
|
|
12355
|
+
}
|
|
12356
|
+
const hourCsv = csvSmallestGap(hour);
|
|
12357
|
+
if (hourCsv !== null)
|
|
12358
|
+
return hourCsv * 60;
|
|
12359
|
+
if (/^\d+$/.test(hour))
|
|
12360
|
+
return 1440;
|
|
12361
|
+
return Infinity;
|
|
12362
|
+
}
|
|
12363
|
+
|
|
12364
|
+
// src/scheduler/tier-selector.ts
|
|
12365
|
+
var DEFAULT_FREQUENT_GAP_MIN = 60;
|
|
12366
|
+
function recommendCronTier(input, frequentGapMin = DEFAULT_FREQUENT_GAP_MIN) {
|
|
12367
|
+
if (input.kind === "poll") {
|
|
12368
|
+
return { tier: "poll", source: "explicit", reason: "declared kind: poll (model-free check)" };
|
|
12369
|
+
}
|
|
12370
|
+
if (input.context === "fresh") {
|
|
12371
|
+
return { tier: "cheap", source: "explicit", reason: "declared context: fresh (cheap cron session)" };
|
|
12372
|
+
}
|
|
12373
|
+
if (input.context === "agent") {
|
|
12374
|
+
return { tier: "main", source: "explicit", reason: "declared context: agent (full live session)" };
|
|
12375
|
+
}
|
|
12376
|
+
if (input.model !== undefined) {
|
|
12377
|
+
return isKnownCheapModel(input.model) ? { tier: "cheap", source: "explicit", reason: `cheap model '${input.model}' → cheap cron session` } : { tier: "main", source: "explicit", reason: `model '${input.model}' is not a known-cheap id → full live session` };
|
|
12378
|
+
}
|
|
12379
|
+
if (input.smallestGapMin <= frequentGapMin) {
|
|
12380
|
+
return {
|
|
12381
|
+
tier: "cheap",
|
|
12382
|
+
source: "cadence-default",
|
|
12383
|
+
reason: `fires every ~${input.smallestGapMin}min (≤ ${frequentGapMin}min) — defaulting to a cheap ` + `session; set context: agent (or an Opus/custom model) if this needs the agent's full context`
|
|
12384
|
+
};
|
|
12385
|
+
}
|
|
12386
|
+
return {
|
|
12387
|
+
tier: "main",
|
|
12388
|
+
source: "cadence-default",
|
|
12389
|
+
reason: `fires every ~${input.smallestGapMin}min (> ${frequentGapMin}min) — defaulting to the agent's ` + `full session; set model: sonnet (or context: fresh) to run it cheaply`
|
|
12390
|
+
};
|
|
12391
|
+
}
|
|
12392
|
+
function applyDefaultTier(entry, frequentGapMin = DEFAULT_FREQUENT_GAP_MIN) {
|
|
12393
|
+
if (entry.kind === "poll" || entry.context !== undefined || entry.model !== undefined) {
|
|
12394
|
+
return entry;
|
|
12395
|
+
}
|
|
12396
|
+
const rec = recommendCronTier({ smallestGapMin: estimateCronGapMin(entry.cron) }, frequentGapMin);
|
|
12397
|
+
return rec.tier === "cheap" ? { ...entry, context: "fresh" } : entry;
|
|
12398
|
+
}
|
|
12399
|
+
|
|
12316
12400
|
// src/agent-scheduler/cheap-cron-wiring.ts
|
|
12317
12401
|
import { lookup as dnsLookup } from "node:dns/promises";
|
|
12318
12402
|
|
|
@@ -14296,7 +14380,8 @@ function registerAgentSchedule(opts) {
|
|
|
14296
14380
|
}
|
|
14297
14381
|
}
|
|
14298
14382
|
const cheapEnabled = opts.cheapCron?.enabled ?? false;
|
|
14299
|
-
const
|
|
14383
|
+
const routed = cheapEnabled ? applyDefaultTier(entry) : entry;
|
|
14384
|
+
const routing = resolveCronRouting(routed, { cheapCronEnabled: cheapEnabled });
|
|
14300
14385
|
const threadId = resolveEntryThreadId(entry, opts.channel);
|
|
14301
14386
|
const record = (fields) => opts.sink.recordFire({
|
|
14302
14387
|
agent: entry.agent,
|
package/dist/cli/switchroom.js
CHANGED
|
@@ -15435,6 +15435,93 @@ var init_cron_routing = __esm(() => {
|
|
|
15435
15435
|
OPUS_MODEL_RE = /opus/i;
|
|
15436
15436
|
});
|
|
15437
15437
|
|
|
15438
|
+
// src/scheduler/cron-cadence.ts
|
|
15439
|
+
function csvSmallestGap(field) {
|
|
15440
|
+
if (!field.includes(","))
|
|
15441
|
+
return null;
|
|
15442
|
+
const parts = field.split(",").map((s) => Number(s)).filter((n) => Number.isInteger(n) && n >= 0);
|
|
15443
|
+
if (parts.length < 2)
|
|
15444
|
+
return null;
|
|
15445
|
+
const sorted = [...parts].sort((a, b) => a - b);
|
|
15446
|
+
let smallest = Infinity;
|
|
15447
|
+
for (let i = 1;i < sorted.length; i++) {
|
|
15448
|
+
const gap = sorted[i] - sorted[i - 1];
|
|
15449
|
+
if (gap > 0 && gap < smallest)
|
|
15450
|
+
smallest = gap;
|
|
15451
|
+
}
|
|
15452
|
+
return Number.isFinite(smallest) ? smallest : null;
|
|
15453
|
+
}
|
|
15454
|
+
function estimateCronGapMin(expr) {
|
|
15455
|
+
const fields = expr.trim().split(/\s+/);
|
|
15456
|
+
if (fields.length < 5)
|
|
15457
|
+
return Infinity;
|
|
15458
|
+
const [min, hour] = fields;
|
|
15459
|
+
if (min === "*")
|
|
15460
|
+
return 1;
|
|
15461
|
+
const minStep = min.match(/^\*\/(\d+)$/);
|
|
15462
|
+
if (minStep) {
|
|
15463
|
+
const n = Number(minStep[1]);
|
|
15464
|
+
return n > 0 ? n : Infinity;
|
|
15465
|
+
}
|
|
15466
|
+
const minCsv = csvSmallestGap(min);
|
|
15467
|
+
if (minCsv !== null)
|
|
15468
|
+
return minCsv;
|
|
15469
|
+
if (!/^\d+$/.test(min))
|
|
15470
|
+
return Infinity;
|
|
15471
|
+
if (hour === "*")
|
|
15472
|
+
return 60;
|
|
15473
|
+
const hourStep = hour.match(/^\*\/(\d+)$/);
|
|
15474
|
+
if (hourStep) {
|
|
15475
|
+
const n = Number(hourStep[1]);
|
|
15476
|
+
return n > 0 ? n * 60 : Infinity;
|
|
15477
|
+
}
|
|
15478
|
+
const hourCsv = csvSmallestGap(hour);
|
|
15479
|
+
if (hourCsv !== null)
|
|
15480
|
+
return hourCsv * 60;
|
|
15481
|
+
if (/^\d+$/.test(hour))
|
|
15482
|
+
return 1440;
|
|
15483
|
+
return Infinity;
|
|
15484
|
+
}
|
|
15485
|
+
|
|
15486
|
+
// src/scheduler/tier-selector.ts
|
|
15487
|
+
function recommendCronTier(input, frequentGapMin = DEFAULT_FREQUENT_GAP_MIN) {
|
|
15488
|
+
if (input.kind === "poll") {
|
|
15489
|
+
return { tier: "poll", source: "explicit", reason: "declared kind: poll (model-free check)" };
|
|
15490
|
+
}
|
|
15491
|
+
if (input.context === "fresh") {
|
|
15492
|
+
return { tier: "cheap", source: "explicit", reason: "declared context: fresh (cheap cron session)" };
|
|
15493
|
+
}
|
|
15494
|
+
if (input.context === "agent") {
|
|
15495
|
+
return { tier: "main", source: "explicit", reason: "declared context: agent (full live session)" };
|
|
15496
|
+
}
|
|
15497
|
+
if (input.model !== undefined) {
|
|
15498
|
+
return isKnownCheapModel(input.model) ? { tier: "cheap", source: "explicit", reason: `cheap model '${input.model}' \u2192 cheap cron session` } : { tier: "main", source: "explicit", reason: `model '${input.model}' is not a known-cheap id \u2192 full live session` };
|
|
15499
|
+
}
|
|
15500
|
+
if (input.smallestGapMin <= frequentGapMin) {
|
|
15501
|
+
return {
|
|
15502
|
+
tier: "cheap",
|
|
15503
|
+
source: "cadence-default",
|
|
15504
|
+
reason: `fires every ~${input.smallestGapMin}min (\u2264 ${frequentGapMin}min) \u2014 defaulting to a cheap ` + `session; set context: agent (or an Opus/custom model) if this needs the agent's full context`
|
|
15505
|
+
};
|
|
15506
|
+
}
|
|
15507
|
+
return {
|
|
15508
|
+
tier: "main",
|
|
15509
|
+
source: "cadence-default",
|
|
15510
|
+
reason: `fires every ~${input.smallestGapMin}min (> ${frequentGapMin}min) \u2014 defaulting to the agent's ` + `full session; set model: sonnet (or context: fresh) to run it cheaply`
|
|
15511
|
+
};
|
|
15512
|
+
}
|
|
15513
|
+
function applyDefaultTier(entry, frequentGapMin = DEFAULT_FREQUENT_GAP_MIN) {
|
|
15514
|
+
if (entry.kind === "poll" || entry.context !== undefined || entry.model !== undefined) {
|
|
15515
|
+
return entry;
|
|
15516
|
+
}
|
|
15517
|
+
const rec = recommendCronTier({ smallestGapMin: estimateCronGapMin(entry.cron) }, frequentGapMin);
|
|
15518
|
+
return rec.tier === "cheap" ? { ...entry, context: "fresh" } : entry;
|
|
15519
|
+
}
|
|
15520
|
+
var DEFAULT_FREQUENT_GAP_MIN = 60;
|
|
15521
|
+
var init_tier_selector = __esm(() => {
|
|
15522
|
+
init_cron_routing();
|
|
15523
|
+
});
|
|
15524
|
+
|
|
15438
15525
|
// src/config/timezone.ts
|
|
15439
15526
|
import { readFileSync as readFileSync3, readlinkSync } from "node:fs";
|
|
15440
15527
|
function defaultReadEtcTimezone() {
|
|
@@ -23262,7 +23349,7 @@ function describeAgents(config) {
|
|
|
23262
23349
|
const resolved = resolveAgentConfig(config.defaults, config.profiles, agent);
|
|
23263
23350
|
const profile = agent.extends ?? "default";
|
|
23264
23351
|
const uid = allocateAgentUid(name);
|
|
23265
|
-
const cronSession = scheduleNeedsCronSession((resolved.schedule ?? []).map((e) => ({ kind: e.kind, model: e.model, context: e.context })), { cheapCronEnabled: true });
|
|
23352
|
+
const cronSession = scheduleNeedsCronSession((resolved.schedule ?? []).map((e) => applyDefaultTier({ cron: e.cron, kind: e.kind, model: e.model, context: e.context })), { cheapCronEnabled: true });
|
|
23266
23353
|
const resources = resolveResourceDefaults(name, profile, resolved.resources, { cronSession });
|
|
23267
23354
|
const strippedCaps = readStrippedCaps(agent);
|
|
23268
23355
|
out.push({
|
|
@@ -23760,6 +23847,7 @@ function emitAgentService(lines, a, imageTag, buildMode, buildContext, homePrefi
|
|
|
23760
23847
|
var AGENT_UID_MIN = 10001, AGENT_UID_MAX = 10999, RESOURCE_BY_PROFILE, CRON_SESSION_MEM_BUMP_MIB = 512, CRON_SESSION_PIDS_BUMP = 128, BIND_MOUNT_SOURCE_DENYLIST, BIND_MOUNT_TARGET_DENYLIST, BIND_MOUNT_EXACT_SOURCE_DENY, CONTAINER_CONFIG_PATH = "/state/config/switchroom.yaml";
|
|
23761
23848
|
var init_compose = __esm(() => {
|
|
23762
23849
|
init_cron_routing();
|
|
23850
|
+
init_tier_selector();
|
|
23763
23851
|
init_merge();
|
|
23764
23852
|
init_timezone();
|
|
23765
23853
|
init_peercred();
|
|
@@ -50235,8 +50323,8 @@ var {
|
|
|
50235
50323
|
} = import__.default;
|
|
50236
50324
|
|
|
50237
50325
|
// src/build-info.ts
|
|
50238
|
-
var VERSION = "0.15.
|
|
50239
|
-
var COMMIT_SHA = "
|
|
50326
|
+
var VERSION = "0.15.9";
|
|
50327
|
+
var COMMIT_SHA = "6ed776e2";
|
|
50240
50328
|
|
|
50241
50329
|
// src/cli/agent.ts
|
|
50242
50330
|
init_source();
|
|
@@ -50347,6 +50435,7 @@ var LEGACY_CRON_SCRIPT_BASENAME_RE = /^cron-(\d+)\.sh$/;
|
|
|
50347
50435
|
// src/agents/scaffold.ts
|
|
50348
50436
|
init_schema();
|
|
50349
50437
|
init_cron_routing();
|
|
50438
|
+
init_tier_selector();
|
|
50350
50439
|
init_merge();
|
|
50351
50440
|
init_timezone();
|
|
50352
50441
|
|
|
@@ -51489,11 +51578,7 @@ function renderFleetInvariants() {
|
|
|
51489
51578
|
`);
|
|
51490
51579
|
}
|
|
51491
51580
|
function buildCronSessionContext(agentConfig) {
|
|
51492
|
-
const entries = (agentConfig.schedule ?? []).map((e) => ({
|
|
51493
|
-
kind: e.kind,
|
|
51494
|
-
model: e.model,
|
|
51495
|
-
context: e.context
|
|
51496
|
-
}));
|
|
51581
|
+
const entries = (agentConfig.schedule ?? []).map((e) => applyDefaultTier({ cron: e.cron, kind: e.kind, model: e.model, context: e.context }));
|
|
51497
51582
|
return {
|
|
51498
51583
|
cronSessionEnabled: scheduleNeedsCronSession(entries, { cheapCronEnabled: true }),
|
|
51499
51584
|
cronModelQ: shellSingleQuote(DEFAULT_CRON_MODEL)
|
package/package.json
CHANGED
|
@@ -53757,11 +53757,11 @@ function readTurnActiveMarkerAgeMs(stateDir, now) {
|
|
|
53757
53757
|
}
|
|
53758
53758
|
|
|
53759
53759
|
// ../src/build-info.ts
|
|
53760
|
-
var VERSION = "0.15.
|
|
53761
|
-
var COMMIT_SHA = "
|
|
53762
|
-
var COMMIT_DATE = "2026-06-
|
|
53763
|
-
var LATEST_PR =
|
|
53764
|
-
var COMMITS_AHEAD_OF_TAG =
|
|
53760
|
+
var VERSION = "0.15.9";
|
|
53761
|
+
var COMMIT_SHA = "6ed776e2";
|
|
53762
|
+
var COMMIT_DATE = "2026-06-13T00:47:35Z";
|
|
53763
|
+
var LATEST_PR = 2300;
|
|
53764
|
+
var COMMITS_AHEAD_OF_TAG = 0;
|
|
53765
53765
|
|
|
53766
53766
|
// gateway/boot-version.ts
|
|
53767
53767
|
function formatRelativeAgo(iso) {
|