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.
- package/README.md +15 -18
- package/dashboard/index.html +140 -0
- package/dist/cli.js +0 -0
- package/dist/commands/server.d.ts.map +1 -1
- package/dist/commands/server.js +103 -22
- package/dist/commands/server.js.map +1 -1
- package/dist/core/types.d.ts +26 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/messaging/DeliveryRetryManager.d.ts.map +1 -1
- package/dist/messaging/DeliveryRetryManager.js +6 -4
- package/dist/messaging/DeliveryRetryManager.js.map +1 -1
- package/dist/messaging/MessageStore.d.ts +10 -0
- package/dist/messaging/MessageStore.d.ts.map +1 -1
- package/dist/messaging/MessageStore.js +52 -0
- package/dist/messaging/MessageStore.js.map +1 -1
- package/dist/messaging/SpawnRequestManager.d.ts +224 -19
- package/dist/messaging/SpawnRequestManager.d.ts.map +1 -1
- package/dist/messaging/SpawnRequestManager.js +600 -70
- package/dist/messaging/SpawnRequestManager.js.map +1 -1
- package/dist/messaging/types.d.ts +11 -1
- package/dist/messaging/types.d.ts.map +1 -1
- package/dist/messaging/types.js +7 -0
- package/dist/messaging/types.js.map +1 -1
- package/dist/monitoring/PresenceProxy.d.ts +8 -0
- package/dist/monitoring/PresenceProxy.d.ts.map +1 -1
- package/dist/monitoring/PresenceProxy.js +30 -0
- package/dist/monitoring/PresenceProxy.js.map +1 -1
- package/dist/monitoring/PromiseBeacon.d.ts +23 -0
- package/dist/monitoring/PromiseBeacon.d.ts.map +1 -1
- package/dist/monitoring/PromiseBeacon.js +68 -6
- package/dist/monitoring/PromiseBeacon.js.map +1 -1
- package/dist/monitoring/SessionWatchdog.d.ts.map +1 -1
- package/dist/monitoring/SessionWatchdog.js +23 -7
- package/dist/monitoring/SessionWatchdog.js.map +1 -1
- package/dist/monitoring/watchdog-notifications.d.ts +15 -0
- package/dist/monitoring/watchdog-notifications.d.ts.map +1 -0
- package/dist/monitoring/watchdog-notifications.js +24 -0
- package/dist/monitoring/watchdog-notifications.js.map +1 -0
- package/dist/server/routes.d.ts.map +1 -1
- package/dist/server/routes.js +186 -0
- package/dist/server/routes.js.map +1 -1
- package/dist/threadline/ThreadlineRouter.d.ts +64 -2
- package/dist/threadline/ThreadlineRouter.d.ts.map +1 -1
- package/dist/threadline/ThreadlineRouter.js +117 -9
- package/dist/threadline/ThreadlineRouter.js.map +1 -1
- package/dist/threadline/client/ThreadlineClient.d.ts +25 -1
- package/dist/threadline/client/ThreadlineClient.d.ts.map +1 -1
- package/dist/threadline/client/ThreadlineClient.js +79 -2
- package/dist/threadline/client/ThreadlineClient.js.map +1 -1
- package/package.json +1 -1
- package/src/data/builtin-manifest.json +51 -51
- package/src/templates/hooks/session-start.sh +19 -0
- package/upgrades/0.28.63.md +43 -0
- package/upgrades/side-effects/0.28.62.md +64 -0
- package/upgrades/side-effects/promise-beacon-followups.md +105 -0
- package/upgrades/side-effects/threadline-cooldown-prereq-1-undelivered-phase.md +73 -0
- package/upgrades/side-effects/threadline-cooldown-sec4.1-branded-trust-union.md +68 -0
- package/upgrades/side-effects/threadline-cooldown-sec4.1-client-affinity.md +71 -0
- package/upgrades/side-effects/threadline-cooldown-sec4.1-receiver-affinity.md +67 -0
- package/upgrades/side-effects/threadline-cooldown-sec4.2-drain-loop.md +92 -0
- package/upgrades/side-effects/threadline-cooldown-sec4.2-infra-soft-limiter.md +75 -0
- package/upgrades/side-effects/threadline-cooldown-sec4.2-state-refactor.md +78 -0
- package/upgrades/side-effects/threadline-cooldown-sec4.3-envelope-byte-cap.md +66 -0
- package/upgrades/side-effects/threadline-cooldown-sec4.3-envelope-hash.md +71 -0
- package/upgrades/side-effects/threadline-cooldown-sec4.3-truncation-and-global-cap.md +69 -0
- package/upgrades/side-effects/threadline-cooldown-sec4.4-config-plumbing.md +77 -0
- package/upgrades/side-effects/threadline-cooldown-sec4.4-drain-consumer-wiring.md +72 -0
- package/upgrades/side-effects/threadline-cooldown-sec4.4-patch-endpoint.md +72 -0
- package/upgrades/side-effects/threadline-cooldown-sec4.5-degradation-reporter.md +83 -0
- package/upgrades/side-effects/threadline-cooldown-sec4.5-triggeredby-plumbing.md +76 -0
- package/upgrades/side-effects/threadline-cooldown-spec-landing.md +48 -0
- package/upgrades/side-effects/watchdog-user-comfort.md +136 -0
- package/dist/core/InitiativeDigestJob.d.ts +0 -54
- package/dist/core/InitiativeDigestJob.d.ts.map +0 -1
- package/dist/core/InitiativeDigestJob.js +0 -128
- package/dist/core/InitiativeDigestJob.js.map +0 -1
- package/upgrades/NEXT.md +0 -53
- /package/upgrades/{0.28.61.md → 0.28.62.md} +0 -0
- /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
|
|
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
|
|
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
|
-
##
|
|
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
|
-
|
|
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
|
-
| **
|
|
91
|
-
| **
|
|
92
|
-
| **
|
|
93
|
-
| **Temporal awareness** | Understands time, context, and what's been happening |
|
|
94
|
-
| **Consistency** | Follows through on commitments
|
|
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
|
-
|
|
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.
|
package/dashboard/index.html
CHANGED
|
@@ -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;
|
|
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"}
|
package/dist/commands/server.js
CHANGED
|
@@ -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
|
-
|
|
3160
|
-
|
|
3161
|
-
|
|
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,
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 = {
|
|
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();
|