tycono 0.1.91 → 0.1.93-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/package.json +1 -1
- package/src/api/src/create-server.ts +20 -20
- package/src/api/src/engine/agent-loop.ts +7 -7
- package/src/api/src/engine/knowledge-gate.ts +32 -2
- package/src/api/src/engine/runners/claude-cli.ts +15 -15
- package/src/api/src/engine/runners/direct-api.ts +1 -1
- package/src/api/src/engine/runners/types.ts +2 -1
- package/src/api/src/routes/active-sessions.ts +9 -21
- package/src/api/src/routes/cost.ts +43 -0
- package/src/api/src/routes/engine.ts +1 -0
- package/src/api/src/routes/execute.ts +192 -333
- package/src/api/src/routes/operations.ts +8 -7
- package/src/api/src/routes/sessions.ts +81 -52
- package/src/api/src/routes/speech.ts +1 -1
- package/src/api/src/services/activity-stream.ts +48 -19
- package/src/api/src/services/execution-manager.ts +849 -0
- package/src/api/src/services/job-manager.ts +14 -893
- package/src/api/src/services/session-store.ts +1 -1
- package/src/api/src/services/token-ledger.ts +13 -2
- package/src/api/src/services/wave-multiplexer.ts +90 -84
- package/src/api/src/services/wave-tracker.ts +44 -86
- package/src/shared/types.ts +48 -65
- package/src/web/dist/assets/index-BLB8Scqo.js +115 -0
- package/src/web/dist/assets/index-rya2vj54.css +1 -0
- package/src/web/dist/assets/{preview-app-B2p8z-M8.js → preview-app-DJl5kOhT.js} +1 -1
- package/src/web/dist/index.html +2 -2
- package/src/web/dist/assets/index--pQ-Ce3E.js +0 -110
- package/src/web/dist/assets/index-DueQ7jub.css +0 -1
|
@@ -2,7 +2,7 @@ import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
|
2
2
|
import fs from 'node:fs';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { COMPANY_ROOT } from '../services/file-reader.js';
|
|
5
|
-
|
|
5
|
+
// activity-tracker removed — executionManager is Single Source of Truth
|
|
6
6
|
import { buildOrgTree, canDispatchTo, getSubordinates } from '../engine/org-tree.js';
|
|
7
7
|
import { createRunner, type RunnerResult } from '../engine/runners/index.js';
|
|
8
8
|
import {
|
|
@@ -13,16 +13,16 @@ import {
|
|
|
13
13
|
type Message,
|
|
14
14
|
type ImageAttachment,
|
|
15
15
|
} from '../services/session-store.js';
|
|
16
|
-
import {
|
|
17
|
-
import { type
|
|
16
|
+
import { executionManager, type Execution } from '../services/execution-manager.js';
|
|
17
|
+
import { type MessageStatus, type WaveRoleStatus, type TeamStatus, messageStatusToRoleStatus, eventTypeToMessageStatus } from '../../../shared/types.js';
|
|
18
18
|
import { ActivityStream, type ActivityEvent, type ActivitySubscriber } from '../services/activity-stream.js';
|
|
19
19
|
import { earnCoinsInternal } from './coins.js';
|
|
20
20
|
import { appendFollowUpToWave } from '../services/wave-tracker.js';
|
|
21
21
|
import { waveMultiplexer } from '../services/wave-multiplexer.js';
|
|
22
22
|
|
|
23
|
-
/* ───
|
|
24
|
-
|
|
25
|
-
waveMultiplexer.
|
|
23
|
+
/* ─── Auto-attach child executions to wave multiplexer ── */
|
|
24
|
+
executionManager.onExecutionCreated((exec) => {
|
|
25
|
+
waveMultiplexer.onExecutionCreated(exec);
|
|
26
26
|
});
|
|
27
27
|
|
|
28
28
|
/* ─── Runner — lazy, re-created when engine changes ── */
|
|
@@ -31,9 +31,7 @@ function getRunner() {
|
|
|
31
31
|
return createRunner();
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
-
/* ───
|
|
35
|
-
|
|
36
|
-
const roleStatus = new Map<string, RoleStatus>();
|
|
34
|
+
/* ─── Execution status via executionManager (Single SoT) ──── */
|
|
37
35
|
|
|
38
36
|
/* ─── Raw HTTP handler (Express 5 SSE 호환 문제 우회) ─── */
|
|
39
37
|
|
|
@@ -48,13 +46,19 @@ export function handleExecRequest(req: IncomingMessage, res: ServerResponse): vo
|
|
|
48
46
|
return;
|
|
49
47
|
}
|
|
50
48
|
|
|
49
|
+
// ── /api/waves/active — restore active waves after refresh ──
|
|
50
|
+
if (method === 'GET' && url === '/api/waves/active') {
|
|
51
|
+
jsonResponse(res, 200, { waves: waveMultiplexer.getActiveWaves() });
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
51
55
|
// ── /api/waves/save ──
|
|
52
56
|
if (method === 'POST' && url === '/api/waves/save') {
|
|
53
57
|
readBody(req).then((body) => handleSaveWave(body, res));
|
|
54
58
|
return;
|
|
55
59
|
}
|
|
56
60
|
|
|
57
|
-
// ── /api/jobs/* routes ──
|
|
61
|
+
// ── /api/jobs/* routes (internal) ──
|
|
58
62
|
if (url.startsWith('/api/jobs')) {
|
|
59
63
|
handleJobsRequest(url, method, req, res);
|
|
60
64
|
return;
|
|
@@ -71,8 +75,6 @@ export function handleExecRequest(req: IncomingMessage, res: ServerResponse): vo
|
|
|
71
75
|
readBody(req).then((body) => handleWave(body, req, res));
|
|
72
76
|
} else if (method === 'GET' && url.endsWith('/status')) {
|
|
73
77
|
handleStatus(res);
|
|
74
|
-
} else if (method === 'POST' && url.endsWith('/activity')) {
|
|
75
|
-
readBody(req).then((body) => handleActivity(body, res));
|
|
76
78
|
} else {
|
|
77
79
|
res.writeHead(404);
|
|
78
80
|
res.end(JSON.stringify({ error: 'Not found' }));
|
|
@@ -80,61 +82,74 @@ export function handleExecRequest(req: IncomingMessage, res: ServerResponse): vo
|
|
|
80
82
|
}
|
|
81
83
|
|
|
82
84
|
/* ═══════════════════════════════════════════════
|
|
83
|
-
/api/jobs/* —
|
|
85
|
+
/api/jobs/* — Internal endpoints
|
|
84
86
|
═══════════════════════════════════════════════ */
|
|
85
87
|
|
|
86
88
|
function handleJobsRequest(url: string, method: string, req: IncomingMessage, res: ServerResponse): void {
|
|
87
|
-
const [
|
|
89
|
+
const [reqPath] = url.split('?');
|
|
88
90
|
|
|
89
|
-
// POST /api/jobs — start a new
|
|
90
|
-
if (method === 'POST' &&
|
|
91
|
+
// POST /api/jobs — start a new execution (creates session + execution)
|
|
92
|
+
if (method === 'POST' && reqPath === '/api/jobs') {
|
|
91
93
|
readBody(req).then((body) => handleStartJob(body, res));
|
|
92
94
|
return;
|
|
93
95
|
}
|
|
94
96
|
|
|
95
|
-
// GET /api/jobs/:id — internal only
|
|
96
|
-
const jobMatch =
|
|
97
|
+
// GET /api/jobs/:id — internal only
|
|
98
|
+
const jobMatch = reqPath.match(/^\/api\/jobs\/([^/]+)$/);
|
|
97
99
|
if (method === 'GET' && jobMatch) {
|
|
98
|
-
const
|
|
99
|
-
const
|
|
100
|
-
if (!
|
|
101
|
-
|
|
102
|
-
|
|
100
|
+
const id = jobMatch[1];
|
|
101
|
+
const exec = executionManager.getExecution(id) ?? executionManager.getActiveExecution(id);
|
|
102
|
+
if (!exec) {
|
|
103
|
+
// Try reading from stream file directly
|
|
104
|
+
if (ActivityStream.exists(id)) {
|
|
105
|
+
const events = ActivityStream.readAll(id);
|
|
106
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
107
|
+
res.end(JSON.stringify({ id, events }));
|
|
108
|
+
} else {
|
|
109
|
+
res.writeHead(404);
|
|
110
|
+
res.end(JSON.stringify({ error: 'Not found' }));
|
|
111
|
+
}
|
|
103
112
|
} else {
|
|
104
113
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
105
|
-
res.end(JSON.stringify(
|
|
114
|
+
res.end(JSON.stringify({
|
|
115
|
+
id: exec.id,
|
|
116
|
+
roleId: exec.roleId,
|
|
117
|
+
task: exec.task,
|
|
118
|
+
status: exec.status,
|
|
119
|
+
sessionId: exec.sessionId,
|
|
120
|
+
createdAt: exec.createdAt,
|
|
121
|
+
}));
|
|
106
122
|
}
|
|
107
123
|
return;
|
|
108
124
|
}
|
|
109
125
|
|
|
110
|
-
// GET /api/jobs/:id/history — internal only
|
|
111
|
-
const historyMatch =
|
|
126
|
+
// GET /api/jobs/:id/history — internal only
|
|
127
|
+
const historyMatch = reqPath.match(/^\/api\/jobs\/([^/]+)\/history$/);
|
|
112
128
|
if (method === 'GET' && historyMatch) {
|
|
113
|
-
const
|
|
114
|
-
const events = ActivityStream.readAll(
|
|
129
|
+
const id = historyMatch[1];
|
|
130
|
+
const events = ActivityStream.readAll(id);
|
|
115
131
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
116
132
|
res.end(JSON.stringify({ events }));
|
|
117
133
|
return;
|
|
118
134
|
}
|
|
119
135
|
|
|
120
|
-
// POST /api/jobs/:id/abort — abort
|
|
121
|
-
const abortMatch =
|
|
136
|
+
// POST /api/jobs/:id/abort — abort by execution ID or session ID
|
|
137
|
+
const abortMatch = reqPath.match(/^\/api\/jobs\/([^/]+)\/abort$/);
|
|
122
138
|
if (method === 'POST' && abortMatch) {
|
|
123
|
-
const
|
|
124
|
-
const success =
|
|
139
|
+
const id = abortMatch[1];
|
|
140
|
+
const success = executionManager.abortExecution(id) || executionManager.abortSession(id);
|
|
125
141
|
if (!success) {
|
|
126
142
|
res.writeHead(404);
|
|
127
|
-
res.end(JSON.stringify({ error: '
|
|
143
|
+
res.end(JSON.stringify({ error: 'Not found or not running' }));
|
|
128
144
|
} else {
|
|
129
145
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
130
|
-
res.end(JSON.stringify({ ok: true
|
|
146
|
+
res.end(JSON.stringify({ ok: true }));
|
|
131
147
|
}
|
|
132
148
|
return;
|
|
133
149
|
}
|
|
134
150
|
|
|
135
|
-
// All other /api/jobs/* endpoints → 410
|
|
136
151
|
res.writeHead(410);
|
|
137
|
-
res.end(JSON.stringify({ error: 'Use /api/sessions/* for client-facing operations.
|
|
152
|
+
res.end(JSON.stringify({ error: 'Use /api/sessions/* for client-facing operations.' }));
|
|
138
153
|
}
|
|
139
154
|
|
|
140
155
|
/* ─── POST /api/jobs ─────────────────────── */
|
|
@@ -146,12 +161,10 @@ function handleStartJob(body: Record<string, unknown>, res: ServerResponse): voi
|
|
|
146
161
|
const directive = body.directive as string;
|
|
147
162
|
const sourceRole = (body.sourceRole as string) || 'ceo';
|
|
148
163
|
const readOnly = body.readOnly === true;
|
|
149
|
-
const
|
|
150
|
-
const parentJobId = body.parentJobId as string | undefined;
|
|
164
|
+
const parentSessionId = body.parentSessionId as string | undefined;
|
|
151
165
|
const waveId = body.waveId as string | undefined;
|
|
152
166
|
const attachments = body.attachments as ImageAttachment[] | undefined;
|
|
153
167
|
|
|
154
|
-
// Wave shorthand — broadcast to C-level direct reports (optionally filtered)
|
|
155
168
|
if (type === 'wave') {
|
|
156
169
|
if (!directive) {
|
|
157
170
|
jsonResponse(res, 400, { error: 'directive is required for wave jobs' });
|
|
@@ -161,7 +174,6 @@ function handleStartJob(body: Record<string, unknown>, res: ServerResponse): voi
|
|
|
161
174
|
const orgTree = buildOrgTree(COMPANY_ROOT);
|
|
162
175
|
let cLevelRoles = getSubordinates(orgTree, 'ceo');
|
|
163
176
|
|
|
164
|
-
// Selective dispatch: filter by targetRoles if provided
|
|
165
177
|
const targetRoles = body.targetRoles as string[] | undefined;
|
|
166
178
|
if (targetRoles && Array.isArray(targetRoles) && targetRoles.length > 0) {
|
|
167
179
|
const allowed = new Set(targetRoles);
|
|
@@ -173,25 +185,19 @@ function handleStartJob(body: Record<string, unknown>, res: ServerResponse): voi
|
|
|
173
185
|
return;
|
|
174
186
|
}
|
|
175
187
|
|
|
176
|
-
// Resolve full targetRoles scope for re-dispatch filtering
|
|
177
|
-
// Include both the C-level roles AND any sub-roles from targetRoles
|
|
178
188
|
const fullTargetScope = targetRoles && targetRoles.length > 0 ? targetRoles : undefined;
|
|
179
189
|
|
|
180
|
-
|
|
181
|
-
const waveId = `wave-${Date.now()}`;
|
|
182
|
-
const jobIds: string[] = [];
|
|
190
|
+
const newWaveId = `wave-${Date.now()}`;
|
|
183
191
|
const sessionIds: string[] = [];
|
|
184
192
|
|
|
185
193
|
for (const cRole of cLevelRoles) {
|
|
186
|
-
// Create a Session for this role (D-014: Wave = Session batch creation)
|
|
187
194
|
const session = createSession(cRole, {
|
|
188
195
|
mode: 'do',
|
|
189
196
|
source: 'wave',
|
|
190
|
-
waveId,
|
|
197
|
+
waveId: newWaveId,
|
|
191
198
|
});
|
|
192
199
|
sessionIds.push(session.id);
|
|
193
200
|
|
|
194
|
-
// Add CEO directive as the first message in the session
|
|
195
201
|
const ceoMsg: Message = {
|
|
196
202
|
id: `msg-${Date.now()}-ceo-${cRole}`,
|
|
197
203
|
from: 'ceo',
|
|
@@ -203,22 +209,19 @@ function handleStartJob(body: Record<string, unknown>, res: ServerResponse): voi
|
|
|
203
209
|
};
|
|
204
210
|
addMessage(session.id, ceoMsg);
|
|
205
211
|
|
|
206
|
-
const
|
|
212
|
+
const exec = executionManager.startExecution({
|
|
207
213
|
type: 'wave',
|
|
208
214
|
roleId: cRole,
|
|
209
215
|
task: `[CEO Wave] ${directive}`,
|
|
210
216
|
sourceRole: 'ceo',
|
|
211
|
-
|
|
217
|
+
parentSessionId,
|
|
212
218
|
targetRoles: fullTargetScope,
|
|
213
|
-
sessionId: session.id,
|
|
219
|
+
sessionId: session.id,
|
|
214
220
|
attachments,
|
|
215
221
|
});
|
|
216
|
-
jobIds.push(job.id);
|
|
217
222
|
|
|
218
|
-
|
|
219
|
-
waveMultiplexer.registerJob(waveId, job);
|
|
223
|
+
waveMultiplexer.registerSession(newWaveId, exec);
|
|
220
224
|
|
|
221
|
-
// Add a role message (will be updated as execution progresses)
|
|
222
225
|
const roleMsg: Message = {
|
|
223
226
|
id: `msg-${Date.now() + 1}-role-${cRole}`,
|
|
224
227
|
from: 'role',
|
|
@@ -226,12 +229,11 @@ function handleStartJob(body: Record<string, unknown>, res: ServerResponse): voi
|
|
|
226
229
|
type: 'conversation',
|
|
227
230
|
status: 'streaming',
|
|
228
231
|
timestamp: new Date().toISOString(),
|
|
229
|
-
jobId: job.id,
|
|
230
232
|
};
|
|
231
233
|
addMessage(session.id, roleMsg, true);
|
|
232
234
|
}
|
|
233
235
|
|
|
234
|
-
jsonResponse(res, 200, {
|
|
236
|
+
jsonResponse(res, 200, { sessionIds, waveId: newWaveId });
|
|
235
237
|
return;
|
|
236
238
|
}
|
|
237
239
|
|
|
@@ -247,129 +249,113 @@ function handleStartJob(body: Record<string, unknown>, res: ServerResponse): voi
|
|
|
247
249
|
return;
|
|
248
250
|
}
|
|
249
251
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
attachments,
|
|
269
|
-
};
|
|
270
|
-
addMessage(session.id, ceoMsg);
|
|
271
|
-
}
|
|
252
|
+
const sessionSource: 'wave' | 'dispatch' = waveId ? 'wave' : 'dispatch';
|
|
253
|
+
const session = createSession(roleId, {
|
|
254
|
+
mode: readOnly ? 'talk' : 'do',
|
|
255
|
+
source: parentSessionId ? 'dispatch' : sessionSource,
|
|
256
|
+
...(waveId && { waveId }),
|
|
257
|
+
});
|
|
258
|
+
const sessionId = session.id;
|
|
259
|
+
|
|
260
|
+
const ceoMsg: Message = {
|
|
261
|
+
id: `msg-${Date.now()}-ceo`,
|
|
262
|
+
from: 'ceo',
|
|
263
|
+
content: task,
|
|
264
|
+
type: readOnly ? 'conversation' : 'directive',
|
|
265
|
+
status: 'done',
|
|
266
|
+
timestamp: new Date().toISOString(),
|
|
267
|
+
attachments,
|
|
268
|
+
};
|
|
269
|
+
addMessage(session.id, ceoMsg);
|
|
272
270
|
|
|
273
|
-
const
|
|
271
|
+
const exec = executionManager.startExecution({
|
|
274
272
|
type: readOnly ? 'consult' : 'assign',
|
|
275
273
|
roleId,
|
|
276
274
|
task,
|
|
277
275
|
sourceRole,
|
|
278
276
|
readOnly,
|
|
279
|
-
|
|
277
|
+
parentSessionId,
|
|
280
278
|
sessionId,
|
|
281
279
|
attachments,
|
|
282
280
|
});
|
|
283
281
|
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
readOnly: readOnly || undefined,
|
|
295
|
-
};
|
|
296
|
-
addMessage(sessionId, roleMsg, true);
|
|
297
|
-
}
|
|
282
|
+
const roleMsg: Message = {
|
|
283
|
+
id: `msg-${Date.now() + 1}-role`,
|
|
284
|
+
from: 'role',
|
|
285
|
+
content: '',
|
|
286
|
+
type: 'conversation',
|
|
287
|
+
status: 'streaming',
|
|
288
|
+
timestamp: new Date().toISOString(),
|
|
289
|
+
readOnly: readOnly || undefined,
|
|
290
|
+
};
|
|
291
|
+
addMessage(sessionId, roleMsg, true);
|
|
298
292
|
|
|
299
|
-
// Follow-up: append this job to the wave JSON so it persists across navigation
|
|
300
293
|
if (waveId) {
|
|
301
|
-
appendFollowUpToWave(waveId,
|
|
294
|
+
appendFollowUpToWave(waveId, sessionId, roleId, task);
|
|
302
295
|
}
|
|
303
296
|
|
|
304
|
-
jsonResponse(res, 200, {
|
|
297
|
+
jsonResponse(res, 200, { sessionId, ...(waveId && { waveId }) });
|
|
305
298
|
}
|
|
306
299
|
|
|
307
|
-
/* ─── Follow-up: wave tracking (delegated to wave-tracker service) ── */
|
|
308
|
-
|
|
309
300
|
/* ─── POST /api/waves/save ──────────────── */
|
|
310
301
|
|
|
311
302
|
function handleSaveWave(body: Record<string, unknown>, res: ServerResponse): void {
|
|
312
303
|
const directive = body.directive as string;
|
|
313
|
-
const
|
|
314
|
-
const sessionIds = body.sessionIds as string[] | undefined;
|
|
304
|
+
const sessionIds = (body.sessionIds ?? body.jobIds) as string[];
|
|
315
305
|
const waveId = body.waveId as string | undefined;
|
|
316
306
|
|
|
317
|
-
if (!directive || !
|
|
318
|
-
jsonResponse(res, 400, { error: 'directive and
|
|
307
|
+
if (!directive || !sessionIds || sessionIds.length === 0) {
|
|
308
|
+
jsonResponse(res, 400, { error: 'directive and sessionIds are required' });
|
|
319
309
|
return;
|
|
320
310
|
}
|
|
321
311
|
|
|
322
312
|
const now = new Date();
|
|
323
313
|
const dateStr = now.toISOString().slice(0, 10);
|
|
324
314
|
|
|
325
|
-
// Structured data for JSON replay
|
|
326
315
|
interface WaveRoleData {
|
|
327
316
|
roleId: string;
|
|
328
317
|
roleName: string;
|
|
329
|
-
|
|
318
|
+
sessionId: string;
|
|
330
319
|
status: WaveRoleStatus;
|
|
331
320
|
events: ReturnType<typeof ActivityStream.readAll>;
|
|
332
|
-
|
|
321
|
+
childSessions: Array<{ roleId: string; roleName: string; sessionId: string; status: WaveRoleStatus; events: ReturnType<typeof ActivityStream.readAll> }>;
|
|
333
322
|
}
|
|
334
323
|
const rolesData: WaveRoleData[] = [];
|
|
335
324
|
|
|
336
|
-
for (const
|
|
337
|
-
const events = ActivityStream.readAll(
|
|
338
|
-
const startEvent = events.find(e => e.type === '
|
|
325
|
+
for (const sid of sessionIds) {
|
|
326
|
+
const events = ActivityStream.readAll(sid);
|
|
327
|
+
const startEvent = events.find(e => e.type === 'msg:start');
|
|
339
328
|
const roleId = startEvent?.roleId ?? 'unknown';
|
|
340
329
|
const roleName = (startEvent?.data?.roleName as string) ?? roleId;
|
|
341
|
-
const doneEvent = events.find(e => e.type === '
|
|
342
|
-
const status: WaveRoleStatus = doneEvent ?
|
|
330
|
+
const doneEvent = events.find(e => e.type === 'msg:done' || e.type === 'msg:awaiting_input' || e.type === 'msg:error');
|
|
331
|
+
const status: WaveRoleStatus = doneEvent ? eventTypeToMessageStatus(doneEvent.type) as WaveRoleStatus : 'unknown';
|
|
343
332
|
|
|
344
|
-
|
|
345
|
-
const childJobs: WaveRoleData['childJobs'] = [];
|
|
333
|
+
const childSessions: WaveRoleData['childSessions'] = [];
|
|
346
334
|
for (const e of events) {
|
|
347
|
-
|
|
348
|
-
|
|
335
|
+
const childSessionId = e.data.childSessionId as string | undefined;
|
|
336
|
+
if (e.type === 'dispatch:start' && childSessionId) {
|
|
349
337
|
const targetRoleId = (e.data.targetRoleId as string) ?? 'unknown';
|
|
350
|
-
const childEvents = ActivityStream.readAll(
|
|
351
|
-
const childDone = childEvents.find(ce => ce.type === '
|
|
352
|
-
const childStatus: WaveRoleStatus = childDone ?
|
|
353
|
-
|
|
338
|
+
const childEvents = ActivityStream.readAll(childSessionId);
|
|
339
|
+
const childDone = childEvents.find(ce => ce.type === 'msg:done' || ce.type === 'msg:error' || ce.type === 'msg:awaiting_input');
|
|
340
|
+
const childStatus: WaveRoleStatus = childDone ? eventTypeToMessageStatus(childDone.type) as WaveRoleStatus : 'unknown';
|
|
341
|
+
childSessions.push({
|
|
354
342
|
roleId: targetRoleId,
|
|
355
|
-
roleName: (childEvents.find(ce => ce.type === '
|
|
356
|
-
|
|
343
|
+
roleName: (childEvents.find(ce => ce.type === 'msg:start')?.data?.roleName as string) ?? targetRoleId,
|
|
344
|
+
sessionId: childSessionId,
|
|
357
345
|
status: childStatus,
|
|
358
346
|
events: childEvents,
|
|
359
347
|
});
|
|
360
348
|
}
|
|
361
349
|
}
|
|
362
350
|
|
|
363
|
-
rolesData.push({ roleId, roleName,
|
|
351
|
+
rolesData.push({ roleId, roleName, sessionId: sid, status, events, childSessions });
|
|
364
352
|
}
|
|
365
353
|
|
|
366
|
-
// Write to operations/waves/
|
|
367
354
|
const wavesDir = path.join(COMPANY_ROOT, 'operations', 'waves');
|
|
368
355
|
if (!fs.existsSync(wavesDir)) {
|
|
369
356
|
fs.mkdirSync(wavesDir, { recursive: true });
|
|
370
357
|
}
|
|
371
358
|
|
|
372
|
-
// Dedup: if waveId matches an existing file, overwrite instead of creating new
|
|
373
359
|
let baseName: string;
|
|
374
360
|
if (waveId) {
|
|
375
361
|
const existing = fs.readdirSync(wavesDir).find(f => {
|
|
@@ -390,15 +376,13 @@ function handleSaveWave(body: Record<string, unknown>, res: ServerResponse): voi
|
|
|
390
376
|
id: baseName,
|
|
391
377
|
directive,
|
|
392
378
|
startedAt: now.toISOString(),
|
|
393
|
-
duration: 0,
|
|
379
|
+
duration: 0,
|
|
394
380
|
roles: rolesData,
|
|
395
|
-
// D-014: Session references for follow-up
|
|
396
381
|
...(waveId && { waveId }),
|
|
397
|
-
...(sessionIds
|
|
382
|
+
...(sessionIds.length > 0 && { sessionIds }),
|
|
398
383
|
};
|
|
399
384
|
fs.writeFileSync(jsonPath, JSON.stringify(waveJson, null, 2), 'utf-8');
|
|
400
385
|
|
|
401
|
-
// EC-012: Wave completion bonus (participating roles × 500 coins)
|
|
402
386
|
const roleCount = rolesData.length;
|
|
403
387
|
if (roleCount > 0) {
|
|
404
388
|
try {
|
|
@@ -409,21 +393,19 @@ function handleSaveWave(body: Record<string, unknown>, res: ServerResponse): voi
|
|
|
409
393
|
jsonResponse(res, 200, { ok: true, path: `operations/waves/${baseName}.json` });
|
|
410
394
|
}
|
|
411
395
|
|
|
412
|
-
/* ─── GET /api/waves/:waveId/stream
|
|
396
|
+
/* ─── GET /api/waves/:waveId/stream ── */
|
|
413
397
|
|
|
414
398
|
function handleWaveStream(waveId: string, url: string, res: ServerResponse, req: IncomingMessage): void {
|
|
415
399
|
const fromMatch = url.match(/[?&]from=(\d+)/);
|
|
416
400
|
const fromWaveSeq = fromMatch ? parseInt(fromMatch[1], 10) : 0;
|
|
417
401
|
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
if (jobIds.length === 0) {
|
|
402
|
+
const sessionIds = waveMultiplexer.getWaveSessionIds(waveId);
|
|
403
|
+
if (sessionIds.length === 0) {
|
|
421
404
|
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
422
|
-
res.end(JSON.stringify({ error: `No
|
|
405
|
+
res.end(JSON.stringify({ error: `No sessions found for wave: ${waveId}` }));
|
|
423
406
|
return;
|
|
424
407
|
}
|
|
425
408
|
|
|
426
|
-
// attach() handles everything: replay history + subscribe to live events
|
|
427
409
|
const client = waveMultiplexer.attach(waveId, res as any, fromWaveSeq);
|
|
428
410
|
|
|
429
411
|
req.on('close', () => {
|
|
@@ -433,7 +415,6 @@ function handleWaveStream(waveId: string, url: string, res: ServerResponse, req:
|
|
|
433
415
|
|
|
434
416
|
/* ═══════════════════════════════════════════════
|
|
435
417
|
Legacy /api/exec/* — kept for backward compat
|
|
436
|
-
Now internally delegates to JobManager where possible
|
|
437
418
|
═══════════════════════════════════════════════ */
|
|
438
419
|
|
|
439
420
|
/* ─── Body parser ────────────────────────────── */
|
|
@@ -466,9 +447,7 @@ function jsonResponse(res: ServerResponse, status: number, body: unknown): void
|
|
|
466
447
|
res.end(JSON.stringify(body));
|
|
467
448
|
}
|
|
468
449
|
|
|
469
|
-
/** SSE timeout: max duration for a single SSE connection (10 minutes) */
|
|
470
450
|
const SSE_TIMEOUT_MS = 10 * 60 * 1000;
|
|
471
|
-
/** SSE heartbeat interval (15 seconds) */
|
|
472
451
|
const SSE_HEARTBEAT_MS = 15 * 1000;
|
|
473
452
|
|
|
474
453
|
function startSSE(res: ServerResponse): void {
|
|
@@ -481,7 +460,6 @@ function startSSE(res: ServerResponse): void {
|
|
|
481
460
|
res.flushHeaders();
|
|
482
461
|
}
|
|
483
462
|
|
|
484
|
-
/** Start SSE heartbeat + timeout. Returns cleanup function. */
|
|
485
463
|
function startSSELifecycle(res: ServerResponse, onTimeout: () => void): () => void {
|
|
486
464
|
const heartbeat = setInterval(() => {
|
|
487
465
|
if (res.destroyed || res.writableEnded) {
|
|
@@ -507,7 +485,6 @@ function startSSELifecycle(res: ServerResponse, onTimeout: () => void): () => vo
|
|
|
507
485
|
}
|
|
508
486
|
|
|
509
487
|
/* ─── POST /api/exec/assign ──────────────────── */
|
|
510
|
-
/* Now delegates to JobManager, streams events back via SSE for backward compat */
|
|
511
488
|
|
|
512
489
|
function handleAssign(body: Record<string, unknown>, req: IncomingMessage, res: ServerResponse): void {
|
|
513
490
|
const roleId = body.roleId as string;
|
|
@@ -529,18 +506,23 @@ function handleAssign(body: Record<string, unknown>, req: IncomingMessage, res:
|
|
|
529
506
|
return;
|
|
530
507
|
}
|
|
531
508
|
|
|
532
|
-
|
|
533
|
-
const
|
|
509
|
+
const session = createSession(roleId, { mode: readOnly ? 'talk' : 'do' });
|
|
510
|
+
const exec = executionManager.startExecution({
|
|
511
|
+
type: 'assign',
|
|
512
|
+
roleId,
|
|
513
|
+
task,
|
|
514
|
+
sourceRole,
|
|
515
|
+
readOnly,
|
|
516
|
+
sessionId: session.id,
|
|
517
|
+
});
|
|
534
518
|
|
|
535
|
-
// Bridge: stream job events as legacy SSE format
|
|
536
519
|
startSSE(res);
|
|
537
|
-
sendSSE(res, 'start', { id:
|
|
520
|
+
sendSSE(res, 'start', { id: exec.id, roleId, task, sourceRole });
|
|
538
521
|
|
|
539
522
|
const cleanupLifecycle = startSSELifecycle(res, () => {
|
|
540
|
-
roleStatus.set(roleId, 'idle');
|
|
541
523
|
sendSSE(res, 'error', { message: 'SSE timeout — connection forcibly closed after 10 minutes' });
|
|
542
524
|
if (!res.writableEnded) res.end();
|
|
543
|
-
|
|
525
|
+
exec.stream.unsubscribe(subscriber);
|
|
544
526
|
});
|
|
545
527
|
|
|
546
528
|
const subscriber = (event: ActivityEvent) => {
|
|
@@ -555,7 +537,7 @@ function handleAssign(body: Record<string, unknown>, req: IncomingMessage, res:
|
|
|
555
537
|
sendSSE(res, 'tool', { name: event.data.name, input: event.data.input });
|
|
556
538
|
break;
|
|
557
539
|
case 'dispatch:start':
|
|
558
|
-
sendSSE(res, 'dispatch', { roleId: event.data.targetRoleId, task: event.data.task,
|
|
540
|
+
sendSSE(res, 'dispatch', { roleId: event.data.targetRoleId, task: event.data.task, childSessionId: event.data.childSessionId });
|
|
559
541
|
break;
|
|
560
542
|
case 'turn:complete':
|
|
561
543
|
sendSSE(res, 'turn', { turn: event.data.turn });
|
|
@@ -563,31 +545,29 @@ function handleAssign(body: Record<string, unknown>, req: IncomingMessage, res:
|
|
|
563
545
|
case 'stderr':
|
|
564
546
|
sendSSE(res, 'stderr', { message: event.data.message });
|
|
565
547
|
break;
|
|
566
|
-
case '
|
|
548
|
+
case 'msg:awaiting_input':
|
|
567
549
|
sendSSE(res, 'awaiting_input', { question: event.data.question, targetRole: event.data.targetRole, reason: event.data.reason });
|
|
568
550
|
break;
|
|
569
|
-
case '
|
|
551
|
+
case 'msg:done':
|
|
570
552
|
cleanupLifecycle();
|
|
571
553
|
sendSSE(res, 'done', event.data);
|
|
572
554
|
if (!res.writableEnded) res.end();
|
|
573
|
-
|
|
555
|
+
exec.stream.unsubscribe(subscriber);
|
|
574
556
|
break;
|
|
575
|
-
case '
|
|
557
|
+
case 'msg:error':
|
|
576
558
|
cleanupLifecycle();
|
|
577
559
|
sendSSE(res, 'error', { message: event.data.message });
|
|
578
560
|
if (!res.writableEnded) res.end();
|
|
579
|
-
|
|
561
|
+
exec.stream.unsubscribe(subscriber);
|
|
580
562
|
break;
|
|
581
563
|
}
|
|
582
564
|
};
|
|
583
565
|
|
|
584
|
-
|
|
566
|
+
exec.stream.subscribe(subscriber);
|
|
585
567
|
|
|
586
|
-
// Client disconnect → unsubscribe only (job keeps running!)
|
|
587
568
|
req.on('close', () => {
|
|
588
569
|
cleanupLifecycle();
|
|
589
|
-
|
|
590
|
-
roleStatus.set(roleId, 'idle');
|
|
570
|
+
exec.stream.unsubscribe(subscriber);
|
|
591
571
|
});
|
|
592
572
|
}
|
|
593
573
|
|
|
@@ -604,7 +584,6 @@ function handleWave(body: Record<string, unknown>, req: IncomingMessage, res: Se
|
|
|
604
584
|
const orgTree = buildOrgTree(COMPANY_ROOT);
|
|
605
585
|
let cLevelRoles = getSubordinates(orgTree, 'ceo');
|
|
606
586
|
|
|
607
|
-
// Selective dispatch: filter by targetRoles if provided
|
|
608
587
|
const targetRoles = body.targetRoles as string[] | undefined;
|
|
609
588
|
if (targetRoles && Array.isArray(targetRoles) && targetRoles.length > 0) {
|
|
610
589
|
const allowed = new Set(targetRoles);
|
|
@@ -616,36 +595,35 @@ function handleWave(body: Record<string, unknown>, req: IncomingMessage, res: Se
|
|
|
616
595
|
return;
|
|
617
596
|
}
|
|
618
597
|
|
|
619
|
-
// Resolve full targetRoles scope for re-dispatch filtering
|
|
620
598
|
const fullTargetScope = targetRoles && targetRoles.length > 0 ? targetRoles : undefined;
|
|
621
599
|
|
|
622
|
-
|
|
623
|
-
const jobs: Job[] = [];
|
|
600
|
+
const executions: Execution[] = [];
|
|
624
601
|
for (const cRole of cLevelRoles) {
|
|
625
|
-
const
|
|
602
|
+
const session = createSession(cRole, { mode: 'do' });
|
|
603
|
+
const exec = executionManager.startExecution({
|
|
626
604
|
type: 'wave',
|
|
627
605
|
roleId: cRole,
|
|
628
606
|
task: `[CEO Wave] ${directive}`,
|
|
629
607
|
sourceRole: 'ceo',
|
|
630
608
|
targetRoles: fullTargetScope,
|
|
609
|
+
sessionId: session.id,
|
|
631
610
|
});
|
|
632
|
-
|
|
611
|
+
executions.push(exec);
|
|
633
612
|
}
|
|
634
613
|
|
|
635
|
-
// Bridge: stream ALL job events as SSE, close when all done
|
|
636
614
|
startSSE(res);
|
|
637
615
|
sendSSE(res, 'start', {
|
|
638
|
-
ids:
|
|
616
|
+
ids: executions.map((e) => e.id),
|
|
639
617
|
directive,
|
|
640
618
|
targetRoles: cLevelRoles,
|
|
641
619
|
});
|
|
642
620
|
|
|
643
621
|
let doneCount = 0;
|
|
644
|
-
const subscribers: Array<{
|
|
622
|
+
const subscribers: Array<{ exec: Execution; sub: ActivitySubscriber }> = [];
|
|
645
623
|
|
|
646
|
-
for (const
|
|
624
|
+
for (const exec of executions) {
|
|
647
625
|
const subscriber: ActivitySubscriber = (event: ActivityEvent) => {
|
|
648
|
-
const rolePrefix =
|
|
626
|
+
const rolePrefix = exec.roleId;
|
|
649
627
|
switch (event.type) {
|
|
650
628
|
case 'text':
|
|
651
629
|
sendSSE(res, 'output', { roleId: rolePrefix, text: event.data.text });
|
|
@@ -657,7 +635,7 @@ function handleWave(body: Record<string, unknown>, req: IncomingMessage, res: Se
|
|
|
657
635
|
sendSSE(res, 'tool', { roleId: rolePrefix, name: event.data.name, input: event.data.input });
|
|
658
636
|
break;
|
|
659
637
|
case 'dispatch:start':
|
|
660
|
-
sendSSE(res, 'dispatch', { roleId: rolePrefix, targetRoleId: event.data.targetRoleId, task: event.data.task,
|
|
638
|
+
sendSSE(res, 'dispatch', { roleId: rolePrefix, targetRoleId: event.data.targetRoleId, task: event.data.task, childSessionId: event.data.childSessionId });
|
|
661
639
|
break;
|
|
662
640
|
case 'turn:complete':
|
|
663
641
|
sendSSE(res, 'turn', { roleId: rolePrefix, turn: event.data.turn });
|
|
@@ -665,21 +643,21 @@ function handleWave(body: Record<string, unknown>, req: IncomingMessage, res: Se
|
|
|
665
643
|
case 'stderr':
|
|
666
644
|
sendSSE(res, 'stderr', { roleId: rolePrefix, message: event.data.message });
|
|
667
645
|
break;
|
|
668
|
-
case '
|
|
646
|
+
case 'msg:awaiting_input':
|
|
669
647
|
sendSSE(res, 'role:awaiting_input', { roleId: rolePrefix, question: event.data.question, targetRole: event.data.targetRole, reason: event.data.reason });
|
|
670
648
|
break;
|
|
671
|
-
case '
|
|
649
|
+
case 'msg:done':
|
|
672
650
|
sendSSE(res, 'role:done', { roleId: rolePrefix, ...event.data });
|
|
673
651
|
doneCount++;
|
|
674
|
-
if (doneCount >=
|
|
652
|
+
if (doneCount >= executions.length) {
|
|
675
653
|
sendSSE(res, 'done', { directive, completedRoles: cLevelRoles });
|
|
676
654
|
res.end();
|
|
677
655
|
}
|
|
678
656
|
break;
|
|
679
|
-
case '
|
|
657
|
+
case 'msg:error':
|
|
680
658
|
sendSSE(res, 'role:error', { roleId: rolePrefix, message: event.data.message });
|
|
681
659
|
doneCount++;
|
|
682
|
-
if (doneCount >=
|
|
660
|
+
if (doneCount >= executions.length) {
|
|
683
661
|
sendSSE(res, 'done', { directive, completedRoles: cLevelRoles });
|
|
684
662
|
res.end();
|
|
685
663
|
}
|
|
@@ -687,14 +665,13 @@ function handleWave(body: Record<string, unknown>, req: IncomingMessage, res: Se
|
|
|
687
665
|
}
|
|
688
666
|
};
|
|
689
667
|
|
|
690
|
-
|
|
691
|
-
subscribers.push({
|
|
668
|
+
exec.stream.subscribe(subscriber);
|
|
669
|
+
subscribers.push({ exec, sub: subscriber });
|
|
692
670
|
}
|
|
693
671
|
|
|
694
|
-
// Client disconnect → unsubscribe all (jobs keep running)
|
|
695
672
|
req.on('close', () => {
|
|
696
|
-
for (const {
|
|
697
|
-
|
|
673
|
+
for (const { exec, sub } of subscribers) {
|
|
674
|
+
exec.stream.unsubscribe(sub);
|
|
698
675
|
}
|
|
699
676
|
});
|
|
700
677
|
}
|
|
@@ -704,82 +681,22 @@ function handleWave(body: Record<string, unknown>, req: IncomingMessage, res: Se
|
|
|
704
681
|
function handleStatus(res: ServerResponse): void {
|
|
705
682
|
const statuses: Record<string, string> = {};
|
|
706
683
|
|
|
707
|
-
|
|
708
|
-
const
|
|
709
|
-
|
|
710
|
-
statuses[activity.roleId] = activity.status;
|
|
684
|
+
const activeExecs = executionManager.listExecutions({ active: true });
|
|
685
|
+
for (const exec of activeExecs) {
|
|
686
|
+
statuses[exec.roleId] = messageStatusToRoleStatus(exec.status as MessageStatus);
|
|
711
687
|
}
|
|
712
688
|
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
const memoryWorking = new Set<string>();
|
|
719
|
-
for (const [rid, st] of roleStatus.entries()) {
|
|
720
|
-
if (isRoleActive(st as RoleStatus)) memoryWorking.add(rid);
|
|
721
|
-
}
|
|
722
|
-
|
|
723
|
-
// 3. Stale cleanup: active in file but NOT in JobManager AND NOT in memory → done
|
|
724
|
-
for (const roleId of Object.keys(statuses)) {
|
|
725
|
-
const s = statuses[roleId];
|
|
726
|
-
if (isRoleActive(s as RoleStatus) && !activeRoles.has(roleId) && !memoryWorking.has(roleId)) {
|
|
727
|
-
statuses[roleId] = 'done';
|
|
728
|
-
completeActivity(roleId);
|
|
729
|
-
}
|
|
730
|
-
}
|
|
731
|
-
|
|
732
|
-
// 4. Active jobs override — use jobStatusToRoleStatus() for canonical mapping
|
|
733
|
-
for (const job of activeJobs) {
|
|
734
|
-
const mappedStatus = jobStatusToRoleStatus(job.status as JobStatus);
|
|
735
|
-
// running job → 'working' always wins over awaiting_input
|
|
736
|
-
if (statuses[job.roleId] === 'working' && mappedStatus === 'awaiting_input') continue;
|
|
737
|
-
statuses[job.roleId] = mappedStatus;
|
|
738
|
-
}
|
|
739
|
-
|
|
740
|
-
// 5. In-memory working (chat streaming) also overrides
|
|
741
|
-
for (const rid of memoryWorking) {
|
|
742
|
-
statuses[rid] = 'working';
|
|
743
|
-
}
|
|
744
|
-
|
|
745
|
-
const activeExecs = activeJobs.map((j) => ({
|
|
746
|
-
id: j.id,
|
|
747
|
-
roleId: j.roleId,
|
|
748
|
-
task: j.task,
|
|
749
|
-
startedAt: j.createdAt,
|
|
689
|
+
const activeExecutions = activeExecs.map((e) => ({
|
|
690
|
+
id: e.id,
|
|
691
|
+
roleId: e.roleId,
|
|
692
|
+
task: e.task,
|
|
693
|
+
startedAt: e.createdAt,
|
|
750
694
|
}));
|
|
751
695
|
|
|
752
|
-
jsonResponse(res, 200, { statuses, activeExecutions
|
|
696
|
+
jsonResponse(res, 200, { statuses, activeExecutions });
|
|
753
697
|
}
|
|
754
698
|
|
|
755
|
-
/* ─── POST /api/exec/activity ────────────────── */
|
|
756
|
-
|
|
757
|
-
function handleActivity(body: Record<string, unknown>, res: ServerResponse): void {
|
|
758
|
-
const roleId = body.roleId as string;
|
|
759
|
-
const action = body.action as string;
|
|
760
|
-
|
|
761
|
-
if (!roleId || !action) {
|
|
762
|
-
jsonResponse(res, 400, { error: 'roleId and action are required' });
|
|
763
|
-
return;
|
|
764
|
-
}
|
|
765
|
-
|
|
766
|
-
switch (action) {
|
|
767
|
-
case 'start':
|
|
768
|
-
setActivity(roleId, (body.task as string) ?? '');
|
|
769
|
-
break;
|
|
770
|
-
case 'update':
|
|
771
|
-
updateActivity(roleId, (body.output as string) ?? '');
|
|
772
|
-
break;
|
|
773
|
-
case 'complete':
|
|
774
|
-
completeActivity(roleId);
|
|
775
|
-
break;
|
|
776
|
-
default:
|
|
777
|
-
jsonResponse(res, 400, { error: `Unknown action: ${action}` });
|
|
778
|
-
return;
|
|
779
|
-
}
|
|
780
699
|
|
|
781
|
-
jsonResponse(res, 200, { ok: true });
|
|
782
|
-
}
|
|
783
700
|
|
|
784
701
|
/* ─── POST /api/exec/session/{id}/message ──── */
|
|
785
702
|
|
|
@@ -799,14 +716,12 @@ function handleSessionMessage(
|
|
|
799
716
|
const mode = (body.mode as 'talk' | 'do') ?? session.mode;
|
|
800
717
|
const attachments = body.attachments as ImageAttachment[] | undefined;
|
|
801
718
|
|
|
802
|
-
// Allow empty content if there are attachments
|
|
803
719
|
if (!content && (!attachments || attachments.length === 0)) {
|
|
804
720
|
jsonResponse(res, 400, { error: 'content or attachments required' });
|
|
805
721
|
return;
|
|
806
722
|
}
|
|
807
723
|
|
|
808
|
-
|
|
809
|
-
const MAX_FILE_SIZE = 5 * 1024 * 1024; // 5MB
|
|
724
|
+
const MAX_FILE_SIZE = 5 * 1024 * 1024;
|
|
810
725
|
const SUPPORTED_TYPES = ['image/png', 'image/jpeg', 'image/gif', 'image/webp'];
|
|
811
726
|
if (attachments && attachments.length > 0) {
|
|
812
727
|
for (const att of attachments) {
|
|
@@ -814,7 +729,6 @@ function handleSessionMessage(
|
|
|
814
729
|
jsonResponse(res, 400, { error: `Unsupported image type: ${att.mediaType}` });
|
|
815
730
|
return;
|
|
816
731
|
}
|
|
817
|
-
// Approximate size check (base64 is ~33% larger than binary)
|
|
818
732
|
const approximateSize = (att.data.length * 3) / 4;
|
|
819
733
|
if (approximateSize > MAX_FILE_SIZE) {
|
|
820
734
|
jsonResponse(res, 400, { error: `File too large: ${att.name}. Max 5MB.` });
|
|
@@ -861,103 +775,60 @@ function handleSessionMessage(
|
|
|
861
775
|
startSSE(res);
|
|
862
776
|
sendSSE(res, 'session', { sessionId, ceoMessageId: ceoMsg.id, roleMessageId: roleMsg.id });
|
|
863
777
|
|
|
864
|
-
// SSE lifecycle: heartbeat keeps connection alive, timeout prevents stuck connections
|
|
865
778
|
const cleanupSSELifecycle = startSSELifecycle(res, () => {
|
|
866
|
-
// Timeout reached — force close the SSE connection
|
|
867
779
|
cleanupChildSubscriptions();
|
|
868
780
|
updateMessage(sessionId, roleMsg.id, { status: 'error' });
|
|
869
|
-
roleStatus.set(roleId, 'idle');
|
|
870
|
-
completeActivity(roleId);
|
|
871
781
|
sendSSE(res, 'error', { message: 'SSE timeout — connection forcibly closed after 10 minutes' });
|
|
872
782
|
if (!res.writableEnded) res.end();
|
|
873
783
|
handle.abort();
|
|
874
784
|
});
|
|
875
785
|
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
// Track child job subscriptions for cleanup
|
|
880
|
-
const childSubscriptions: Array<{ job: Job; subscriber: ActivitySubscriber }> = [];
|
|
881
|
-
const pendingDispatches = new Set<string>(); // roleIds we expect child jobs for
|
|
786
|
+
const childSubscriptions: Array<{ exec: Execution; subscriber: ActivitySubscriber }> = [];
|
|
787
|
+
const pendingDispatches = new Set<string>();
|
|
882
788
|
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
// Only match jobs for roles we dispatched to from this session
|
|
886
|
-
if (childJob.type !== 'assign') return;
|
|
789
|
+
const unwatchExecs = executionManager.onExecutionCreated((childExec) => {
|
|
790
|
+
if (childExec.type !== 'assign') return;
|
|
887
791
|
if (roleMsg.status !== 'streaming') return;
|
|
888
|
-
if (!pendingDispatches.has(
|
|
889
|
-
pendingDispatches.delete(
|
|
792
|
+
if (!pendingDispatches.has(childExec.roleId)) return;
|
|
793
|
+
pendingDispatches.delete(childExec.roleId);
|
|
890
794
|
|
|
891
795
|
const subscriber: ActivitySubscriber = (event) => {
|
|
892
796
|
switch (event.type) {
|
|
893
797
|
case 'text':
|
|
894
|
-
sendSSE(res, 'dispatch:progress', {
|
|
895
|
-
roleId: event.roleId,
|
|
896
|
-
type: 'text',
|
|
897
|
-
text: event.data.text,
|
|
898
|
-
});
|
|
798
|
+
sendSSE(res, 'dispatch:progress', { roleId: event.roleId, type: 'text', text: event.data.text });
|
|
899
799
|
break;
|
|
900
800
|
case 'thinking':
|
|
901
|
-
sendSSE(res, 'dispatch:progress', {
|
|
902
|
-
roleId: event.roleId,
|
|
903
|
-
type: 'thinking',
|
|
904
|
-
text: event.data.text,
|
|
905
|
-
});
|
|
801
|
+
sendSSE(res, 'dispatch:progress', { roleId: event.roleId, type: 'thinking', text: event.data.text });
|
|
906
802
|
break;
|
|
907
803
|
case 'tool:start':
|
|
908
|
-
sendSSE(res, 'dispatch:progress', {
|
|
909
|
-
roleId: event.roleId,
|
|
910
|
-
type: 'tool',
|
|
911
|
-
name: event.data.name,
|
|
912
|
-
input: event.data.input,
|
|
913
|
-
});
|
|
804
|
+
sendSSE(res, 'dispatch:progress', { roleId: event.roleId, type: 'tool', name: event.data.name, input: event.data.input });
|
|
914
805
|
break;
|
|
915
|
-
case '
|
|
916
|
-
sendSSE(res, 'dispatch:progress', {
|
|
917
|
-
roleId: event.roleId,
|
|
918
|
-
type: 'awaiting_input',
|
|
919
|
-
question: event.data.question,
|
|
920
|
-
targetRole: event.data.targetRole,
|
|
921
|
-
});
|
|
806
|
+
case 'msg:awaiting_input':
|
|
807
|
+
sendSSE(res, 'dispatch:progress', { roleId: event.roleId, type: 'awaiting_input', question: event.data.question, targetRole: event.data.targetRole });
|
|
922
808
|
break;
|
|
923
|
-
case '
|
|
924
|
-
sendSSE(res, 'dispatch:progress', {
|
|
925
|
-
|
|
926
|
-
type: 'done',
|
|
927
|
-
});
|
|
928
|
-
childJob.stream.unsubscribe(subscriber);
|
|
809
|
+
case 'msg:done':
|
|
810
|
+
sendSSE(res, 'dispatch:progress', { roleId: event.roleId, type: 'done' });
|
|
811
|
+
childExec.stream.unsubscribe(subscriber);
|
|
929
812
|
break;
|
|
930
|
-
case '
|
|
931
|
-
sendSSE(res, 'dispatch:progress', {
|
|
932
|
-
|
|
933
|
-
type: 'error',
|
|
934
|
-
message: event.data.message,
|
|
935
|
-
});
|
|
936
|
-
childJob.stream.unsubscribe(subscriber);
|
|
813
|
+
case 'msg:error':
|
|
814
|
+
sendSSE(res, 'dispatch:progress', { roleId: event.roleId, type: 'error', message: event.data.message });
|
|
815
|
+
childExec.stream.unsubscribe(subscriber);
|
|
937
816
|
break;
|
|
938
817
|
}
|
|
939
818
|
};
|
|
940
|
-
|
|
941
|
-
childSubscriptions.push({
|
|
819
|
+
childExec.stream.subscribe(subscriber);
|
|
820
|
+
childSubscriptions.push({ exec: childExec, subscriber });
|
|
942
821
|
});
|
|
943
822
|
|
|
944
|
-
// Build team status from active jobs using schema helpers
|
|
945
823
|
const teamStatus: TeamStatus = {};
|
|
946
|
-
for (const
|
|
947
|
-
const mapped =
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
teamStatus[j.roleId] = { status: mapped, task: j.task };
|
|
951
|
-
}
|
|
952
|
-
// Also include roleStatus for roles working via session (not tracked as jobs)
|
|
953
|
-
for (const [rid, status] of roleStatus) {
|
|
954
|
-
if (isRoleActive(status as RoleStatus) && rid !== roleId && !teamStatus[rid]) {
|
|
955
|
-
teamStatus[rid] = { status: status as RoleStatus };
|
|
956
|
-
}
|
|
824
|
+
for (const e of executionManager.listExecutions({ active: true })) {
|
|
825
|
+
const mapped = messageStatusToRoleStatus(e.status as MessageStatus);
|
|
826
|
+
if (teamStatus[e.roleId]?.status === 'working' && mapped === 'awaiting_input') continue;
|
|
827
|
+
teamStatus[e.roleId] = { status: mapped, task: e.task };
|
|
957
828
|
}
|
|
958
829
|
|
|
959
830
|
const handle = getRunner().execute(
|
|
960
|
-
{ companyRoot: COMPANY_ROOT, roleId, task: fullTask, sourceRole: 'ceo', orgTree, readOnly, model: orgTree.nodes.get(roleId)?.model, attachments, teamStatus },
|
|
831
|
+
{ companyRoot: COMPANY_ROOT, roleId, task: fullTask, sourceRole: 'ceo', orgTree, readOnly, model: orgTree.nodes.get(roleId)?.model, attachments, teamStatus, sessionId },
|
|
961
832
|
{
|
|
962
833
|
onText: (text) => {
|
|
963
834
|
roleMsg.content += text;
|
|
@@ -971,8 +842,6 @@ function handleSessionMessage(
|
|
|
971
842
|
sendSSE(res, 'tool', { name, input: input ? summarizeInput(input) : undefined });
|
|
972
843
|
},
|
|
973
844
|
onDispatch: (subRoleId, subTask) => {
|
|
974
|
-
roleStatus.set(subRoleId, 'working');
|
|
975
|
-
setActivity(subRoleId, subTask);
|
|
976
845
|
pendingDispatches.add(subRoleId);
|
|
977
846
|
sendSSE(res, 'dispatch', { roleId: subRoleId, task: subTask });
|
|
978
847
|
},
|
|
@@ -986,9 +855,9 @@ function handleSessionMessage(
|
|
|
986
855
|
);
|
|
987
856
|
|
|
988
857
|
const cleanupChildSubscriptions = () => {
|
|
989
|
-
|
|
990
|
-
for (const {
|
|
991
|
-
|
|
858
|
+
unwatchExecs();
|
|
859
|
+
for (const { exec, subscriber } of childSubscriptions) {
|
|
860
|
+
exec.stream.unsubscribe(subscriber);
|
|
992
861
|
}
|
|
993
862
|
childSubscriptions.length = 0;
|
|
994
863
|
};
|
|
@@ -1003,12 +872,6 @@ function handleSessionMessage(
|
|
|
1003
872
|
turns: result.turns,
|
|
1004
873
|
tokens: result.totalTokens,
|
|
1005
874
|
});
|
|
1006
|
-
roleStatus.set(roleId, 'idle');
|
|
1007
|
-
completeActivity(roleId);
|
|
1008
|
-
for (const d of result.dispatches) {
|
|
1009
|
-
roleStatus.set(d.roleId, 'idle');
|
|
1010
|
-
completeActivity(d.roleId);
|
|
1011
|
-
}
|
|
1012
875
|
sendSSE(res, 'done', {
|
|
1013
876
|
roleMessageId: roleMsg.id,
|
|
1014
877
|
output: roleMsg.content.slice(-500),
|
|
@@ -1021,8 +884,6 @@ function handleSessionMessage(
|
|
|
1021
884
|
cleanupSSELifecycle();
|
|
1022
885
|
cleanupChildSubscriptions();
|
|
1023
886
|
updateMessage(sessionId, roleMsg.id, { status: 'error' });
|
|
1024
|
-
roleStatus.set(roleId, 'idle');
|
|
1025
|
-
completeActivity(roleId);
|
|
1026
887
|
sendSSE(res, 'error', { message: err.message });
|
|
1027
888
|
if (!res.writableEnded) res.end();
|
|
1028
889
|
});
|
|
@@ -1033,8 +894,6 @@ function handleSessionMessage(
|
|
|
1033
894
|
if (roleMsg.status === 'streaming') {
|
|
1034
895
|
handle.abort();
|
|
1035
896
|
updateMessage(sessionId, roleMsg.id, { status: 'error' });
|
|
1036
|
-
roleStatus.set(roleId, 'idle');
|
|
1037
|
-
completeActivity(roleId);
|
|
1038
897
|
}
|
|
1039
898
|
});
|
|
1040
899
|
}
|