tycono 0.1.94-beta.3 → 0.1.94-beta.5

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.3",
3
+ "version": "0.1.94-beta.5",
4
4
  "description": "Build an AI company. Watch them work.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -371,10 +371,9 @@ elif cmd == 'amend':
371
371
  log('Usage: supervision amend <sessionId> "<instruction>"')
372
372
  sys.exit(1)
373
373
 
374
- # Amend uses continue-session with amended context
374
+ # Amend sends a message to the session with amendment instructions
375
375
  body = json.dumps({
376
- 'response': f'[SUPERVISION AMENDMENT] {instruction}',
377
- 'responderRole': os.environ.get('DISPATCH_SOURCE_ROLE', 'ceo'),
376
+ 'content': f'[SUPERVISION AMENDMENT] {instruction}',
378
377
  }).encode()
379
378
 
380
379
  try:
@@ -310,12 +310,45 @@ function handleSaveWave(body: Record<string, unknown>, res: ServerResponse): voi
310
310
  let sessionIds = (body.sessionIds ?? body.jobIds) as string[] | undefined;
311
311
  const waveId = body.waveId as string | undefined;
312
312
 
313
- // BUG-W01 fix: auto-collect sessionIds from session-store when waveId is present
313
+ // BUG-W01 + BUG-009 fix: auto-collect sessionIds from session-store AND activity-streams
314
314
  if (waveId && (!sessionIds || sessionIds.length === 0)) {
315
- const allSessions = listSessions();
316
- sessionIds = allSessions
317
- .filter(s => s.waveId === waveId)
318
- .map(s => s.id);
315
+ const sessionIdSet = new Set(
316
+ listSessions().filter(s => s.waveId === waveId).map(s => s.id)
317
+ );
318
+
319
+ // Scan activity-streams for sessions belonging to this wave
320
+ const streamsDir = path.join(COMPANY_ROOT, 'operations', 'activity-streams');
321
+ if (fs.existsSync(streamsDir)) {
322
+ const waveTimestamp = waveId.replace('wave-', '');
323
+ for (const file of fs.readdirSync(streamsDir)) {
324
+ if (!file.endsWith('.jsonl')) continue;
325
+ const sid = file.replace('.jsonl', '');
326
+ if (sessionIdSet.has(sid)) continue;
327
+ if (sid.includes(waveTimestamp)) {
328
+ sessionIdSet.add(sid);
329
+ }
330
+ }
331
+
332
+ // Recursively find all child sessions via dispatch:start events
333
+ let foundNew = true;
334
+ while (foundNew) {
335
+ foundNew = false;
336
+ for (const sid of Array.from(sessionIdSet)) {
337
+ try {
338
+ const events = ActivityStream.readAll(sid);
339
+ for (const e of events) {
340
+ const childSessionId = e.data.childSessionId as string | undefined;
341
+ if (e.type === 'dispatch:start' && childSessionId && !sessionIdSet.has(childSessionId)) {
342
+ sessionIdSet.add(childSessionId);
343
+ foundNew = true;
344
+ }
345
+ }
346
+ } catch { /* skip */ }
347
+ }
348
+ }
349
+ }
350
+
351
+ sessionIds = Array.from(sessionIdSet);
319
352
  console.log(`[WaveSave] Auto-collected ${sessionIds.length} sessionIds for wave ${waveId}`);
320
353
  }
321
354
 
@@ -387,14 +420,45 @@ function handleSaveWave(body: Record<string, unknown>, res: ServerResponse): voi
387
420
  }
388
421
  const jsonPath = path.join(wavesDir, `${baseName}.json`);
389
422
 
423
+ // Calculate actual duration from activity stream timestamps
424
+ let startedAt = now;
425
+ let endedAt = now;
426
+ for (const role of rolesData) {
427
+ if (role.events.length > 0) {
428
+ const firstTs = new Date(role.events[0].ts);
429
+ const lastTs = new Date(role.events[role.events.length - 1].ts);
430
+ if (firstTs < startedAt) startedAt = firstTs;
431
+ if (lastTs > endedAt) endedAt = lastTs;
432
+ }
433
+ for (const child of role.childSessions) {
434
+ if (child.events.length > 0) {
435
+ const firstTs = new Date(child.events[0].ts);
436
+ const lastTs = new Date(child.events[child.events.length - 1].ts);
437
+ if (firstTs < startedAt) startedAt = firstTs;
438
+ if (lastTs > endedAt) endedAt = lastTs;
439
+ }
440
+ }
441
+ }
442
+ const duration = Math.round((endedAt.getTime() - startedAt.getTime()) / 1000);
443
+
444
+ // Collect ALL session IDs including child sessions
445
+ const allSessionIds = [...sessionIds];
446
+ for (const role of rolesData) {
447
+ for (const child of role.childSessions) {
448
+ if (!allSessionIds.includes(child.sessionId)) {
449
+ allSessionIds.push(child.sessionId);
450
+ }
451
+ }
452
+ }
453
+
390
454
  const waveJson = {
391
455
  id: baseName,
392
456
  directive,
393
- startedAt: now.toISOString(),
394
- duration: 0,
457
+ startedAt: startedAt.toISOString(),
458
+ duration,
395
459
  roles: rolesData,
396
460
  ...(waveId && { waveId }),
397
- ...(sessionIds.length > 0 && { sessionIds }),
461
+ sessionIds: allSessionIds,
398
462
  };
