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.
Files changed (34) hide show
  1. package/dist/commands/server.d.ts.map +1 -1
  2. package/dist/commands/server.js +14 -0
  3. package/dist/commands/server.js.map +1 -1
  4. package/dist/config/ConfigDefaults.d.ts.map +1 -1
  5. package/dist/config/ConfigDefaults.js +1 -0
  6. package/dist/config/ConfigDefaults.js.map +1 -1
  7. package/dist/core/types.d.ts +3 -0
  8. package/dist/core/types.d.ts.map +1 -1
  9. package/dist/core/types.js.map +1 -1
  10. package/dist/messaging/TelegramAdapter.d.ts +71 -0
  11. package/dist/messaging/TelegramAdapter.d.ts.map +1 -1
  12. package/dist/messaging/TelegramAdapter.js +144 -0
  13. package/dist/messaging/TelegramAdapter.js.map +1 -1
  14. package/dist/monitoring/StaleSessionBackstop.d.ts +22 -0
  15. package/dist/monitoring/StaleSessionBackstop.d.ts.map +1 -1
  16. package/dist/monitoring/StaleSessionBackstop.js +31 -6
  17. package/dist/monitoring/StaleSessionBackstop.js.map +1 -1
  18. package/dist/monitoring/sentinelWiring.d.ts +4 -0
  19. package/dist/monitoring/sentinelWiring.d.ts.map +1 -1
  20. package/dist/monitoring/sentinelWiring.js.map +1 -1
  21. package/dist/scaffold/templates.d.ts.map +1 -1
  22. package/dist/scaffold/templates.js +2 -0
  23. package/dist/scaffold/templates.js.map +1 -1
  24. package/dist/server/routes.d.ts.map +1 -1
  25. package/dist/server/routes.js +21 -8
  26. package/dist/server/routes.js.map +1 -1
  27. package/package.json +1 -1
  28. package/src/data/builtin-manifest.json +46 -46
  29. package/src/scaffold/templates.ts +2 -0
  30. package/upgrades/1.3.260.md +72 -0
  31. package/upgrades/1.3.261.md +45 -0
  32. package/upgrades/side-effects/notification-ux-coherence.md +59 -0
  33. package/upgrades/side-effects/stalebackstop-accuracy.md +52 -0
  34. 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