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.
@@ -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
 
@@ -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/* — Job-based API
85
+ /api/jobs/* — Internal endpoints
90
86
  ═══════════════════════════════════════════════ */
91
87
 
92
88
  function handleJobsRequest(url: string, method: string, req: IncomingMessage, res: ServerResponse): void {
93
- const [path] = url.split('?');
89
+ const [reqPath] = url.split('?');
94
90
 
95
- // POST /api/jobs — start a new job (creates session + job)
96
- if (method === 'POST' && path === '/api/jobs') {
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 (used by dispatch bridge Python script)
102
- const jobMatch = path.match(/^\/api\/jobs\/([^/]+)$/);
97
+ // GET /api/jobs/:id — internal only
98
+ const jobMatch = reqPath.match(/^\/api\/jobs\/([^/]+)$/);
103
99
  if (method === 'GET' && jobMatch) {
104
- const jobId = jobMatch[1];
105
- const info = jobManager.getJobInfo(jobId);
106
- if (!info) {
107
- res.writeHead(404);
108
- 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
+ }
109
112
  } else {
110
113
  res.writeHead(200, { 'Content-Type': 'application/json' });
111
- 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
+ }));
112
122
  }
113
123
  return;
114
124
  }
115
125
 
116
- // GET /api/jobs/:id/history — internal only (used by engine Python sub-processes)
117
- const historyMatch = path.match(/^\/api\/jobs\/([^/]+)\/history$/);
126
+ // GET /api/jobs/:id/history — internal only
127
+ const historyMatch = reqPath.match(/^\/api\/jobs\/([^/]+)\/history$/);
118
128
  if (method === 'GET' && historyMatch) {
119
- const jobId = historyMatch[1];
120
- const events = ActivityStream.readAll(jobId);
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 a running job directly by jobId
127
- 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$/);
128
138
  if (method === 'POST' && abortMatch) {
129
- const jobId = abortMatch[1];
130
- const success = jobManager.abortJob(jobId);
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: 'Job not found or not running' }));
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, jobId }));
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. /api/jobs/:id and /api/jobs/:id/history are internal only.' }));
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 targetRole = (body.targetRole as string) || 'cto';
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
- // D-014: Create Wave meta + Sessions for each target role
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 job = jobManager.startJob({
212
+ const exec = executionManager.startExecution({
213
213
  type: 'wave',
214
214
  roleId: cRole,
215
215
  task: `[CEO Wave] ${directive}`,
216
216
  sourceRole: 'ceo',
217
- parentJobId,
217
+ parentSessionId,
218
218
  targetRoles: fullTargetScope,
219
- sessionId: session.id, // D-014: link job to session
219
+ sessionId: session.id,
220
220
  attachments,
221
221
  });
222
- jobIds.push(job.id);
223
222
 
224
- // SSE-001: Register wave job with multiplexer
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, { jobIds, waveId, sessionIds });
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
- // D-014: Create/find session for CEO assigns (not for dispatch child jobs)
257
- let sessionId: string | undefined;
258
- if (sourceRole === 'ceo' && !parentJobId) {
259
- const session = createSession(roleId, {
260
- mode: readOnly ? 'talk' : 'do',
261
- source: waveId ? 'wave' : 'dispatch',
262
- ...(waveId && { waveId }),
263
- });
264
- sessionId = session.id;
265
-
266
- // Add CEO message
267
- const ceoMsg: Message = {
268
- id: `msg-${Date.now()}-ceo`,
269
- from: 'ceo',
270
- content: task,
271
- type: readOnly ? 'conversation' : 'directive',
272
- status: 'done',
273
- timestamp: new Date().toISOString(),
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 job = jobManager.startJob({
271
+ const exec = executionManager.startExecution({
280
272
  type: readOnly ? 'consult' : 'assign',
281
273
  roleId,
282
274
  task,
283
275
  sourceRole,
284
276
  readOnly,
285
- parentJobId,
277
+ parentSessionId,
286
278
  sessionId,
287
279
  attachments,
288
280
  });
289
281
 
290
- // D-014: Add role message linked to job
291
- if (sessionId) {
292
- const roleMsg: Message = {
293
- id: `msg-${Date.now() + 1}-role`,
294
- from: 'role',
295
- content: '',
296
- type: 'conversation',
297
- status: 'streaming',
298
- timestamp: new Date().toISOString(),
299
- jobId: job.id,
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, job.id, roleId, task, sessionId);
294
+ appendFollowUpToWave(waveId, sessionId, roleId, task);
308
295
  }
309
296
 
310
- jsonResponse(res, 200, { jobId: job.id, ...(sessionId && { sessionId }), ...(waveId && { waveId }) });
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 jobIds = body.jobIds as string[];
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 || !jobIds || jobIds.length === 0) {
324
- 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' });
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
- jobId: string;
318
+ sessionId: string;
336
319
  status: WaveRoleStatus;
337
320
  events: ReturnType<typeof ActivityStream.readAll>;
338
- 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> }>;
339
322
  }
340
323
  const rolesData: WaveRoleData[] = [];
341
324
 
342
- for (const jobId of jobIds) {
343
- const events = ActivityStream.readAll(jobId);
344
- 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');
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 === 'job:done' || e.type === 'job:awaiting_input' || e.type === 'job:error');
348
- 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';
349
332
 
350
- // Collect child jobs (dispatched sub-roles)
351
- const childJobs: WaveRoleData['childJobs'] = [];
333
+ const childSessions: WaveRoleData['childSessions'] = [];
352
334
  for (const e of events) {
353
- if (e.type === 'dispatch:start' && e.data.childJobId) {
354
- const childJobId = e.data.childJobId as string;
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(childJobId);
357
- const childDone = childEvents.find(ce => ce.type === 'job:done' || ce.type === 'job:error' || ce.type === 'job:awaiting_input');
358
- const childStatus: WaveRoleStatus = childDone ? eventTypeToJobStatus(childDone.type) as WaveRoleStatus : 'unknown';
359
- 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({
360
342
  roleId: targetRoleId,
361
- roleName: (childEvents.find(ce => ce.type === 'job:start')?.data?.roleName as string) ?? targetRoleId,
362
- jobId: childJobId,
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, jobId, status, events, childJobs });
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, // Could be computed from events
379
+ duration: 0,
400
380
  roles: rolesData,
401
- // D-014: Session references for follow-up
402
381
  ...(waveId && { waveId }),
403
- ...(sessionIds && sessionIds.length > 0 && { 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 — SSE multiplexed wave stream (SSE-002) ── */
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
- // Check if wave has any registered jobs
425
- const jobIds = waveMultiplexer.getWaveJobIds(waveId);
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 jobs found for wave: ${waveId}` }));
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
- // Start job via JobManager (JobManager is source of truth for job status)
539
- 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
+ });
540
518
 
541
- // Bridge: stream job events as legacy SSE format
542
519
  startSSE(res);
543
- sendSSE(res, 'start', { id: job.id, roleId, task, sourceRole });
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
- job.stream.unsubscribe(subscriber);
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, childJobId: event.data.childJobId });
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 'job:awaiting_input':
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 'job:done':
551
+ case 'msg:done':
576
552
  cleanupLifecycle();
577
553
  sendSSE(res, 'done', event.data);
578
554
  if (!res.writableEnded) res.end();
579
- job.stream.unsubscribe(subscriber);
555
+ exec.stream.unsubscribe(subscriber);
580
556
  break;
581
- case 'job:error':
557
+ case 'msg:error':
582
558
  cleanupLifecycle();
583
559
  sendSSE(res, 'error', { message: event.data.message });
584
560
  if (!res.writableEnded) res.end();
585
- job.stream.unsubscribe(subscriber);
561
+ exec.stream.unsubscribe(subscriber);
586
562
  break;
587
563
  }
588
564
  };
589
565
 
590
- job.stream.subscribe(subscriber);
566
+ exec.stream.subscribe(subscriber);
591
567
 
592
- // Client disconnect → unsubscribe only (job keeps running!)
593
568
  req.on('close', () => {
594
569
  cleanupLifecycle();
595
- job.stream.unsubscribe(subscriber);
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
- // Start a job for EACH C-level role
629
- const jobs: Job[] = [];
600
+ const executions: Execution[] = [];
630
601
  for (const cRole of cLevelRoles) {
631
- const job = jobManager.startJob({
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
- jobs.push(job);
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: jobs.map((j) => j.id),
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<{ job: Job; sub: ActivitySubscriber }> = [];
622
+ const subscribers: Array<{ exec: Execution; sub: ActivitySubscriber }> = [];
651
623
 
652
- for (const job of jobs) {
624
+ for (const exec of executions) {
653
625
  const subscriber: ActivitySubscriber = (event: ActivityEvent) => {
654
- const rolePrefix = job.roleId;
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, childJobId: event.data.childJobId });
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 'job:awaiting_input':
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 'job:done':
649
+ case 'msg:done':
678
650
  sendSSE(res, 'role:done', { roleId: rolePrefix, ...event.data });
679
651
  doneCount++;
680
- if (doneCount >= jobs.length) {
652
+ if (doneCount >= executions.length) {
681
653
  sendSSE(res, 'done', { directive, completedRoles: cLevelRoles });
682
654
  res.end();
683
655
  }
684
656
  break;
685
- case 'job:error':
657
+ case 'msg:error':
686
658
  sendSSE(res, 'role:error', { roleId: rolePrefix, message: event.data.message });
687
659
  doneCount++;
688
- if (doneCount >= jobs.length) {
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
- job.stream.subscribe(subscriber);
697
- subscribers.push({ job, sub: subscriber });
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 { job, sub } of subscribers) {
703
- job.stream.unsubscribe(sub);
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
- // 1. File-backed activity tracker (baseline)
714
- const fileActivities = getAllActivities();
715
- for (const activity of fileActivities) {
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 activeExecs = activeJobs.map((j) => ({
752
- id: j.id,
753
- roleId: j.roleId,
754
- task: j.task,
755
- 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,
756
694
  }));
757
695
 
758
- jsonResponse(res, 200, { statuses, activeExecutions: activeExecs });
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
- // Validate attachments if present
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
- roleStatus.set(roleId, 'working');
883
- setActivity(roleId, content.slice(0, 80));
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
- // Watch for child jobs created via dispatch bridge
890
- const unwatchJobs = jobManager.onJobCreated((childJob) => {
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(childJob.roleId)) return;
895
- pendingDispatches.delete(childJob.roleId);
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 'job:awaiting_input':
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 'job:done':
930
- sendSSE(res, 'dispatch:progress', {
931
- roleId: event.roleId,
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 'job:error':
937
- sendSSE(res, 'dispatch:progress', {
938
- roleId: event.roleId,
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
- childJob.stream.subscribe(subscriber);
947
- childSubscriptions.push({ job: childJob, subscriber });
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 j of jobManager.listJobs({ active: true })) {
953
- const mapped = jobStatusToRoleStatus(j.status as JobStatus);
954
- // 'working' takes priority over 'awaiting_input' for same role
955
- if (teamStatus[j.roleId]?.status === 'working' && mapped === 'awaiting_input') continue;
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
- unwatchJobs();
996
- for (const { job, subscriber } of childSubscriptions) {
997
- job.stream.unsubscribe(subscriber);
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
  }