instar 1.3.259 → 1.3.261
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/commands/server.d.ts.map +1 -1
- package/dist/commands/server.js +14 -0
- package/dist/commands/server.js.map +1 -1
- package/dist/config/ConfigDefaults.d.ts.map +1 -1
- package/dist/config/ConfigDefaults.js +1 -0
- package/dist/config/ConfigDefaults.js.map +1 -1
- package/dist/core/types.d.ts +3 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js.map +1 -1
- package/dist/messaging/TelegramAdapter.d.ts +71 -0
- package/dist/messaging/TelegramAdapter.d.ts.map +1 -1
- package/dist/messaging/TelegramAdapter.js +144 -0
- package/dist/messaging/TelegramAdapter.js.map +1 -1
- package/dist/monitoring/StaleSessionBackstop.d.ts +22 -0
- package/dist/monitoring/StaleSessionBackstop.d.ts.map +1 -1
- package/dist/monitoring/StaleSessionBackstop.js +31 -6
- package/dist/monitoring/StaleSessionBackstop.js.map +1 -1
- package/dist/monitoring/sentinelWiring.d.ts +4 -0
- package/dist/monitoring/sentinelWiring.d.ts.map +1 -1
- package/dist/monitoring/sentinelWiring.js.map +1 -1
- package/dist/scaffold/templates.d.ts.map +1 -1
- package/dist/scaffold/templates.js +2 -0
- package/dist/scaffold/templates.js.map +1 -1
- package/dist/server/routes.d.ts.map +1 -1
- package/dist/server/routes.js +21 -8
- package/dist/server/routes.js.map +1 -1
- package/package.json +1 -1
- package/src/data/builtin-manifest.json +46 -46
- package/src/scaffold/templates.ts +2 -0
- package/upgrades/1.3.260.md +72 -0
- package/upgrades/1.3.261.md +45 -0
- package/upgrades/side-effects/notification-ux-coherence.md +59 -0
- package/upgrades/side-effects/stalebackstop-accuracy.md +52 -0
- package/upgrades/1.3.259.md +0 -32
|
@@ -167,6 +167,15 @@ export class TelegramAdapter {
|
|
|
167
167
|
// for one bucket share ONE topic creation (no double-create race).
|
|
168
168
|
floodNoticePending = new Map();
|
|
169
169
|
attentionSuppressedLogPath;
|
|
170
|
+
// ── Agent-Health lane (the calm self-health notice lane) ──────────────────
|
|
171
|
+
/** Resolved lane config (defaults applied in the constructor). */
|
|
172
|
+
agentHealthLaneCfg;
|
|
173
|
+
/** The single reused lane topic id (created lazily, once). */
|
|
174
|
+
agentHealthTopicId = null;
|
|
175
|
+
/** In-flight single creation guard (no double-create race). */
|
|
176
|
+
agentHealthPending = null;
|
|
177
|
+
/** entity key -> last-posted epoch ms (suppression-dedup ring, insertion-ordered). */
|
|
178
|
+
agentHealthKeyRing = new Map();
|
|
170
179
|
// Stall detection
|
|
171
180
|
pendingMessages = new Map(); // key = topicId-timestamp
|
|
172
181
|
stallCheckInterval = null;
|
|
@@ -380,6 +389,13 @@ export class TelegramAdapter {
|
|
|
380
389
|
this.attentionFilePath = path.join(this.botStateDir, 'state', 'attention-items.json');
|
|
381
390
|
this.attentionSuppressedLogPath = path.join(this.botStateDir, 'state', 'attention-suppressed.jsonl');
|
|
382
391
|
this.attentionTopicGuard = new AttentionTopicGuard(config.attentionTopicGuard ?? {});
|
|
392
|
+
const alc = config.agentHealthLane ?? {};
|
|
393
|
+
this.agentHealthLaneCfg = {
|
|
394
|
+
enabled: alc.enabled !== false,
|
|
395
|
+
topicName: (typeof alc.topicName === 'string' && alc.topicName.trim()) ? alc.topicName : '🩺 Agent Health',
|
|
396
|
+
maxTrackedKeys: Number.isFinite(alc.maxTrackedKeys) && alc.maxTrackedKeys > 0 ? Math.floor(alc.maxTrackedKeys) : 256,
|
|
397
|
+
dedupWindowMs: Number.isFinite(alc.dedupWindowMs) && alc.dedupWindowMs > 0 ? alc.dedupWindowMs : 30 * 60 * 1000,
|
|
398
|
+
};
|
|
383
399
|
this.loadRegistry();
|
|
384
400
|
this.loadOffset();
|
|
385
401
|
this.loadAttentionItems();
|
|
@@ -2911,6 +2927,22 @@ export class TelegramAdapter {
|
|
|
2911
2927
|
createdAt: now,
|
|
2912
2928
|
updatedAt: now,
|
|
2913
2929
|
};
|
|
2930
|
+
// ── Agent-Health lane (calm self-health notices) ─────────────────────
|
|
2931
|
+
// A routine self-health/housekeeping notice routes into ONE named "🩺 Agent
|
|
2932
|
+
// Health" topic from the very first item — it never spawns its own topic
|
|
2933
|
+
// (even under budget, even if mis-tagged HIGH) and same-entity re-escalations
|
|
2934
|
+
// are suppression-deduped. This runs BEFORE the flood guard and bypasses it
|
|
2935
|
+
// entirely, so a stale-session/peer-unreachable feature can never flood
|
|
2936
|
+
// topic-after-topic. Only items that explicitly opt in are affected.
|
|
2937
|
+
if (this.agentHealthLaneCfg.enabled && item.lane === 'agent-health') {
|
|
2938
|
+
const laneTopicId = await this.routeToAgentHealthLane(attention);
|
|
2939
|
+
attention.coalesced = true;
|
|
2940
|
+
if (laneTopicId !== null)
|
|
2941
|
+
attention.topicId = laneTopicId;
|
|
2942
|
+
this.attentionItems.set(item.id, attention);
|
|
2943
|
+
this.saveAttentionItems();
|
|
2944
|
+
return attention;
|
|
2945
|
+
}
|
|
2914
2946
|
// ── Topic-flood circuit breaker (2026-05-28 lockdown) ──────────────
|
|
2915
2947
|
// Before spawning a per-item forum topic, consult the per-source guard.
|
|
2916
2948
|
// HIGH/URGENT always pass (critical items must always be visible). A
|
|
@@ -2985,6 +3017,118 @@ export class TelegramAdapter {
|
|
|
2985
3017
|
this.saveAttentionItems();
|
|
2986
3018
|
return attention;
|
|
2987
3019
|
}
|
|
3020
|
+
/**
|
|
3021
|
+
* Build a calm, named, actionable self-health notice. Resolves a session's
|
|
3022
|
+
* topic id to its HUMAN topic name (never emits a bare `topic-<n>`), and ends
|
|
3023
|
+
* with a plain-language next step the user can just reply to. Used by the
|
|
3024
|
+
* self-health escalators so every Agent-Health notice reads like
|
|
3025
|
+
* "Heads-up on the 'EXO 3.0' session — … reply 'check EXO 3.0' …".
|
|
3026
|
+
*/
|
|
3027
|
+
buildHealthNotice(opts) {
|
|
3028
|
+
const resolved = (typeof opts.topicId === 'number' ? this.getTopicName(opts.topicId) : null);
|
|
3029
|
+
// Prefer a real human name; fall back to the session name only if it isn't the
|
|
3030
|
+
// useless `topic-<n>` form.
|
|
3031
|
+
const display = (resolved && !/^topic-\d+$/.test(resolved))
|
|
3032
|
+
? resolved
|
|
3033
|
+
: (!/^topic-\d+$/.test(opts.sessionName) ? opts.sessionName : (resolved ?? opts.sessionName));
|
|
3034
|
+
return {
|
|
3035
|
+
title: `Heads-up on the "${display}" session`,
|
|
3036
|
+
summary: `${opts.what}. It's still running — reply "${opts.nextStep}" and I'll take a look, or ignore this if you know it's fine.`,
|
|
3037
|
+
};
|
|
3038
|
+
}
|
|
3039
|
+
/** Derive the suppression-dedup entity key for an Agent-Health-lane item. */
|
|
3040
|
+
healthKeyFor(item) {
|
|
3041
|
+
if (item.healthKey && item.healthKey.trim())
|
|
3042
|
+
return item.healthKey.trim();
|
|
3043
|
+
if (item.sourceContext && item.sourceContext.trim())
|
|
3044
|
+
return item.sourceContext.trim();
|
|
3045
|
+
// Strip a trailing -<n> episode suffix from the id so episodes of one entity
|
|
3046
|
+
// share a key (e.g. "stale-abc123-2" -> "stale-abc123").
|
|
3047
|
+
return item.id.replace(/-\d+$/, '');
|
|
3048
|
+
}
|
|
3049
|
+
/**
|
|
3050
|
+
* Route a self-health notice into the ONE calm "🩺 Agent Health" lane topic.
|
|
3051
|
+
* Per-entity suppression dedup: a same-key re-escalation within the dedup
|
|
3052
|
+
* window is logged + suppressed (not reposted) so the lane stays quiet. The
|
|
3053
|
+
* lane topic is created lazily once and reused. Returns the lane topic id, or
|
|
3054
|
+
* null if Telegram is unavailable (the item is still recorded in the store).
|
|
3055
|
+
*/
|
|
3056
|
+
async routeToAgentHealthLane(item) {
|
|
3057
|
+
const key = this.healthKeyFor(item);
|
|
3058
|
+
const now = Date.now();
|
|
3059
|
+
const last = this.agentHealthKeyRing.get(key);
|
|
3060
|
+
const suppressed = last !== undefined && (now - last) < this.agentHealthLaneCfg.dedupWindowMs;
|
|
3061
|
+
// Refresh recency + maintain insertion order (delete then set moves to newest).
|
|
3062
|
+
this.agentHealthKeyRing.delete(key);
|
|
3063
|
+
this.agentHealthKeyRing.set(key, now);
|
|
3064
|
+
while (this.agentHealthKeyRing.size > this.agentHealthLaneCfg.maxTrackedKeys) {
|
|
3065
|
+
const oldest = this.agentHealthKeyRing.keys().next().value;
|
|
3066
|
+
if (oldest === undefined)
|
|
3067
|
+
break;
|
|
3068
|
+
this.agentHealthKeyRing.delete(oldest);
|
|
3069
|
+
}
|
|
3070
|
+
if (suppressed) {
|
|
3071
|
+
// Same entity re-escalating while its prior notice is still fresh — keep the
|
|
3072
|
+
// lane calm: record it to the audit trail, do not repost.
|
|
3073
|
+
this.writeSuppressedAttentionLog(item, `agent-health:${key}`, 0);
|
|
3074
|
+
console.log(`[telegram] agent-health lane: suppressed duplicate "${item.title}" (key=${key}) — within dedup window`);
|
|
3075
|
+
return this.agentHealthTopicId;
|
|
3076
|
+
}
|
|
3077
|
+
let topicId;
|
|
3078
|
+
try {
|
|
3079
|
+
topicId = await this.ensureAgentHealthLaneTopic();
|
|
3080
|
+
}
|
|
3081
|
+
catch (err) {
|
|
3082
|
+
console.error(`[telegram] agent-health lane topic creation failed: ${err}`);
|
|
3083
|
+
topicId = this.agentHealthTopicId;
|
|
3084
|
+
}
|
|
3085
|
+
if (topicId === null)
|
|
3086
|
+
return null;
|
|
3087
|
+
const line = [
|
|
3088
|
+
`<b>${this.escapeHtml(item.title)}</b>`,
|
|
3089
|
+
this.escapeHtml(String(item.summary ?? '').slice(0, 400)),
|
|
3090
|
+
].filter(Boolean).join('\n');
|
|
3091
|
+
// @silent-fallback-ok — best-effort lane post; the item is already recorded in
|
|
3092
|
+
// the attention store, so a transient send failure is non-fatal. If the topic
|
|
3093
|
+
// was deleted out from under us, drop the cached id so it's recreated next time.
|
|
3094
|
+
await this.sendToTopic(topicId, line).catch(() => { this.agentHealthTopicId = null; });
|
|
3095
|
+
return topicId;
|
|
3096
|
+
}
|
|
3097
|
+
/** Lazily create (once) and return the reused "🩺 Agent Health" lane topic id. */
|
|
3098
|
+
async ensureAgentHealthLaneTopic() {
|
|
3099
|
+
if (this.agentHealthTopicId !== null)
|
|
3100
|
+
return this.agentHealthTopicId;
|
|
3101
|
+
if (this.agentHealthPending)
|
|
3102
|
+
return this.agentHealthPending;
|
|
3103
|
+
this.agentHealthPending = (async () => {
|
|
3104
|
+
const name = this.agentHealthLaneCfg.topicName;
|
|
3105
|
+
const topic = await this.findOrCreateForumTopic(name, TOPIC_STYLE.SYSTEM.color);
|
|
3106
|
+
this.agentHealthTopicId = topic.topicId;
|
|
3107
|
+
if (!topic.reused) {
|
|
3108
|
+
const intro = [
|
|
3109
|
+
`This is your <b>calm agent-health lane</b>. When I notice something about my OWN sessions — one that looks stuck, a peer I can't reach — the routine heads-up lands HERE instead of spawning a new topic each time.`,
|
|
3110
|
+
``,
|
|
3111
|
+
`These are low-key and never urgent. Each names the session and ends with a plain next step you can just reply to. Nothing here blocks anything; ignore what you know is fine.`,
|
|
3112
|
+
].join('\n');
|
|
3113
|
+
const introParams = {
|
|
3114
|
+
chat_id: this.config.chatId,
|
|
3115
|
+
text: intro,
|
|
3116
|
+
parse_mode: 'HTML',
|
|
3117
|
+
_formatMode: 'html',
|
|
3118
|
+
};
|
|
3119
|
+
if (!isGeneralTopic(topic.topicId))
|
|
3120
|
+
introParams.message_thread_id = topic.topicId;
|
|
3121
|
+
await this.apiCall('sendMessage', introParams);
|
|
3122
|
+
}
|
|
3123
|
+
return this.agentHealthTopicId;
|
|
3124
|
+
})();
|
|
3125
|
+
try {
|
|
3126
|
+
return await this.agentHealthPending;
|
|
3127
|
+
}
|
|
3128
|
+
finally {
|
|
3129
|
+
this.agentHealthPending = null;
|
|
3130
|
+
}
|
|
3131
|
+
}
|
|
2988
3132
|
/**
|
|
2989
3133
|
* Append a suppressed attention item to the audit trail. This is the
|
|
2990
3134
|
* "housekeeping goes to the logs" path for the topic-flood guard — the item
|