tycono 0.1.94-beta.1 → 0.1.94-beta.2

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.94-beta.1",
3
+ "version": "0.1.94-beta.2",
4
4
  "description": "Build an AI company. Watch them work.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -21,7 +21,6 @@ import { earnCoinsInternal } from './coins.js';
21
21
  import { appendFollowUpToWave } from '../services/wave-tracker.js';
22
22
  import { waveMultiplexer } from '../services/wave-multiplexer.js';
23
23
  import { supervisorHeartbeat } from '../services/supervisor-heartbeat.js';
24
- import { readConfig } from '../services/company-config.js';
25
24
 
26
25
  /* ─── Auto-attach child executions to wave multiplexer ── */
27
26
  executionManager.onExecutionCreated((exec) => {
@@ -220,12 +219,8 @@ function handleStartJob(body: Record<string, unknown>, res: ServerResponse): voi
220
219
 
221
220
  const targetRoles = body.targetRoles as string[] | undefined;
222
221
 
223
- // Check supervision mode from config (same as legacy /api/exec/wave)
224
- const config = readConfig(COMPANY_ROOT);
225
- const supervisionMode = config.supervision?.mode ?? 'direct';
226
-
227
- if (supervisionMode === 'supervisor') {
228
- // Supervisor mode: start a single CEO Supervisor session that dispatches C-Levels
222
+ // Always use supervisor mode CEO supervises C-Levels who supervise members
223
+ {
229
224
  const state = supervisorHeartbeat.start(
230
225
  `wave-${Date.now()}`,
231
226
  directive,
@@ -245,71 +240,6 @@ function handleStartJob(body: Record<string, unknown>, res: ServerResponse): voi
245
240
  });
246
241
  return;
247
242
  }
248
-
249
- // Direct mode (default): dispatch all C-Levels simultaneously
250
- const orgTree = buildOrgTree(COMPANY_ROOT);
251
- let cLevelRoles = getSubordinates(orgTree, 'ceo');
252
-
253
- if (targetRoles && Array.isArray(targetRoles) && targetRoles.length > 0) {
254
- const allowed = new Set(targetRoles);
255
- cLevelRoles = cLevelRoles.filter(r => allowed.has(r));
256
- }
257
-
258
- if (cLevelRoles.length === 0) {
259
- jsonResponse(res, 400, { error: 'No C-level roles found to dispatch wave.' });
260
- return;
261
- }
262
-
263
- const fullTargetScope = targetRoles && targetRoles.length > 0 ? targetRoles : undefined;
264
-
265
- const newWaveId = `wave-${Date.now()}`;
266
- const sessionIds: string[] = [];
267
-
268
- for (const cRole of cLevelRoles) {
269
- const session = createSession(cRole, {
270
- mode: 'do',
271
- source: 'wave',
272
- waveId: newWaveId,
273
- });
274
- sessionIds.push(session.id);
275
-
276
- const ceoMsg: Message = {
277
- id: `msg-${Date.now()}-ceo-${cRole}`,
278
- from: 'ceo',
279
- content: directive,
280
- type: 'directive',
281
- status: 'done',
282
- timestamp: new Date().toISOString(),
283
- attachments,
284
- };
285
- addMessage(session.id, ceoMsg);
286
-
287
- const exec = executionManager.startExecution({
288
- type: 'wave',
289
- roleId: cRole,
290
- task: `[CEO Wave] ${directive}`,
291
- sourceRole: 'ceo',
292
- parentSessionId,
293
- targetRoles: fullTargetScope,
294
- sessionId: session.id,
295
- attachments,
296
- });
297
-
298
- waveMultiplexer.registerSession(newWaveId, exec);
299
-
300
- const roleMsg: Message = {
301
- id: `msg-${Date.now() + 1}-role-${cRole}`,
302
- from: 'role',
303
- content: '',
304
- type: 'conversation',
305
- status: 'streaming',
306
- timestamp: new Date().toISOString(),
307
- };
308
- addMessage(session.id, roleMsg, true);
309
- }
310
-
311
- jsonResponse(res, 200, { sessionIds, waveId: newWaveId });
312
- return;
313
243
  }
314
244
 
315
245
  // Assign
@@ -684,15 +614,8 @@ function handleWave(body: Record<string, unknown>, req: IncomingMessage, res: Se
684
614
 
685
615
  const targetRoles = body.targetRoles as string[] | undefined;
686
616
 
687
- // Check supervision mode from config
688
- const config = readConfig(COMPANY_ROOT);
689
- const supervisionMode = config.supervision?.mode ?? 'direct';
690
-
691
- if (supervisionMode === 'supervisor') {
692
- handleWaveSupervisor(directive, targetRoles, req, res);
693
- } else {
694
- handleWaveDirect(directive, targetRoles, req, res);
695
- }
617
+ // Always supervisor mode CEO supervises C-Levels
618
+ handleWaveSupervisor(directive, targetRoles, req, res);
696
619
  }
697
620
 
698
621
  /**
@@ -721,105 +644,6 @@ function handleWaveSupervisor(directive: string, targetRoles: string[] | undefin
721
644
  });
722
645
  }
723
646
 
724
- /**
725
- * Direct mode (legacy): Dispatch all C-Levels simultaneously.
726
- * No CEO Supervisor — each C-Level runs independently.
727
- */
728
- function handleWaveDirect(directive: string, targetRoles: string[] | undefined, req: IncomingMessage, res: ServerResponse): void {
729
- const orgTree = buildOrgTree(COMPANY_ROOT);
730
- let cLevelRoles = getSubordinates(orgTree, 'ceo');
731
-
732
- if (targetRoles && Array.isArray(targetRoles) && targetRoles.length > 0) {
733
- const allowed = new Set(targetRoles);
734
- cLevelRoles = cLevelRoles.filter(r => allowed.has(r));
735
- }
736
-
737
- if (cLevelRoles.length === 0) {
738
- jsonResponse(res, 400, { error: 'No C-level roles found to dispatch wave.' });
739
- return;
740
- }
741
-
742
- const fullTargetScope = targetRoles && targetRoles.length > 0 ? targetRoles : undefined;
743
-
744
- const executions: Execution[] = [];
745
- for (const cRole of cLevelRoles) {
746
- const session = createSession(cRole, { mode: 'do' });
747
- const exec = executionManager.startExecution({
748
- type: 'wave',
749
- roleId: cRole,
750
- task: `[CEO Wave] ${directive}`,
751
- sourceRole: 'ceo',
752
- targetRoles: fullTargetScope,
753
- sessionId: session.id,
754
- });
755
- executions.push(exec);
756
- }
757
-
758
- startSSE(res);
759
- sendSSE(res, 'start', {
760
- ids: executions.map((e) => e.id),
761
- directive,
762
- targetRoles: cLevelRoles,
763
- });
764
-
765
- let doneCount = 0;
766
- const subscribers: Array<{ exec: Execution; sub: ActivitySubscriber }> = [];
767
-
768
- for (const exec of executions) {
769
- const subscriber: ActivitySubscriber = (event: ActivityEvent) => {
770
- const rolePrefix = exec.roleId;
771
- switch (event.type) {
772
- case 'text':
773
- sendSSE(res, 'output', { roleId: rolePrefix, text: event.data.text });
774
- break;
775
- case 'thinking':
776
- sendSSE(res, 'thinking', { roleId: rolePrefix, text: event.data.text });
777
- break;
778
- case 'tool:start':
779
- sendSSE(res, 'tool', { roleId: rolePrefix, name: event.data.name, input: event.data.input });
780
- break;
781
- case 'dispatch:start':
782
- sendSSE(res, 'dispatch', { roleId: rolePrefix, targetRoleId: event.data.targetRoleId, task: event.data.task, childSessionId: event.data.childSessionId });
783
- break;
784
- case 'msg:turn-complete':
785
- sendSSE(res, 'turn', { roleId: rolePrefix, turn: event.data.turn });
786
- break;
787
- case 'stderr':
788
- sendSSE(res, 'stderr', { roleId: rolePrefix, message: event.data.message });
789
- break;
790
- case 'msg:awaiting_input':
791
- sendSSE(res, 'role:awaiting_input', { roleId: rolePrefix, question: event.data.question, targetRole: event.data.targetRole, reason: event.data.reason });
792
- break;
793
- case 'msg:done':
794
- sendSSE(res, 'role:done', { roleId: rolePrefix, ...event.data });
795
- doneCount++;
796
- if (doneCount >= executions.length) {
797
- sendSSE(res, 'done', { directive, completedRoles: cLevelRoles });
798
- res.end();
799
- }
800
- break;
801
- case 'msg:error':
802
- sendSSE(res, 'role:error', { roleId: rolePrefix, message: event.data.message });
803
- doneCount++;
804
- if (doneCount >= executions.length) {
805
- sendSSE(res, 'done', { directive, completedRoles: cLevelRoles });
806
- res.end();
807
- }
808
- break;
809
- }
810
- };
811
-
812
- exec.stream.subscribe(subscriber);
813
- subscribers.push({ exec, sub: subscriber });
814
- }
815
-
816
- req.on('close', () => {
817
- for (const { exec, sub } of subscribers) {
818
- exec.stream.unsubscribe(sub);
819
- }
820
- });
821
- }
822
-
823
647
  /* ─── POST /api/waves/:waveId/directive ──────── */
824
648
 
825
649
  function handleWaveDirective(waveId: string, body: Record<string, unknown>, res: ServerResponse): void {
@@ -12,10 +12,11 @@
12
12
  * - On restart, digest catches up with all missed events
13
13
  */
14
14
  import { executionManager, type Execution } from './execution-manager.js';
15
- import { createSession, getSession, listSessions } from './session-store.js';
15
+ import { createSession, getSession, listSessions, addMessage, type Message } from './session-store.js';
16
16
  import { buildOrgTree, getSubordinates } from '../engine/org-tree.js';
17
17
  import { COMPANY_ROOT } from './file-reader.js';
18
18
  import { ActivityStream } from './activity-stream.js';
19
+ import { saveCompletedWave } from './wave-tracker.js';
19
20
 
20
21
  /* ─── Types ──────────────────────────────────── */
21
22
 
@@ -290,6 +291,17 @@ ${recoveryContext}
290
291
  state.supervisorSessionId = session.id;
291
292
  state.status = 'running';
292
293
 
294
+ // Add the directive as CEO message so the session isn't empty (prevents deleteEmpty cleanup)
295
+ const ceoMsg: Message = {
296
+ id: `msg-${Date.now()}-ceo-supervisor`,
297
+ from: 'ceo',
298
+ content: state.directive,
299
+ type: 'directive',
300
+ status: 'done',
301
+ timestamp: new Date().toISOString(),
302
+ };
303
+ addMessage(session.id, ceoMsg);
304
+
293
305
  try {
294
306
  const exec = executionManager.startExecution({
295
307
  type: 'wave',
@@ -341,6 +353,18 @@ ${recoveryContext}
341
353
  } else {
342
354
  console.log(`[Supervisor] Wave ${state.waveId} complete. All subordinates done.`);
343
355
  state.status = 'stopped';
356
+
357
+ // Auto-save the completed wave to operations/waves/
358
+ try {
359
+ const result = saveCompletedWave(state.waveId, state.directive);
360
+ if (result.ok) {
361
+ console.log(`[Supervisor] Wave auto-saved: ${result.path}`);
362
+ } else {
363
+ console.warn(`[Supervisor] Wave auto-save returned no result for ${state.waveId}`);
364
+ }
365
+ } catch (err) {
366
+ console.error(`[Supervisor] Failed to auto-save wave ${state.waveId}:`, err);
367
+ }
344
368
  }
345
369
  }
346
370
 
@@ -7,6 +7,7 @@ import path from 'node:path';
7
7
  import { COMPANY_ROOT } from './file-reader.js';
8
8
  import { ActivityStream, type ActivityEvent } from './activity-stream.js';
9
9
  import { executionManager } from './execution-manager.js';
10
+ import { listSessions } from './session-store.js';
10
11
  import { type WaveRoleStatus, eventTypeToMessageStatus } from '../../../shared/types.js';
11
12
 
12
13
  /* ─── Find wave file ──────────────────────── */
@@ -158,6 +159,109 @@ export function updateFollowUpInWave(waveId: string, sessionId: string, roleId:
158
159
  }
159
160
  }
160
161
 
162
+ /* ─── Save completed wave to operations/waves/ ── */
163
+
164
+ /**
165
+ * Auto-save a completed wave to disk.
166
+ * Called by supervisor-heartbeat when all children are done.
167
+ * Mirrors the logic of handleSaveWave in execute.ts but callable from services.
168
+ */
169
+ export function saveCompletedWave(waveId: string, directive: string): { ok: boolean; path?: string } {
170
+ try {
171
+ // Collect all sessionIds for this wave from session-store
172
+ const allSessions = listSessions();
173
+ const sessionIds = allSessions
174
+ .filter(s => s.waveId === waveId)
175
+ .map(s => s.id);
176
+
177
+ if (sessionIds.length === 0) {
178
+ console.warn(`[WaveTracker] No sessions found for wave ${waveId}, skipping save`);
179
+ return { ok: false };
180
+ }
181
+
182
+ console.log(`[WaveTracker] Auto-saving wave ${waveId} with ${sessionIds.length} sessions`);
183
+
184
+ interface WaveRoleData {
185
+ roleId: string;
186
+ roleName: string;
187
+ sessionId: string;
188
+ status: WaveRoleStatus | 'unknown';
189
+ events: ReturnType<typeof ActivityStream.readAll>;
190
+ childSessions: Array<{ roleId: string; roleName: string; sessionId: string; status: WaveRoleStatus | 'unknown'; events: ReturnType<typeof ActivityStream.readAll> }>;
191
+ }
192
+ const rolesData: WaveRoleData[] = [];
193
+
194
+ for (const sid of sessionIds) {
195
+ const events = ActivityStream.readAll(sid);
196
+ const startEvent = events.find(e => e.type === 'msg:start');
197
+ const roleId = startEvent?.roleId ?? 'unknown';
198
+ const roleName = (startEvent?.data?.roleName as string) ?? roleId;
199
+ const doneEvent = events.find(e => e.type === 'msg:done' || e.type === 'msg:awaiting_input' || e.type === 'msg:error');
200
+ const status: WaveRoleStatus | 'unknown' = doneEvent ? eventTypeToMessageStatus(doneEvent.type) as WaveRoleStatus : 'unknown';
201
+
202
+ const childSessions: WaveRoleData['childSessions'] = [];
203
+ for (const e of events) {
204
+ const childSessionId = e.data.childSessionId as string | undefined;
205
+ if (e.type === 'dispatch:start' && childSessionId) {
206
+ const targetRoleId = (e.data.targetRoleId as string) ?? 'unknown';
207
+ const childEvents = ActivityStream.readAll(childSessionId);
208
+ const childDone = childEvents.find(ce => ce.type === 'msg:done' || ce.type === 'msg:error' || ce.type === 'msg:awaiting_input');
209
+ const childStatus: WaveRoleStatus | 'unknown' = childDone ? eventTypeToMessageStatus(childDone.type) as WaveRoleStatus : 'unknown';
210
+ childSessions.push({
211
+ roleId: targetRoleId,
212
+ roleName: (childEvents.find(ce => ce.type === 'msg:start')?.data?.roleName as string) ?? targetRoleId,
213
+ sessionId: childSessionId,
214
+ status: childStatus,
215
+ events: childEvents,
216
+ });
217
+ }
218
+ }
219
+
220
+ rolesData.push({ roleId, roleName, sessionId: sid, status, events, childSessions });
221
+ }
222
+
223
+ const wavesDir = path.join(COMPANY_ROOT, 'operations', 'waves');
224
+ if (!fs.existsSync(wavesDir)) {
225
+ fs.mkdirSync(wavesDir, { recursive: true });
226
+ }
227
+
228
+ // Check if wave file already exists (e.g. from appendFollowUp)
229
+ const existing = findWaveFile(waveId);
230
+ const baseName = existing
231
+ ? path.basename(existing, '.json')
232
+ : waveId;
233
+ const jsonPath = existing ?? path.join(wavesDir, `${baseName}.json`);
234
+
235
+ const now = new Date();
236
+ const waveJson = {
237
+ id: baseName,
238
+ directive,
239
+ startedAt: now.toISOString(),
240
+ duration: 0,
241
+ roles: rolesData,
242
+ waveId,
243
+ sessionIds,
244
+ };
245
+ fs.writeFileSync(jsonPath, JSON.stringify(waveJson, null, 2), 'utf-8');
246
+
247
+ const relativePath = `operations/waves/${baseName}.json`;
248
+ console.log(`[WaveTracker] Wave saved: ${relativePath} (${rolesData.length} roles)`);
249
+
250
+ // Earn coins for wave completion (non-critical)
251
+ try {
252
+ const { earnCoinsInternal } = require('../routes/coins.js');
253
+ if (rolesData.length > 0) {
254
+ earnCoinsInternal(rolesData.length * 500, `Wave done: ${rolesData.length} roles`, `wave:${baseName}`);
255
+ }
256
+ } catch { /* non-critical */ }
257
+
258
+ return { ok: true, path: relativePath };
259
+ } catch (err) {
260
+ console.error(`[WaveTracker] Failed to auto-save wave ${waveId}:`, err);
261
+ return { ok: false };
262
+ }
263
+ }
264
+
161
265
  /* ─── Helpers ─────────────────────────────── */
162
266
 
163
267
  function watchExecutionCompletion(waveId: string, sessionId: string, roleId: string): void {