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.
- package/package.json +1 -1
- package/src/api/src/create-server.ts +13 -20
- package/src/api/src/engine/agent-loop.ts +7 -7
- package/src/api/src/engine/knowledge-gate.ts +32 -2
- package/src/api/src/engine/runners/claude-cli.ts +15 -15
- package/src/api/src/engine/runners/direct-api.ts +1 -1
- package/src/api/src/engine/runners/types.ts +2 -1
- package/src/api/src/routes/active-sessions.ts +9 -21
- package/src/api/src/routes/cost.ts +43 -0
- package/src/api/src/routes/engine.ts +1 -0
- package/src/api/src/routes/execute.ts +186 -333
- package/src/api/src/routes/operations.ts +8 -7
- package/src/api/src/routes/sessions.ts +81 -52
- package/src/api/src/routes/speech.ts +1 -1
- package/src/api/src/services/activity-stream.ts +48 -19
- package/src/api/src/services/execution-manager.ts +849 -0
- package/src/api/src/services/job-manager.ts +14 -893
- package/src/api/src/services/session-store.ts +1 -1
- package/src/api/src/services/token-ledger.ts +13 -2
- package/src/api/src/services/wave-multiplexer.ts +62 -110
- package/src/api/src/services/wave-tracker.ts +44 -86
- package/src/shared/types.ts +48 -65
- package/src/web/dist/assets/index-BLB8Scqo.js +115 -0
- package/src/web/dist/assets/index-rya2vj54.css +1 -0
- package/src/web/dist/assets/{preview-app-DmFg3Yi9.js → preview-app-DJl5kOhT.js} +1 -1
- package/src/web/dist/index.html +2 -2
- package/src/web/dist/assets/index-DueQ7jub.css +0 -1
- package/src/web/dist/assets/index-HiHZjTU-.js +0 -110
package/package.json
CHANGED
|
@@ -20,8 +20,7 @@ import { handleExecRequest } from './routes/execute.js';
|
|
|
20
20
|
import { engineRouter } from './routes/engine.js';
|
|
21
21
|
import { sessionsRouter } from './routes/sessions.js';
|
|
22
22
|
import { setupRouter } from './routes/setup.js';
|
|
23
|
-
|
|
24
|
-
import { type RoleStatus, isRoleActive } from '../../shared/types.js';
|
|
23
|
+
// activity-tracker removed — executionManager resets on restart
|
|
25
24
|
import { knowledgeRouter } from './routes/knowledge.js';
|
|
26
25
|
import { preferencesRouter } from './routes/preferences.js';
|
|
27
26
|
import { saveRouter } from './routes/save.js';
|
|
@@ -43,21 +42,6 @@ const __dirname = path.dirname(__filename);
|
|
|
43
42
|
const isProd = process.env.NODE_ENV === 'production';
|
|
44
43
|
const corsOrigin = isProd ? true : /^http:\/\/localhost:\d+$/;
|
|
45
44
|
|
|
46
|
-
/**
|
|
47
|
-
* 서버 시작 시 stale "working" activity 파일을 정리한다.
|
|
48
|
-
* tsx watch 모드에서 재시작 시 in-memory 상태가 초기화되어도
|
|
49
|
-
* 파일이 "working"으로 남는 버그를 방지한다.
|
|
50
|
-
*/
|
|
51
|
-
function cleanupStaleActivities(): void {
|
|
52
|
-
const activities = getAllActivities();
|
|
53
|
-
for (const activity of activities) {
|
|
54
|
-
if (isRoleActive(activity.status as RoleStatus)) {
|
|
55
|
-
completeActivity(activity.roleId);
|
|
56
|
-
console.log(`[STARTUP] Cleaned stale activity: ${activity.roleId} (was working on "${activity.currentTask}")`);
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
|
|
61
45
|
/* ─── Raw HTTP handler for import-knowledge SSE ─── */
|
|
62
46
|
|
|
63
47
|
function handleImportKnowledge(req: http.IncomingMessage, res: http.ServerResponse): void {
|
|
@@ -109,7 +93,6 @@ function handleImportKnowledge(req: http.IncomingMessage, res: http.ServerRespon
|
|
|
109
93
|
export function createHttpServer(): http.Server {
|
|
110
94
|
// Only cleanup/ensure if a company is already initialized (avoid creating dirs in CWD)
|
|
111
95
|
if (COMPANY_ROOT && fs.existsSync(path.join(COMPANY_ROOT, 'CLAUDE.md'))) {
|
|
112
|
-
cleanupStaleActivities();
|
|
113
96
|
ensureClaudeMd(COMPANY_ROOT);
|
|
114
97
|
}
|
|
115
98
|
|
|
@@ -133,6 +116,16 @@ export function createHttpServer(): http.Server {
|
|
|
133
116
|
return;
|
|
134
117
|
}
|
|
135
118
|
|
|
119
|
+
// D-014: POST /api/sessions/:id/message — delegate to execute handler for SSE streaming
|
|
120
|
+
const sessionMessageMatch = url.match(/^\/api\/sessions\/([^/]+)\/message$/);
|
|
121
|
+
if (sessionMessageMatch && method === 'POST') {
|
|
122
|
+
setExecCors(req, res);
|
|
123
|
+
// Rewrite URL to legacy format for handleExecRequest
|
|
124
|
+
req.url = `/api/exec/session/${sessionMessageMatch[1]}/message`;
|
|
125
|
+
handleExecRequest(req, res);
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
|
|
136
129
|
// SSE 엔드포인트: Express 우회하여 raw HTTP로 처리
|
|
137
130
|
if ((url.startsWith('/api/exec/') || url.startsWith('/api/jobs') || url === '/api/waves/save' || url === '/api/setup/import-knowledge') && method === 'POST') {
|
|
138
131
|
setExecCors(req, res);
|
|
@@ -144,8 +137,8 @@ export function createHttpServer(): http.Server {
|
|
|
144
137
|
return;
|
|
145
138
|
}
|
|
146
139
|
|
|
147
|
-
// CORS preflight for exec/jobs endpoints
|
|
148
|
-
if ((url.startsWith('/api/exec/') || url.startsWith('/api/jobs')) && method === 'OPTIONS') {
|
|
140
|
+
// CORS preflight for exec/jobs/sessions endpoints
|
|
141
|
+
if ((url.startsWith('/api/exec/') || url.startsWith('/api/jobs') || url.match(/^\/api\/sessions\/[^/]+\/message$/)) && method === 'OPTIONS') {
|
|
149
142
|
setExecCors(req, res);
|
|
150
143
|
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, DELETE, OPTIONS');
|
|
151
144
|
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
|
|
@@ -24,7 +24,7 @@ export interface AgentConfig {
|
|
|
24
24
|
visitedRoles?: Set<string>; // Circular dispatch detection
|
|
25
25
|
abortSignal?: AbortSignal; // Abort signal for cancellation
|
|
26
26
|
teamStatus?: TeamStatus; // Current team member statuses
|
|
27
|
-
|
|
27
|
+
sessionId: string; // D-014: Session ID for token tracking (required)
|
|
28
28
|
model?: string; // LLM model name for cost tracking
|
|
29
29
|
tokenLedger?: TokenLedger; // Token usage ledger (optional)
|
|
30
30
|
attachments?: ImageAttachment[]; // Image attachments for vision
|
|
@@ -169,7 +169,7 @@ export async function runAgentLoop(config: AgentConfig): Promise<AgentResult> {
|
|
|
169
169
|
depth: depth + 1,
|
|
170
170
|
visitedRoles: new Set(visitedRoles), // Copy for parallel dispatch support
|
|
171
171
|
abortSignal,
|
|
172
|
-
|
|
172
|
+
sessionId: config.sessionId,
|
|
173
173
|
model: config.model,
|
|
174
174
|
tokenLedger: config.tokenLedger,
|
|
175
175
|
onText: (text) => onText?.(`[${targetRoleId}] ${text}`),
|
|
@@ -210,7 +210,7 @@ export async function runAgentLoop(config: AgentConfig): Promise<AgentResult> {
|
|
|
210
210
|
depth: depth + 1,
|
|
211
211
|
visitedRoles: new Set(visitedRoles),
|
|
212
212
|
abortSignal,
|
|
213
|
-
|
|
213
|
+
sessionId: config.sessionId,
|
|
214
214
|
model: config.model,
|
|
215
215
|
tokenLedger: config.tokenLedger,
|
|
216
216
|
onText: (text) => onText?.(`[consult:${targetRoleId}] ${text}`),
|
|
@@ -288,7 +288,7 @@ export async function runAgentLoop(config: AgentConfig): Promise<AgentResult> {
|
|
|
288
288
|
// Record token usage
|
|
289
289
|
config.tokenLedger?.record({
|
|
290
290
|
ts: new Date().toISOString(),
|
|
291
|
-
|
|
291
|
+
sessionId: config.sessionId,
|
|
292
292
|
roleId,
|
|
293
293
|
model: config.model ?? 'unknown',
|
|
294
294
|
inputTokens: response.usage.inputTokens,
|
|
@@ -452,7 +452,7 @@ export async function runAgentLoop(config: AgentConfig): Promise<AgentResult> {
|
|
|
452
452
|
totalOutput += supResponse.usage.outputTokens;
|
|
453
453
|
config.tokenLedger?.record({
|
|
454
454
|
ts: new Date().toISOString(),
|
|
455
|
-
|
|
455
|
+
sessionId: config.sessionId,
|
|
456
456
|
roleId,
|
|
457
457
|
model: config.model ?? 'unknown',
|
|
458
458
|
inputTokens: supResponse.usage.inputTokens,
|
|
@@ -532,7 +532,7 @@ export async function runAgentLoop(config: AgentConfig): Promise<AgentResult> {
|
|
|
532
532
|
totalOutput += verifyResponse.usage.outputTokens;
|
|
533
533
|
config.tokenLedger?.record({
|
|
534
534
|
ts: new Date().toISOString(),
|
|
535
|
-
|
|
535
|
+
sessionId: config.sessionId,
|
|
536
536
|
roleId,
|
|
537
537
|
model: config.model ?? 'unknown',
|
|
538
538
|
inputTokens: verifyResponse.usage.inputTokens,
|
|
@@ -578,7 +578,7 @@ export async function runAgentLoop(config: AgentConfig): Promise<AgentResult> {
|
|
|
578
578
|
totalOutput += summaryResponse.usage.outputTokens;
|
|
579
579
|
config.tokenLedger?.record({
|
|
580
580
|
ts: new Date().toISOString(),
|
|
581
|
-
|
|
581
|
+
sessionId: config.sessionId,
|
|
582
582
|
roleId,
|
|
583
583
|
model: config.model ?? 'unknown',
|
|
584
584
|
inputTokens: summaryResponse.usage.inputTokens,
|
|
@@ -26,7 +26,9 @@ export interface PostKnowledgingResult {
|
|
|
26
26
|
export interface DecayReport {
|
|
27
27
|
health: number;
|
|
28
28
|
orphanDocs: string[];
|
|
29
|
+
staleDocs: string[];
|
|
29
30
|
brokenLinks: Array<{ file: string; link: string }>;
|
|
31
|
+
suggestions: string[];
|
|
30
32
|
totalDocs: number;
|
|
31
33
|
linkedDocs: number;
|
|
32
34
|
}
|
|
@@ -257,6 +259,7 @@ export function postKnowledgingCheck(
|
|
|
257
259
|
export function detectDecay(companyRoot: string): DecayReport {
|
|
258
260
|
const searchDirs = ['knowledge', 'architecture'];
|
|
259
261
|
const orphanDocs: string[] = [];
|
|
262
|
+
const staleDocs: string[] = [];
|
|
260
263
|
const brokenLinks: Array<{ file: string; link: string }> = [];
|
|
261
264
|
let totalDocs = 0;
|
|
262
265
|
let linkedDocs = 0;
|
|
@@ -282,10 +285,20 @@ export function detectDecay(companyRoot: string): DecayReport {
|
|
|
282
285
|
linkedDocs++;
|
|
283
286
|
}
|
|
284
287
|
|
|
285
|
-
// Check for broken links in the file
|
|
288
|
+
// Check for broken links and stale status in the file
|
|
286
289
|
const filePath = path.join(dirPath, file);
|
|
287
290
|
try {
|
|
288
291
|
const content = fs.readFileSync(filePath, 'utf-8');
|
|
292
|
+
|
|
293
|
+
// Check for deprecated/stale status in frontmatter
|
|
294
|
+
const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
|
|
295
|
+
if (frontmatterMatch) {
|
|
296
|
+
const frontmatter = frontmatterMatch[1];
|
|
297
|
+
if (/status:\s*(deprecated|stale)/i.test(frontmatter)) {
|
|
298
|
+
staleDocs.push(path.join(dir, file));
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
289
302
|
const linkRegex = /\[.*?\]\(\.\/(.*?\.md)\)/g;
|
|
290
303
|
let match;
|
|
291
304
|
while ((match = linkRegex.exec(content)) !== null) {
|
|
@@ -322,13 +335,30 @@ export function detectDecay(companyRoot: string): DecayReport {
|
|
|
322
335
|
}
|
|
323
336
|
|
|
324
337
|
const health = totalDocs > 0
|
|
325
|
-
? Math.round(((totalDocs - orphanDocs.length - brokenLinks.length) / totalDocs) * 100)
|
|
338
|
+
? Math.round(((totalDocs - orphanDocs.length - staleDocs.length - brokenLinks.length) / totalDocs) * 100)
|
|
326
339
|
: 100;
|
|
327
340
|
|
|
341
|
+
// Build suggestions
|
|
342
|
+
const suggestions: string[] = [];
|
|
343
|
+
if (orphanDocs.length > 0) {
|
|
344
|
+
suggestions.push(`${orphanDocs.length}개의 고아 문서를 Hub에 등록하세요`);
|
|
345
|
+
}
|
|
346
|
+
if (staleDocs.length > 0) {
|
|
347
|
+
suggestions.push(`${staleDocs.length}개의 오래된 문서를 업데이트하거나 삭제하세요`);
|
|
348
|
+
}
|
|
349
|
+
if (brokenLinks.length > 0) {
|
|
350
|
+
suggestions.push(`${brokenLinks.length}개의 깨진 링크를 수정하세요`);
|
|
351
|
+
}
|
|
352
|
+
if (orphanDocs.length === 0 && staleDocs.length === 0 && brokenLinks.length === 0) {
|
|
353
|
+
suggestions.push('모든 문서가 건강합니다! 🎉');
|
|
354
|
+
}
|
|
355
|
+
|
|
328
356
|
return {
|
|
329
357
|
health: Math.max(0, Math.min(100, health)),
|
|
330
358
|
orphanDocs,
|
|
359
|
+
staleDocs,
|
|
331
360
|
brokenLinks,
|
|
361
|
+
suggestions,
|
|
332
362
|
totalDocs,
|
|
333
363
|
linkedDocs,
|
|
334
364
|
};
|
|
@@ -20,7 +20,8 @@ const DISPATCH_SCRIPT = `#!/usr/bin/env python3
|
|
|
20
20
|
|
|
21
21
|
환경변수:
|
|
22
22
|
DISPATCH_API_URL — API 서버 URL (default: http://localhost:3001)
|
|
23
|
-
|
|
23
|
+
DISPATCH_PARENT_SESSION — 부모 Session ID (자동 설정)
|
|
24
|
+
DISPATCH_PARENT_JOB — (deprecated) 부모 Job ID, DISPATCH_PARENT_SESSION 사용
|
|
24
25
|
DISPATCH_SOURCE_ROLE — 현재 Role ID (자동 설정)
|
|
25
26
|
"""
|
|
26
27
|
import sys, os, json, time, urllib.request, urllib.error
|
|
@@ -40,7 +41,7 @@ def get_result(job_id, retries=3):
|
|
|
40
41
|
for e in events:
|
|
41
42
|
if e['type'] == 'text':
|
|
42
43
|
text_parts.append(e['data'].get('text', ''))
|
|
43
|
-
elif e['type']
|
|
44
|
+
elif e['type'] in ('msg:error', 'job:error'):
|
|
44
45
|
text_parts.append('\\nERROR: ' + e['data'].get('message', ''))
|
|
45
46
|
result = ''.join(text_parts)
|
|
46
47
|
if result:
|
|
@@ -62,15 +63,15 @@ def get_status(job_id):
|
|
|
62
63
|
return get_job_info(job_id).get('status', 'unknown')
|
|
63
64
|
|
|
64
65
|
def start_job(role_id, task):
|
|
65
|
-
|
|
66
|
+
parent_session = os.environ.get('DISPATCH_PARENT_SESSION', os.environ.get('DISPATCH_PARENT_JOB', ''))
|
|
66
67
|
source_role = os.environ.get('DISPATCH_SOURCE_ROLE', 'ceo')
|
|
67
68
|
body = json.dumps({
|
|
68
69
|
'type': 'assign',
|
|
69
70
|
'roleId': role_id,
|
|
70
71
|
'task': task,
|
|
71
72
|
'sourceRole': source_role,
|
|
72
|
-
'
|
|
73
|
-
|
|
73
|
+
'parentSessionId': parent_session if parent_session else None,
|
|
74
|
+
}).encode()
|
|
74
75
|
req = urllib.request.Request(f'{api}/api/jobs', body, {'Content-Type': 'application/json'})
|
|
75
76
|
resp = json.loads(urllib.request.urlopen(req, timeout=10).read())
|
|
76
77
|
return resp['jobId']
|
|
@@ -164,7 +165,7 @@ def get_result(job_id, retries=3):
|
|
|
164
165
|
for e in events:
|
|
165
166
|
if e['type'] == 'text':
|
|
166
167
|
text_parts.append(e['data'].get('text', ''))
|
|
167
|
-
elif e['type']
|
|
168
|
+
elif e['type'] in ('msg:error', 'job:error'):
|
|
168
169
|
text_parts.append('\\nERROR: ' + e['data'].get('message', ''))
|
|
169
170
|
result = ''.join(text_parts)
|
|
170
171
|
if result:
|
|
@@ -211,7 +212,7 @@ if len(sys.argv) < 3:
|
|
|
211
212
|
|
|
212
213
|
role_id = sys.argv[1]
|
|
213
214
|
question = ' '.join(sys.argv[2:])
|
|
214
|
-
|
|
215
|
+
parent_session = os.environ.get('CONSULT_PARENT_SESSION', os.environ.get('DISPATCH_PARENT_SESSION', os.environ.get('DISPATCH_PARENT_JOB', '')))
|
|
215
216
|
source_role = os.environ.get('CONSULT_SOURCE_ROLE', os.environ.get('DISPATCH_SOURCE_ROLE', 'ceo'))
|
|
216
217
|
|
|
217
218
|
# Start job (readOnly + consult type)
|
|
@@ -222,7 +223,7 @@ body = json.dumps({
|
|
|
222
223
|
'task': task,
|
|
223
224
|
'sourceRole': source_role,
|
|
224
225
|
'readOnly': True,
|
|
225
|
-
'
|
|
226
|
+
'parentSessionId': parent_session if parent_session else None,
|
|
226
227
|
}).encode()
|
|
227
228
|
|
|
228
229
|
try:
|
|
@@ -344,9 +345,8 @@ export class ClaudeCliRunner implements ExecutionRunner {
|
|
|
344
345
|
cleanEnv.DISPATCH_API_URL = `http://localhost:${apiPort}`;
|
|
345
346
|
cleanEnv.DISPATCH_SOURCE_ROLE = roleId;
|
|
346
347
|
cleanEnv.DISPATCH_SUBORDINATES = subordinates.join(', ');
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
}
|
|
348
|
+
cleanEnv.DISPATCH_PARENT_SESSION = config.sessionId;
|
|
349
|
+
cleanEnv.DISPATCH_PARENT_JOB = config.sessionId; // deprecated, kept for backward compat
|
|
350
350
|
// dispatch 명령어 경로를 PATH에 추가하지 않고 절대 경로로 사용
|
|
351
351
|
cleanEnv.DISPATCH_CMD = dispatchScript;
|
|
352
352
|
cleanEnv.CONSULT_CMD = consultScript;
|
|
@@ -360,7 +360,7 @@ export class ClaudeCliRunner implements ExecutionRunner {
|
|
|
360
360
|
// Inject repo paths so agents never confuse repos
|
|
361
361
|
cleanEnv.TYCONO_CODE_ROOT = codeRoot;
|
|
362
362
|
cleanEnv.TYCONO_AKB_ROOT = companyRoot;
|
|
363
|
-
console.log(`[Runner] Spawning claude -p: role=${roleId}, model=${modelName}, maxTurns=${maxTurns},
|
|
363
|
+
console.log(`[Runner] Spawning claude -p: role=${roleId}, model=${modelName}, maxTurns=${maxTurns}, sessionId=${config.sessionId}, cwd=${cwd}, subordinates=[${subordinates.join(',')}]`);
|
|
364
364
|
|
|
365
365
|
const proc = spawn('claude', args, {
|
|
366
366
|
cwd,
|
|
@@ -422,7 +422,7 @@ export class ClaudeCliRunner implements ExecutionRunner {
|
|
|
422
422
|
addToolCall: (name, input) => {
|
|
423
423
|
toolCalls.push({ name, input });
|
|
424
424
|
// Dispatch detection removed — child jobs created by the Python
|
|
425
|
-
// dispatch bridge script via POST /api/jobs with
|
|
425
|
+
// dispatch bridge script via POST /api/jobs with parentSessionId.
|
|
426
426
|
// JobManager.startJob() now auto-emits dispatch:start on parent stream.
|
|
427
427
|
},
|
|
428
428
|
incrementTurn: () => { turnCount++; callbacks.onTurnComplete?.(turnCount); },
|
|
@@ -431,7 +431,7 @@ export class ClaudeCliRunner implements ExecutionRunner {
|
|
|
431
431
|
totalOutput += out;
|
|
432
432
|
tokenLedger.record({
|
|
433
433
|
ts: new Date().toISOString(),
|
|
434
|
-
|
|
434
|
+
sessionId: config.sessionId,
|
|
435
435
|
roleId,
|
|
436
436
|
model: modelName,
|
|
437
437
|
inputTokens: input,
|
|
@@ -471,7 +471,7 @@ export class ClaudeCliRunner implements ExecutionRunner {
|
|
|
471
471
|
totalOutput += out;
|
|
472
472
|
tokenLedger.record({
|
|
473
473
|
ts: new Date().toISOString(),
|
|
474
|
-
|
|
474
|
+
sessionId: config.sessionId,
|
|
475
475
|
roleId,
|
|
476
476
|
model: modelName,
|
|
477
477
|
inputTokens: input,
|
|
@@ -38,7 +38,7 @@ export class DirectApiRunner implements ExecutionRunner {
|
|
|
38
38
|
codeRoot: config.codeRoot,
|
|
39
39
|
llm: this.llm,
|
|
40
40
|
abortSignal: abortController.signal,
|
|
41
|
-
|
|
41
|
+
sessionId: config.sessionId,
|
|
42
42
|
model: config.model,
|
|
43
43
|
tokenLedger,
|
|
44
44
|
attachments: config.attachments,
|
|
@@ -35,7 +35,8 @@ export interface RunnerConfig {
|
|
|
35
35
|
readOnly?: boolean;
|
|
36
36
|
maxTurns?: number;
|
|
37
37
|
model?: string;
|
|
38
|
-
|
|
38
|
+
/** D-014: Session ID for tracking (required — primary identifier for token ledger). */
|
|
39
|
+
sessionId: string;
|
|
39
40
|
teamStatus?: TeamStatus;
|
|
40
41
|
attachments?: ImageAttachment[];
|
|
41
42
|
/** Selective dispatch scope — only these roles can be dispatched to */
|
|
@@ -2,29 +2,25 @@
|
|
|
2
2
|
* active-sessions.ts — Active session visibility API
|
|
3
3
|
*
|
|
4
4
|
* Exposes session + port + worktree state for both UI and AI agents.
|
|
5
|
-
* All sessions sharing the same tycono server origin can query this.
|
|
6
5
|
*/
|
|
7
6
|
import { Router } from 'express';
|
|
8
7
|
import { portRegistry } from '../services/port-registry.js';
|
|
9
|
-
import {
|
|
8
|
+
import { executionManager } from '../services/execution-manager.js';
|
|
10
9
|
|
|
11
10
|
export const activeSessionsRouter = Router();
|
|
12
11
|
|
|
13
12
|
/**
|
|
14
13
|
* GET /api/active-sessions
|
|
15
|
-
* Returns all active sessions with port + worktree info.
|
|
16
|
-
* Used by both the web UI and AI agents (curl).
|
|
17
14
|
*/
|
|
18
15
|
activeSessionsRouter.get('/', (_req, res) => {
|
|
19
16
|
const sessions = portRegistry.getAll();
|
|
20
17
|
|
|
21
|
-
// Enrich with job info where available
|
|
22
18
|
const enriched = sessions.map(s => {
|
|
23
|
-
const
|
|
19
|
+
const exec = executionManager.getActiveExecution(s.sessionId);
|
|
24
20
|
return {
|
|
25
21
|
...s,
|
|
26
|
-
|
|
27
|
-
roleName:
|
|
22
|
+
messageStatus: exec?.status ?? null,
|
|
23
|
+
roleName: exec?.roleId ?? s.roleId,
|
|
28
24
|
alive: s.pid ? isAlive(s.pid) : null,
|
|
29
25
|
};
|
|
30
26
|
});
|
|
@@ -37,7 +33,6 @@ activeSessionsRouter.get('/', (_req, res) => {
|
|
|
37
33
|
|
|
38
34
|
/**
|
|
39
35
|
* GET /api/active-sessions/:id
|
|
40
|
-
* Get detailed info for a specific session.
|
|
41
36
|
*/
|
|
42
37
|
activeSessionsRouter.get('/:id', (req, res) => {
|
|
43
38
|
const session = portRegistry.get(req.params.id);
|
|
@@ -46,20 +41,19 @@ activeSessionsRouter.get('/:id', (req, res) => {
|
|
|
46
41
|
return;
|
|
47
42
|
}
|
|
48
43
|
|
|
49
|
-
const
|
|
44
|
+
const exec = executionManager.getActiveExecution(session.sessionId);
|
|
50
45
|
|
|
51
46
|
res.json({
|
|
52
47
|
...session,
|
|
53
|
-
|
|
54
|
-
roleName:
|
|
48
|
+
messageStatus: exec?.status ?? null,
|
|
49
|
+
roleName: exec?.roleId ?? session.roleId,
|
|
55
50
|
alive: session.pid ? isAlive(session.pid) : null,
|
|
56
|
-
|
|
51
|
+
execution: exec ? { id: exec.id, status: exec.status, roleId: exec.roleId, task: exec.task } : null,
|
|
57
52
|
});
|
|
58
53
|
});
|
|
59
54
|
|
|
60
55
|
/**
|
|
61
56
|
* DELETE /api/active-sessions/:id
|
|
62
|
-
* Stop a session — release ports + clean up.
|
|
63
57
|
*/
|
|
64
58
|
activeSessionsRouter.delete('/:id', (req, res) => {
|
|
65
59
|
const sessionId = req.params.id;
|
|
@@ -70,10 +64,7 @@ activeSessionsRouter.delete('/:id', (req, res) => {
|
|
|
70
64
|
return;
|
|
71
65
|
}
|
|
72
66
|
|
|
73
|
-
|
|
74
|
-
jobManager.abortJob(sessionId);
|
|
75
|
-
|
|
76
|
-
// Release ports
|
|
67
|
+
executionManager.abortSession(sessionId);
|
|
77
68
|
portRegistry.release(sessionId);
|
|
78
69
|
|
|
79
70
|
res.json({ ok: true, released: session.ports });
|
|
@@ -81,7 +72,6 @@ activeSessionsRouter.delete('/:id', (req, res) => {
|
|
|
81
72
|
|
|
82
73
|
/**
|
|
83
74
|
* POST /api/active-sessions/cleanup
|
|
84
|
-
* Clean up all dead sessions (PID gone).
|
|
85
75
|
*/
|
|
86
76
|
activeSessionsRouter.post('/cleanup', (_req, res) => {
|
|
87
77
|
const result = portRegistry.cleanup();
|
|
@@ -98,7 +88,6 @@ activeSessionsRouter.post('/cleanup', (_req, res) => {
|
|
|
98
88
|
|
|
99
89
|
/**
|
|
100
90
|
* POST /api/active-sessions/register
|
|
101
|
-
* Manually register a session (for external Claude Code sessions).
|
|
102
91
|
*/
|
|
103
92
|
activeSessionsRouter.post('/register', async (req, res) => {
|
|
104
93
|
const { sessionId, roleId, task, pid, worktreePath } = req.body;
|
|
@@ -108,7 +97,6 @@ activeSessionsRouter.post('/register', async (req, res) => {
|
|
|
108
97
|
return;
|
|
109
98
|
}
|
|
110
99
|
|
|
111
|
-
// Check if already registered
|
|
112
100
|
const existing = portRegistry.get(sessionId);
|
|
113
101
|
if (existing) {
|
|
114
102
|
res.json({ ok: true, ports: existing.ports, existing: true });
|
|
@@ -61,6 +61,7 @@ costRouter.get('/summary', (req: Request, res: Response, next: NextFunction) =>
|
|
|
61
61
|
});
|
|
62
62
|
|
|
63
63
|
/* ── W-T602: GET /api/cost/jobs/:jobId ───── */
|
|
64
|
+
/* @deprecated D-014: use /api/cost/sessions/:sessionId */
|
|
64
65
|
|
|
65
66
|
costRouter.get('/jobs/:jobId', (req: Request, res: Response, next: NextFunction) => {
|
|
66
67
|
try {
|
|
@@ -96,3 +97,45 @@ costRouter.get('/jobs/:jobId', (req: Request, res: Response, next: NextFunction)
|
|
|
96
97
|
next(err);
|
|
97
98
|
}
|
|
98
99
|
});
|
|
100
|
+
|
|
101
|
+
/* ── D-014: GET /api/cost/sessions/:sessionId ───── */
|
|
102
|
+
|
|
103
|
+
costRouter.get('/sessions/:sessionId', (req: Request, res: Response, next: NextFunction) => {
|
|
104
|
+
try {
|
|
105
|
+
const sessionId = req.params.sessionId as string;
|
|
106
|
+
const ledger = getTokenLedger(COMPANY_ROOT);
|
|
107
|
+
|
|
108
|
+
// D-014: Try sessionId field first, fall back to jobId for legacy entries
|
|
109
|
+
let summary = ledger.query({ sessionId });
|
|
110
|
+
if (summary.entries.length === 0) {
|
|
111
|
+
summary = ledger.query({ jobId: sessionId });
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (summary.entries.length === 0) {
|
|
115
|
+
res.status(404).json({ error: `No cost data found for session ${sessionId}` });
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
let totalCostUsd = 0;
|
|
120
|
+
for (const entry of summary.entries) {
|
|
121
|
+
totalCostUsd += estimateCost(entry.inputTokens, entry.outputTokens, entry.model);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
res.json({
|
|
125
|
+
sessionId,
|
|
126
|
+
totalInputTokens: summary.totalInput,
|
|
127
|
+
totalOutputTokens: summary.totalOutput,
|
|
128
|
+
totalCostUsd,
|
|
129
|
+
entries: summary.entries.map((e) => ({
|
|
130
|
+
ts: e.ts,
|
|
131
|
+
roleId: e.roleId,
|
|
132
|
+
model: e.model,
|
|
133
|
+
inputTokens: e.inputTokens,
|
|
134
|
+
outputTokens: e.outputTokens,
|
|
135
|
+
costUsd: estimateCost(e.inputTokens, e.outputTokens, e.model),
|
|
136
|
+
})),
|
|
137
|
+
});
|
|
138
|
+
} catch (err) {
|
|
139
|
+
next(err);
|
|
140
|
+
}
|
|
141
|
+
});
|