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