tycono 0.1.107 → 0.1.109

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.1.107",
3
+ "version": "0.1.109",
4
4
  "description": "Build an AI company. Watch them work.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -129,8 +129,35 @@ class SupervisorHeartbeat {
129
129
  * Dispatch Protocol Principle 2: tick이 유일한 동기화 지점.
130
130
  */
131
131
  addDirective(waveId: string, text: string): PendingDirective | null {
132
- const state = this.supervisors.get(waveId);
133
- if (!state) return null;
132
+ let state = this.supervisors.get(waveId);
133
+
134
+ // If wave not in memory (e.g., server restarted), restore from disk
135
+ if (!state) {
136
+ // Check if this wave existed before (has sessions in session-store)
137
+ const waveSessions = listSessions().filter(s => s.waveId === waveId);
138
+ if (waveSessions.length > 0) {
139
+ // Restore supervisor state for this wave
140
+ const ceoSession = waveSessions.find(s => s.roleId === 'ceo');
141
+ state = {
142
+ waveId,
143
+ directive: text,
144
+ continuous: false,
145
+ supervisorSessionId: ceoSession?.id ?? null,
146
+ executionId: null,
147
+ status: 'stopped',
148
+ crashCount: 0,
149
+ maxCrashRetries: 10,
150
+ restartTimer: null,
151
+ pendingDirectives: [],
152
+ pendingQuestions: [],
153
+ createdAt: ceoSession?.createdAt ?? new Date().toISOString(),
154
+ };
155
+ this.supervisors.set(waveId, state);
156
+ console.log(`[Supervisor] Restored wave ${waveId} from disk (${waveSessions.length} sessions)`);
157
+ } else {
158
+ return null;
159
+ }
160
+ }
134
161
 
135
162
  const directive: PendingDirective = {
136
163
  id: `dir-${Date.now()}`,
@@ -283,18 +310,72 @@ class SupervisorHeartbeat {
283
310
  * Spawn a lightweight conversation session (no dispatch tools).
284
311
  * CEO reads files and answers directly.
285
312
  */
313
+ /**
314
+ * Load conversation history from activity-stream files for a wave.
315
+ * Used when supervisor restarts (e.g., TUI restarted) to restore context.
316
+ */
317
+ private loadWaveHistory(waveId: string): string {
318
+ try {
319
+ // Find CEO sessions for this wave from session-store
320
+ const allSessions = listSessions();
321
+ const waveCeoSessions = allSessions
322
+ .filter(s => s.waveId === waveId && s.roleId === 'ceo')
323
+ .sort((a, b) => a.createdAt.localeCompare(b.createdAt));
324
+
325
+ if (waveCeoSessions.length === 0) return '';
326
+
327
+ const history: string[] = [];
328
+ for (const ses of waveCeoSessions) {
329
+ if (!ActivityStream.exists(ses.id)) continue;
330
+ const events = ActivityStream.readAll(ses.id);
331
+
332
+ // Extract CEO directives (from messages) and supervisor text responses
333
+ for (const e of events) {
334
+ if (e.type === 'msg:start' && e.data.task) {
335
+ // Extract directive from task (remove system prompt noise)
336
+ const task = String(e.data.task);
337
+ const directiveMatch = task.match(/\[CEO (?:Supervisor|Question)\]\s*(.*?)(?:\n|$)/);
338
+ if (directiveMatch) {
339
+ history.push(`CEO: "${directiveMatch[1].slice(0, 100)}"`);
340
+ }
341
+ }
342
+ if (e.type === 'text' && e.roleId === 'ceo') {
343
+ const text = String(e.data.text ?? '').trim();
344
+ if (text && text.length > 10 && !text.startsWith('#') && !text.startsWith('⛔')) {
345
+ history.push(`Supervisor: ${text.slice(0, 150)}`);
346
+ }
347
+ }
348
+ }
349
+ }
350
+
351
+ if (history.length === 0) return '';
352
+
353
+ // Keep last 10 exchanges to fit in context
354
+ const recent = history.slice(-10);
355
+ return `\n[Previous Conversation in Wave ${waveId}]\n${recent.join('\n')}\n`;
356
+ } catch {
357
+ return '';
358
+ }
359
+ }
360
+
286
361
  private spawnConversation(state: SupervisorState, directive: string): void {
287
- // Build conversation context: previous directives + last execution summary
362
+ // Build conversation context: in-memory directives + disk history
288
363
  const deliveredDirectives = state.pendingDirectives.filter(d => d.delivered);
289
364
  const directiveHistory = deliveredDirectives.length > 0
290
365
  ? deliveredDirectives.map(d => `- CEO: "${d.text}"`).join('\n')
291
366
  : '';
292
367
 
368
+ // If no in-memory history, load from disk (restart case)
369
+ const diskHistory = !directiveHistory ? this.loadWaveHistory(state.waveId) : '';
370
+
293
371
  // Extract last execution's output from activity stream (what "just happened")
294
372
  let lastExecutionSummary = '';
295
- if (state.supervisorSessionId) {
373
+ // Try current supervisorSessionId first, then search by waveId
374
+ const sessionIdToCheck = state.supervisorSessionId
375
+ || listSessions().find(s => s.waveId === state.waveId && s.roleId === 'ceo')?.id;
376
+ if (sessionIdToCheck) {
296
377
  try {
297
- const events = ActivityStream.readAll(state.supervisorSessionId);
378
+ const events = ActivityStream.readAll(sessionIdToCheck);
298
379
  // Get last text outputs (the supervisor's final response)
299
380
  const textEvents = events.filter(e => e.type === 'text' && e.roleId === 'ceo');
300
381
  const toolEvents = events.filter(e => e.type === 'tool:start' && e.roleId === 'ceo');
@@ -315,7 +396,7 @@ class SupervisorHeartbeat {
315
396
  } catch { /* ignore */ }
316
397
  }
317
398
 
318
- const context = [directiveHistory, lastExecutionSummary].filter(Boolean).join('\n');
399
+ const context = [directiveHistory || diskHistory, lastExecutionSummary].filter(Boolean).join('\n');
319
400
 
320
401
  const task = `${context ? context + '\n' : ''}[CEO Question] ${directive}
321
402
 
package/src/tui/app.tsx CHANGED
@@ -286,28 +286,16 @@ export const App: React.FC = () => {
286
286
  setFocusedWaveId(apiWaves[apiWaves.length - 1].waveId);
287
287
  autoWaveCreated.current = true;
288
288
  } else if (api.loaded && api.pastWaves.length > 0) {
289
- // No active waves, but past waves exist — load them + create a new one
289
+ // No active waves, past waves exist — resume last wave (don't create new)
290
290
  autoWaveCreated.current = true;
291
291
  const pastEntries: WaveInfo[] = api.pastWaves.slice(0, 10).map(pw => ({
292
292
  waveId: pw.id,
293
293
  directive: pw.directive || '',
294
294
  startedAt: pw.startedAt ? new Date(pw.startedAt).getTime() : 0,
295
295
  }));
296
-
297
- dispatchWave().then(result => {
298
- const newWave: WaveInfo = {
299
- waveId: result.waveId,
300
- directive: '',
301
- startedAt: Date.now(),
302
- };
303
- setWaves([...pastEntries, newWave]);
304
- setFocusedWaveId(result.waveId);
305
- }).catch(() => {
306
- // Even if new wave fails, show past waves
307
- setWaves(pastEntries);
308
- setFocusedWaveId(pastEntries[pastEntries.length - 1]?.waveId ?? null);
309
- autoWaveCreated.current = true;
310
- });
296
+ setWaves(pastEntries);
297
+ // Focus last wave — user can /new if they want a fresh one
298
+ setFocusedWaveId(pastEntries[pastEntries.length - 1]?.waveId ?? null);
311
299
  } else if (api.loaded) {
312
300
  // No active waves, no past waves — fresh start
313
301
  autoWaveCreated.current = true;