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
|
@@ -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
|
-
//
|
|
224
|
-
|
|
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
|
-
//
|
|
688
|
-
|
|
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 {
|