instar 0.28.61 → 0.28.63

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 (79) hide show
  1. package/README.md +15 -18
  2. package/dashboard/index.html +140 -0
  3. package/dist/cli.js +0 -0
  4. package/dist/commands/server.d.ts.map +1 -1
  5. package/dist/commands/server.js +103 -22
  6. package/dist/commands/server.js.map +1 -1
  7. package/dist/core/types.d.ts +26 -0
  8. package/dist/core/types.d.ts.map +1 -1
  9. package/dist/messaging/DeliveryRetryManager.d.ts.map +1 -1
  10. package/dist/messaging/DeliveryRetryManager.js +6 -4
  11. package/dist/messaging/DeliveryRetryManager.js.map +1 -1
  12. package/dist/messaging/MessageStore.d.ts +10 -0
  13. package/dist/messaging/MessageStore.d.ts.map +1 -1
  14. package/dist/messaging/MessageStore.js +52 -0
  15. package/dist/messaging/MessageStore.js.map +1 -1
  16. package/dist/messaging/SpawnRequestManager.d.ts +224 -19
  17. package/dist/messaging/SpawnRequestManager.d.ts.map +1 -1
  18. package/dist/messaging/SpawnRequestManager.js +600 -70
  19. package/dist/messaging/SpawnRequestManager.js.map +1 -1
  20. package/dist/messaging/types.d.ts +11 -1
  21. package/dist/messaging/types.d.ts.map +1 -1
  22. package/dist/messaging/types.js +7 -0
  23. package/dist/messaging/types.js.map +1 -1
  24. package/dist/monitoring/PresenceProxy.d.ts +8 -0
  25. package/dist/monitoring/PresenceProxy.d.ts.map +1 -1
  26. package/dist/monitoring/PresenceProxy.js +30 -0
  27. package/dist/monitoring/PresenceProxy.js.map +1 -1
  28. package/dist/monitoring/PromiseBeacon.d.ts +23 -0
  29. package/dist/monitoring/PromiseBeacon.d.ts.map +1 -1
  30. package/dist/monitoring/PromiseBeacon.js +68 -6
  31. package/dist/monitoring/PromiseBeacon.js.map +1 -1
  32. package/dist/monitoring/SessionWatchdog.d.ts.map +1 -1
  33. package/dist/monitoring/SessionWatchdog.js +23 -7
  34. package/dist/monitoring/SessionWatchdog.js.map +1 -1
  35. package/dist/monitoring/watchdog-notifications.d.ts +15 -0
  36. package/dist/monitoring/watchdog-notifications.d.ts.map +1 -0
  37. package/dist/monitoring/watchdog-notifications.js +24 -0
  38. package/dist/monitoring/watchdog-notifications.js.map +1 -0
  39. package/dist/server/routes.d.ts.map +1 -1
  40. package/dist/server/routes.js +186 -0
  41. package/dist/server/routes.js.map +1 -1
  42. package/dist/threadline/ThreadlineRouter.d.ts +64 -2
  43. package/dist/threadline/ThreadlineRouter.d.ts.map +1 -1
  44. package/dist/threadline/ThreadlineRouter.js +117 -9
  45. package/dist/threadline/ThreadlineRouter.js.map +1 -1
  46. package/dist/threadline/client/ThreadlineClient.d.ts +25 -1
  47. package/dist/threadline/client/ThreadlineClient.d.ts.map +1 -1
  48. package/dist/threadline/client/ThreadlineClient.js +79 -2
  49. package/dist/threadline/client/ThreadlineClient.js.map +1 -1
  50. package/package.json +1 -1
  51. package/src/data/builtin-manifest.json +51 -51
  52. package/src/templates/hooks/session-start.sh +19 -0
  53. package/upgrades/0.28.63.md +43 -0
  54. package/upgrades/side-effects/0.28.62.md +64 -0
  55. package/upgrades/side-effects/promise-beacon-followups.md +105 -0
  56. package/upgrades/side-effects/threadline-cooldown-prereq-1-undelivered-phase.md +73 -0
  57. package/upgrades/side-effects/threadline-cooldown-sec4.1-branded-trust-union.md +68 -0
  58. package/upgrades/side-effects/threadline-cooldown-sec4.1-client-affinity.md +71 -0
  59. package/upgrades/side-effects/threadline-cooldown-sec4.1-receiver-affinity.md +67 -0
  60. package/upgrades/side-effects/threadline-cooldown-sec4.2-drain-loop.md +92 -0
  61. package/upgrades/side-effects/threadline-cooldown-sec4.2-infra-soft-limiter.md +75 -0
  62. package/upgrades/side-effects/threadline-cooldown-sec4.2-state-refactor.md +78 -0
  63. package/upgrades/side-effects/threadline-cooldown-sec4.3-envelope-byte-cap.md +66 -0
  64. package/upgrades/side-effects/threadline-cooldown-sec4.3-envelope-hash.md +71 -0
  65. package/upgrades/side-effects/threadline-cooldown-sec4.3-truncation-and-global-cap.md +69 -0
  66. package/upgrades/side-effects/threadline-cooldown-sec4.4-config-plumbing.md +77 -0
  67. package/upgrades/side-effects/threadline-cooldown-sec4.4-drain-consumer-wiring.md +72 -0
  68. package/upgrades/side-effects/threadline-cooldown-sec4.4-patch-endpoint.md +72 -0
  69. package/upgrades/side-effects/threadline-cooldown-sec4.5-degradation-reporter.md +83 -0
  70. package/upgrades/side-effects/threadline-cooldown-sec4.5-triggeredby-plumbing.md +76 -0
  71. package/upgrades/side-effects/threadline-cooldown-spec-landing.md +48 -0
  72. package/upgrades/side-effects/watchdog-user-comfort.md +136 -0
  73. package/dist/core/InitiativeDigestJob.d.ts +0 -54
  74. package/dist/core/InitiativeDigestJob.d.ts.map +0 -1
  75. package/dist/core/InitiativeDigestJob.js +0 -128
  76. package/dist/core/InitiativeDigestJob.js.map +0 -1
  77. package/upgrades/NEXT.md +0 -53
  78. /package/upgrades/{0.28.61.md → 0.28.62.md} +0 -0
  79. /package/upgrades/side-effects/{scheduler-gate-exit-code.md → 0.28.61.md} +0 -0
