instar 0.23.14 → 0.23.16
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/dashboard/index.html +353 -2
- package/dist/commands/server.d.ts.map +1 -1
- package/dist/commands/server.js +70 -5
- package/dist/commands/server.js.map +1 -1
- package/dist/core/PostUpdateMigrator.d.ts +11 -0
- package/dist/core/PostUpdateMigrator.d.ts.map +1 -1
- package/dist/core/PostUpdateMigrator.js +69 -0
- package/dist/core/PostUpdateMigrator.js.map +1 -1
- package/dist/core/SessionManager.d.ts +18 -0
- package/dist/core/SessionManager.d.ts.map +1 -1
- package/dist/core/SessionManager.js +82 -2
- package/dist/core/SessionManager.js.map +1 -1
- package/dist/core/TopicResumeMap.d.ts +14 -2
- package/dist/core/TopicResumeMap.d.ts.map +1 -1
- package/dist/core/TopicResumeMap.js +36 -41
- package/dist/core/TopicResumeMap.js.map +1 -1
- package/dist/core/types.d.ts +2 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js.map +1 -1
- package/dist/data/http-hook-templates.js +10 -10
- package/dist/data/http-hook-templates.js.map +1 -1
- package/dist/memory/TopicMemory.js +3 -3
- package/dist/memory/TopicMemory.js.map +1 -1
- package/dist/monitoring/SessionRecovery.d.ts +110 -0
- package/dist/monitoring/SessionRecovery.d.ts.map +1 -0
- package/dist/monitoring/SessionRecovery.js +432 -0
- package/dist/monitoring/SessionRecovery.js.map +1 -0
- package/dist/monitoring/crash-detector.d.ts +50 -0
- package/dist/monitoring/crash-detector.d.ts.map +1 -0
- package/dist/monitoring/crash-detector.js +224 -0
- package/dist/monitoring/crash-detector.js.map +1 -0
- package/dist/monitoring/jsonl-truncator.d.ts +44 -0
- package/dist/monitoring/jsonl-truncator.d.ts.map +1 -0
- package/dist/monitoring/jsonl-truncator.js +224 -0
- package/dist/monitoring/jsonl-truncator.js.map +1 -0
- package/dist/monitoring/stall-detector.d.ts +34 -0
- package/dist/monitoring/stall-detector.d.ts.map +1 -0
- package/dist/monitoring/stall-detector.js +151 -0
- package/dist/monitoring/stall-detector.js.map +1 -0
- package/dist/scheduler/JobScheduler.d.ts.map +1 -1
- package/dist/scheduler/JobScheduler.js +16 -2
- package/dist/scheduler/JobScheduler.js.map +1 -1
- package/dist/server/WebSocketManager.d.ts +2 -0
- package/dist/server/WebSocketManager.d.ts.map +1 -1
- package/dist/server/WebSocketManager.js +24 -3
- package/dist/server/WebSocketManager.js.map +1 -1
- package/dist/server/routes.d.ts.map +1 -1
- package/dist/server/routes.js +118 -5
- package/dist/server/routes.js.map +1 -1
- package/package.json +2 -1
- package/scripts/pre-push-gate.js +149 -0
- package/src/data/builtin-manifest.json +63 -63
- package/src/data/http-hook-templates.ts +10 -10
- package/src/templates/hooks/telegram-topic-context.sh +4 -4
- package/upgrades/0.23.10.md +19 -0
- package/upgrades/0.23.11.md +21 -0
- package/upgrades/0.23.15.md +30 -0
- package/upgrades/0.23.16.md +25 -0
package/dashboard/index.html
CHANGED
|
@@ -1206,6 +1206,213 @@
|
|
|
1206
1206
|
to { opacity: 1; transform: translateY(0); }
|
|
1207
1207
|
}
|
|
1208
1208
|
|
|
1209
|
+
/* ── New Session modal ────────────────────────────── */
|
|
1210
|
+
.modal-backdrop {
|
|
1211
|
+
position: fixed;
|
|
1212
|
+
inset: 0;
|
|
1213
|
+
background: rgba(0,0,0,0.6);
|
|
1214
|
+
z-index: 5000;
|
|
1215
|
+
display: flex;
|
|
1216
|
+
align-items: center;
|
|
1217
|
+
justify-content: center;
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
.modal-dialog {
|
|
1221
|
+
background: var(--bg-panel);
|
|
1222
|
+
border: 1px solid var(--border);
|
|
1223
|
+
border-radius: 12px;
|
|
1224
|
+
padding: 24px;
|
|
1225
|
+
width: 380px;
|
|
1226
|
+
max-width: 90vw;
|
|
1227
|
+
box-shadow: 0 8px 32px rgba(0,0,0,0.6);
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
.modal-dialog h3 {
|
|
1231
|
+
color: var(--text-bright);
|
|
1232
|
+
font-size: 16px;
|
|
1233
|
+
margin-bottom: 16px;
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
.modal-field {
|
|
1237
|
+
margin-bottom: 14px;
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
.modal-field label {
|
|
1241
|
+
display: block;
|
|
1242
|
+
font-size: 12px;
|
|
1243
|
+
color: var(--text-dim);
|
|
1244
|
+
margin-bottom: 4px;
|
|
1245
|
+
text-transform: uppercase;
|
|
1246
|
+
letter-spacing: 0.04em;
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
.modal-field input[type="text"] {
|
|
1250
|
+
width: 100%;
|
|
1251
|
+
padding: 10px 12px;
|
|
1252
|
+
border-radius: 6px;
|
|
1253
|
+
border: 1px solid var(--border);
|
|
1254
|
+
background: var(--bg);
|
|
1255
|
+
color: var(--text-bright);
|
|
1256
|
+
font-size: 14px;
|
|
1257
|
+
outline: none;
|
|
1258
|
+
box-sizing: border-box;
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
.modal-field input[type="text"]:focus {
|
|
1262
|
+
border-color: var(--accent-dim);
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1265
|
+
.modal-toggle {
|
|
1266
|
+
display: flex;
|
|
1267
|
+
align-items: center;
|
|
1268
|
+
gap: 10px;
|
|
1269
|
+
cursor: pointer;
|
|
1270
|
+
user-select: none;
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
.modal-toggle input[type="checkbox"] {
|
|
1274
|
+
appearance: none;
|
|
1275
|
+
width: 36px;
|
|
1276
|
+
height: 20px;
|
|
1277
|
+
border-radius: 10px;
|
|
1278
|
+
background: #333;
|
|
1279
|
+
position: relative;
|
|
1280
|
+
cursor: pointer;
|
|
1281
|
+
transition: background 0.2s;
|
|
1282
|
+
flex-shrink: 0;
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1285
|
+
.modal-toggle input[type="checkbox"]::after {
|
|
1286
|
+
content: '';
|
|
1287
|
+
position: absolute;
|
|
1288
|
+
top: 2px;
|
|
1289
|
+
left: 2px;
|
|
1290
|
+
width: 16px;
|
|
1291
|
+
height: 16px;
|
|
1292
|
+
border-radius: 50%;
|
|
1293
|
+
background: #888;
|
|
1294
|
+
transition: transform 0.2s, background 0.2s;
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
.modal-toggle input[type="checkbox"]:checked {
|
|
1298
|
+
background: var(--accent-dim);
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1301
|
+
.modal-toggle input[type="checkbox"]:checked::after {
|
|
1302
|
+
transform: translateX(16px);
|
|
1303
|
+
background: #fff;
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1306
|
+
.modal-toggle span {
|
|
1307
|
+
font-size: 13px;
|
|
1308
|
+
color: var(--text);
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1311
|
+
.modal-hint {
|
|
1312
|
+
font-size: 11px;
|
|
1313
|
+
color: var(--text-dim);
|
|
1314
|
+
margin-top: 4px;
|
|
1315
|
+
font-style: italic;
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1318
|
+
.modal-actions {
|
|
1319
|
+
display: flex;
|
|
1320
|
+
gap: 8px;
|
|
1321
|
+
margin-top: 20px;
|
|
1322
|
+
justify-content: flex-end;
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1325
|
+
.modal-actions button {
|
|
1326
|
+
padding: 8px 18px;
|
|
1327
|
+
border-radius: 6px;
|
|
1328
|
+
font-size: 13px;
|
|
1329
|
+
font-weight: 500;
|
|
1330
|
+
cursor: pointer;
|
|
1331
|
+
border: 1px solid var(--border);
|
|
1332
|
+
}
|
|
1333
|
+
|
|
1334
|
+
.modal-btn-cancel {
|
|
1335
|
+
background: var(--bg);
|
|
1336
|
+
color: var(--text);
|
|
1337
|
+
}
|
|
1338
|
+
|
|
1339
|
+
.modal-btn-cancel:hover {
|
|
1340
|
+
background: var(--bg-hover);
|
|
1341
|
+
}
|
|
1342
|
+
|
|
1343
|
+
.modal-btn-create {
|
|
1344
|
+
background: var(--accent-dim);
|
|
1345
|
+
color: #000;
|
|
1346
|
+
border-color: var(--accent-dim);
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
.modal-btn-create:hover {
|
|
1350
|
+
background: var(--accent);
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1353
|
+
.modal-btn-create:disabled {
|
|
1354
|
+
opacity: 0.4;
|
|
1355
|
+
cursor: not-allowed;
|
|
1356
|
+
}
|
|
1357
|
+
|
|
1358
|
+
/* ── New session button ───────────────────────────── */
|
|
1359
|
+
.new-session-btn {
|
|
1360
|
+
width: 28px;
|
|
1361
|
+
height: 28px;
|
|
1362
|
+
border-radius: 6px;
|
|
1363
|
+
border: 1px solid var(--border);
|
|
1364
|
+
background: var(--bg);
|
|
1365
|
+
color: var(--accent);
|
|
1366
|
+
font-size: 18px;
|
|
1367
|
+
cursor: pointer;
|
|
1368
|
+
display: flex;
|
|
1369
|
+
align-items: center;
|
|
1370
|
+
justify-content: center;
|
|
1371
|
+
transition: all 0.15s;
|
|
1372
|
+
line-height: 1;
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1375
|
+
.new-session-btn:hover {
|
|
1376
|
+
background: var(--bg-hover);
|
|
1377
|
+
border-color: var(--accent-dim);
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1380
|
+
/* ── Session close button ─────────────────────────── */
|
|
1381
|
+
.session-close-btn {
|
|
1382
|
+
background: none;
|
|
1383
|
+
border: none;
|
|
1384
|
+
color: var(--text-dim);
|
|
1385
|
+
cursor: pointer;
|
|
1386
|
+
font-size: 14px;
|
|
1387
|
+
padding: 2px 6px;
|
|
1388
|
+
border-radius: 4px;
|
|
1389
|
+
opacity: 0;
|
|
1390
|
+
transition: opacity 0.15s, color 0.15s;
|
|
1391
|
+
flex-shrink: 0;
|
|
1392
|
+
line-height: 1;
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1395
|
+
.session-item:hover .session-close-btn,
|
|
1396
|
+
.session-item.active .session-close-btn {
|
|
1397
|
+
opacity: 1;
|
|
1398
|
+
}
|
|
1399
|
+
|
|
1400
|
+
.session-close-btn:hover {
|
|
1401
|
+
color: var(--red);
|
|
1402
|
+
background: rgba(239, 68, 68, 0.1);
|
|
1403
|
+
}
|
|
1404
|
+
|
|
1405
|
+
.session-name-row {
|
|
1406
|
+
display: flex;
|
|
1407
|
+
align-items: center;
|
|
1408
|
+
gap: 4px;
|
|
1409
|
+
}
|
|
1410
|
+
|
|
1411
|
+
.session-name-row .session-name {
|
|
1412
|
+
flex: 1;
|
|
1413
|
+
min-width: 0;
|
|
1414
|
+
}
|
|
1415
|
+
|
|
1209
1416
|
/* Conflict dialog */
|
|
1210
1417
|
.conflict-overlay {
|
|
1211
1418
|
position: fixed;
|
|
@@ -1493,7 +1700,10 @@
|
|
|
1493
1700
|
<aside class="sidebar" id="sessionsTab">
|
|
1494
1701
|
<div class="sidebar-header">
|
|
1495
1702
|
<h2>Sessions</h2>
|
|
1496
|
-
<
|
|
1703
|
+
<div style="display:flex;align-items:center;gap:8px;">
|
|
1704
|
+
<span class="session-count" id="sessionCount">0</span>
|
|
1705
|
+
<button class="new-session-btn" onclick="openNewSessionModal()" title="New session">+</button>
|
|
1706
|
+
</div>
|
|
1497
1707
|
</div>
|
|
1498
1708
|
<div class="session-list" id="sessionList">
|
|
1499
1709
|
<div class="empty-state" id="emptyState">
|
|
@@ -1528,6 +1738,8 @@
|
|
|
1528
1738
|
<button class="action-btn action-arrow" onclick="sendKey('Down')" title="Arrow Down">↓</button>
|
|
1529
1739
|
<button class="action-btn action-arrow" onclick="sendKey('Left')" title="Arrow Left">←</button>
|
|
1530
1740
|
<button class="action-btn action-arrow" onclick="sendKey('Right')" title="Arrow Right">→</button>
|
|
1741
|
+
<span class="action-separator"></span>
|
|
1742
|
+
<button class="action-btn danger" onclick="closeActiveSession()" title="Close session">× Close</button>
|
|
1531
1743
|
</div>
|
|
1532
1744
|
</div>
|
|
1533
1745
|
<div class="terminal-container" id="terminalContainer"></div>
|
|
@@ -1581,6 +1793,29 @@
|
|
|
1581
1793
|
</div>
|
|
1582
1794
|
</div>
|
|
1583
1795
|
|
|
1796
|
+
<!-- New Session modal -->
|
|
1797
|
+
<div class="modal-backdrop" id="newSessionModal" style="display:none" onclick="closeNewSessionModal(event)">
|
|
1798
|
+
<div class="modal-dialog" onclick="event.stopPropagation()">
|
|
1799
|
+
<h3>New Session</h3>
|
|
1800
|
+
<div class="modal-field">
|
|
1801
|
+
<label for="newSessionName">Topic Name</label>
|
|
1802
|
+
<input type="text" id="newSessionName" placeholder="e.g. dashboard-redesign" maxlength="128"
|
|
1803
|
+
onkeydown="if(event.key==='Enter')createNewSession()" autofocus>
|
|
1804
|
+
</div>
|
|
1805
|
+
<div class="modal-field">
|
|
1806
|
+
<label class="modal-toggle">
|
|
1807
|
+
<input type="checkbox" id="newSessionHeadless">
|
|
1808
|
+
<span>Headless (no Telegram topic)</span>
|
|
1809
|
+
</label>
|
|
1810
|
+
<div class="modal-hint">Headless sessions still appear in the dashboard but won't create a Telegram topic.</div>
|
|
1811
|
+
</div>
|
|
1812
|
+
<div class="modal-actions">
|
|
1813
|
+
<button class="modal-btn-cancel" onclick="closeNewSessionModal()">Cancel</button>
|
|
1814
|
+
<button class="modal-btn-create" id="createSessionBtn" onclick="createNewSession()">Create Session</button>
|
|
1815
|
+
</div>
|
|
1816
|
+
</div>
|
|
1817
|
+
</div>
|
|
1818
|
+
|
|
1584
1819
|
<!-- Drop Zone tab -->
|
|
1585
1820
|
<div class="dropzone-container" id="dropzoneTab" style="display:none">
|
|
1586
1821
|
<div class="dropzone-panel">
|
|
@@ -1646,6 +1881,9 @@
|
|
|
1646
1881
|
let activeSession = null;
|
|
1647
1882
|
let term = null;
|
|
1648
1883
|
let fitAddon = null;
|
|
1884
|
+
let historyLinesLoaded = 2000; // matches initial subscribe capture
|
|
1885
|
+
let historyLoading = false;
|
|
1886
|
+
let historyExhausted = false; // true when we've loaded all available history
|
|
1649
1887
|
|
|
1650
1888
|
// ── Auth ─────────────────────────────────────────────────
|
|
1651
1889
|
function authenticate() {
|
|
@@ -1769,6 +2007,26 @@
|
|
|
1769
2007
|
}
|
|
1770
2008
|
break;
|
|
1771
2009
|
|
|
2010
|
+
case 'history':
|
|
2011
|
+
if (msg.session === activeSession && term) {
|
|
2012
|
+
const prevLineCount = historyLinesLoaded;
|
|
2013
|
+
historyLinesLoaded = msg.lines;
|
|
2014
|
+
// Check if we got more content than before
|
|
2015
|
+
const newLineCount = (msg.data || '').split('\n').length;
|
|
2016
|
+
const oldLineCount = prevLineCount;
|
|
2017
|
+
if (newLineCount <= oldLineCount + 10) {
|
|
2018
|
+
// No meaningful new content — we've hit the buffer limit
|
|
2019
|
+
historyExhausted = true;
|
|
2020
|
+
}
|
|
2021
|
+
// Rewrite terminal with expanded history
|
|
2022
|
+
term.clear();
|
|
2023
|
+
term.write(msg.data);
|
|
2024
|
+
// Don't auto-scroll — user is reading history
|
|
2025
|
+
historyLoading = false;
|
|
2026
|
+
hideHistorySpinner();
|
|
2027
|
+
}
|
|
2028
|
+
break;
|
|
2029
|
+
|
|
1772
2030
|
case 'session_ended':
|
|
1773
2031
|
if (msg.session === activeSession) {
|
|
1774
2032
|
// Session ended — show in terminal
|
|
@@ -1892,7 +2150,10 @@
|
|
|
1892
2150
|
}
|
|
1893
2151
|
|
|
1894
2152
|
el.innerHTML = `
|
|
1895
|
-
<div class="session-name"
|
|
2153
|
+
<div class="session-name-row">
|
|
2154
|
+
<div class="session-name">${escapeHtml(session.name)}</div>
|
|
2155
|
+
<button class="session-close-btn" data-tmux="${escapeHtml(session.tmuxSession)}" data-name="${escapeHtml(session.name)}" onclick="event.stopPropagation();closeSession(this.dataset.tmux,this.dataset.name)" title="Close session">×</button>
|
|
2156
|
+
</div>
|
|
1896
2157
|
<div class="session-meta">
|
|
1897
2158
|
<span class="type-badge ${sType}">${sType}</span>
|
|
1898
2159
|
<span class="model-badge ${model}">${model}</span>
|
|
@@ -2050,6 +2311,96 @@
|
|
|
2050
2311
|
wsSend({ type: 'key', session: activeSession, key: text });
|
|
2051
2312
|
}
|
|
2052
2313
|
|
|
2314
|
+
// ── New Session Modal ────────────────────────────────────
|
|
2315
|
+
function openNewSessionModal() {
|
|
2316
|
+
document.getElementById('newSessionModal').style.display = 'flex';
|
|
2317
|
+
const nameInput = document.getElementById('newSessionName');
|
|
2318
|
+
nameInput.value = '';
|
|
2319
|
+
document.getElementById('newSessionHeadless').checked = false;
|
|
2320
|
+
setTimeout(() => nameInput.focus(), 50);
|
|
2321
|
+
}
|
|
2322
|
+
|
|
2323
|
+
function closeNewSessionModal(e) {
|
|
2324
|
+
if (e && e.target !== e.currentTarget) return;
|
|
2325
|
+
document.getElementById('newSessionModal').style.display = 'none';
|
|
2326
|
+
}
|
|
2327
|
+
|
|
2328
|
+
async function createNewSession() {
|
|
2329
|
+
const nameInput = document.getElementById('newSessionName');
|
|
2330
|
+
const headless = document.getElementById('newSessionHeadless').checked;
|
|
2331
|
+
const name = nameInput.value.trim();
|
|
2332
|
+
const btn = document.getElementById('createSessionBtn');
|
|
2333
|
+
|
|
2334
|
+
if (!name) {
|
|
2335
|
+
nameInput.focus();
|
|
2336
|
+
nameInput.style.borderColor = 'var(--red)';
|
|
2337
|
+
setTimeout(() => nameInput.style.borderColor = '', 2000);
|
|
2338
|
+
return;
|
|
2339
|
+
}
|
|
2340
|
+
|
|
2341
|
+
btn.disabled = true;
|
|
2342
|
+
btn.textContent = 'Creating...';
|
|
2343
|
+
|
|
2344
|
+
try {
|
|
2345
|
+
const resp = await fetch('/sessions/create', {
|
|
2346
|
+
method: 'POST',
|
|
2347
|
+
headers: {
|
|
2348
|
+
'Authorization': `Bearer ${token}`,
|
|
2349
|
+
'Content-Type': 'application/json',
|
|
2350
|
+
},
|
|
2351
|
+
body: JSON.stringify({ name, headless }),
|
|
2352
|
+
});
|
|
2353
|
+
const data = await resp.json();
|
|
2354
|
+
|
|
2355
|
+
if (!resp.ok) {
|
|
2356
|
+
showToast(data.error || 'Failed to create session', 'error');
|
|
2357
|
+
return;
|
|
2358
|
+
}
|
|
2359
|
+
|
|
2360
|
+
showToast(`Session "${name}" created${data.topicId ? ' with Telegram topic' : ' (headless)'}`, 'success');
|
|
2361
|
+
document.getElementById('newSessionModal').style.display = 'none';
|
|
2362
|
+
} catch (err) {
|
|
2363
|
+
showToast('Network error: ' + (err.message || 'Cannot reach server'), 'error');
|
|
2364
|
+
} finally {
|
|
2365
|
+
btn.disabled = false;
|
|
2366
|
+
btn.textContent = 'Create Session';
|
|
2367
|
+
}
|
|
2368
|
+
}
|
|
2369
|
+
|
|
2370
|
+
// ── Close Session ─────────────────────────────────────────
|
|
2371
|
+
async function closeSession(tmuxSession, sessionName) {
|
|
2372
|
+
if (!confirm(`Close session "${sessionName}"?`)) return;
|
|
2373
|
+
|
|
2374
|
+
try {
|
|
2375
|
+
const resp = await fetch(`/sessions/${encodeURIComponent(tmuxSession)}`, {
|
|
2376
|
+
method: 'DELETE',
|
|
2377
|
+
headers: { 'Authorization': `Bearer ${token}` },
|
|
2378
|
+
});
|
|
2379
|
+
const data = await resp.json();
|
|
2380
|
+
|
|
2381
|
+
if (!resp.ok) {
|
|
2382
|
+
showToast(data.error || 'Failed to close session', 'error');
|
|
2383
|
+
return;
|
|
2384
|
+
}
|
|
2385
|
+
|
|
2386
|
+
showToast(`Session "${sessionName}" closed`, 'success');
|
|
2387
|
+
|
|
2388
|
+
// If this was the active session, clear terminal
|
|
2389
|
+
if (activeSession === tmuxSession) {
|
|
2390
|
+
goBack();
|
|
2391
|
+
}
|
|
2392
|
+
} catch (err) {
|
|
2393
|
+
showToast('Network error: ' + (err.message || 'Cannot reach server'), 'error');
|
|
2394
|
+
}
|
|
2395
|
+
}
|
|
2396
|
+
|
|
2397
|
+
function closeActiveSession() {
|
|
2398
|
+
if (!activeSession) return;
|
|
2399
|
+
const session = sessions.find(s => s.tmuxSession === activeSession);
|
|
2400
|
+
const name = session ? session.name : activeSession;
|
|
2401
|
+
closeSession(activeSession, name);
|
|
2402
|
+
}
|
|
2403
|
+
|
|
2053
2404
|
// ── Helpers ───────────────────────────────────────────────
|
|
2054
2405
|
function formatElapsed(isoStr) {
|
|
2055
2406
|
const ms = Date.now() - new Date(isoStr).getTime();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/commands/server.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAwPH,UAAU,YAAY;IACpB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb;2DACuD;IACvD,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;
|
|
1
|
+
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/commands/server.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAwPH,UAAU,YAAY;IACpB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb;2DACuD;IACvD,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAgkCD,wBAAsB,WAAW,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAy5EtE;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
|
@@ -283,7 +283,7 @@ async function spawnSessionForTopic(sessionManager, telegram, sessionName, topic
|
|
|
283
283
|
let usedFallback = false;
|
|
284
284
|
if (topicMemory?.isReady()) {
|
|
285
285
|
try {
|
|
286
|
-
contextContent = topicMemory.formatContextForSession(topicId,
|
|
286
|
+
contextContent = topicMemory.formatContextForSession(topicId, 50);
|
|
287
287
|
}
|
|
288
288
|
catch (err) {
|
|
289
289
|
// @silent-fallback-ok — TopicMemory format, JSONL fallback
|
|
@@ -294,12 +294,16 @@ async function spawnSessionForTopic(sessionManager, telegram, sessionName, topic
|
|
|
294
294
|
if (!contextContent) {
|
|
295
295
|
usedFallback = true;
|
|
296
296
|
try {
|
|
297
|
-
const history = telegram.getTopicHistory(topicId,
|
|
297
|
+
const history = telegram.getTopicHistory(topicId, 50);
|
|
298
298
|
if (history.length > 0) {
|
|
299
299
|
const lines = [];
|
|
300
300
|
lines.push(`--- Thread History (last ${history.length} messages) ---`);
|
|
301
301
|
lines.push(`IMPORTANT: Read this history carefully before taking any action.`);
|
|
302
302
|
lines.push(`Your task is to continue THIS conversation, not start something new.`);
|
|
303
|
+
const topicName = telegram.getTopicName?.(topicId);
|
|
304
|
+
if (topicName) {
|
|
305
|
+
lines.push(`Topic: ${topicName}`);
|
|
306
|
+
}
|
|
303
307
|
lines.push(``);
|
|
304
308
|
for (const m of history) {
|
|
305
309
|
// Use actual sender name if available (multi-user topics), fall back to generic
|
|
@@ -307,7 +311,7 @@ async function spawnSessionForTopic(sessionManager, telegram, sessionName, topic
|
|
|
307
311
|
? (m.senderName || 'User')
|
|
308
312
|
: 'Agent';
|
|
309
313
|
const ts = m.timestamp ? new Date(m.timestamp).toISOString().slice(11, 19) : '??:??';
|
|
310
|
-
const text = (m.text || '').slice(0,
|
|
314
|
+
const text = (m.text || '').slice(0, 2000);
|
|
311
315
|
lines.push(`[${ts}] ${sender}: ${text}`);
|
|
312
316
|
}
|
|
313
317
|
lines.push(``);
|
|
@@ -434,13 +438,30 @@ async function spawnSessionForTopic(sessionManager, telegram, sessionName, topic
|
|
|
434
438
|
if (resumeSessionId) {
|
|
435
439
|
_topicResumeMap?.remove(topicId);
|
|
436
440
|
}
|
|
441
|
+
// Proactive UUID save — schedule an immediate discovery attempt after spawn.
|
|
442
|
+
// The JSONL file appears ~3-5 seconds after Claude Code starts.
|
|
443
|
+
// This is belt-and-suspenders alongside the 60s heartbeat and beforeSessionKill.
|
|
444
|
+
if (_topicResumeMap && !resumeSessionId) {
|
|
445
|
+
setTimeout(() => {
|
|
446
|
+
try {
|
|
447
|
+
const uuid = _topicResumeMap.findClaudeSessionUuid();
|
|
448
|
+
if (uuid) {
|
|
449
|
+
_topicResumeMap.save(topicId, uuid, newSessionName);
|
|
450
|
+
console.log(`[spawnSessionForTopic] Proactive UUID save: ${uuid} for topic ${topicId}`);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
catch (err) {
|
|
454
|
+
console.error(`[spawnSessionForTopic] Proactive UUID save failed:`, err);
|
|
455
|
+
}
|
|
456
|
+
}, 8000);
|
|
457
|
+
}
|
|
437
458
|
return newSessionName;
|
|
438
459
|
}
|
|
439
460
|
/**
|
|
440
461
|
* Respawn a session for a topic, including thread history in the bootstrap.
|
|
441
462
|
* This prevents "thread drift" where respawned sessions lose context.
|
|
442
463
|
*/
|
|
443
|
-
async function respawnSessionForTopic(sessionManager, telegram, targetSession, topicId, latestMessage, topicMemory, userProfile) {
|
|
464
|
+
async function respawnSessionForTopic(sessionManager, telegram, targetSession, topicId, latestMessage, topicMemory, userProfile, recoveryPrompt) {
|
|
444
465
|
console.log(`[telegram→session] Session "${targetSession}" needs respawn for topic ${topicId}`);
|
|
445
466
|
// Save the old session's Claude UUID before respawning so --resume can reattach context
|
|
446
467
|
if (_topicResumeMap) {
|
|
@@ -459,7 +480,12 @@ async function respawnSessionForTopic(sessionManager, telegram, targetSession, t
|
|
|
459
480
|
// Use topic name, not tmux session name — tmux names include the project prefix
|
|
460
481
|
// which causes cascading names like ai-guy-ai-guy-ai-guy-topic-1 on each respawn.
|
|
461
482
|
const topicName = storedName || `topic-${topicId}`;
|
|
462
|
-
|
|
483
|
+
// If this is a recovery respawn, prepend the recovery context to the message
|
|
484
|
+
// so the session knows what happened and can avoid repeating the failure.
|
|
485
|
+
const effectiveMessage = recoveryPrompt
|
|
486
|
+
? `${recoveryPrompt}\n\n${latestMessage || 'Session recovered — continue where you left off.'}`
|
|
487
|
+
: latestMessage;
|
|
488
|
+
const newSessionName = await spawnSessionForTopic(sessionManager, telegram, topicName, topicId, effectiveMessage, topicMemory, userProfile);
|
|
463
489
|
telegram.registerTopicSession(topicId, newSessionName, topicName);
|
|
464
490
|
await telegram.sendToTopic(topicId, `Session respawned.`);
|
|
465
491
|
console.log(`[telegram→session] Respawned "${newSessionName}" for topic ${topicId}`);
|
|
@@ -2013,6 +2039,26 @@ export async function startServer(options) {
|
|
|
2013
2039
|
resumeHeartbeatInterval.unref();
|
|
2014
2040
|
console.log(pc.green(' Resume heartbeat: active (60s interval)'));
|
|
2015
2041
|
}
|
|
2042
|
+
// Save Claude session UUID before any session kill so the topic can be
|
|
2043
|
+
// resumed later with --resume. This fires BEFORE the tmux session is
|
|
2044
|
+
// destroyed, so the UUID can still be discovered from the JSONL mtime.
|
|
2045
|
+
if (_topicResumeMap && telegram) {
|
|
2046
|
+
sessionManager.on('beforeSessionKill', (session) => {
|
|
2047
|
+
try {
|
|
2048
|
+
const topicId = telegram.getTopicForSession(session.tmuxSession);
|
|
2049
|
+
if (!topicId)
|
|
2050
|
+
return;
|
|
2051
|
+
const uuid = _topicResumeMap.findUuidForSession(session.tmuxSession);
|
|
2052
|
+
if (uuid) {
|
|
2053
|
+
_topicResumeMap.save(topicId, uuid, session.tmuxSession);
|
|
2054
|
+
console.log(`[beforeSessionKill] Saved resume UUID ${uuid} for topic ${topicId} (session: ${session.name})`);
|
|
2055
|
+
}
|
|
2056
|
+
}
|
|
2057
|
+
catch (err) {
|
|
2058
|
+
console.error(`[beforeSessionKill] Failed to save resume UUID:`, err);
|
|
2059
|
+
}
|
|
2060
|
+
});
|
|
2061
|
+
}
|
|
2016
2062
|
if (scheduler) {
|
|
2017
2063
|
sessionManager.on('sessionComplete', (session) => {
|
|
2018
2064
|
scheduler.processQueue();
|
|
@@ -2450,6 +2496,25 @@ export async function startServer(options) {
|
|
|
2450
2496
|
const { SubagentTracker } = await import('../monitoring/SubagentTracker.js');
|
|
2451
2497
|
const subagentTracker = new SubagentTracker({ stateDir: config.stateDir });
|
|
2452
2498
|
console.log(pc.green(' Subagent tracker enabled'));
|
|
2499
|
+
// Wire subagent awareness into zombie cleanup — prevents killing sessions
|
|
2500
|
+
// that are idle at the prompt but waiting for subagent results.
|
|
2501
|
+
const MAX_SUBAGENT_WAIT_MS = 60 * 60_000; // 60 minutes — stale subagent safety cap
|
|
2502
|
+
sessionManager.setSubagentChecker((session) => {
|
|
2503
|
+
if (!session.claudeSessionId)
|
|
2504
|
+
return false;
|
|
2505
|
+
const active = subagentTracker.getActiveSubagents(session.claudeSessionId);
|
|
2506
|
+
if (active.length === 0)
|
|
2507
|
+
return false;
|
|
2508
|
+
// Safety cap: if ALL active subagents have been running > 60 minutes,
|
|
2509
|
+
// treat them as stale (likely missed a SubagentStop event) and allow the kill.
|
|
2510
|
+
const now = Date.now();
|
|
2511
|
+
const allStale = active.every(a => now - new Date(a.startedAt).getTime() > MAX_SUBAGENT_WAIT_MS);
|
|
2512
|
+
if (allStale) {
|
|
2513
|
+
console.warn(`[SessionManager] Session "${session.name}" has ${active.length} stale subagent(s) (>60m). Allowing zombie kill.`);
|
|
2514
|
+
return false;
|
|
2515
|
+
}
|
|
2516
|
+
return true;
|
|
2517
|
+
});
|
|
2453
2518
|
// Worktree Monitor — detects orphaned worktrees after sessions complete
|
|
2454
2519
|
const { WorktreeMonitor } = await import('../monitoring/WorktreeMonitor.js');
|
|
2455
2520
|
const worktreeMonitor = new WorktreeMonitor({
|