switchroom 0.15.9 → 0.15.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent-scheduler/index.js +2 -80
- package/dist/cli/switchroom.js +6 -82
- package/dist/cli/ui/index.html +71 -1
- package/package.json +1 -1
- package/profiles/_base/cron-session.sh.hbs +10 -6
- package/telegram-plugin/dist/gateway/gateway.js +87 -26
- package/telegram-plugin/gateway/cron-session.ts +34 -0
- package/telegram-plugin/gateway/gateway.ts +118 -35
- package/telegram-plugin/gateway/obligation-ledger.ts +56 -15
- package/telegram-plugin/history.ts +57 -0
- package/telegram-plugin/tests/cron-session.test.ts +36 -0
- package/telegram-plugin/tests/history.test.ts +83 -0
- package/telegram-plugin/tests/obligation-ledger.test.ts +213 -5
- package/telegram-plugin/tests/obligation-store.test.ts +17 -0
|
@@ -12313,88 +12313,10 @@ 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
12316
|
// src/scheduler/tier-selector.ts
|
|
12365
12317
|
var DEFAULT_FREQUENT_GAP_MIN = 60;
|
|
12366
|
-
function
|
|
12367
|
-
|
|
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;
|
|
12318
|
+
function applyDefaultTier(entry, _frequentGapMin = DEFAULT_FREQUENT_GAP_MIN) {
|
|
12319
|
+
return entry;
|
|
12398
12320
|
}
|
|
12399
12321
|
|
|
12400
12322
|
// src/agent-scheduler/cheap-cron-wiring.ts
|
package/dist/cli/switchroom.js
CHANGED
|
@@ -15435,87 +15435,9 @@ 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
15438
|
// src/scheduler/tier-selector.ts
|
|
15487
|
-
function
|
|
15488
|
-
|
|
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;
|
|
15439
|
+
function applyDefaultTier(entry, _frequentGapMin = DEFAULT_FREQUENT_GAP_MIN) {
|
|
15440
|
+
return entry;
|
|
15519
15441
|
}
|
|
15520
15442
|
var DEFAULT_FREQUENT_GAP_MIN = 60;
|
|
15521
15443
|
var init_tier_selector = __esm(() => {
|
|
@@ -24526,6 +24448,8 @@ function getAgentLogs(name, follow) {
|
|
|
24526
24448
|
function classifyChangeKind(path) {
|
|
24527
24449
|
if (/\/telegram\/cron-(?:\d+|[0-9a-f]{12})\.sh$/.test(path))
|
|
24528
24450
|
return "cron";
|
|
24451
|
+
if (path.includes("/.claude-cron/"))
|
|
24452
|
+
return "cron";
|
|
24529
24453
|
if (path.includes("/.claude/skills/"))
|
|
24530
24454
|
return "skill";
|
|
24531
24455
|
if (path.endsWith("/.claude/settings.json"))
|
|
@@ -50323,8 +50247,8 @@ var {
|
|
|
50323
50247
|
} = import__.default;
|
|
50324
50248
|
|
|
50325
50249
|
// src/build-info.ts
|
|
50326
|
-
var VERSION = "0.15.
|
|
50327
|
-
var COMMIT_SHA = "
|
|
50250
|
+
var VERSION = "0.15.11";
|
|
50251
|
+
var COMMIT_SHA = "43331954";
|
|
50328
50252
|
|
|
50329
50253
|
// src/cli/agent.ts
|
|
50330
50254
|
init_source();
|
package/dist/cli/ui/index.html
CHANGED
|
@@ -119,6 +119,40 @@
|
|
|
119
119
|
|
|
120
120
|
.meta-item span { color: var(--text); }
|
|
121
121
|
|
|
122
|
+
.scope-toggle {
|
|
123
|
+
cursor: pointer;
|
|
124
|
+
color: var(--text);
|
|
125
|
+
border-bottom: 1px dotted var(--text-dim);
|
|
126
|
+
user-select: none;
|
|
127
|
+
}
|
|
128
|
+
.scope-toggle:hover { color: var(--blue); }
|
|
129
|
+
.scope-toggle:focus-visible { outline: 2px solid var(--blue); outline-offset: 2px; }
|
|
130
|
+
.scope-caret {
|
|
131
|
+
display: inline-block;
|
|
132
|
+
width: 0.9em;
|
|
133
|
+
margin-right: 0.15rem;
|
|
134
|
+
color: var(--text-dim);
|
|
135
|
+
font-size: 0.75em;
|
|
136
|
+
}
|
|
137
|
+
.scope-list {
|
|
138
|
+
list-style: none;
|
|
139
|
+
margin: 0.35rem 0 0;
|
|
140
|
+
padding: 0.5rem 0.75rem;
|
|
141
|
+
width: 100%;
|
|
142
|
+
background: var(--surface-hover);
|
|
143
|
+
border: 1px solid var(--border);
|
|
144
|
+
border-radius: 6px;
|
|
145
|
+
font-size: 0.78rem;
|
|
146
|
+
line-height: 1.5;
|
|
147
|
+
}
|
|
148
|
+
.scope-list li {
|
|
149
|
+
color: var(--text);
|
|
150
|
+
word-break: break-all;
|
|
151
|
+
padding: 0.1rem 0;
|
|
152
|
+
border-bottom: 1px solid var(--border);
|
|
153
|
+
}
|
|
154
|
+
.scope-list li:last-child { border-bottom: none; }
|
|
155
|
+
|
|
122
156
|
.card-actions {
|
|
123
157
|
padding: 0.75rem 1.25rem;
|
|
124
158
|
border-top: 1px solid var(--border);
|
|
@@ -1225,6 +1259,7 @@
|
|
|
1225
1259
|
const _dimC = (s) => `<span style="color:var(--text-dim)">${escapeHtml(s)}</span>`;
|
|
1226
1260
|
|
|
1227
1261
|
let _accessBtnSeq = 0;
|
|
1262
|
+
let _scopeSeq = 0;
|
|
1228
1263
|
|
|
1229
1264
|
// One OAuth-account card (Google or Microsoft — same shape; Microsoft
|
|
1230
1265
|
// adds an account-type pill). When agentNames is supplied, renders a
|
|
@@ -1255,6 +1290,24 @@
|
|
|
1255
1290
|
}).join('')}
|
|
1256
1291
|
</div>`
|
|
1257
1292
|
: '';
|
|
1293
|
+
// Scope: clickable "N scope(s)" that expands a full-width list of the
|
|
1294
|
+
// individual scopes below the meta row (the old version hid them in a
|
|
1295
|
+
// title tooltip only). The list is a sibling block after .card-meta so
|
|
1296
|
+
// it isn't squished inside the flex-wrapped meta-items.
|
|
1297
|
+
const scopeList = a.scope ? a.scope.split(/\s+/).filter(Boolean) : [];
|
|
1298
|
+
let scopeCount, scopeBlock = '';
|
|
1299
|
+
if (scopeList.length) {
|
|
1300
|
+
const sid = `scopes-${++_scopeSeq}`;
|
|
1301
|
+
scopeCount = `<span class="scope-toggle" id="${sid}-t" role="button" tabindex="0" aria-expanded="false" aria-controls="${sid}"
|
|
1302
|
+
onclick="toggleScopes('${sid}')"
|
|
1303
|
+
onkeydown="if(event.key==='Enter'||event.key===' '){event.preventDefault();toggleScopes('${sid}');}"
|
|
1304
|
+
><span class="scope-caret">▸</span>${escapeHtml(scopeList.length + ' scope' + (scopeList.length === 1 ? '' : 's'))}</span>`;
|
|
1305
|
+
scopeBlock = `<ul class="scope-list" id="${sid}" hidden>${
|
|
1306
|
+
scopeList.map(s => `<li title="${escapeHtml(s)}">${escapeHtml(s)}</li>`).join('')
|
|
1307
|
+
}</ul>`;
|
|
1308
|
+
} else {
|
|
1309
|
+
scopeCount = _dimC('—');
|
|
1310
|
+
}
|
|
1258
1311
|
return `
|
|
1259
1312
|
<div class="account-card">
|
|
1260
1313
|
<div class="account-card-header">
|
|
@@ -1264,9 +1317,10 @@
|
|
|
1264
1317
|
<div class="account-usage"><label style="color:var(--text-dim);opacity:.7">Enabled for: </label>${acl}</div>
|
|
1265
1318
|
<div class="card-meta" style="padding:0">
|
|
1266
1319
|
<div class="meta-item"><label>Expires </label><span>${expires}</span></div>
|
|
1267
|
-
<div class="meta-item"
|
|
1320
|
+
<div class="meta-item"><label>Scope </label><span>${scopeCount}</span></div>
|
|
1268
1321
|
<div class="meta-item"><label>Client </label><span>${a.clientId ? escapeHtml(a.clientId.slice(0, 16) + '…') : _dimC('—')}</span></div>
|
|
1269
1322
|
</div>
|
|
1323
|
+
${scopeBlock}
|
|
1270
1324
|
${manage}
|
|
1271
1325
|
</div>`;
|
|
1272
1326
|
}
|
|
@@ -1531,6 +1585,22 @@
|
|
|
1531
1585
|
}
|
|
1532
1586
|
}
|
|
1533
1587
|
|
|
1588
|
+
// Expand/collapse a connection card's scope list in place. Self-contained
|
|
1589
|
+
// (no global state / re-render) — just flips the list's [hidden] and the
|
|
1590
|
+
// toggle's caret + aria-expanded.
|
|
1591
|
+
function toggleScopes(id) {
|
|
1592
|
+
const list = document.getElementById(id);
|
|
1593
|
+
const toggle = document.getElementById(id + '-t');
|
|
1594
|
+
if (!list) return;
|
|
1595
|
+
const open = list.hidden;
|
|
1596
|
+
list.hidden = !open;
|
|
1597
|
+
if (toggle) {
|
|
1598
|
+
toggle.setAttribute('aria-expanded', String(open));
|
|
1599
|
+
const caret = toggle.querySelector('.scope-caret');
|
|
1600
|
+
if (caret) caret.textContent = open ? '▾' : '▸';
|
|
1601
|
+
}
|
|
1602
|
+
}
|
|
1603
|
+
|
|
1534
1604
|
async function toggleLogs(name) {
|
|
1535
1605
|
if (openLogs.has(name)) {
|
|
1536
1606
|
openLogs.delete(name);
|
package/package.json
CHANGED
|
@@ -17,15 +17,19 @@
|
|
|
17
17
|
set -u
|
|
18
18
|
|
|
19
19
|
# Runtime kill-switch. The fork is baked into start.sh whenever {{name}} has a
|
|
20
|
-
#
|
|
21
|
-
# runs when
|
|
22
|
-
#
|
|
20
|
+
# cron entry the value-gate routes to a cheap session, but the session only
|
|
21
|
+
# actually runs when cheap-cron is enabled at runtime. Cheap-cron is ON by
|
|
22
|
+
# DEFAULT (matches isCheapCronEnabled in src/scheduler/cron-routing.ts — only
|
|
23
|
+
# SWITCHROOM_CHEAP_CRON=0/false/off disables it); the old "off unless =1" gate
|
|
24
|
+
# here meant the cron session quarantined itself even with cheap-by-default on,
|
|
25
|
+
# so every Tier-1 fire fell back to the main session and saved nothing. Exit 78
|
|
26
|
+
# (EX_CONFIG) on the explicit kill-switch so the supervisor cleanly idles.
|
|
23
27
|
case "${SWITCHROOM_CHEAP_CRON:-}" in
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
echo "cron-session: SWITCHROOM_CHEAP_CRON off — not starting (exit 78, no respawn)" >&2
|
|
28
|
+
0 | false | off | OFF | False | FALSE)
|
|
29
|
+
echo "cron-session: SWITCHROOM_CHEAP_CRON disabled (=0) — not starting (exit 78, no respawn)" >&2
|
|
27
30
|
exit 78
|
|
28
31
|
;;
|
|
32
|
+
*) : ;;
|
|
29
33
|
esac
|
|
30
34
|
|
|
31
35
|
CRON_NAME="{{name}}-cron"
|
|
@@ -28268,6 +28268,7 @@ __export(exports_history, {
|
|
|
28268
28268
|
pruneMessagesOlderThanDays: () => pruneMessagesOlderThanDays,
|
|
28269
28269
|
lookupMessageRoleAndText: () => lookupMessageRoleAndText,
|
|
28270
28270
|
initHistory: () => initHistory,
|
|
28271
|
+
hasOutboundDeliveredSince: () => hasOutboundDeliveredSince,
|
|
28271
28272
|
getRecentOutboundCount: () => getRecentOutboundCount,
|
|
28272
28273
|
getLatestInboundMessageId: () => getLatestInboundMessageId,
|
|
28273
28274
|
deleteFromHistory: () => deleteFromHistory,
|
|
@@ -28462,6 +28463,26 @@ function getRecentOutboundCount(chatId, withinSeconds) {
|
|
|
28462
28463
|
const row = requireDb().prepare("SELECT COUNT(*) as cnt FROM messages WHERE chat_id = ? AND role = ? AND ts >= ?").get(chatId, "assistant", cutoff);
|
|
28463
28464
|
return row?.cnt ?? 0;
|
|
28464
28465
|
}
|
|
28466
|
+
function hasOutboundDeliveredSince(chatId, sinceMs, threadId) {
|
|
28467
|
+
try {
|
|
28468
|
+
const cutoffSec = Math.floor(sinceMs / 1000);
|
|
28469
|
+
const params = [chatId, cutoffSec];
|
|
28470
|
+
let sql = "SELECT 1 FROM messages WHERE chat_id = ? AND role = 'assistant' AND ts >= ? AND LENGTH(text) >= 200";
|
|
28471
|
+
if (threadId !== undefined) {
|
|
28472
|
+
if (threadId === null) {
|
|
28473
|
+
sql += " AND thread_id IS NULL";
|
|
28474
|
+
} else {
|
|
28475
|
+
sql += " AND thread_id = ?";
|
|
28476
|
+
params.push(threadId);
|
|
28477
|
+
}
|
|
28478
|
+
}
|
|
28479
|
+
sql += " LIMIT 1";
|
|
28480
|
+
const row = requireDb().prepare(sql).get(...params);
|
|
28481
|
+
return row != null;
|
|
28482
|
+
} catch {
|
|
28483
|
+
return false;
|
|
28484
|
+
}
|
|
28485
|
+
}
|
|
28465
28486
|
function query(opts) {
|
|
28466
28487
|
const limit = Math.min(MAX_LIMIT, Math.max(1, opts.limit ?? DEFAULT_LIMIT));
|
|
28467
28488
|
const params = [opts.chat_id];
|
|
@@ -48238,6 +48259,16 @@ function isCronIdentity(name) {
|
|
|
48238
48259
|
function resolveInjectTarget(agentName3, meta) {
|
|
48239
48260
|
return meta?.session === "cron" ? cronIdentity(agentName3) : agentName3;
|
|
48240
48261
|
}
|
|
48262
|
+
function deliverInjectWithFallback(agentName3, meta, send) {
|
|
48263
|
+
const target = resolveInjectTarget(agentName3, meta);
|
|
48264
|
+
const wantedCron = target !== agentName3;
|
|
48265
|
+
if (send(target))
|
|
48266
|
+
return { target, delivered: true, fellBackToMain: false };
|
|
48267
|
+
if (wantedCron && send(agentName3)) {
|
|
48268
|
+
return { target: agentName3, delivered: true, fellBackToMain: true };
|
|
48269
|
+
}
|
|
48270
|
+
return { target, delivered: false, fellBackToMain: false };
|
|
48271
|
+
}
|
|
48241
48272
|
|
|
48242
48273
|
// gateway/obligation-ledger.ts
|
|
48243
48274
|
class ObligationLedger {
|
|
@@ -48295,21 +48326,23 @@ class ObligationLedger {
|
|
|
48295
48326
|
return best;
|
|
48296
48327
|
}
|
|
48297
48328
|
decideAtIdle(opts) {
|
|
48298
|
-
const useEligible = opts != null && (opts.graceMs > 0 || opts.backgroundWorkActive === true);
|
|
48299
|
-
const o = useEligible ? this.oldestEligible(opts.now, opts.graceMs, opts.backgroundWorkActive === true, opts.backgroundGraceMs ?? 0) : this.oldest();
|
|
48329
|
+
const useEligible = opts != null && (opts.graceMs > 0 || opts.backgroundWorkActive === true || (opts.representGraceMs ?? 0) > 0);
|
|
48330
|
+
const o = useEligible ? this.oldestEligible(opts.now, opts.graceMs, opts.backgroundWorkActive === true, opts.backgroundGraceMs ?? 0, opts.representGraceMs ?? 0) : this.oldest();
|
|
48300
48331
|
if (o === undefined)
|
|
48301
48332
|
return { action: "none" };
|
|
48302
48333
|
if (o.representCount >= this.maxRepresents)
|
|
48303
48334
|
return { action: "escalate", obligation: o };
|
|
48304
48335
|
return { action: "represent", obligation: o };
|
|
48305
48336
|
}
|
|
48306
|
-
oldestEligible(now, graceMs, backgroundWorkActive, backgroundGraceMs) {
|
|
48337
|
+
oldestEligible(now, graceMs, backgroundWorkActive, backgroundGraceMs, representGraceMs) {
|
|
48307
48338
|
let best;
|
|
48308
48339
|
for (const o of this.open.values()) {
|
|
48309
48340
|
if (o.lastTurnEndedAt != null && now - o.lastTurnEndedAt < graceMs)
|
|
48310
48341
|
continue;
|
|
48311
48342
|
if (backgroundWorkActive && backgroundGraceMs > 0 && now - o.openedAt < backgroundGraceMs)
|
|
48312
48343
|
continue;
|
|
48344
|
+
if (representGraceMs > 0 && o.lastRepresentedAt != null && now - o.lastRepresentedAt < representGraceMs)
|
|
48345
|
+
continue;
|
|
48313
48346
|
if (best === undefined || o.openedAt < best.openedAt)
|
|
48314
48347
|
best = o;
|
|
48315
48348
|
}
|
|
@@ -48322,18 +48355,21 @@ class ObligationLedger {
|
|
|
48322
48355
|
o.lastTurnEndedAt = ts;
|
|
48323
48356
|
this.persist();
|
|
48324
48357
|
}
|
|
48325
|
-
resolveCloseTarget(echoedTurnId, liveTurnId) {
|
|
48358
|
+
resolveCloseTarget(echoedTurnId, liveTurnId, routedOriginId) {
|
|
48326
48359
|
if (echoedTurnId != null)
|
|
48327
48360
|
return echoedTurnId;
|
|
48328
|
-
if (
|
|
48361
|
+
if (routedOriginId != null)
|
|
48362
|
+
return routedOriginId;
|
|
48363
|
+
if (liveTurnId != null && this.open.has(liveTurnId))
|
|
48329
48364
|
return liveTurnId;
|
|
48330
48365
|
return null;
|
|
48331
48366
|
}
|
|
48332
|
-
markRepresented(originTurnId) {
|
|
48367
|
+
markRepresented(originTurnId, now = Date.now()) {
|
|
48333
48368
|
const o = this.open.get(originTurnId);
|
|
48334
48369
|
if (o === undefined)
|
|
48335
48370
|
return 0;
|
|
48336
48371
|
o.representCount += 1;
|
|
48372
|
+
o.lastRepresentedAt = now;
|
|
48337
48373
|
this.persist();
|
|
48338
48374
|
return o.representCount;
|
|
48339
48375
|
}
|
|
@@ -53757,10 +53793,10 @@ function readTurnActiveMarkerAgeMs(stateDir, now) {
|
|
|
53757
53793
|
}
|
|
53758
53794
|
|
|
53759
53795
|
// ../src/build-info.ts
|
|
53760
|
-
var VERSION = "0.15.
|
|
53761
|
-
var COMMIT_SHA = "
|
|
53762
|
-
var COMMIT_DATE = "2026-06-
|
|
53763
|
-
var LATEST_PR =
|
|
53796
|
+
var VERSION = "0.15.11";
|
|
53797
|
+
var COMMIT_SHA = "43331954";
|
|
53798
|
+
var COMMIT_DATE = "2026-06-13T03:24:01Z";
|
|
53799
|
+
var LATEST_PR = 2308;
|
|
53764
53800
|
var COMMITS_AHEAD_OF_TAG = 0;
|
|
53765
53801
|
|
|
53766
53802
|
// gateway/boot-version.ts
|
|
@@ -54993,6 +55029,13 @@ var OBLIGATION_BACKGROUND_WORK_GRACE_MS = (() => {
|
|
|
54993
55029
|
const n = Number(raw);
|
|
54994
55030
|
return Number.isFinite(n) && n >= 0 ? n : 1200000;
|
|
54995
55031
|
})();
|
|
55032
|
+
var OBLIGATION_REPRESENT_GRACE_MS = (() => {
|
|
55033
|
+
const raw = process.env.SWITCHROOM_OBLIGATION_REPRESENT_GRACE_MS;
|
|
55034
|
+
if (raw == null || raw === "")
|
|
55035
|
+
return 120000;
|
|
55036
|
+
const n = Number(raw);
|
|
55037
|
+
return Number.isFinite(n) && n >= 0 ? n : 120000;
|
|
55038
|
+
})();
|
|
54996
55039
|
var TURN_ACTIVE_MARKER_FRESH_MS = 90000;
|
|
54997
55040
|
var AUTOCLASSIFY_MIDTURN_SHADOW = process.env.SWITCHROOM_AUTOCLASSIFY_MIDTURN_SHADOW !== "0";
|
|
54998
55041
|
var lastAgentOutputAt = new Map;
|
|
@@ -55148,11 +55191,12 @@ function hasDifferentThreadedRecentTurn(chatId, liveThreadId) {
|
|
|
55148
55191
|
}
|
|
55149
55192
|
return false;
|
|
55150
55193
|
}
|
|
55151
|
-
function closeObligationOnSubstantiveReply(args, liveTurn) {
|
|
55194
|
+
function closeObligationOnSubstantiveReply(args, liveTurn, routedOriginTurn) {
|
|
55152
55195
|
if (!OBLIGATION_LEDGER_ENABLED)
|
|
55153
55196
|
return;
|
|
55154
55197
|
const echoed = findTurnByOriginId(args.origin_turn_id);
|
|
55155
|
-
const
|
|
55198
|
+
const routedOriginId = routedOriginTurn != null && echoed == null ? routedOriginTurn.turnId : null;
|
|
55199
|
+
const target = obligationLedger.resolveCloseTarget(echoed?.turnId, liveTurn?.turnId, routedOriginId);
|
|
55156
55200
|
if (target != null)
|
|
55157
55201
|
obligationLedger.close(target);
|
|
55158
55202
|
}
|
|
@@ -56528,11 +56572,12 @@ function obligationSweep() {
|
|
|
56528
56572
|
const agent = process.env.SWITCHROOM_AGENT_NAME ?? "";
|
|
56529
56573
|
const now = Date.now();
|
|
56530
56574
|
const backgroundWorkActive = OBLIGATION_BACKGROUND_WORK_GRACE_MS > 0 && agentHasInFlightBackgroundWork(now);
|
|
56531
|
-
const decision = obligationLedger.decideAtIdle(OBLIGATION_ESCALATE_GRACE_MS > 0 || backgroundWorkActive ? {
|
|
56575
|
+
const decision = obligationLedger.decideAtIdle(OBLIGATION_ESCALATE_GRACE_MS > 0 || backgroundWorkActive || OBLIGATION_REPRESENT_GRACE_MS > 0 ? {
|
|
56532
56576
|
now,
|
|
56533
56577
|
graceMs: OBLIGATION_ESCALATE_GRACE_MS,
|
|
56534
56578
|
backgroundWorkActive,
|
|
56535
|
-
backgroundGraceMs: OBLIGATION_BACKGROUND_WORK_GRACE_MS
|
|
56579
|
+
backgroundGraceMs: OBLIGATION_BACKGROUND_WORK_GRACE_MS,
|
|
56580
|
+
representGraceMs: OBLIGATION_REPRESENT_GRACE_MS
|
|
56536
56581
|
} : undefined);
|
|
56537
56582
|
const o = decision.obligation;
|
|
56538
56583
|
if (decision.action === "none" || o == null) {
|
|
@@ -56552,6 +56597,12 @@ function obligationSweep() {
|
|
|
56552
56597
|
`);
|
|
56553
56598
|
return;
|
|
56554
56599
|
}
|
|
56600
|
+
if (HISTORY_ENABLED && hasOutboundDeliveredSince(o.chatId, o.openedAt, o.threadId)) {
|
|
56601
|
+
process.stderr.write(`telegram gateway: obligation closed silently \u2014 outbound delivered since open origin=${o.originTurnId}
|
|
56602
|
+
`);
|
|
56603
|
+
obligationLedger.close(o.originTurnId);
|
|
56604
|
+
return;
|
|
56605
|
+
}
|
|
56555
56606
|
driveEscalation({
|
|
56556
56607
|
escId: o.originTurnId,
|
|
56557
56608
|
inFlight: obligationEscalateInFlight,
|
|
@@ -57116,12 +57167,14 @@ var ipcServer = createIpcServer({
|
|
|
57116
57167
|
onInjectInbound(_client, msg) {
|
|
57117
57168
|
const promptKey = typeof msg.inbound.meta?.prompt_key === "string" ? msg.inbound.meta.prompt_key : "unknown";
|
|
57118
57169
|
const source = typeof msg.inbound.meta?.source === "string" ? msg.inbound.meta.source : "unknown";
|
|
57119
|
-
const target =
|
|
57120
|
-
|
|
57121
|
-
|
|
57122
|
-
|
|
57170
|
+
const { target, delivered, fellBackToMain } = deliverInjectWithFallback(msg.agentName, msg.inbound.meta, (t) => ipcServer.sendToAgent(t, msg.inbound));
|
|
57171
|
+
if (fellBackToMain) {
|
|
57172
|
+
process.stderr.write(`telegram gateway: cron fire fell back to main session (no cron bridge) agent=${msg.agentName} prompt_key=${promptKey}
|
|
57173
|
+
`);
|
|
57174
|
+
}
|
|
57175
|
+
if (delivered && target === msg.agentName)
|
|
57123
57176
|
markClaudeBusyForInbound(msg.inbound);
|
|
57124
|
-
process.stderr.write(`telegram gateway: inject_inbound agent=${msg.agentName} target=${target} source=${source} prompt_key=${promptKey} delivered=${delivered}
|
|
57177
|
+
process.stderr.write(`telegram gateway: inject_inbound agent=${msg.agentName} target=${target}${fellBackToMain ? " (fellback)" : ""} source=${source} prompt_key=${promptKey} delivered=${delivered}
|
|
57125
57178
|
`);
|
|
57126
57179
|
if (!delivered) {
|
|
57127
57180
|
pendingInboundBuffer.push(target, msg.inbound);
|
|
@@ -57432,12 +57485,14 @@ ${url}`;
|
|
|
57432
57485
|
effectiveText = text;
|
|
57433
57486
|
}
|
|
57434
57487
|
assertAllowedChat(chat_id);
|
|
57488
|
+
let replyRoutedOriginTurn = null;
|
|
57435
57489
|
let threadId;
|
|
57436
57490
|
if (TURN_ORIGIN_ROUTING_ENABLED) {
|
|
57437
57491
|
const explicit = args.message_thread_id != null ? Number(args.message_thread_id) : undefined;
|
|
57438
57492
|
const echoedTurn = findTurnByOriginId(args.origin_turn_id);
|
|
57439
57493
|
const quotedTurn = echoedTurn == null ? findTurnByQuotedMessageId(chat_id, args.reply_to) : null;
|
|
57440
57494
|
const originTurn = echoedTurn ?? quotedTurn;
|
|
57495
|
+
replyRoutedOriginTurn = originTurn ?? null;
|
|
57441
57496
|
threadId = resolveAnswerThreadWithLog(chat_id, Number.isFinite(explicit) ? explicit : undefined, originTurn, originTurn == null ? null : echoedTurn != null ? "echo" : "quoted", turn, "reply");
|
|
57442
57497
|
} else {
|
|
57443
57498
|
threadId = resolveThreadId(chat_id, args.message_thread_id ?? (turn?.sessionThreadId != null ? turn.sessionThreadId : undefined));
|
|
@@ -57571,7 +57626,7 @@ ${url}`;
|
|
|
57571
57626
|
disableNotification
|
|
57572
57627
|
});
|
|
57573
57628
|
if (turn2.finalAnswerSubstantive)
|
|
57574
|
-
closeObligationOnSubstantiveReply(args, turn2);
|
|
57629
|
+
closeObligationOnSubstantiveReply(args, turn2, replyRoutedOriginTurn);
|
|
57575
57630
|
}
|
|
57576
57631
|
outboundDedup.record(chat_id, threadId, decision.mergedText, Date.now(), turn2?.registryKey ?? null);
|
|
57577
57632
|
silentAnchorEditDone = true;
|
|
@@ -57774,7 +57829,7 @@ ${url}`;
|
|
|
57774
57829
|
turn.finalAnswerSubstantive = isSubstantiveFinalReply({ text: rawText, disableNotification });
|
|
57775
57830
|
finalizeStatusReaction(chat_id, threadId, "done");
|
|
57776
57831
|
if (turn.finalAnswerSubstantive)
|
|
57777
|
-
closeObligationOnSubstantiveReply(args, turn);
|
|
57832
|
+
closeObligationOnSubstantiveReply(args, turn, replyRoutedOriginTurn);
|
|
57778
57833
|
}
|
|
57779
57834
|
releaseTurnBufferGate(statusKey(chat_id, threadId), turn ?? undefined);
|
|
57780
57835
|
if (turn?.finalAnswerDelivered === true) {
|
|
@@ -57794,13 +57849,19 @@ async function executeStreamReply(args) {
|
|
|
57794
57849
|
throw new Error("stream_reply: chat_id is required");
|
|
57795
57850
|
if (args.text == null || args.text === "")
|
|
57796
57851
|
throw new Error("stream_reply: text is required and cannot be empty");
|
|
57852
|
+
let streamRoutedOriginTurn = null;
|
|
57853
|
+
let streamOriginVia = null;
|
|
57854
|
+
if (TURN_ORIGIN_ROUTING_ENABLED) {
|
|
57855
|
+
const echoedTurn = findTurnByOriginId(args.origin_turn_id);
|
|
57856
|
+
const quotedTurn = echoedTurn == null ? findTurnByQuotedMessageId(String(args.chat_id), args.reply_to) : null;
|
|
57857
|
+
const originTurn = echoedTurn ?? quotedTurn;
|
|
57858
|
+
streamRoutedOriginTurn = originTurn ?? null;
|
|
57859
|
+
streamOriginVia = originTurn == null ? null : echoedTurn != null ? "echo" : "quoted";
|
|
57860
|
+
}
|
|
57797
57861
|
if (args.message_thread_id == null) {
|
|
57798
57862
|
let injected;
|
|
57799
57863
|
if (TURN_ORIGIN_ROUTING_ENABLED) {
|
|
57800
|
-
|
|
57801
|
-
const quotedTurn = echoedTurn == null ? findTurnByQuotedMessageId(String(args.chat_id), args.reply_to) : null;
|
|
57802
|
-
const originTurn = echoedTurn ?? quotedTurn;
|
|
57803
|
-
injected = resolveAnswerThreadWithLog(String(args.chat_id), undefined, originTurn, originTurn == null ? null : echoedTurn != null ? "echo" : "quoted", turn, "stream_reply");
|
|
57864
|
+
injected = resolveAnswerThreadWithLog(String(args.chat_id), undefined, streamRoutedOriginTurn, streamOriginVia, turn, "stream_reply");
|
|
57804
57865
|
} else {
|
|
57805
57866
|
injected = turn?.sessionThreadId;
|
|
57806
57867
|
}
|
|
@@ -57943,7 +58004,7 @@ async function executeStreamReply(args) {
|
|
|
57943
58004
|
done: args.done === true
|
|
57944
58005
|
});
|
|
57945
58006
|
if (turn.finalAnswerSubstantive)
|
|
57946
|
-
closeObligationOnSubstantiveReply(args, turn);
|
|
58007
|
+
closeObligationOnSubstantiveReply(args, turn, streamRoutedOriginTurn);
|
|
57947
58008
|
const streamThreadIdForClear = args.message_thread_id != null ? Number(args.message_thread_id) : undefined;
|
|
57948
58009
|
clearSilentEndState(statusKey(streamChatId, streamThreadIdForClear));
|
|
57949
58010
|
}
|
|
@@ -43,3 +43,37 @@ export function baseAgent(name: string): string {
|
|
|
43
43
|
export function resolveInjectTarget(agentName: string, meta: Record<string, string> | undefined): string {
|
|
44
44
|
return meta?.session === "cron" ? cronIdentity(agentName) : agentName;
|
|
45
45
|
}
|
|
46
|
+
|
|
47
|
+
export interface InjectDelivery {
|
|
48
|
+
/** The bridge identity the fire was actually delivered to (or last tried). */
|
|
49
|
+
target: string;
|
|
50
|
+
delivered: boolean;
|
|
51
|
+
/** True iff a Tier-1 (cron) fire fell back to the main agent bridge. */
|
|
52
|
+
fellBackToMain: boolean;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Deliver an inject with graceful Tier-1 fallback (cheap-crons JTBD: a cron
|
|
57
|
+
* must NEVER be dropped because of tier routing).
|
|
58
|
+
*
|
|
59
|
+
* Tries the routed target via `send` (returns true iff delivered). The
|
|
60
|
+
* cron-session bridge is boot-forked, so a frequent cron added by hot-reload
|
|
61
|
+
* or agent self-authoring has no live `<agent>-cron` bridge until the next
|
|
62
|
+
* restart — and a crashed cron session has none either. When a cron-routed
|
|
63
|
+
* fire isn't delivered, fall back to the MAIN agent bridge so the fire lands
|
|
64
|
+
* now (full session); it routes cheap again once the cron session is up.
|
|
65
|
+
* Cheap when available, never broken. Pure: `send` is injected.
|
|
66
|
+
*/
|
|
67
|
+
export function deliverInjectWithFallback(
|
|
68
|
+
agentName: string,
|
|
69
|
+
meta: Record<string, string> | undefined,
|
|
70
|
+
send: (target: string) => boolean,
|
|
71
|
+
): InjectDelivery {
|
|
72
|
+
const target = resolveInjectTarget(agentName, meta);
|
|
73
|
+
const wantedCron = target !== agentName;
|
|
74
|
+
if (send(target)) return { target, delivered: true, fellBackToMain: false };
|
|
75
|
+
if (wantedCron && send(agentName)) {
|
|
76
|
+
return { target: agentName, delivered: true, fellBackToMain: true };
|
|
77
|
+
}
|
|
78
|
+
return { target, delivered: false, fellBackToMain: false };
|
|
79
|
+
}
|