tycono 0.1.73 → 0.1.74-beta.0
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/README.md +129 -48
- package/package.json +1 -1
- package/src/api/src/routes/execute.ts +17 -2
- package/src/api/src/routes/operations.ts +4 -1
- package/src/api/src/routes/sessions.ts +42 -14
- package/src/api/src/services/activity-stream.ts +5 -0
- package/src/api/src/services/job-manager.ts +72 -2
- package/src/api/src/services/wave-tracker.ts +217 -0
- package/src/web/dist/assets/index-0e6kn9Ne.js +108 -0
- package/src/web/dist/assets/index-BLBYHBP_.css +1 -0
- package/src/web/dist/assets/{preview-app-BT_tZB55.js → preview-app-B3kFNGV1.js} +1 -1
- package/src/web/dist/index.html +2 -2
- package/src/web/dist/assets/index-BQaKrdJG.css +0 -1
- package/src/web/dist/assets/index-DVuy5FDy.js +0 -108
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wave Tracker — tracks follow-up jobs in wave JSON files.
|
|
3
|
+
* Persists follow-up state so navigating between waves doesn't lose progress.
|
|
4
|
+
*/
|
|
5
|
+
import fs from 'node:fs';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import { COMPANY_ROOT } from './file-reader.js';
|
|
8
|
+
import { ActivityStream, type ActivityEvent } from './activity-stream.js';
|
|
9
|
+
import { jobManager } from './job-manager.js';
|
|
10
|
+
|
|
11
|
+
/* ─── Find wave file ──────────────────────── */
|
|
12
|
+
|
|
13
|
+
export function findWaveFile(waveId: string): string | null {
|
|
14
|
+
const wavesDir = path.join(COMPANY_ROOT, 'operations', 'waves');
|
|
15
|
+
if (!fs.existsSync(wavesDir)) return null;
|
|
16
|
+
|
|
17
|
+
// Direct match
|
|
18
|
+
const direct = path.join(wavesDir, `${waveId}.json`);
|
|
19
|
+
if (fs.existsSync(direct)) return direct;
|
|
20
|
+
|
|
21
|
+
// Search by waveId/id field
|
|
22
|
+
try {
|
|
23
|
+
for (const f of fs.readdirSync(wavesDir)) {
|
|
24
|
+
if (!f.endsWith('.json')) continue;
|
|
25
|
+
try {
|
|
26
|
+
const data = JSON.parse(fs.readFileSync(path.join(wavesDir, f), 'utf-8'));
|
|
27
|
+
if (data.waveId === waveId || data.id === waveId) {
|
|
28
|
+
return path.join(wavesDir, f);
|
|
29
|
+
}
|
|
30
|
+
} catch { /* skip */ }
|
|
31
|
+
}
|
|
32
|
+
} catch { /* skip */ }
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/* ─── Append follow-up to wave ────────────── */
|
|
37
|
+
|
|
38
|
+
export function appendFollowUpToWave(
|
|
39
|
+
waveId: string, jobId: string, roleId: string, task: string, sessionId?: string,
|
|
40
|
+
): void {
|
|
41
|
+
const waveFile = findWaveFile(waveId);
|
|
42
|
+
if (!waveFile) {
|
|
43
|
+
console.warn(`[WaveTracker] Wave file not found for ${waveId}`);
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
try {
|
|
48
|
+
const data = JSON.parse(fs.readFileSync(waveFile, 'utf-8'));
|
|
49
|
+
if (!data.roles) data.roles = [];
|
|
50
|
+
|
|
51
|
+
// Add follow-up entry with running status
|
|
52
|
+
data.roles.push({
|
|
53
|
+
roleId,
|
|
54
|
+
roleName: roleId,
|
|
55
|
+
jobId,
|
|
56
|
+
sessionId,
|
|
57
|
+
status: 'running',
|
|
58
|
+
events: [],
|
|
59
|
+
childJobs: [],
|
|
60
|
+
isFollowUp: true,
|
|
61
|
+
followUpTask: task,
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
fs.writeFileSync(waveFile, JSON.stringify(data, null, 2), 'utf-8');
|
|
65
|
+
console.log(`[WaveTracker] Appended job ${jobId} to wave ${waveId}`);
|
|
66
|
+
|
|
67
|
+
watchJobCompletion(waveId, jobId, roleId);
|
|
68
|
+
} catch (err) {
|
|
69
|
+
console.error(`[WaveTracker] Failed to append to wave:`, err);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/* ─── Update follow-up entry on reply (continuation) ── */
|
|
74
|
+
|
|
75
|
+
export function updateFollowUpForReply(
|
|
76
|
+
waveId: string, roleId: string, oldJobId: string | undefined, newJobId: string, sessionId?: string,
|
|
77
|
+
): void {
|
|
78
|
+
const waveFile = findWaveFile(waveId);
|
|
79
|
+
if (!waveFile) return;
|
|
80
|
+
|
|
81
|
+
try {
|
|
82
|
+
const data = JSON.parse(fs.readFileSync(waveFile, 'utf-8'));
|
|
83
|
+
if (!data.roles) return;
|
|
84
|
+
|
|
85
|
+
// Find the latest follow-up entry for this roleId
|
|
86
|
+
let idx = -1;
|
|
87
|
+
if (oldJobId) {
|
|
88
|
+
idx = data.roles.findIndex((r: { jobId?: string }) => r.jobId === oldJobId);
|
|
89
|
+
}
|
|
90
|
+
if (idx < 0) {
|
|
91
|
+
// Find latest entry for this roleId
|
|
92
|
+
for (let i = data.roles.length - 1; i >= 0; i--) {
|
|
93
|
+
if (data.roles[i].roleId === roleId) { idx = i; break; }
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (idx >= 0) {
|
|
98
|
+
const entry = data.roles[idx];
|
|
99
|
+
// Preserve existing events, add old jobId to chain for event replay
|
|
100
|
+
const jobChain: string[] = entry.jobChain ?? [];
|
|
101
|
+
if (entry.jobId && entry.jobId !== newJobId) {
|
|
102
|
+
jobChain.push(entry.jobId);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Read events from old job(s) to preserve history
|
|
106
|
+
const existingEvents = collectChainEvents(jobChain, entry.events);
|
|
107
|
+
|
|
108
|
+
data.roles[idx] = {
|
|
109
|
+
...entry,
|
|
110
|
+
jobId: newJobId,
|
|
111
|
+
sessionId: sessionId ?? entry.sessionId,
|
|
112
|
+
status: 'running',
|
|
113
|
+
events: existingEvents,
|
|
114
|
+
jobChain,
|
|
115
|
+
};
|
|
116
|
+
} else {
|
|
117
|
+
// No existing entry — create new
|
|
118
|
+
data.roles.push({
|
|
119
|
+
roleId,
|
|
120
|
+
roleName: roleId,
|
|
121
|
+
jobId: newJobId,
|
|
122
|
+
sessionId,
|
|
123
|
+
status: 'running',
|
|
124
|
+
events: [],
|
|
125
|
+
childJobs: [],
|
|
126
|
+
isFollowUp: true,
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
fs.writeFileSync(waveFile, JSON.stringify(data, null, 2), 'utf-8');
|
|
131
|
+
console.log(`[WaveTracker] Updated follow-up for reply: ${roleId} → ${newJobId}`);
|
|
132
|
+
|
|
133
|
+
watchJobCompletion(waveId, newJobId, roleId);
|
|
134
|
+
} catch (err) {
|
|
135
|
+
console.error(`[WaveTracker] Failed to update follow-up for reply:`, err);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/* ─── Update follow-up entry on job completion ── */
|
|
140
|
+
|
|
141
|
+
export function updateFollowUpInWave(waveId: string, jobId: string, roleId: string): void {
|
|
142
|
+
const waveFile = findWaveFile(waveId);
|
|
143
|
+
if (!waveFile) return;
|
|
144
|
+
|
|
145
|
+
try {
|
|
146
|
+
const data = JSON.parse(fs.readFileSync(waveFile, 'utf-8'));
|
|
147
|
+
if (!data.roles) return;
|
|
148
|
+
|
|
149
|
+
const newEvents = ActivityStream.readAll(jobId);
|
|
150
|
+
const doneEvent = newEvents.find(e => e.type === 'job:done' || e.type === 'job:error' || e.type === 'job:awaiting_input');
|
|
151
|
+
const status = doneEvent?.type === 'job:done' ? 'done'
|
|
152
|
+
: doneEvent?.type === 'job:error' ? 'error'
|
|
153
|
+
: doneEvent?.type === 'job:awaiting_input' ? 'awaiting_input' : 'running';
|
|
154
|
+
|
|
155
|
+
// Collect child jobs
|
|
156
|
+
const childJobs: Array<{ roleId: string; roleName: string; jobId: string; status: string; events: ReturnType<typeof ActivityStream.readAll> }> = [];
|
|
157
|
+
for (const e of newEvents) {
|
|
158
|
+
if (e.type === 'dispatch:start' && e.data.childJobId) {
|
|
159
|
+
const childJobId = e.data.childJobId as string;
|
|
160
|
+
const targetRoleId = (e.data.targetRoleId as string) ?? 'unknown';
|
|
161
|
+
const childEvents = ActivityStream.readAll(childJobId);
|
|
162
|
+
const childDone = childEvents.find(ce => ce.type === 'job:done' || ce.type === 'job:error');
|
|
163
|
+
const childStatus = childDone?.type === 'job:done' ? 'done' : childDone?.type === 'job:error' ? 'error' : 'unknown';
|
|
164
|
+
childJobs.push({ roleId: targetRoleId, roleName: targetRoleId, jobId: childJobId, status: childStatus, events: childEvents });
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Find entry by jobId
|
|
169
|
+
const idx = data.roles.findIndex((r: { jobId?: string }) => r.jobId === jobId);
|
|
170
|
+
if (idx >= 0) {
|
|
171
|
+
const entry = data.roles[idx];
|
|
172
|
+
// Merge: keep preserved chain events + append new events
|
|
173
|
+
const chainEvents = entry.events ?? [];
|
|
174
|
+
const mergedEvents = [...chainEvents, ...newEvents];
|
|
175
|
+
|
|
176
|
+
data.roles[idx] = { ...entry, status, events: mergedEvents, childJobs };
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
fs.writeFileSync(waveFile, JSON.stringify(data, null, 2), 'utf-8');
|
|
180
|
+
console.log(`[WaveTracker] Updated job ${jobId} in wave ${waveId} → ${status}`);
|
|
181
|
+
} catch (err) {
|
|
182
|
+
console.error(`[WaveTracker] Failed to update wave:`, err);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/* ─── Helpers ─────────────────────────────── */
|
|
187
|
+
|
|
188
|
+
function watchJobCompletion(waveId: string, jobId: string, roleId: string): void {
|
|
189
|
+
const job = jobManager.getJob(jobId);
|
|
190
|
+
if (!job) return;
|
|
191
|
+
|
|
192
|
+
const subscriber = (event: ActivityEvent) => {
|
|
193
|
+
if (event.type === 'job:done' || event.type === 'job:error' || event.type === 'job:awaiting_input') {
|
|
194
|
+
updateFollowUpInWave(waveId, jobId, roleId);
|
|
195
|
+
job.stream.unsubscribe(subscriber);
|
|
196
|
+
}
|
|
197
|
+
};
|
|
198
|
+
job.stream.subscribe(subscriber);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/** Collect events from a chain of previous job IDs */
|
|
202
|
+
function collectChainEvents(
|
|
203
|
+
jobChain: string[],
|
|
204
|
+
existingEvents: ActivityEvent[],
|
|
205
|
+
): ActivityEvent[] {
|
|
206
|
+
// existingEvents already contains merged events from previous iterations
|
|
207
|
+
if (existingEvents && existingEvents.length > 0) {
|
|
208
|
+
return existingEvents;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Fallback: read from activity stream files
|
|
212
|
+
const events: ActivityEvent[] = [];
|
|
213
|
+
for (const jid of jobChain) {
|
|
214
|
+
events.push(...ActivityStream.readAll(jid));
|
|
215
|
+
}
|
|
216
|
+
return events;
|
|
217
|
+
}
|