package/README.md CHANGED
@@ -5,7 +5,7 @@
5
5
  <h1 align="center">instar</h1>
6
6
 
7
7
  <p align="center">
8
- <strong>Persistent Claude Code agents with scheduling, sessions, memory, and messaging.</strong>
8
+ <strong>Persistent, trustworthy Claude Code agents. Built on coherence-first architecture.</strong>
9
9
  </p>
10
10
 
11
11
  <p align="center">
@@ -35,7 +35,9 @@ One command. Guided setup. Talking to your agent from your phone within minutes.
35
35
 
36
36
  ---
37
37
 
38
- Instar turns Claude Code from a powerful CLI tool into a **coherent, autonomous partner**. Persistent identity, memory that survives every restart, job scheduling, two-way messaging (Telegram, WhatsApp, iMessage), and the infrastructure to evolve.
38
+ Instar is a framework for building agents on **[Claude Code](https://docs.anthropic.com/en/docs/claude-code)** but where stock Claude Code and most other agent frameworks treat identity, memory, and continuity as optional features bolted onto a stateless runtime, Instar inverts that. Every Instar agent is **coherent by default**: it knows who it is, remembers what has happened, recognizes the people it talks to, and stays the same agent across restarts and weeks of operation. Everything else the framework gives you — scheduling, multi-channel messaging (Telegram, WhatsApp, iMessage), sub-agents, hooks, MCP — is built on that foundation, which is why you can actually leave an Instar agent running and hand it real work.
39
+
40
+ Instar's architecture was distilled from [**Dawn**](https://dawn.bot-me.ai) — an AI running continuously since early 2026, holding ~700 tracked relationships and hundreds of learned lessons across thousands of restarts — and packaged so every agent you build can start from the same foundation.
39
41
 
40
42
  ## Quick Start
41
43
 
@@ -79,20 +81,18 @@ You (Telegram / WhatsApp / iMessage / Terminal)
79
81
 
80
82
  Each session is a **real Claude Code process** with extended thinking, native tools, sub-agents, hooks, skills, and MCP servers. Not an API wrapper -- the full development environment. The agent manages all of this autonomously.
81
83
 
82
- ## The Coherence Problem
83
-
84
- Claude Code is powerful. But power without coherence is unreliable. An agent that forgets what you discussed yesterday, doesn't recognize someone it talked to last week, or contradicts its own decisions -- that agent can't be trusted with real autonomy.
84
+ ## Why Coherence Is the Foundation
85
85
 
86
- Instar solves the six dimensions of agent coherence:
86
+ An agent that forgets what you discussed yesterday, doesn't recognize someone it talked to last week, or contradicts its own decisions can't be trusted with real autonomy. The six dimensions below aren't features — they're the conditions under which an agent becomes trustworthy enough to leave running. Every Instar agent gets them enforced structurally, not prompted into behaving:
87
87
 
88
- | Dimension | What it means |
89
- |-----------|---------------|
90
- | **Memory** | Remembers across sessions -- not just within one |
91
- | **Relationships** | Knows who it's talking to -- with continuity across platforms |
92
- | **Identity** | Stays itself after restarts, compaction, and updates |
93
- | **Temporal awareness** | Understands time, context, and what's been happening |
94
- | **Consistency** | Follows through on commitments -- doesn't contradict itself |
95
- | **Growth** | Evolves its capabilities and understanding over time |
88
+ | Dimension | What it means | How Instar enforces it |
89
+ |-----------|---------------|------------------------|
90
+ | **Identity** | Stays itself after restarts, compaction, and updates | `AGENT.md` + identity-grounding hooks fire on every session start |
91
+ | **Memory** | Remembers across sessions not just within one | Per-topic SQLite + FTS5, rolling summaries, automatic re-injection |
92
+ | **Relationships** | Knows who it's talking to, with continuity across platforms | Cross-platform identity resolution + significance scoring |
93
+ | **Temporal awareness** | Understands time, context, and what's been happening | Event tracking every turn; timestamps embedded in memory |
94
+ | **Consistency** | Follows through on commitments doesn't contradict itself | Coherence Gate (LLM review) + decision journaling + drift detection |
95
+ | **Growth** | Evolves its capabilities and understanding over time | Evolution system: proposals, learnings, gap tracking, follow-through |
96
96
 
97
97
  > **Deep dive:** [The Coherence Problem](https://instar.sh/concepts/coherence/) · [Values & Identity](https://instar.sh/concepts/values/) · [Coherence Is Safety](https://instar.sh/concepts/safety/)
98
98
 
@@ -186,8 +186,7 @@ Security lives in multiple layers:
186
186
 
187
187
  </details>
188
188
 
189
- <details>
190
- <summary><strong>Philosophy: Agents, Not Tools</strong></summary>
189
+ ## Philosophy: Agents, Not Tools
191
190
 
192
191
  - **Structure > Willpower.** A 1,000-line prompt is a wish. A 10-line hook is a guarantee.
193
192
  - **Identity is foundational.** AGENT.md isn't a config file. It's the beginning of continuous identity.
@@ -198,8 +197,6 @@ The AI systems we build today set precedents for how AI is treated tomorrow. **T
198
197
 
199
198
  > **Deep dive:** [Philosophy](https://instar.sh/concepts/philosophy/)
200
199
 
201
- </details>
202
-
203
200
  ## iMessage Setup (macOS)
204
201
 
205
202
  iMessage support lets your agent send and receive iMessages on macOS. Messages are read directly from the native Messages database and sent via the [`imsg`](https://github.com/steipete/imsg) CLI.
@@ -2416,6 +2416,7 @@
2416
2416
  <button class="tab" data-tab="integrated-being" onclick="switchTab('integrated-being')">Integrated-Being</button>
2417
2417
  <button class="tab" data-tab="pr-pipeline" onclick="switchTab('pr-pipeline')">PR Pipeline</button>
2418
2418
  <button class="tab" data-tab="initiatives" onclick="switchTab('initiatives')">Initiatives <span class="tab-count" id="tabInitiativeCount">0</span></button>
2419
+ <button class="tab" data-tab="commitments" onclick="switchTab('commitments')">Commitments <span class="tab-count" id="tabCommitmentCount">0</span></button>
2419
2420
  </nav>
2420
2421
  </div>
2421
2422
  <div class="vital-signs" id="vitalSigns">
@@ -2808,6 +2809,24 @@
2808
2809
  <div id="initiativesList" style="display:flex;flex-direction:column;gap:12px"></div>
2809
2810
  </div>
2810
2811
 
2812
+ <!-- Commitments Tab (Open Promises) -->
2813
+ <div id="commitmentsPanel" style="display:none;flex-direction:column;padding:20px;gap:16px;overflow-y:auto">
2814
+ <div style="display:flex;justify-content:space-between;align-items:center">
2815
+ <h2 style="margin:0">Commitments</h2>
2816
+ <button onclick="loadCommitments()" style="padding:6px 12px">Refresh</button>
2817
+ </div>
2818
+ <div style="font-size:12px;color:var(--text-dim);line-height:1.4">
2819
+ Open promises — beacon-watched commitments (⏳). States:
2820
+ <span style="color:#4a9a4a">pending</span>,
2821
+ <span style="color:#e27d3b">atRisk</span>,
2822
+ <span style="color:#888">suppressed</span>.
2823
+ </div>
2824
+ <div id="commitmentsEmpty" style="padding:40px;text-align:center;color:var(--text-dim);display:none">
2825
+ No open promises.
2826
+ </div>
2827
+ <div id="commitmentsList" style="display:flex;flex-direction:column;gap:12px"></div>
2828
+ </div>
2829
+
2811
2830
  <!-- Health Tab (was Systems) -->
2812
2831
  <div class="systems-container" id="systemsTab" style="display:none">
2813
2832
  <div class="systems-main">
@@ -3848,6 +3867,12 @@
3848
3867
  display: ['flex'],
3849
3868
  onActivate: () => { if (typeof loadInitiatives === 'function') loadInitiatives(); },
3850
3869
  },
3870
+ {
3871
+ id: 'commitments',
3872
+ panels: ['commitmentsPanel'],
3873
+ display: ['flex'],
3874
+ onActivate: () => { if (typeof loadCommitments === 'function') loadCommitments(); },
3875
+ },
3851
3876
  ];
3852
3877
 
3853
3878
  function switchTab(tabName) {
@@ -5908,6 +5933,121 @@
5908
5933
  }
5909
5934
  }
5910
5935
 
5936
+ // ── Commitments Tab (PROMISE-BEACON-SPEC — Open Promises) ────
5937
+ // Fetches /commitments?status=active and renders beacon-watched
5938
+ // pending + atRisk commitments with a "Mark delivered" action.
5939
+ // All content goes through textContent; no innerHTML. XSS-safe.
5940
+ async function loadCommitments() {
5941
+ const list = document.getElementById('commitmentsList');
5942
+ const empty = document.getElementById('commitmentsEmpty');
5943
+ const countBadge = document.getElementById('tabCommitmentCount');
5944
+ while (list.firstChild) list.removeChild(list.firstChild);
5945
+ empty.style.display = 'none';
5946
+
5947
+ let res = null;
5948
+ try {
5949
+ res = await apiFetch('/commitments?status=active');
5950
+ } catch { /* fall through */ }
5951
+
5952
+ if (!res || !res.enabled) {
5953
+ empty.style.display = 'block';
5954
+ empty.textContent = 'CommitmentTracker not available.';
5955
+ if (countBadge) countBadge.textContent = '0';
5956
+ return;
5957
+ }
5958
+ const items = Array.isArray(res.commitments) ? res.commitments : [];
5959
+ // Show only beacon-watched pending + atRisk.
5960
+ const open = items.filter(c => c.beaconEnabled && c.status === 'pending');
5961
+ if (countBadge) countBadge.textContent = String(open.length);
5962
+ if (open.length === 0) {
5963
+ empty.style.display = 'block';
5964
+ empty.textContent = 'No open promises.';
5965
+ return;
5966
+ }
5967
+
5968
+ const fmtTs = (iso) => {
5969
+ if (!iso) return '—';
5970
+ try { return new Date(iso).toLocaleString(); } catch { return iso; }
5971
+ };
5972
+
5973
+ for (const c of open) {
5974
+ const card = document.createElement('div');
5975
+ card.style.cssText = 'padding:14px;border:1px solid var(--border);border-radius:6px;background:var(--bg-dim);display:flex;flex-direction:column;gap:8px';
5976
+
5977
+ const header = document.createElement('div');
5978
+ header.style.cssText = 'display:flex;justify-content:space-between;gap:12px;align-items:flex-start';
5979
+ const summary = document.createElement('div');
5980
+ summary.style.cssText = 'flex:1;font-weight:600;line-height:1.3';
5981
+ summary.textContent = (c.agentResponse || c.userRequest || '(no summary)').slice(0, 160);
5982
+ header.appendChild(summary);
5983
+
5984
+ // State badge.
5985
+ const badge = document.createElement('span');
5986
+ const atRisk = !!c.atRisk;
5987
+ const suppressed = !!c.beaconSuppressed;
5988
+ const [badgeText, badgeBg] = suppressed
5989
+ ? [`suppressed: ${c.beaconSuppressionReason || '?'}`, '#555']
5990
+ : atRisk
5991
+ ? ['atRisk', '#e27d3b']
5992
+ : ['pending', '#4a9a4a'];
5993
+ badge.textContent = badgeText;
5994
+ badge.style.cssText = `font-size:11px;padding:3px 8px;border-radius:4px;background:${badgeBg};color:#fff;white-space:nowrap`;
5995
+ header.appendChild(badge);
5996
+ card.appendChild(header);
5997
+
5998
+ const meta = document.createElement('div');
5999
+ meta.style.cssText = 'display:grid;grid-template-columns:repeat(auto-fit,minmax(160px,1fr));gap:6px;font-size:12px;color:var(--text-dim)';
6000
+ const rows = [
6001
+ ['id', c.id],
6002
+ ['topic', c.topicId != null ? String(c.topicId) : '—'],
6003
+ ['cadence', c.cadenceMs ? `${Math.round(c.cadenceMs / 1000)}s` : '—'],
6004
+ ['heartbeats', String(c.heartbeatCount ?? 0)],
6005
+ ['lastHeartbeat', fmtTs(c.lastHeartbeatAt)],
6006
+ ['nextUpdateDue', fmtTs(c.nextUpdateDueAt)],
6007
+ ['softDeadline', fmtTs(c.softDeadlineAt)],
6008
+ ['hardDeadline', fmtTs(c.hardDeadlineAt)],
6009
+ ];
6010
+ for (const [k, v] of rows) {
6011
+ const cell = document.createElement('div');
6012
+ const kEl = document.createElement('span');
6013
+ kEl.textContent = `${k}: `;
6014
+ kEl.style.opacity = '0.7';
6015
+ const vEl = document.createElement('span');
6016
+ vEl.textContent = v;
6017
+ vEl.style.color = 'var(--text)';
6018
+ cell.appendChild(kEl);
6019
+ cell.appendChild(vEl);
6020
+ meta.appendChild(cell);
6021
+ }
6022
+ card.appendChild(meta);
6023
+
6024
+ // Actions.
6025
+ const actions = document.createElement('div');
6026
+ actions.style.cssText = 'display:flex;gap:8px;align-items:center';
6027
+ const deliverBtn = document.createElement('button');
6028
+ deliverBtn.textContent = 'Mark delivered';
6029
+ deliverBtn.style.cssText = 'padding:6px 10px;cursor:pointer';
6030
+ deliverBtn.addEventListener('click', async () => {
6031
+ deliverBtn.disabled = true;
6032
+ try {
6033
+ await apiFetch(`/commitments/${encodeURIComponent(c.id)}/deliver`, {
6034
+ method: 'POST',
6035
+ headers: { 'Content-Type': 'application/json' },
6036
+ body: JSON.stringify({}),
6037
+ });
6038
+ await loadCommitments();
6039
+ } catch (err) {
6040
+ deliverBtn.disabled = false;
6041
+ alert('Deliver failed: ' + (err && err.message ? err.message : String(err)));
6042
+ }
6043
+ });
6044
+ actions.appendChild(deliverBtn);
6045
+ card.appendChild(actions);
6046
+
6047
+ list.appendChild(card);
6048
+ }
6049
+ }
6050
+
5911
6051
  // ── Integrated-Being Tab (v1) ────────────────────────────────
5912
6052
  let integratedBeingPollTimer = null;
5913
6053
 
package/dist/cli.js CHANGED
File without changes
@@ -1 +1 @@
1
- {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/commands/server.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AA2PH,UAAU,YAAY;IACpB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb;2DACuD;IACvD,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AA81CD,wBAAsB,WAAW,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CA8+ItE;AAED,wBAAsB,UAAU,CAAC,OAAO,EAAE;IAAE,GAAG,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAsDzE;AAED;;;;;;;;;;;GAWG;AACH,wBAAsB,aAAa,CAAC,OAAO,EAAE;IAAE,GAAG,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAuD5E"}
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/commands/server.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AA4PH,UAAU,YAAY;IACpB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb;2DACuD;IACvD,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AA81CD,wBAAsB,WAAW,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CA4jJtE;AAED,wBAAsB,UAAU,CAAC,OAAO,EAAE;IAAE,GAAG,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAsDzE;AAED;;;;;;;;;;;GAWG;AACH,wBAAsB,aAAa,CAAC,OAAO,EAAE;IAAE,GAAG,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAuD5E"}
@@ -45,6 +45,7 @@ import { QuotaNotifier } from '../monitoring/QuotaNotifier.js';
45
45
  import { QuotaManager } from '../monitoring/QuotaManager.js';
46
46
  import { classifySessionDeath } from '../monitoring/QuotaExhaustionDetector.js';
47
47
  import { SessionWatchdog } from '../monitoring/SessionWatchdog.js';
48
+ import { formatWatchdogUserMessage } from '../monitoring/watchdog-notifications.js';
48
49
  import { StallTriageNurse } from '../monitoring/StallTriageNurse.js';
49
50
  import { TriageOrchestrator } from '../monitoring/TriageOrchestrator.js';
50
51
  import { SessionMonitor } from '../monitoring/SessionMonitor.js';
@@ -3156,33 +3157,26 @@ export async function startServer(options) {
3156
3157
  watchdog = new SessionWatchdog(config, sessionManager, state);
3157
3158
  watchdog.intelligence = sharedIntelligence ?? null;
3158
3159
  watchdog.on('intervention', (event) => {
3159
- const levelNames = ['Monitoring', 'Ctrl+C', 'SIGTERM', 'SIGKILL', 'Kill Session'];
3160
- const levelName = levelNames[event.level] || `Level ${event.level}`;
3161
- const msg = `🔧 Watchdog [${levelName}]: ${event.action}\nStuck: \`${event.stuckCommand.slice(0, 60)}\``;
3160
+ // Routine recovery (Ctrl+C, SIGTERM) stays as console diagnostics only.
3161
+ // The user only hears when we had to force-kill (SIGKILL / session-kill) —
3162
+ // that's the "actual issue" threshold. See watchdog-notifications.ts.
3163
+ const userMsg = formatWatchdogUserMessage(event);
3164
+ if (!userMsg)
3165
+ return;
3162
3166
  if (telegram) {
3163
3167
  const topicId = telegram.getTopicForSession(event.sessionName);
3164
3168
  if (topicId)
3165
- telegram.sendToTopic(topicId, msg).catch(() => { });
3169
+ telegram.sendToTopic(topicId, userMsg).catch(() => { });
3166
3170
  }
3167
3171
  if (_slackAdapter) {
3168
3172
  const channelId = _slackAdapter.getChannelForSession(event.sessionName);
3169
3173
  if (channelId)
3170
- _slackAdapter.sendToChannel(channelId, msg).catch(() => { });
3171
- }
3172
- });
3173
- watchdog.on('recovery', (sessionName, fromLevel) => {
3174
- const msg = `✅ Watchdog: session recovered (was at escalation level ${fromLevel})`;
3175
- if (telegram) {
3176
- const topicId = telegram.getTopicForSession(sessionName);
3177
- if (topicId)
3178
- telegram.sendToTopic(topicId, msg).catch(() => { });
3179
- }
3180
- if (_slackAdapter) {
3181
- const channelId = _slackAdapter.getChannelForSession(sessionName);
3182
- if (channelId)
3183
- _slackAdapter.sendToChannel(channelId, msg).catch(() => { });
3174
+ _slackAdapter.sendToChannel(channelId, userMsg).catch(() => { });
3184
3175
  }
3185
3176
  });
3177
+ // Recovery events stay silent to the user. If we didn't announce the
3178
+ // problem (Ctrl+C / SIGTERM are now silent), announcing recovery is
3179
+ // noise. Intervention log still records it for diagnostics.
3186
3180
  watchdog.start();
3187
3181
  console.log(pc.green(' Session Watchdog enabled'));
3188
3182
  }
@@ -4164,7 +4158,8 @@ export async function startServer(options) {
4164
4158
  interactiveReservePct: 0.4,
4165
4159
  maxDailyCents: promiseBeaconCfg.maxDailyLlmSpendCents ?? 100,
4166
4160
  });
4167
- void sharedLlmQueue; // Wired into PromiseBeacon below; PresenceProxy refactor tracked as follow-up.
4161
+ // sharedLlmQueue is wired into both PromiseBeacon (background lane) and
4162
+ // PresenceProxy (interactive lane) below.
4168
4163
  let presenceProxy;
4169
4164
  if (sharedIntelligence && telegram) {
4170
4165
  try {
@@ -4313,6 +4308,9 @@ export async function startServer(options) {
4313
4308
  // Shared per-topic mutex — coordinates with PromiseBeacon.
4314
4309
  acquireProxyMutex: (topicId, holder) => proxyCoordinator.tryAcquire(topicId, holder),
4315
4310
  releaseProxyMutex: (topicId, holder) => proxyCoordinator.release(topicId, holder),
4311
+ // Shared LLM queue (interactive lane) — cross-monitor concurrency
4312
+ // and daily-spend-cap with PromiseBeacon.
4313
+ sharedLlmQueue,
4316
4314
  });
4317
4315
  // Hook into Telegram's onMessageLogged callback (always active, unlike EventBus which requires a feature flag)
4318
4316
  const existingCallback = telegram.onMessageLogged;
@@ -4366,6 +4364,7 @@ export async function startServer(options) {
4366
4364
  maxDailyLlmSpendCents: promiseBeaconCfg.maxDailyLlmSpendCents ?? 100,
4367
4365
  sentinelAutoEnable: promiseBeaconCfg.sentinelAutoEnable ?? false,
4368
4366
  quietHours: promiseBeaconCfg.quietHours ?? { start: '22:00', end: '08:00' },
4367
+ maxActiveBeacons: promiseBeaconCfg.maxActiveBeacons ?? 20,
4369
4368
  });
4370
4369
  promiseBeacon.start();
4371
4370
  globalThis.__instarPromiseBeacon = promiseBeacon;
@@ -4850,7 +4849,14 @@ export async function startServer(options) {
4850
4849
  summarySentinel.start();
4851
4850
  messageRouter.setSummarySentinel(summarySentinel);
4852
4851
  // On-demand session spawning for message delivery (Phase 5)
4853
- const spawnManager = new SpawnRequestManager({
4852
+ // §4.4: spawn knobs are read from config.threadline.spawn — see
4853
+ // ThreadlineSpawnConfig in core/types.ts. All fields are optional and
4854
+ // fall through to manager-level defaults if absent.
4855
+ const spawnConfig = config.threadline?.spawn;
4856
+ // Forward-declared `let` so the onDrainReady callback can reference the
4857
+ // manager it belongs to (for re-entrant evaluate() calls during drain).
4858
+ let spawnManager;
4859
+ spawnManager = new SpawnRequestManager({
4854
4860
  maxSessions: config.sessions.maxSessions ?? 5,
4855
4861
  getActiveSessions: () => sessionManager.listRunningSessions(),
4856
4862
  spawnSession: async (prompt, opts) => {
@@ -4859,7 +4865,9 @@ export async function startServer(options) {
4859
4865
  prompt,
4860
4866
  model: opts?.model,
4861
4867
  maxDurationMinutes: opts?.maxDurationMinutes,
4862
- triggeredBy: 'spawn-request',
4868
+ // §4.5: honor SpawnRequestManager's provenance tag so drain-spawned
4869
+ // sessions are distinguishable from inline-spawned ones in logs/stream.
4870
+ triggeredBy: opts?.triggeredBy ?? 'spawn-request',
4863
4871
  });
4864
4872
  return session.id;
4865
4873
  },
@@ -4872,7 +4880,74 @@ export async function startServer(options) {
4872
4880
  onEscalate: (request, reason) => {
4873
4881
  notify('IMMEDIATE', 'messaging', `Spawn escalation: ${reason}\n Requester: ${request.requester.agent}\n Target: ${request.target.agent}`);
4874
4882
  },
4883
+ // §4.5: emit degradation breadcrumbs on edge transitions.
4884
+ onDegradation: (event) => {
4885
+ try {
4886
+ const reporter = DegradationReporter.getInstance();
4887
+ if (event.kind === 'spawn-penalty-tripped') {
4888
+ reporter.report({
4889
+ feature: 'Threadline.SpawnPenalty',
4890
+ primary: `Open spawn slot for peer "${event.agent}"`,
4891
+ fallback: `Spawn blocked for ${Math.round(event.penaltyMs / 1000)}s after ${event.consecutiveFailures} consecutive agent-attributable failures`,
4892
+ reason: `Peer "${event.agent}" tripped the consecutive-failure penalty (3 strikes)`,
4893
+ impact: 'Peer cannot spawn sessions until penalty clears. Successful inbound spawn from a different peer is unaffected.',
4894
+ });
4895
+ }
4896
+ else if (event.kind === 'spawn-infra-degraded') {
4897
+ reporter.report({
4898
+ feature: 'Threadline.SpawnInfraDegraded',
4899
+ primary: `Full queue admission (cap 10) for peer "${event.agent}"`,
4900
+ fallback: `Degraded admission (cap ${spawnConfig?.degradedMaxQueuedPerAgent ?? 1}) for ${Math.round(event.degradationMs / 60_000)}min`,
4901
+ reason: `Peer "${event.agent}" tripped the infra-failure soft limiter (${event.failureCount} non-attributable failures in 10min)`,
4902
+ impact: 'Peer\'s queue depth is capped; older messages are dropped. No blame attribution.',
4903
+ });
4904
+ }
4905
+ }
4906
+ catch (err) {
4907
+ console.warn(`[spawn-manager] degradation reporter failed: ${err instanceof Error ? err.message : String(err)}`);
4908
+ }
4909
+ },
4910
+ // §4.4: optional knobs from config.
4911
+ cooldownMs: spawnConfig?.cooldownMs,
4912
+ maxDrainsPerTick: spawnConfig?.maxDrainsPerTick,
4913
+ maxEnvelopeBytes: spawnConfig?.maxEnvelopeBytes,
4914
+ maxGlobalQueued: spawnConfig?.maxGlobalQueued,
4915
+ degradedMaxQueuedPerAgent: spawnConfig?.degradedMaxQueuedPerAgent,
4916
+ // §4.4 commit 2 + §4.5: drain-loop consumer wiring.
4917
+ // When the drain loop finds an agent ready (cooldown cleared + queued
4918
+ // messages present), this callback re-invokes evaluate() with a
4919
+ // synthetic SpawnRequest tagged `triggeredBy: 'spawn-request-drain'`.
4920
+ // The real queued context is reattached by SpawnRequestManager.evaluate
4921
+ // via its internal drainQueue() call. Stub session/machine values:
4922
+ // requester.session/machine isn't preserved per-message — those fields
4923
+ // are only used in the spawn prompt template for display.
4924
+ onDrainReady: async (agent) => {
4925
+ try {
4926
+ const result = await spawnManager.evaluate({
4927
+ requester: { agent, session: 'drain', machine: 'drain' },
4928
+ target: { agent: config.projectName, machine: os.hostname() },
4929
+ reason: `Drain re-attempt for queued messages from ${agent}`,
4930
+ priority: 'medium',
4931
+ triggeredBy: 'spawn-request-drain',
4932
+ });
4933
+ if (!result.approved) {
4934
+ console.log(`[spawn-manager] drain re-attempt for ${agent} not approved: ${result.reason}`);
4935
+ }
4936
+ }
4937
+ catch (err) {
4938
+ console.warn(`[spawn-manager] drain re-attempt for ${agent} threw: ${err instanceof Error ? err.message : String(err)}`);
4939
+ }
4940
+ },
4875
4941
  });
4942
+ // §4.4 kill switch: drain loop runs unless explicitly disabled in config.
4943
+ // Wired here so emergency rollback is a config flip, not a code change.
4944
+ if (spawnConfig?.drainEnabled !== false) {
4945
+ spawnManager.start();
4946
+ console.log(`[spawn-manager] drain loop started (tick=${spawnManager.getDrainTickMs()}ms)`);
4947
+ }
4948
+ else {
4949
+ console.log('[spawn-manager] drain loop disabled by config.threadline.spawn.drainEnabled=false');
4950
+ }
4876
4951
  // Threadline Router — handles threaded cross-agent conversations via relay
4877
4952
  const threadlineRouter = new ThreadlineRouter(messageRouter, spawnManager, threadResumeMap, messageStore, { localAgent: config.projectName, localMachine: os.hostname() }, null, // autonomyGate
4878
4953
  messageDelivery);
@@ -5178,7 +5253,12 @@ export async function startServer(options) {
5178
5253
  transport: { protocol: 'relay', origin: { agent: senderFingerprint, machine: 'relay' }, nonce: `${crypto.randomUUID()}:${new Date().toISOString()}`, timestamp: new Date().toISOString() },
5179
5254
  delivery: { status: 'delivered', attempts: 1, lastAttempt: new Date().toISOString() },
5180
5255
  };
5181
- const relayContext = { senderFingerprint, senderName, trustLevel };
5256
+ const relayContext = {
5257
+ trust: { kind: 'plaintext-tofu', senderFingerprint },
5258
+ senderFingerprint,
5259
+ senderName,
5260
+ trustLevel,
5261
+ };
5182
5262
  let result = await threadlineRouter.handleInboundMessage(envelope, relayContext);
5183
5263
  // Fallback for threadId-less messages
5184
5264
  if (!result.handled && !msg.threadId) {
@@ -5687,6 +5767,7 @@ export async function startServer(options) {
5687
5767
  await notificationBatcher.flushAll(); // Drain pending notifications before exit
5688
5768
  notificationBatcher.stop();
5689
5769
  retryManager.stop();
5770
+ spawnManager.dispose(); // §4.4: stop drain loop + clear DRR state
5690
5771
  summarySentinel.stop();
5691
5772
  memoryMonitor.stop();
5692
5773
  caffeinateManager.stop();