tycono 0.1.93-beta.0 → 0.1.93-beta.2

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tycono",
3
- "version": "0.1.93-beta.0",
3
+ "version": "0.1.93-beta.2",
4
4
  "description": "Build an AI company. Watch them work.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -595,6 +595,84 @@ export async function runAgentLoop(config: AgentConfig): Promise<AgentResult> {
595
595
 
596
596
  onTurnComplete?.(turns);
597
597
  }
598
+
599
+ // ── Post-Knowledging: ④⑤ 수행 프롬프트 (KP-008) ──
600
+ // Engineer/CTO가 구현 완료 후 The Loop 마무리 (Knowledge 업데이트 + Task 상태 갱신)를 수행하도록 유도
601
+ const postKPrompt = [
602
+ '[POST-KNOWLEDGING] 구현이 완료되었습니다. The Loop 마무리를 수행하세요:',
603
+ '',
604
+ '## ④ Knowledge 업데이트 (The Loop Step 4)',
605
+ '다음 중 해당하는 항목을 수행하세요:',
606
+ '- 본인 journal 업데이트 (`roles/' + roleId + '/journal/YYYY-MM-DD.md` — 오늘 날짜 파일)',
607
+ '- 구현 중 새로 발견한 패턴/아키텍처 결정이 있다면 관련 문서 업데이트',
608
+ ' (예: architecture/web-app-ia.md, architecture/session-worktree-isolation.md 등)',
609
+ '- 중요한 기술 결정은 operations/decisions/ 또는 architecture/ 반영',
610
+ '',
611
+ '## ⑤ Task 상태 갱신 (The Loop Step 5)',
612
+ '- `projects/tycono-platform/tasks.md` (또는 관련 tasks 파일)에서 완료한 태스크 상태를 DONE으로 변경',
613
+ '- 다음 작업이 있다면 식별하여 메모',
614
+ '',
615
+ '⛔ **필수**: ④와 ⑤를 모두 수행해야 The Loop이 완료됩니다.',
616
+ '이제 ④⑤를 수행하세요 (Read, Edit 도구 사용).',
617
+ ].join('\n');
618
+
619
+ messages.push({ role: 'user', content: postKPrompt });
620
+
621
+ // Run Post-K loop (최대 2턴 — C-Level의 3턴보다 짧게)
622
+ const maxPostKRounds = 2;
623
+ for (let round = 0; round < maxPostKRounds && turns < maxTurns; round++) {
624
+ if (abortSignal?.aborted) break;
625
+ turns++;
626
+
627
+ const postKResponse = await llm.chat(context.systemPrompt, messages, tools, abortSignal);
628
+ totalInput += postKResponse.usage.inputTokens;
629
+ totalOutput += postKResponse.usage.outputTokens;
630
+ config.tokenLedger?.record({
631
+ ts: new Date().toISOString(),
632
+ sessionId: config.sessionId,
633
+ roleId,
634
+ model: config.model ?? 'unknown',
635
+ inputTokens: postKResponse.usage.inputTokens,
636
+ outputTokens: postKResponse.usage.outputTokens,
637
+ });
638
+
639
+ messages.push({ role: 'assistant', content: postKResponse.content });
640
+ for (const block of postKResponse.content) {
641
+ if (block.type === 'text' && block.text) {
642
+ outputParts.push(block.text);
643
+ onText?.(block.text);
644
+ }
645
+ }
646
+
647
+ // If no tool calls, Post-K is done
648
+ if (postKResponse.stopReason !== 'tool_use') break;
649
+
650
+ // Execute Post-K tool calls
651
+ const postKToolCalls = postKResponse.content.filter(
652
+ (b): b is MessageContent & { type: 'tool_use' } => b.type === 'tool_use',
653
+ );
654
+ const postKResults: ToolResult[] = [];
655
+ for (const tc of postKToolCalls) {
656
+ allToolCalls.push({ name: tc.name, input: tc.input });
657
+ const result = await executeTool(
658
+ { id: tc.id, name: tc.name, input: tc.input },
659
+ toolExecOptions,
660
+ );
661
+ postKResults.push(result);
662
+ }
663
+
664
+ messages.push({
665
+ role: 'user',
666
+ content: postKResults.map((r) => ({
667
+ type: 'tool_result' as const,
668
+ tool_use_id: r.tool_use_id,
669
+ content: r.content,
670
+ is_error: r.is_error,
671
+ })) as unknown as MessageContent[],
672
+ });
673
+
674
+ onTurnComplete?.(turns);
675
+ }
598
676
  }
