tycono 0.1.96-beta.40 → 0.1.96-beta.42
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/package.json
CHANGED
|
@@ -51,25 +51,21 @@ export function handleExecRequest(req: IncomingMessage, res: ServerResponse): vo
|
|
|
51
51
|
// ── /api/waves/active — restore active waves after refresh ──
|
|
52
52
|
if (method === 'GET' && url === '/api/waves/active') {
|
|
53
53
|
// Recovery: rebuild wave→session mapping from session-store if lost
|
|
54
|
+
// Only recover ACTIVE sessions to prevent OOM on large datasets (140+ sessions)
|
|
54
55
|
const waves = waveMultiplexer.getActiveWaves();
|
|
55
56
|
if (waves.length === 0) {
|
|
56
57
|
const allSessions = listSessions();
|
|
57
|
-
|
|
58
|
+
let recovered = 0;
|
|
58
59
|
for (const ses of allSessions) {
|
|
59
|
-
if (ses.waveId)
|
|
60
|
-
|
|
61
|
-
|
|
60
|
+
if (!ses.waveId || ses.status !== 'active') continue;
|
|
61
|
+
const exec = executionManager.getActiveExecution(ses.id);
|
|
62
|
+
if (exec) {
|
|
63
|
+
waveMultiplexer.registerSession(ses.waveId, exec);
|
|
64
|
+
recovered++;
|
|
62
65
|
}
|
|
63
66
|
}
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
for (const sid of sids) {
|
|
67
|
-
// getActiveExecution falls back to stream recovery for completed sessions
|
|
68
|
-
const exec = executionManager.getActiveExecution(sid);
|
|
69
|
-
if (exec) {
|
|
70
|
-
waveMultiplexer.registerSession(wid, exec);
|
|
71
|
-
}
|
|
72
|
-
}
|
|
67
|
+
if (recovered > 0) {
|
|
68
|
+
console.log(`[WaveRecovery] Recovered ${recovered} active sessions`);
|
|
73
69
|
}
|
|
74
70
|
}
|
|
75
71
|
jsonResponse(res, 200, { waves: waveMultiplexer.getActiveWaves() });
|
|
@@ -480,13 +476,11 @@ function handleWaveStream(waveId: string, url: string, res: ServerResponse, req:
|
|
|
480
476
|
|
|
481
477
|
let sessionIds = waveMultiplexer.getWaveSessionIds(waveId);
|
|
482
478
|
|
|
483
|
-
// Recovery:
|
|
484
|
-
// rebuild from session-store (sessions have waveId) + executionManager
|
|
479
|
+
// Recovery: only recover ACTIVE sessions for this wave (not all 140)
|
|
485
480
|
if (sessionIds.length === 0) {
|
|
486
481
|
const allSessions = listSessions();
|
|
487
|
-
const waveSessions = allSessions.filter(s => s.waveId === waveId);
|
|
482
|
+
const waveSessions = allSessions.filter(s => s.waveId === waveId && s.status === 'active');
|
|
488
483
|
for (const ses of waveSessions) {
|
|
489
|
-
// getActiveExecution recovers from stream files for completed sessions too
|
|
490
484
|
const exec = executionManager.getActiveExecution(ses.id);
|
|
491
485
|
if (exec) {
|
|
492
486
|
waveMultiplexer.registerSession(waveId, exec);
|
|
@@ -763,25 +757,26 @@ function handleStatus(res: ServerResponse): void {
|
|
|
763
757
|
);
|
|
764
758
|
|
|
765
759
|
const recovered: typeof activeExecs = [];
|
|
766
|
-
|
|
767
|
-
|
|
760
|
+
// Limit recovery scan to prevent OOM on large session stores
|
|
761
|
+
const MAX_RECOVERY_SCAN = 20;
|
|
762
|
+
const recentActive = activeSessions.slice(-MAX_RECOVERY_SCAN);
|
|
763
|
+
|
|
764
|
+
for (const ses of recentActive) {
|
|
768
765
|
if (!ActivityStream.exists(ses.id)) continue;
|
|
766
|
+
// Only read last few events to check done/error (not entire stream)
|
|
769
767
|
const events = ActivityStream.readFrom(ses.id, 0);
|
|
770
768
|
if (events.length === 0) continue;
|
|
771
769
|
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
const errorEvent = events.find(e => e.type === 'msg:error');
|
|
777
|
-
|
|
778
|
-
// Only include sessions that haven't finished yet
|
|
779
|
-
if (doneEvent || errorEvent) continue;
|
|
770
|
+
// Check last 5 events for done/error (optimization: don't scan entire file)
|
|
771
|
+
const tail = events.slice(-5);
|
|
772
|
+
const isDone = tail.some(e => e.type === 'msg:done' || e.type === 'msg:error');
|
|
773
|
+
if (isDone) continue;
|
|
780
774
|
|
|
781
|
-
const
|
|
775
|
+
const startEvent = events.find(e => e.type === 'msg:start');
|
|
776
|
+
const task = (startEvent?.data?.task as string) ?? ses.title ?? '';
|
|
782
777
|
recovered.push({
|
|
783
778
|
id: `recovered-${ses.id}`,
|
|
784
|
-
type: (startEvent
|
|
779
|
+
type: (startEvent?.data?.type as string ?? 'assign') as 'assign' | 'wave' | 'consult',
|
|
785
780
|
roleId: ses.roleId,
|
|
786
781
|
task,
|
|
787
782
|
status: 'running',
|
package/src/api/src/server.ts
CHANGED
|
@@ -1,11 +1,40 @@
|
|
|
1
1
|
import { COMPANY_ROOT } from './services/file-reader.js';
|
|
2
2
|
import { applyConfig } from './services/company-config.js';
|
|
3
3
|
import { createHttpServer } from './create-server.js';
|
|
4
|
+
import { listSessions, updateSession } from './services/session-store.js';
|
|
5
|
+
import { ActivityStream } from './services/activity-stream.js';
|
|
4
6
|
|
|
5
7
|
// Load .tycono/config.json and apply to process.env
|
|
6
8
|
const config = applyConfig(COMPANY_ROOT);
|
|
7
9
|
console.log(`[STARTUP] Engine: ${config.engine}, API key: ${config.apiKey ? 'set' : 'none'}`);
|
|
8
10
|
|
|
11
|
+
// Startup: mark orphaned 'active' sessions as 'interrupted'
|
|
12
|
+
// These are sessions from a previous server that crashed or was killed
|
|
13
|
+
{
|
|
14
|
+
const allSessions = listSessions();
|
|
15
|
+
let orphaned = 0;
|
|
16
|
+
for (const ses of allSessions) {
|
|
17
|
+
if (ses.status !== 'active') continue;
|
|
18
|
+
// Check activity stream — if it has msg:done/msg:error, mark done
|
|
19
|
+
// If not, mark interrupted (previous server died mid-execution)
|
|
20
|
+
if (ActivityStream.exists(ses.id)) {
|
|
21
|
+
const events = ActivityStream.readFrom(ses.id, 0);
|
|
22
|
+
const tail = events.slice(-5);
|
|
23
|
+
const isDone = tail.some(e => e.type === 'msg:done' || e.type === 'msg:error');
|
|
24
|
+
if (isDone) {
|
|
25
|
+
updateSession(ses.id, { status: 'done' });
|
|
26
|
+
orphaned++;
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
updateSession(ses.id, { status: 'interrupted' as any });
|
|
31
|
+
orphaned++;
|
|
32
|
+
}
|
|
33
|
+
if (orphaned > 0) {
|
|
34
|
+
console.log(`[STARTUP] Cleaned ${orphaned} orphaned sessions (active → done/interrupted)`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
9
38
|
const PORT = Number(process.env.PORT) || 3001;
|
|
10
39
|
const server = createHttpServer();
|
|
11
40
|
|
|
@@ -13,3 +42,18 @@ server.listen(PORT, () => {
|
|
|
13
42
|
console.log(`[API] Server running on http://localhost:${PORT}`);
|
|
14
43
|
console.log(`[API] COMPANY_ROOT: ${COMPANY_ROOT}`);
|
|
15
44
|
});
|
|
45
|
+
|
|
46
|
+
// Graceful shutdown: mark running sessions as interrupted
|
|
47
|
+
function gracefulShutdown(signal: string) {
|
|
48
|
+
console.log(`[SHUTDOWN] ${signal} received, marking active sessions as interrupted...`);
|
|
49
|
+
const sessions = listSessions();
|
|
50
|
+
for (const ses of sessions) {
|
|
51
|
+
if (ses.status === 'active') {
|
|
52
|
+
updateSession(ses.id, { status: 'interrupted' as any });
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
process.exit(0);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
process.on('SIGINT', () => gracefulShutdown('SIGINT'));
|
|
59
|
+
process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));
|
|
@@ -10,7 +10,7 @@ import { estimateCost } from './pricing.js';
|
|
|
10
10
|
import { readConfig, getConversationLimits, resolveCodeRoot } from './company-config.js';
|
|
11
11
|
import { postKnowledgingCheck, type KnowledgeDebtItem } from '../engine/knowledge-gate.js';
|
|
12
12
|
import { earnCoinsInternal } from '../routes/coins.js';
|
|
13
|
-
import { getSession, createSession, addMessage, updateMessage as updateSessionMessage, appendMessageEvent, type Message, type ImageAttachment } from './session-store.js';
|
|
13
|
+
import { getSession, createSession, addMessage, updateMessage as updateSessionMessage, updateSession, appendMessageEvent, type Message, type ImageAttachment } from './session-store.js';
|
|
14
14
|
import { portRegistry, type PortAllocation } from './port-registry.js';
|
|
15
15
|
import { type MessageStatus, isMessageActive, canTransition, messageStatusToRoleStatus } from '../../../shared/types.js';
|
|
16
16
|
|
|
@@ -603,6 +603,12 @@ class ExecutionManager {
|
|
|
603
603
|
knowledgeDebt: execution.knowledgeDebt.map(d => ({ type: d.type, file: d.file, message: d.message })),
|
|
604
604
|
}),
|
|
605
605
|
});
|
|
606
|
+
|
|
607
|
+
// Mark session as done in session-store (persisted to file)
|
|
608
|
+
// Skip CEO supervisor sessions — they stay active for wave lifecycle
|
|
609
|
+
if (session.roleId !== 'ceo' || session.source !== 'wave') {
|
|
610
|
+
updateSession(execution.sessionId, { status: 'done' });
|
|
611
|
+
}
|
|
606
612
|
}
|
|
607
613
|
|
|
608
614
|
private cleanupOrphanedChildren(parentSessionId: string): void {
|