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.
@@ -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
- import { getAllActivities, setActivity, updateActivity, completeActivity } from '../services/activity-tracker.js';
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 { jobManager, type Job } from '../services/job-manager.js';
17
- import { type JobStatus, type RoleStatus, type WaveRoleStatus, type TeamStatus, isJobActive, isRoleActive, jobStatusToRoleStatus, eventTypeToJobStatus } from '../../../shared/types.js';
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
- /* ─── SSE-003: Auto-attach child dispatch jobs to wave multiplexer ── */
24
- jobManager.onJobCreated((job) => {
25
- waveMultiplexer.onJobCreated(job);
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
- /* ─── Active execution tracking (legacy, kept for /api/exec/status compat) ──── */
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/* — Job-based API
85
+ /api/jobs/* — Internal endpoints
84
86
  ═══════════════════════════════════════════════ */
85
87
 
86
88
  function handleJobsRequest(url: string, method: string, req: IncomingMessage, res: ServerResponse): void {
87
- const [path] = url.split('?');
89
+ const [reqPath] = url.split('?');
88
90
 
89
- // POST /api/jobs — start a new job (creates session + job)
90
- if (method === 'POST' && path === '/api/jobs') {
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 (used by dispatch bridge Python script)
96
- const jobMatch = path.match(/^\/api\/jobs\/([^/]+)$/);
97
+ // GET /api/jobs/:id — internal only
98
+ const jobMatch = reqPath.match(/^\/api\/jobs\/([^/]+)$/);
97
99
  if (method === 'GET' && jobMatch) {
98
- const jobId = jobMatch[1];
99
- const info = jobManager.getJobInfo(jobId);
100
- if (!info) {
101
- res.writeHead(404);
102
- res.end(JSON.stringify({ error: 'Job not found' }));
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(info));
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 (used by engine Python sub-processes)
111
- const historyMatch = path.match(/^\/api\/jobs\/([^/]+)\/history$/);
126
+ // GET /api/jobs/:id/history — internal only
127
+ const historyMatch = reqPath.match(/^\/api\/jobs\/([^/]+)\/history$/);
112
128
  if (method === 'GET' && historyMatch) {
113
- const jobId = historyMatch[1];
114
- const events = ActivityStream.readAll(jobId);
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 a running job directly by jobId
121
- const abortMatch = path.match(/^\/api\/jobs\/([^/]+)\/abort$/);
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 jobId = abortMatch[1];
124
- const success = jobManager.abortJob(jobId);
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: 'Job not found or not running' }));
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, jobId }));
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. /api/jobs/:id and /api/jobs/:id/history are internal only.' }));
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 targetRole = (body.targetRole as string) || 'cto';
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
- // D-014: Create Wave meta + Sessions for each target role
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 job = jobManager.startJob({
212
+ const exec = executionManager.startExecution({
207
213
  type: 'wave',
208
214
  roleId: cRole,
209
215
  task: `[CEO Wave] ${directive}`,
210
216
  sourceRole: 'ceo',
211
- parentJobId,
217
+ parentSessionId,
212
218
  targetRoles: fullTargetScope,
213
- sessionId: session.id, // D-014: link job to session
219
+ sessionId: session.id,
214
220
  attachments,
215
221
  });
216
- jobIds.push(job.id);
217
222
 
218
- // SSE-001: Register wave job with multiplexer
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, { jobIds, waveId, sessionIds });
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
- // D-014: Create/find session for CEO assigns (not for dispatch child jobs)
251
- let sessionId: string | undefined;
252
- if (sourceRole === 'ceo' && !parentJobId) {
253
- const session = createSession(roleId, {
254
- mode: readOnly ? 'talk' : 'do',
255
- source: waveId ? 'wave' : 'dispatch',
256
- ...(waveId && { waveId }),
257
- });
258
- sessionId = session.id;
259
-
260
- // Add CEO message
261
- const ceoMsg: Message = {
262
- id: `msg-${Date.now()}-ceo`,
263
- from: 'ceo',
264
- content: task,
265
- type: readOnly ? 'conversation' : 'directive',
266
- status: 'done',
267
- timestamp: new Date().toISOString(),
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 job = jobManager.startJob({
271
+ const exec = executionManager.startExecution({
274
272
  type: readOnly ? 'consult' : 'assign',
275
273
  roleId,
276
274
  task,
277
275
  sourceRole,
278
276
  readOnly,
279
- parentJobId,
277
+ parentSessionId,
280
278
  sessionId,
281
279
  attachments,
282
280
  });
283
281
 
284
- // D-014: Add role message linked to job
285
- if (sessionId) {
286
- const roleMsg: Message = {
287
- id: `msg-${Date.now() + 1}-role`,
288
- from: 'role',
289
- content: '',
290
- type: 'conversation',
291
- status: 'streaming',
292
- timestamp: new Date().toISOString(),
293
- jobId: job.id,
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, job.id, roleId, task, sessionId);
294
+ appendFollowUpToWave(waveId, sessionId, roleId, task);
302
295
  }
303
296
 
304
- jsonResponse(res, 200, { jobId: job.id, ...(sessionId && { sessionId }), ...(waveId && { waveId }) });
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 jobIds = body.jobIds as string[];
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 || !jobIds || jobIds.length === 0) {
318
- jsonResponse(res, 400, { error: 'directive and jobIds are required' });
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
- jobId: string;
318
+ sessionId: string;
330
319
  status: WaveRoleStatus;
331
320
  events: ReturnType<typeof ActivityStream.readAll>;
332
- childJobs: Array<{ roleId: string; roleName: string; jobId: string; status: WaveRoleStatus; events: ReturnType<typeof ActivityStream.readAll> }>;
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 jobId of jobIds) {
337
- const events = ActivityStream.readAll(jobId);
338
- const startEvent = events.find(e => e.type === 'job:start');
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 === 'job:done' || e.type === 'job:awaiting_input' || e.type === 'job:error');
342
- const status: WaveRoleStatus = doneEvent ? eventTypeToJobStatus(doneEvent.type) as WaveRoleStatus : 'unknown';
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
- // Collect child jobs (dispatched sub-roles)
345
- const childJobs: WaveRoleData['childJobs'] = [];
333
+ const childSessions: WaveRoleData['childSessions'] = [];
346
334
  for (const e of events) {
347
- if (e.type === 'dispatch:start' && e.data.childJobId) {
348
- const childJobId = e.data.childJobId as string;
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(childJobId);
351
- const childDone = childEvents.find(ce => ce.type === 'job:done' || ce.type === 'job:error' || ce.type === 'job:awaiting_input');
352
- const childStatus: WaveRoleStatus = childDone ? eventTypeToJobStatus(childDone.type) as WaveRoleStatus : 'unknown';
353
- childJobs.push({
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 === 'job:start')?.data?.roleName as string) ?? targetRoleId,
356
- jobId: childJobId,
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, jobId, status, events, childJobs });
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, // Could be computed from events
379
+ duration: 0,
394
380
  roles: rolesData,
395
- // D-014: Session references for follow-up
396
381
  ...(waveId && { waveId }),
397
- ...(sessionIds && sessionIds.length > 0 && { 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 — SSE multiplexed wave stream (SSE-002) ── */
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
- // Check if wave has any registered jobs
419
- const jobIds = waveMultiplexer.getWaveJobIds(waveId);
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 jobs found for wave: ${waveId}` }));
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
- // Start job via JobManager (JobManager is source of truth for job status)
533
- const job = jobManager.startJob({ type: 'assign', roleId, task, sourceRole, readOnly });
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: job.id, roleId, task, sourceRole });
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
- job.stream.unsubscribe(subscriber);
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, childJobId: event.data.childJobId });
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 'job:awaiting_input':
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 'job:done':
551
+ case 'msg:done':
570
552
  cleanupLifecycle();
571
553
  sendSSE(res, 'done', event.data);
572
554
  if (!res.writableEnded) res.end();
573
- job.stream.unsubscribe(subscriber);
555
+ exec.stream.unsubscribe(subscriber);
574
556
  break;
575
- case 'job:error':
557
+ case 'msg:error':
576
558
  cleanupLifecycle();
577
559
  sendSSE(res, 'error', { message: event.data.message });
578
560
  if (!res.writableEnded) res.end();
579
- job.stream.unsubscribe(subscriber);
561
+ exec.stream.unsubscribe(subscriber);
580
562
  break;
581
563
  }
582
564
  };
583
565
 
584
- job.stream.subscribe(subscriber);
566
+ exec.stream.subscribe(subscriber);
585
567
 
586
- // Client disconnect → unsubscribe only (job keeps running!)
587
568
  req.on('close', () => {
588
569
  cleanupLifecycle();
589
- job.stream.unsubscribe(subscriber);
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
- // Start a job for EACH C-level role
623
- const jobs: Job[] = [];
600
+ const executions: Execution[] = [];
624
601
  for (const cRole of cLevelRoles) {
625
- const job = jobManager.startJob({
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
- jobs.push(job);
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: jobs.map((j) => j.id),
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<{ job: Job; sub: ActivitySubscriber }> = [];
622
+ const subscribers: Array<{ exec: Execution; sub: ActivitySubscriber }> = [];
645
623
 
646
- for (const job of jobs) {
624
+ for (const exec of executions) {
647
625
  const subscriber: ActivitySubscriber = (event: ActivityEvent) => {
648
- const rolePrefix = job.roleId;
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, childJobId: event.data.childJobId });
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 'job:awaiting_input':
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 'job:done':
649
+ case 'msg:done':
672
650
  sendSSE(res, 'role:done', { roleId: rolePrefix, ...event.data });
673
651
  doneCount++;
674
- if (doneCount >= jobs.length) {
652
+ if (doneCount >= executions.length) {
675
653
  sendSSE(res, 'done', { directive, completedRoles: cLevelRoles });
676
654
  res.end();
677
655
  }
678
656
  break;
679
- case 'job:error':
657
+ case 'msg:error':
680
658
  sendSSE(res, 'role:error', { roleId: rolePrefix, message: event.data.message });
681
659
  doneCount++;
682
- if (doneCount >= jobs.length) {
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
- job.stream.subscribe(subscriber);
691
- subscribers.push({ job, sub: subscriber });
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 { job, sub } of subscribers) {
697
- job.stream.unsubscribe(sub);
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
- // 1. File-backed activity tracker (baseline)
708
- const fileActivities = getAllActivities();
709
- for (const activity of fileActivities) {
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
- // 2. JobManager active jobs (isJobActive: running | awaiting_input)
714
- const activeJobs = jobManager.listJobs({ active: true });
715
- const activeRoles = new Set(activeJobs.map(j => j.roleId));
716
-
717
- // 2b. In-memory roleStatus (includes chat streaming sessions, not just jobs)
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: activeExecs });
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
- // Validate attachments if present
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
- roleStatus.set(roleId, 'working');
877
- setActivity(roleId, content.slice(0, 80));
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
- // Watch for child jobs created via dispatch bridge
884
- const unwatchJobs = jobManager.onJobCreated((childJob) => {
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(childJob.roleId)) return;
889
- pendingDispatches.delete(childJob.roleId);
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 'job:awaiting_input':
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 'job:done':
924
- sendSSE(res, 'dispatch:progress', {
925
- roleId: event.roleId,
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 'job:error':
931
- sendSSE(res, 'dispatch:progress', {
932
- roleId: event.roleId,
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
- childJob.stream.subscribe(subscriber);
941
- childSubscriptions.push({ job: childJob, subscriber });
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 j of jobManager.listJobs({ active: true })) {
947
- const mapped = jobStatusToRoleStatus(j.status as JobStatus);
948
- // 'working' takes priority over 'awaiting_input' for same role
949
- if (teamStatus[j.roleId]?.status === 'working' && mapped === 'awaiting_input') continue;
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
- unwatchJobs();
990
- for (const { job, subscriber } of childSubscriptions) {
991
- job.stream.unsubscribe(subscriber);
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
  }