599
677
  }
600
678
  }
@@ -6,6 +6,7 @@ import { assembleContext } from '../context-assembler.js';
6
6
  import { getSubordinates } from '../org-tree.js';
7
7
  import { readConfig, resolveCodeRoot } from '../../services/company-config.js';
8
8
  import { getTokenLedger } from '../../services/token-ledger.js';
9
+ import { getSession } from '../../services/session-store.js';
9
10
  import type { ExecutionRunner, RunnerConfig, RunnerCallbacks, RunnerHandle, RunnerResult } from './types.js';
10
11
 
11
12
  /* ─── Dispatch Bridge Script (Python3) ────── */
@@ -65,13 +66,17 @@ def get_status(job_id):
65
66
  def start_job(role_id, task):
66
67
  parent_session = os.environ.get('DISPATCH_PARENT_SESSION', os.environ.get('DISPATCH_PARENT_JOB', ''))
67
68
  source_role = os.environ.get('DISPATCH_SOURCE_ROLE', 'ceo')
68
- body = json.dumps({
69
+ wave_id = os.environ.get('DISPATCH_WAVE_ID', '')
70
+ payload = {
69
71
  'type': 'assign',
70
72
  'roleId': role_id,
71
73
  'task': task,
72
74
  'sourceRole': source_role,
73
75
  'parentSessionId': parent_session if parent_session else None,
74
- }).encode()
76
+ }
77
+ if wave_id:
78
+ payload['waveId'] = wave_id
79
+ body = json.dumps(payload).encode()
75
80
  req = urllib.request.Request(f'{api}/api/jobs', body, {'Content-Type': 'application/json'})
76
81
  resp = json.loads(urllib.request.urlopen(req, timeout=10).read())
77
82
  return resp['jobId']