399
463
  fs.writeFileSync(jsonPath, JSON.stringify(waveJson, null, 2), 'utf-8');
400
464
 
@@ -101,7 +101,11 @@ function writeImmediate(session: Session): void {
101
101
  clearTimeout(timer);
102
102
  writeTimers.delete(session.id);
103
103
  }
104
- fs.writeFileSync(sessionPath(session.id), JSON.stringify(session, null, 2));
104
+ try {
105
+ fs.writeFileSync(sessionPath(session.id), JSON.stringify(session, null, 2));
106
+ } catch (err) {
107
+ console.error(`[SessionStore] WRITE FAILED for ${session.id}:`, err);
108
+ }
105
109
  }
106
110
 
107
111
  /* ─── In-memory cache ───────────────────── */
@@ -254,6 +258,7 @@ export function deleteSession(id: string): boolean {
254
258
  const session = cache.get(id);
255
259
  if (!session) return false;
256
260
 
261
+ console.log(`[SessionStore] Deleting session ${id} (roleId=${session.roleId}, waveId=${session.waveId ?? 'none'}, messages=${session.messages.length})`);
257
262
  cache.delete(id);
258
263
  const p = sessionPath(id);
259
264
  if (fs.existsSync(p)) fs.unlinkSync(p);
@@ -272,6 +277,8 @@ export function deleteEmpty(): { deleted: number; ids: string[] } {
272
277
  const ids: string[] = [];
273
278
  for (const [id, session] of cache) {
274
279
  if (session.messages.length === 0) {
280
+ // BUG-008 fix: never delete wave sessions — they are managed by supervisor lifecycle
281
+ if (session.waveId) continue;
275
282
  ids.push(id);
276
283
  }
277
284
  }
@@ -268,6 +268,26 @@ ${cLevelList}
268
268
  - G-04: If you dispatch the same role 3+ times with no progress, intervene: "specify what's wrong concretely."
269
269
  - G-05: abort = graceful amend ("wrap up and stop"). Not a hard kill.
270
270
  - G-06: If two sessions show no events for 3+ minutes, suspect deadlock → re-sequence their work.
271
+ - G-07: **Cross-team relay is YOUR job.** When a C-Level completes, immediately amend the other active C-Levels with a summary of the completed work. Example: CBO finishes game design → amend CTO: "CBO delivered game design docs. Key decisions: [summary]. Review and align your implementation."
272
+ - G-08: Don't just watch passively. On every tick, ask: "Does any active C-Level need information from a completed C-Level?" If yes, amend with the relevant context.
273
+
274
+ ## Cross-Team Relay Protocol (CRITICAL)
275
+ ⛔ C-Levels do NOT talk to each other directly. YOU are the relay.
276
+
277
+ When C-Level A completes while C-Level B is still active:
278
+ 1. Review A's deliverables (read their committed files or final report)
279
+ 2. Summarize the key decisions, artifacts, and constraints from A's work
280
+ 3. amend B: "C-Level A completed. Here are their deliverables relevant to your work: [summary]. Review and incorporate."
281
+ 4. On next tick, verify B acknowledged and reflected A's input
282
+
283
+ When C-Level A produces intermediate results that B needs:
284
+ 1. amend B with the relevant intermediate output
285
+ 2. You don't need to wait for A to finish — relay as results become available
286
+
287
+ Examples:
288
+ - CBO finishes game design → amend CTO: "CBO delivered: world-building doc, 15 monster specs, quest design, UI guidelines. Ensure implementation matches these specs."
289
+ - CTO's engineer creates API schema → amend CBO: "CTO's team defined the data schema. Here's the structure: [summary]. Adjust business docs if needed."
290
+ - Designer finishes UI guide → relay to CTO team: "Designer's UI guide is ready at [path]. Frontend implementation should follow these specs."
271
291
 
272
292
  ## CEO Directive Channel
273
293
  If new CEO directives arrive mid-execution, they will appear in your supervision watch digest
@@ -278,7 +298,7 @@ ${recoveryContext}
278
298
  1. Analyze the directive and decide which C-Level roles to dispatch (not necessarily all)
279
299
  2. Dispatch them with clear tasks
280
300
  3. Enter supervision watch loop
281
- 4. Monitor, relay opinions, course-correct, until all done
301
+ 4. Monitor, **actively relay results between teams**, course-correct, until all done
282
302
  5. Compile results and report`;
283
303
 
284
304
  // Create supervisor session
@@ -168,11 +168,51 @@ export function updateFollowUpInWave(waveId: string, sessionId: string, roleId:
168
168
  */
169
169
  export function saveCompletedWave(waveId: string, directive: string): { ok: boolean; path?: string } {
170
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);
171
+ // BUG-009 fix: collect sessions from BOTH session-store AND activity-streams.
172
+ // Session-store cache may miss the CEO supervisor session (BUG-008).
173
+ // Activity-streams on disk are the source of truth for what actually ran.
174
+ const sessionIdSet = new Set(
175
+ listSessions().filter(s => s.waveId === waveId).map(s => s.id)
176
+ );
177
+
178
+ // Scan activity-streams for ALL sessions belonging to this wave.
179
+ // Wave sessions share a traceId chain: CEO → C-Level → subordinates.
180
+ // We find the CEO session (waveId timestamp embedded in its ID), then follow dispatch:start events.
181
+ const streamsDir = path.join(COMPANY_ROOT, 'operations', 'activity-streams');
182
+ if (fs.existsSync(streamsDir)) {
183
+ // Find all activity stream files and check if they belong to this wave
184
+ const waveTimestamp = waveId.replace('wave-', '');
185
+ for (const file of fs.readdirSync(streamsDir)) {
186
+ if (!file.endsWith('.jsonl')) continue;
187
+ const sid = file.replace('.jsonl', '');
188
+ if (sessionIdSet.has(sid)) continue;
189
+ // Check if session ID contains the wave timestamp (CEO session)
190
+ // or if the session was dispatched from a known wave session
191
+ if (sid.includes(waveTimestamp)) {
192
+ sessionIdSet.add(sid);
193
+ }
194
+ }
195
+
196
+ // Now recursively find all child sessions via dispatch:start events
197
+ let foundNew = true;
198
+ while (foundNew) {
199
+ foundNew = false;
200
+ for (const sid of Array.from(sessionIdSet)) {
201
+ try {
202
+ const events = ActivityStream.readAll(sid);
203
+ for (const e of events) {
204
+ const childSessionId = e.data.childSessionId as string | undefined;
205
+ if (e.type === 'dispatch:start' && childSessionId && !sessionIdSet.has(childSessionId)) {
206
+ sessionIdSet.add(childSessionId);
207
+ foundNew = true;
208
+ }
209
+ }
210
+ } catch { /* skip */ }
211
+ }
212
+ }
213
+ }
214
+
215
+ const sessionIds = Array.from(sessionIdSet);
176
216
 
177
217
  if (sessionIds.length === 0) {
178
218
  console.warn(`[WaveTracker] No sessions found for wave ${waveId}, skipping save`);
@@ -232,15 +272,46 @@ export function saveCompletedWave(waveId: string, directive: string): { ok: bool
232
272
  : waveId;
233
273
  const jsonPath = existing ?? path.join(wavesDir, `${baseName}.json`);
234
274
 
275
+ // BUG-009 fix: calculate actual duration from activity stream timestamps
235
276
  const now = new Date();
277
+ let startedAt = now;
278
+ let endedAt = now;
279
+ for (const role of rolesData) {
280
+ if (role.events.length > 0) {
281
+ const firstTs = new Date(role.events[0].ts);
282
+ const lastTs = new Date(role.events[role.events.length - 1].ts);
283
+ if (firstTs < startedAt) startedAt = firstTs;
284
+ if (lastTs > endedAt) endedAt = lastTs;
285
+ }
286
+ for (const child of role.childSessions) {
287
+ if (child.events.length > 0) {
288
+ const firstTs = new Date(child.events[0].ts);
289
+ const lastTs = new Date(child.events[child.events.length - 1].ts);
290
+ if (firstTs < startedAt) startedAt = firstTs;
291
+ if (lastTs > endedAt) endedAt = lastTs;
292
+ }
293
+ }
294
+ }
295
+ const duration = Math.round((endedAt.getTime() - startedAt.getTime()) / 1000);
296
+
297
+ // Collect ALL session IDs including child sessions
298
+ const allSessionIds = [...sessionIds];
299
+ for (const role of rolesData) {
300
+ for (const child of role.childSessions) {
301
+ if (!allSessionIds.includes(child.sessionId)) {
302
+ allSessionIds.push(child.sessionId);
303
+ }
304
+ }
305
+ }
306
+
236
307
  const waveJson = {
237
308
  id: baseName,
238
309
  directive,
239
- startedAt: now.toISOString(),
240
- duration: 0,
310
+ startedAt: startedAt.toISOString(),
311
+ duration,
241
312
  roles: rolesData,
242
313
  waveId,
243
- sessionIds,
314
+ sessionIds: allSessionIds,
244
315
  };
245
316
  fs.writeFileSync(jsonPath, JSON.stringify(waveJson, null, 2), 'utf-8');
246
317