tycono 0.1.93-beta.1 → 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.1",
3
+ "version": "0.1.93-beta.2",
4
4
  "description": "Build an AI company. Watch them work.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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;
@@ -60,10 +60,14 @@ export function handleExecRequest(req: IncomingMessage, res: ServerResponse): vo
60
60
  waveGroups.get(ses.waveId)!.push(ses.id);
61
61
  }
62
62
  }
63
+ // BUG-W03 fix: register ALL wave sessions (including completed) for tree display
63
64
  for (const [wid, sids] of waveGroups) {
64
65
  for (const sid of sids) {
66
+ // getActiveExecution falls back to stream recovery for completed sessions
65
67
  const exec = executionManager.getActiveExecution(sid);
66
- if (exec) waveMultiplexer.registerSession(wid, exec);
68
+ if (exec) {
69
+ waveMultiplexer.registerSession(wid, exec);
70
+ }
67
71
  }
68
72
  }
69
73
  }
@@ -272,6 +276,7 @@ function handleStartJob(body: Record<string, unknown>, res: ServerResponse): voi
272
276
  const session = createSession(roleId, {
273
277
  mode: readOnly ? 'talk' : 'do',
274
278
  source: parentSessionId ? 'dispatch' : sessionSource,
279
+ ...(parentSessionId && { parentSessionId }),
275
280
  ...(waveId && { waveId }),
276
281
  });
277
282
  const sessionId = session.id;
@@ -320,9 +325,18 @@ function handleStartJob(body: Record<string, unknown>, res: ServerResponse): voi
320
325
 
321
326
  function handleSaveWave(body: Record<string, unknown>, res: ServerResponse): void {
322
327
  const directive = body.directive as string;
323
- const sessionIds = (body.sessionIds ?? body.jobIds) as string[];
328
+ let sessionIds = (body.sessionIds ?? body.jobIds) as string[] | undefined;
324
329
  const waveId = body.waveId as string | undefined;
325
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
+
326
340
  if (!directive || !sessionIds || sessionIds.length === 0) {
327
341
  jsonResponse(res, 400, { error: 'directive and sessionIds are required' });
328
342
  return;
@@ -426,6 +440,7 @@ function handleWaveStream(waveId: string, url: string, res: ServerResponse, req:
426
440
  const allSessions = listSessions();
427
441
  const waveSessions = allSessions.filter(s => s.waveId === waveId);
428
442
  for (const ses of waveSessions) {
443
+ // getActiveExecution recovers from stream files for completed sessions too
429
444
  const exec = executionManager.getActiveExecution(ses.id);
430
445
  if (exec) {
431
446
  waveMultiplexer.registerSession(waveId, exec);
@@ -715,7 +730,51 @@ function handleWave(body: Record<string, unknown>, req: IncomingMessage, res: Se
715
730
  function handleStatus(res: ServerResponse): void {
716
731
  const statuses: Record<string, string> = {};
717
732
 
718
- 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
+
719
778
  for (const exec of activeExecs) {
720
779
  // ExecStatus 'running' → RoleStatus 'working' (not MessageStatus 'streaming')
721
780
  statuses[exec.roleId] = exec.status === 'running' ? 'working'
@@ -141,10 +141,12 @@ gitRouter.delete('/worktree/{*path}', (req: Request, res: Response, next: NextFu
141
141
  }
142
142
 
143
143
  try {
144
- git(`worktree remove ${JSON.stringify(worktreePath)}`, COMPANY_ROOT);
144
+ const repoRoot = resolveRepoRoot('code');
145
+ git(`worktree remove ${JSON.stringify(worktreePath)}`, repoRoot);
145
146
  } catch {
146
147
  // Try force remove if normal remove fails
147
- git(`worktree remove --force ${JSON.stringify(worktreePath)}`, COMPANY_ROOT);
148
+ const repoRoot = resolveRepoRoot('code');
149
+ git(`worktree remove --force ${JSON.stringify(worktreePath)}`, repoRoot);
148
150
  }
149
151
 
150
152
  res.json({ success: true, removed: worktreePath });
@@ -174,7 +176,8 @@ gitRouter.delete('/branch/{*name}', (req: Request, res: Response, next: NextFunc
174
176
 
175
177
  // Delete local branch
176
178
  try {
177
- git(`branch -d ${JSON.stringify(branchName)}`, COMPANY_ROOT);
179
+ const repoRoot = resolveRepoRoot('code');
180
+ git(`branch -d ${JSON.stringify(branchName)}`, repoRoot);
178
181
  } catch (err) {
179
182
  const msg = err instanceof Error ? err.message : 'Unknown error';
180
183
  // If branch is not fully merged, report but continue to try remote
@@ -187,7 +190,8 @@ gitRouter.delete('/branch/{*name}', (req: Request, res: Response, next: NextFunc
187
190
 
188
191
  // Delete remote branch
189
192
  try {
190
- git(`push origin --delete ${JSON.stringify(branchName)}`, COMPANY_ROOT);
193
+ const repoRoot = resolveRepoRoot('code');
194
+ git(`push origin --delete ${JSON.stringify(branchName)}`, repoRoot);
191
195
  } catch (err) {
192
196
  const msg = err instanceof Error ? err.message : 'Unknown error';
193
197
  if (!msg.includes('remote ref does not exist')) {
@@ -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);