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
|
@@ -371,10 +371,9 @@ elif cmd == 'amend':
|
|
|
371
371
|
log('Usage: supervision amend <sessionId> "<instruction>"')
|
|
372
372
|
sys.exit(1)
|
|
373
373
|
|
|
374
|
-
# Amend
|
|
374
|
+
# Amend sends a message to the session with amendment instructions
|
|
375
375
|
body = json.dumps({
|
|
376
|
-
'
|
|
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
|
|
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
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
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:
|
|
394
|
-
duration
|
|
457
|
+
startedAt: startedAt.toISOString(),
|
|
458
|
+
duration,
|
|
395
459
|
roles: rolesData,
|
|
396
460
|
...(waveId && { waveId }),
|
|
397
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
//
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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:
|
|
240
|
-
duration
|
|
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
|
|