@@ -347,6 +352,11 @@ export class ClaudeCliRunner implements ExecutionRunner {
347
352
  cleanEnv.DISPATCH_SUBORDINATES = subordinates.join(', ');
348
353
  cleanEnv.DISPATCH_PARENT_SESSION = config.sessionId;
349
354
  cleanEnv.DISPATCH_PARENT_JOB = config.sessionId; // deprecated, kept for backward compat
355
+ // BUG-W02 fix: propagate waveId to child dispatches via env
356
+ if (config.sessionId) {
357
+ const parentSes = getSession(config.sessionId);
358
+ if (parentSes?.waveId) cleanEnv.DISPATCH_WAVE_ID = parentSes.waveId;
359
+ }
350
360
  // dispatch 명령어 경로를 PATH에 추가하지 않고 절대 경로로 사용
351
361
  cleanEnv.DISPATCH_CMD = dispatchScript;
352
362
  cleanEnv.CONSULT_CMD = consultScript;
@@ -10,6 +10,7 @@ import {
10
10
  createSession,
11
11
  addMessage,
12
12
  updateMessage,
13
+ listSessions,
13
14
  type Message,
14
15
  type ImageAttachment,
15
16
  } from '../services/session-store.js';
@@ -48,6 +49,28 @@ export function handleExecRequest(req: IncomingMessage, res: ServerResponse): vo
48
49
 
49
50
  // ── /api/waves/active — restore active waves after refresh ──
50
51
  if (method === 'GET' && url === '/api/waves/active') {
52
+ // Recovery: rebuild wave→session mapping from session-store if lost
53
+ const waves = waveMultiplexer.getActiveWaves();
54
+ if (waves.length === 0) {
55
+ const allSessions = listSessions();
56
+ const waveGroups = new Map<string, string[]>();
57
+ for (const ses of allSessions) {
58
+ if (ses.waveId) {
59
+ if (!waveGroups.has(ses.waveId)) waveGroups.set(ses.waveId, []);
60
+ waveGroups.get(ses.waveId)!.push(ses.id);
61
+ }
62
+ }
63
+ // BUG-W03 fix: register ALL wave sessions (including completed) for tree display
64
+ for (const [wid, sids] of waveGroups) {
65
+ for (const sid of sids) {
66
+ // getActiveExecution falls back to stream recovery for completed sessions
67
+ const exec = executionManager.getActiveExecution(sid);
68
+ if (exec) {
69
+ waveMultiplexer.registerSession(wid, exec);
70
+ }
71
+ }
72
+ }
73
+ }
51
74
  jsonResponse(res, 200, { waves: waveMultiplexer.getActiveWaves() });
52
75
  return;
53
76
  }
@@ -253,6 +276,7 @@ function handleStartJob(body: Record<string, unknown>, res: ServerResponse): voi
253
276
  const session = createSession(roleId, {
254
277
  mode: readOnly ? 'talk' : 'do',
255
278
  source: parentSessionId ? 'dispatch' : sessionSource,
279
+ ...(parentSessionId && { parentSessionId }),
256
280
  ...(waveId && { waveId }),
257
281
  });
258
282
  const sessionId = session.id;
@@ -301,9 +325,18 @@ function handleStartJob(body: Record<string, unknown>, res: ServerResponse): voi
301
325
 
302
326
  function handleSaveWave(body: Record<string, unknown>, res: ServerResponse): void {
303
327
  const directive = body.directive as string;
304
- const sessionIds = (body.sessionIds ?? body.jobIds) as string[];
328
+ let sessionIds = (body.sessionIds ?? body.jobIds) as string[] | undefined;
305
329
  const waveId = body.waveId as string | undefined;
306
330
 
331
+ // BUG-W01 fix: auto-collect sessionIds from session-store when waveId is present
332
+ if (waveId && (!sessionIds || sessionIds.length === 0)) {
333
+ const allSessions = listSessions();
334
+ sessionIds = allSessions
335
+ .filter(s => s.waveId === waveId)
336
+ .map(s => s.id);
337
+ console.log(`[WaveSave] Auto-collected ${sessionIds.length} sessionIds for wave ${waveId}`);
338
+ }
339
+
307
340
  if (!directive || !sessionIds || sessionIds.length === 0) {
308
341
  jsonResponse(res, 400, { error: 'directive and sessionIds are required' });
309
342
  return;
@@ -399,7 +432,23 @@ function handleWaveStream(waveId: string, url: string, res: ServerResponse, req:
399
432
  const fromMatch = url.match(/[?&]from=(\d+)/);
400
433
  const fromWaveSeq = fromMatch ? parseInt(fromMatch[1], 10) : 0;
401
434
 
402
- const sessionIds = waveMultiplexer.getWaveSessionIds(waveId);
435
+ let sessionIds = waveMultiplexer.getWaveSessionIds(waveId);
436
+
437
+ // Recovery: if wave→session mapping was lost (e.g. server restart),
438
+ // rebuild from session-store (sessions have waveId) + executionManager
439
+ if (sessionIds.length === 0) {
440
+ const allSessions = listSessions();
441
+ const waveSessions = allSessions.filter(s => s.waveId === waveId);
442
+ for (const ses of waveSessions) {
443
+ // getActiveExecution recovers from stream files for completed sessions too
444
+ const exec = executionManager.getActiveExecution(ses.id);
445
+ if (exec) {
446
+ waveMultiplexer.registerSession(waveId, exec);
447
+ }
448
+ }
449
+ sessionIds = waveMultiplexer.getWaveSessionIds(waveId);
450
+ }
451
+
403
452
  if (sessionIds.length === 0) {
404
453
  res.writeHead(404, { 'Content-Type': 'application/json' });
405
454
  res.end(JSON.stringify({ error: `No sessions found for wave: ${waveId}` }));
@@ -681,9 +730,56 @@ function handleWave(body: Record<string, unknown>, req: IncomingMessage, res: Se
681
730
  function handleStatus(res: ServerResponse): void {
682
731
  const statuses: Record<string, string> = {};
683
732
 
684
- const activeExecs = executionManager.listExecutions({ active: true });
733
+ let activeExecs = executionManager.listExecutions({ active: true });
734
+
735
+ // Recovery: if in-memory map is empty (e.g. after server restart),
736
+ // rebuild active executions from persisted session-store + activity-streams
737
+ if (activeExecs.length === 0) {
738
+ const allSessions = listSessions();
739
+ const activeSessions = allSessions.filter(s =>
740
+ s.status === 'active' &&
741
+ (s.source === 'wave' || s.source === 'dispatch' || s.source === 'chat')
742
+ );
743
+
744
+ const recovered: typeof activeExecs = [];
745
+ for (const ses of activeSessions) {
746
+ // Check activity-stream for actual running state
747
+ if (!ActivityStream.exists(ses.id)) continue;
748
+ const events = ActivityStream.readFrom(ses.id, 0);
749
+ if (events.length === 0) continue;
750
+
751
+ const startEvent = events.find(e => e.type === 'msg:start');
752
+ if (!startEvent) continue;
753
+
754
+ const doneEvent = events.find(e => e.type === 'msg:done');
755
+ const errorEvent = events.find(e => e.type === 'msg:error');
756
+
757
+ // Only include sessions that haven't finished yet
758
+ if (doneEvent || errorEvent) continue;
759
+
760
+ const task = (startEvent.data?.task as string) ?? ses.title ?? '';
761
+ recovered.push({
762
+ id: `recovered-${ses.id}`,
763
+ type: (startEvent.data?.type as string ?? 'assign') as 'assign' | 'wave' | 'consult',
764
+ roleId: ses.roleId,
765
+ task,
766
+ status: 'running',
767
+ childSessionIds: [],
768
+ createdAt: ses.createdAt,
769
+ });
770
+ }
771
+
772
+ if (recovered.length > 0) {
773
+ activeExecs = recovered;
774
+ console.log(`[ExecStatus] Recovered ${recovered.length} active executions from session-store`);
775
+ }
776
+ }
777
+
685
778
  for (const exec of activeExecs) {
686
- statuses[exec.roleId] = messageStatusToRoleStatus(exec.status as MessageStatus);
779
+ // ExecStatus 'running' → RoleStatus 'working' (not MessageStatus 'streaming')
780
+ statuses[exec.roleId] = exec.status === 'running' ? 'working'
781
+ : exec.status === 'awaiting_input' ? 'awaiting_input'
782
+ : 'done';
687
783
  }
688
784
 
689
785
  const activeExecutions = activeExecs.map((e) => ({
@@ -1,9 +1,12 @@
1
1
  import { Router, Request, Response, NextFunction } from 'express';
2
2
  import { execSync } from 'node:child_process';
3
3
  import { COMPANY_ROOT } from '../services/file-reader.js';
4
+ import { resolveCodeRoot } from '../services/company-config.js';
4
5
 
5
6
  export const gitRouter = Router();
6
7
 
8
+ type RepoType = 'akb' | 'code';
9
+
7
10
  interface WorktreeInfo {
8
11
  path: string;
9
12
  branch: string;
@@ -17,17 +20,42 @@ interface LastCommit {
17
20
  date: string;
18
21
  }
19
22
 
20
- function git(cmd: string): string {
21
- return execSync(`git ${cmd}`, { cwd: COMPANY_ROOT, encoding: 'utf-8', timeout: 5000 }).trim();
23
+ /**
24
+ * Resolve repository root based on repo type
25
+ */
26
+ function resolveRepoRoot(repo: RepoType = 'akb'): string {
27
+ if (repo === 'akb') {
28
+ return COMPANY_ROOT;
29
+ }
30
+ return resolveCodeRoot(COMPANY_ROOT);
22
31
  }
23
32
 
24
- // GET /api/git/status
25
- gitRouter.get('/status', (_req: Request, res: Response, next: NextFunction) => {
33
+ function git(cmd: string, cwd: string): string {
34
+ return execSync(`git ${cmd}`, { cwd, encoding: 'utf-8', timeout: 5000 }).trim();
35
+ }
36
+
37
+ // GET /api/git/status?repo=akb|code
38
+ gitRouter.get('/status', (req: Request, res: Response, next: NextFunction) => {
26
39
  try {
40
+ const repoParam = (req.query.repo as string) ?? 'akb';
41
+ const repo: RepoType = repoParam === 'code' ? 'code' : 'akb';
42
+
43
+ let repoRoot: string;
44
+ try {
45
+ repoRoot = resolveRepoRoot(repo);
46
+ } catch (err) {
47
+ res.status(400).json({
48
+ error: repo === 'code'
49
+ ? 'Code root not configured or inaccessible'
50
+ : 'Company root not found'
51
+ });
52
+ return;
53
+ }
54
+
27
55
  // Current branch
28
56
  let currentBranch: string;
29
57
  try {
30
- currentBranch = git('rev-parse --abbrev-ref HEAD');
58
+ currentBranch = git('rev-parse --abbrev-ref HEAD', repoRoot);
31
59
  } catch {
32
60
  res.status(500).json({ error: 'Not a git repository or git is not available' });
33
61
  return;
@@ -36,7 +64,7 @@ gitRouter.get('/status', (_req: Request, res: Response, next: NextFunction) => {
36
64
  // Worktrees
37
65
  let worktrees: WorktreeInfo[] = [];
38
66
  try {
39
- const raw = git('worktree list --porcelain');
67
+ const raw = git('worktree list --porcelain', repoRoot);
40
68
  const blocks = raw.split('\n\n').filter(Boolean);
41
69
  for (const block of blocks) {
42
70
  const lines = block.split('\n');
@@ -44,13 +72,13 @@ gitRouter.get('/status', (_req: Request, res: Response, next: NextFunction) => {
44
72
  const commitHash = lines.find(l => l.startsWith('HEAD '))?.replace('HEAD ', '') ?? '';
45
73
  const branchLine = lines.find(l => l.startsWith('branch '));
46
74
  const branch = branchLine ? branchLine.replace('branch refs/heads/', '') : '(detached)';
47
- const isMain = lines.some(l => l === 'worktree ' + COMPANY_ROOT) ||
75
+ const isMain = lines.some(l => l === 'worktree ' + repoRoot) ||
48
76
  (!branchLine && lines.some(l => l === 'bare'));
49
77
  worktrees.push({
50
78
  path: wtPath,
51
79
  branch,
52
80
  commitHash,
53
- isMain: wtPath === COMPANY_ROOT,
81
+ isMain: wtPath === repoRoot,
54
82
  });
55
83
  }
56
84
  } catch {
@@ -60,7 +88,7 @@ gitRouter.get('/status', (_req: Request, res: Response, next: NextFunction) => {
60
88
  // Stale (unmerged) branches
61
89
  let staleBranches: string[] = [];
62
90
  try {
63
- const raw = git('branch --no-merged develop');
91
+ const raw = git('branch --no-merged develop', repoRoot);
64
92
  staleBranches = raw
65
93
  .split('\n')
66
94
  .map(b => b.trim().replace(/^\*\s*/, ''))
@@ -72,7 +100,7 @@ gitRouter.get('/status', (_req: Request, res: Response, next: NextFunction) => {
72
100
  // Unsaved changes count
73
101
  let unsavedChanges = 0;
74
102
  try {
75
- const raw = git('status --porcelain');
103
+ const raw = git('status --porcelain', repoRoot);
76
104
  unsavedChanges = raw ? raw.split('\n').filter(Boolean).length : 0;
77
105
  } catch {
78
106
  unsavedChanges = 0;
@@ -81,7 +109,7 @@ gitRouter.get('/status', (_req: Request, res: Response, next: NextFunction) => {
81
109
  // Last commit
82
110
  let lastCommit: LastCommit | null = null;
83
111
  try {
84
- const raw = git('log -1 --format=%H%n%s%n%aI');
112
+ const raw = git('log -1 --format=%H%n%s%n%aI', repoRoot);
85
113
  const [hash, message, date] = raw.split('\n');
86
114
  if (hash) {
87
115
  lastCommit = { hash, message: message ?? '', date: date ?? '' };
@@ -113,10 +141,12 @@ gitRouter.delete('/worktree/{*path}', (req: Request, res: Response, next: NextFu
113
141
  }
114
142
 
115
143
  try {
116
- git(`worktree remove ${JSON.stringify(worktreePath)}`);
144
+ const repoRoot = resolveRepoRoot('code');
145
+ git(`worktree remove ${JSON.stringify(worktreePath)}`, repoRoot);
117
146
  } catch {
118
147
  // Try force remove if normal remove fails
119
- git(`worktree remove --force ${JSON.stringify(worktreePath)}`);
148
+ const repoRoot = resolveRepoRoot('code');
149
+ git(`worktree remove --force ${JSON.stringify(worktreePath)}`, repoRoot);
120
150
  }
121
151
 
122
152
  res.json({ success: true, removed: worktreePath });
@@ -146,7 +176,8 @@ gitRouter.delete('/branch/{*name}', (req: Request, res: Response, next: NextFunc
146
176
 
147
177
  // Delete local branch
148
178
  try {
149
- git(`branch -d ${JSON.stringify(branchName)}`);
179
+ const repoRoot = resolveRepoRoot('code');
180
+ git(`branch -d ${JSON.stringify(branchName)}`, repoRoot);
150
181
  } catch (err) {
151
182
  const msg = err instanceof Error ? err.message : 'Unknown error';
152
183
  // If branch is not fully merged, report but continue to try remote
@@ -159,7 +190,8 @@ gitRouter.delete('/branch/{*name}', (req: Request, res: Response, next: NextFunc
159
190
 
160
191
  // Delete remote branch
161
192
  try {
162
- git(`push origin --delete ${JSON.stringify(branchName)}`);
193
+ const repoRoot = resolveRepoRoot('code');
194
+ git(`push origin --delete ${JSON.stringify(branchName)}`, repoRoot);
163
195
  } catch (err) {
164
196
  const msg = err instanceof Error ? err.message : 'Unknown error';
165
197
  if (!msg.includes('remote ref does not exist')) {
@@ -32,9 +32,15 @@ saveRouter.post('/', (req: Request, res: Response, next: NextFunction) => {
32
32
  const result = gitSave(COMPANY_ROOT, message, getRepo(req));
33
33
  res.json({ ok: true, ...result });
34
34
  } catch (err) {
35
- if (err instanceof Error && err.message === 'No changes to save') {
36
- res.status(400).json({ error: err.message });
37
- return;
35
+ if (err instanceof Error) {
36
+ if (err.message === 'No changes to save') {
37
+ res.status(400).json({ error: err.message });
38
+ return;
39
+ }
40
+ if (err.message.includes('Not a git repository') || err.message.includes('codeRoot')) {
41
+ res.status(400).json({ error: 'Repository not initialized. Run git init first.' });
42
+ return;
43
+ }
38
44
  }
39
45
  next(err);
40
46
  }
@@ -64,7 +70,7 @@ saveRouter.post('/init', (req: Request, res: Response, next: NextFunction) => {
64
70
  }
65
71
  });
66
72
 
67
- // POST /api/save/restore
73
+ // POST /api/save/restore?repo=akb|code
68
74
  saveRouter.post('/restore', (req: Request, res: Response, next: NextFunction) => {
69
75
  try {
70
76
  const { sha, paths } = req.body ?? {};
@@ -72,9 +78,13 @@ saveRouter.post('/restore', (req: Request, res: Response, next: NextFunction) =>
72
78
  res.status(400).json({ error: 'sha is required' });
73
79
  return;
74
80
  }
75
- const result = gitRestore(COMPANY_ROOT, sha, paths);
81
+ const result = gitRestore(COMPANY_ROOT, sha, paths, getRepo(req));
76
82
  res.json({ ok: true, ...result });
77
83
  } catch (err) {
84
+ if (err instanceof Error && (err.message.includes('Not a git repository') || err.message.includes('codeRoot'))) {
85
+ res.status(400).json({ error: 'Repository not initialized. Run git init first.' });
86
+ return;
87
+ }
78
88
  next(err);
79
89
  }
80
90
  });
@@ -287,9 +287,15 @@ class ExecutionManager {
287
287
  }
288
288
  }
289
289
 
290
+ // BUG-W02 fix: propagate waveId from parent session to child
291
+ const parentSession = getSession(execution.sessionId);
292
+ const parentWaveId = parentSession?.waveId;
293
+
290
294
  const childSession = createSession(subRoleId, {
291
295
  mode: 'do',
292
296
  source: 'dispatch',
297
+ parentSessionId: execution.sessionId,
298
+ ...(parentWaveId && { waveId: parentWaveId }),
293
299
  });
294
300
  const dispatchMsg: Message = {
295
301
  id: `msg-${Date.now()}-dispatch-${subRoleId}`,
@@ -761,7 +767,7 @@ class ExecutionManager {
761
767
  const status: ExecStatus = awaitingEvent && !doneEvent ? 'awaiting_input'
762
768
  : doneEvent ? 'done'
763
769
  : errorEvent ? 'error'
764
- : 'done';
770
+ : 'running'; // No done/error event = still running
765
771
 
766
772
  const candidate = {
767
773
  streamId,
@@ -816,7 +822,7 @@ class ExecutionManager {
816
822
  const status: ExecStatus = awaitingEvent && !doneEvent ? 'awaiting_input'
817
823
  : doneEvent ? 'done'
818
824
  : errorEvent ? 'error'
819
- : 'done';
825
+ : 'running'; // No done/error event = still running
820
826
 
821
827
  const stream = ActivityStream.getOrCreate(sessionId, startEvent.roleId);
822
828
  const execution: Execution = {
@@ -1,5 +1,6 @@
1
1
  import { ActivityStream, type ActivityEvent, type ActivitySubscriber } from './activity-stream.js';
2
2
  import type { Execution } from './execution-manager.js';
3
+ import { getSession } from './session-store.js';
3
4
  import type { Response } from 'express';
4
5
 
5
6
  /* ─── Types ──────────────────────────────── */
@@ -181,7 +182,19 @@ class WaveMultiplexer {
181
182
  }
182
183
 
183
184
  onExecutionCreated(execution: Execution): void {
184
- const waveId = this.findWaveIdForSession(execution.sessionId) ?? this.findWaveIdForSession(execution.parentSessionId ?? '');
185
+ // Check multiplexer's in-memory map first
186
+ let waveId = this.findWaveIdForSession(execution.sessionId) ?? this.findWaveIdForSession(execution.parentSessionId ?? '');
187
+
188
+ // BUG-W02 fix: also check session-store for waveId (propagated from parent)
189
+ if (!waveId) {
190
+ const session = getSession(execution.sessionId);
191
+ if (session?.waveId) waveId = session.waveId;
192
+ }
193
+ if (!waveId && execution.parentSessionId) {
194
+ const parentSession = getSession(execution.parentSessionId);
195
+ if (parentSession?.waveId) waveId = parentSession.waveId;
196
+ }
197
+
185
198
  if (!waveId) return;
186
199
 
187
200
  this.registerSession(waveId, execution);