tycono 0.3.11-beta.0 → 0.3.11

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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tycono",
3
- "version": "0.3.11-beta.0",
3
+ "version": "0.3.11",
4
4
  "description": "Build an AI company. Watch them work.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -214,7 +214,7 @@ class ExecutionManager {
214
214
  let harnessTurnCount = 0;
215
215
  let softLimitWarned = false;
216
216
  let hardLimitReached = false;
217
- let accumulatedOutput = '';
217
+ const outputChunks: string[] = [];
218
218
 
219
219
  const teamStatus: import('../../../shared/types').TeamStatus = {};
220
220
  for (const [, e] of this.executions) {
@@ -258,7 +258,7 @@ class ExecutionManager {
258
258
  },
259
259
  {
260
260
  onText: (text) => {
261
- accumulatedOutput += text;
261
+ outputChunks.push(text);
262
262
  execution.stream.emit('text', params.roleId, { text });
263
263
  if (execution.sessionId) {
264
264
  this.updateSessionRoleMessage(execution, text);
@@ -501,7 +501,7 @@ class ExecutionManager {
501
501
  .catch((err: Error) => {
502
502
  if (hardLimitReached) {
503
503
  execution.result = {
504
- output: accumulatedOutput,
504
+ output: outputChunks.join(''),
505
505
  turns: harnessTurnCount,
506
506
  totalTokens: { input: 0, output: 0 },
507
507
  toolCalls: [],
@@ -109,16 +109,44 @@ function writeImmediate(session: Session): void {
109
109
  }
110
110
 
111
111
  /* ─── In-memory cache ───────────────────── */
112
+ /*
113
+ * OOM prevention: inactive sessions cache metadata only (no messages).
114
+ * Messages are loaded on-demand from disk when getSession() is called.
115
+ * Active sessions keep messages in memory for streaming writes.
116
+ *
117
+ * Before: 61 sessions × 50KB avg messages = 3MB+ baseline, grows to 100s of MB
118
+ * After: 61 sessions × 1KB metadata + 5 active × 50KB = ~310KB baseline
119
+ */
112
120
 
113
121
  const cache = new Map<string, Session>();
114
122
 
123
+ /** Strip messages from session to save memory (keeps metadata) */
124
+ function stripMessages(session: Session): Session {
125
+ if (session.messages.length === 0) return session;
126
+ return { ...session, messages: [] };
127
+ }
128
+
129
+ /** Load full session from disk (with messages) */
130
+ function loadFromDisk(id: string): Session | undefined {
131
+ const p = sessionPath(id);
132
+ if (!fs.existsSync(p)) return undefined;
133
+ try {
134
+ return JSON.parse(fs.readFileSync(p, 'utf-8')) as Session;
135
+ } catch { return undefined; }
136
+ }
137
+
115
138
  function loadAll(): void {
116
139
  ensureDir();
117
140
  const files = fs.readdirSync(sessionsDir()).filter((f) => f.endsWith('.json'));
118
141
  for (const file of files) {
119
142
  try {
120
143
  const data = JSON.parse(fs.readFileSync(path.join(sessionsDir(), file), 'utf-8')) as Session;
121
- cache.set(data.id, data);
144
+ // Only keep messages for active sessions; strip for done/interrupted
145
+ if (data.status === 'active' || data.status === 'awaiting_input') {
146
+ cache.set(data.id, data);
147
+ } else {
148
+ cache.set(data.id, stripMessages(data));
149
+ }
122
150
  } catch { /* skip corrupted */ }
123
151
  }
124
152
  }
@@ -165,7 +193,15 @@ export function createSession(roleId: string, opts: CreateSessionOptions = {}):
165
193
 
166
194
  export function getSession(id: string): Session | undefined {
167
195
  ensureLoaded();
168
- return cache.get(id);
196
+ const cached = cache.get(id);
197
+ if (!cached) return undefined;
198
+
199
+ // If messages were stripped (inactive session), reload from disk on demand
200
+ if (cached.messages.length === 0 && cached.status !== 'active') {
201
+ const full = loadFromDisk(id);
202
+ if (full) return full; // Return disk version (don't cache — it's a one-time read)
203
+ }
204
+ return cached;
169
205
  }
170
206
 
171
207
  export function listSessions(): Omit<Session, 'messages'>[] {
@@ -252,6 +288,11 @@ export function updateSession(id: string, updates: Partial<Pick<Session, 'title'
252
288
  session.updatedAt = new Date().toISOString();
253
289
  writeImmediate(session);
254
290
 
291
+ // OOM prevention: when session becomes inactive, strip messages from cache
292
+ if (updates.status && updates.status !== 'active' && updates.status !== 'awaiting_input') {
293
+ cache.set(id, stripMessages(session));
294
+ }
295
+
255
296
  return session;
256
297
  }
257
298
 
@@ -19,6 +19,7 @@ import path from 'node:path';
19
19
  import { COMPANY_ROOT } from './file-reader.js';
20
20
  import { ActivityStream } from './activity-stream.js';
21
21
  import { saveCompletedWave } from './wave-tracker.js';
22
+ import { waveMultiplexer } from './wave-multiplexer.js';
22
23
 
23
24
  /* ─── Types ──────────────────────────────────── */
24
25
 
@@ -768,9 +769,17 @@ ${state.continuous ? `## Continuous Improvement Mode (ON)
768
769
  console.error(`[Supervisor] Failed to auto-save wave ${state.waveId}:`, err);
769
770
  }
770
771
 
771
- // OOM prevention: clear accumulated directive/question history
772
+ // OOM prevention: clear accumulated state + wave multiplexer sessions
772
773
  state.pendingDirectives = [];
773
774
  state.pendingQuestions = [];
775
+
776
+ // Delayed cleanup: remove wave sessions from multiplexer + supervisor map
777
+ // (delay allows SSE clients to receive final events)
778
+ setTimeout(() => {
779
+ waveMultiplexer.cleanupWave(state.waveId);
780
+ this.supervisors.delete(state.waveId);
781
+ console.log(`[Supervisor] Cleaned up wave ${state.waveId} from memory`);
782
+ }, 60_000).unref(); // 1 minute after wave done
774
783
  }
775
784
  }
776
785
 
@@ -182,6 +182,12 @@ class WaveMultiplexer {
182
182
  const key = `${event.roleId}:${event.seq}`;
183
183
  if (client.sentEvents.has(key)) return;
184
184
  client.sentEvents.add(key);
185
+ // OOM prevention: cap sentEvents to prevent unbounded Set growth
186
+ if (client.sentEvents.size > 5000) {
187
+ const entries = Array.from(client.sentEvents);
188
+ client.sentEvents.clear();
189
+ for (const e of entries.slice(-2000)) client.sentEvents.add(e);
190
+ }
185
191
 
186
192
  const waveSeq = client.waveSeq++;
187
193
  sendSSE(client, 'wave:event', {