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 +1 -1
- package/src/api/src/engine/runners/claude-cli.ts +12 -2
- package/src/api/src/routes/execute.ts +62 -3
- package/src/api/src/routes/git.ts +8 -4
- package/src/api/src/services/execution-manager.ts +8 -2
- package/src/api/src/services/wave-multiplexer.ts +14 -1
- package/src/web/dist/assets/{index-BLB8Scqo.js → index-BoJAXuTo.js} +48 -48
- package/src/web/dist/assets/{preview-app-DJl5kOhT.js → preview-app-CywDONM1.js} +1 -1
- package/src/web/dist/index.html +1 -1
package/package.json
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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)
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
: '
|
|
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
|
-
: '
|
|
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
|
-
|
|
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);
|