tycono 0.1.96-beta.6 → 0.1.96-beta.60
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/README.md +16 -14
- package/bin/tycono.ts +62 -27
- package/package.json +1 -1
- package/src/api/src/create-server.ts +5 -1
- package/src/api/src/routes/active-sessions.ts +3 -0
- package/src/api/src/routes/execute.ts +30 -41
- package/src/api/src/server.ts +44 -0
- package/src/api/src/services/execution-manager.ts +7 -1
- package/src/api/src/services/supervisor-heartbeat.ts +186 -11
- package/src/api/src/services/wave-multiplexer.ts +21 -7
- package/src/tui/api.ts +67 -5
- package/src/tui/app.tsx +518 -164
- package/src/tui/components/CommandMode.tsx +348 -0
- package/src/tui/components/OrgTree.tsx +17 -18
- package/src/tui/components/PanelMode.tsx +550 -0
- package/src/tui/components/SetupWizard.tsx +22 -7
- package/src/tui/components/StatusBar.tsx +44 -25
- package/src/tui/components/StreamView.tsx +171 -0
- package/src/tui/hooks/useApi.ts +42 -7
- package/src/tui/hooks/useCommand.ts +199 -0
- package/src/tui/hooks/useSSE.ts +130 -27
- package/src/tui/store.ts +12 -0
- package/src/tui/utils/markdown.tsx +102 -0
- package/src/tui/components/CommandInput.tsx +0 -32
- package/src/tui/components/HelpOverlay.tsx +0 -51
- package/src/tui/components/SessionList.tsx +0 -74
- package/src/tui/components/StreamPanel.tsx +0 -182
- package/src/tui/components/WaveDialog.tsx +0 -56
- package/src/tui/hooks/useKeyboard.ts +0 -62
package/README.md
CHANGED
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
<h1 align="center">tycono</h1>
|
|
6
6
|
|
|
7
7
|
<p align="center">
|
|
8
|
-
<strong>
|
|
9
|
-
<sub>
|
|
8
|
+
<strong>Cursor gives you one AI developer. Tycono gives you an AI team.</strong><br>
|
|
9
|
+
<sub>Give one order. Watch your AI team plan, build, and learn together.</sub>
|
|
10
10
|
</p>
|
|
11
11
|
|
|
12
12
|
<p align="center">
|
|
@@ -25,9 +25,11 @@
|
|
|
25
25
|
|
|
26
26
|
---
|
|
27
27
|
|
|
28
|
-
|
|
28
|
+
Cursor, Lovable, Bolt — they all give you **one AI agent**. It helps, but you still drive everything.
|
|
29
29
|
|
|
30
|
-
|
|
30
|
+
**tycono** gives you an **AI team**. A CTO reviews architecture. Engineers write code. A PM breaks down tasks. QA catches bugs. You just give the order and watch them work.
|
|
31
|
+
|
|
32
|
+
One command. Your AI team is running.
|
|
31
33
|
|
|
32
34
|
```bash
|
|
33
35
|
npx tycono
|
|
@@ -83,16 +85,16 @@ Session 50 is dramatically smarter than session 1. Your company learns.
|
|
|
83
85
|
|
|
84
86
|
## Why Tycono?
|
|
85
87
|
|
|
86
|
-
|
|
88
|
+
Same goal as Cursor, Lovable, Bolt — **get AI to do your work**. Different method.
|
|
87
89
|
|
|
88
|
-
| |
|
|
90
|
+
| | Cursor / Lovable / Bolt | Tycono |
|
|
89
91
|
|---|---|---|
|
|
90
|
-
| **
|
|
91
|
-
| **
|
|
92
|
-
| **
|
|
93
|
-
| **
|
|
94
|
-
| **Scale** | 1
|
|
95
|
-
| **Visibility** |
|
|
92
|
+
| **Agents** | 1 AI helps you | **AI team works for you** |
|
|
93
|
+
| **Your role** | Keep directing | **Give one order, watch** |
|
|
94
|
+
| **Knowledge** | Resets every session | **Compounds forever** |
|
|
95
|
+
| **Quality** | You review everything | **QA agent catches bugs** |
|
|
96
|
+
| **Scale** | 1 task at a time | **Parallel across roles** |
|
|
97
|
+
| **Visibility** | Editor / chat | **Real-time org tree** |
|
|
96
98
|
|
|
97
99
|
## Company-as-Code
|
|
98
100
|
|
|
@@ -258,9 +260,9 @@ npx tycono --version # Show version
|
|
|
258
260
|
- [x] CEO Wave dispatch with org-tree targeting
|
|
259
261
|
- [x] AKB — Pre-K / Post-K knowledge loop
|
|
260
262
|
- [x] Port Registry for multi-agent isolation
|
|
261
|
-
- [ ] **TUI mode** — terminal-native multi-panel interface
|
|
263
|
+
- [ ] **TUI mode** — terminal-native multi-panel interface *(in progress)*
|
|
262
264
|
- [ ] Git worktree isolation per agent session
|
|
263
|
-
- [ ] Desktop app (.dmg / .exe) — background execution,
|
|
265
|
+
- [ ] **Desktop app** (.dmg / .exe) — background execution, notifications, no API key setup needed
|
|
264
266
|
- [ ] Multi-LLM support (OpenAI, local models)
|
|
265
267
|
|
|
266
268
|
## Built with Tycono
|
package/bin/tycono.ts
CHANGED
|
@@ -19,18 +19,17 @@ function printHelp(): void {
|
|
|
19
19
|
Build an AI company. Watch them work.
|
|
20
20
|
|
|
21
21
|
Usage:
|
|
22
|
-
tycono [path] Start
|
|
23
|
-
tycono
|
|
24
|
-
tycono
|
|
22
|
+
tycono [path] Start TUI (default, optionally point to a company directory)
|
|
23
|
+
tycono --classic Start pixel office web UI
|
|
24
|
+
tycono --attach Connect TUI to existing API server
|
|
25
25
|
tycono --help Show this help message
|
|
26
26
|
tycono --version Show version
|
|
27
27
|
|
|
28
28
|
Examples:
|
|
29
|
-
tycono Start in current directory
|
|
30
|
-
tycono ./my-company Start with existing company folder
|
|
31
|
-
tycono
|
|
32
|
-
tycono
|
|
33
|
-
PORT=3000 tycono tui --attach Attach TUI to running server
|
|
29
|
+
tycono Start TUI in current directory
|
|
30
|
+
tycono ./my-company Start TUI with existing company folder
|
|
31
|
+
tycono --classic Start pixel office web UI
|
|
32
|
+
PORT=3000 tycono --attach Attach TUI to running server
|
|
34
33
|
|
|
35
34
|
AI Engine (auto-detected):
|
|
36
35
|
1. Claude Code CLI Install from https://claude.ai/download (recommended)
|
|
@@ -219,8 +218,16 @@ async function startServerForTui(): Promise<void> {
|
|
|
219
218
|
const logFd = fs.openSync(logFile, 'a');
|
|
220
219
|
const logStream = fs.createWriteStream(logFile, { fd: logFd });
|
|
221
220
|
const origStdoutWrite = process.stdout.write.bind(process.stdout);
|
|
222
|
-
const
|
|
223
|
-
|
|
221
|
+
const origLog = (...args: unknown[]) => origStdoutWrite(args.join(' ') + '\n');
|
|
222
|
+
|
|
223
|
+
// Redirect console + stdout.write to suppress server logs
|
|
224
|
+
// ⛔ Do NOT redirect process.stderr.write — breaks Node.js http client
|
|
225
|
+
console.log = (...a: unknown[]) => { logStream.write(a.join(' ') + '\n'); };
|
|
226
|
+
console.error = (...a: unknown[]) => { logStream.write(a.join(' ') + '\n'); };
|
|
227
|
+
console.warn = (...a: unknown[]) => { logStream.write(a.join(' ') + '\n'); };
|
|
228
|
+
console.info = (...a: unknown[]) => { logStream.write(a.join(' ') + '\n'); };
|
|
229
|
+
|
|
230
|
+
// Intercept stdout.write — allow Ink (ANSI), redirect server text to log
|
|
224
231
|
const isInkOutput = (s: string) => s.includes('\x1b[') || s.includes('\x1b(');
|
|
225
232
|
process.stdout.write = ((chunk: any, ...args: any[]) => {
|
|
226
233
|
const str = typeof chunk === 'string' ? chunk : chunk.toString();
|
|
@@ -228,19 +235,12 @@ async function startServerForTui(): Promise<void> {
|
|
|
228
235
|
logStream.write(str);
|
|
229
236
|
return true;
|
|
230
237
|
}) as any;
|
|
231
|
-
process.stderr.write = ((chunk: any, ...args: any[]) => {
|
|
232
|
-
logStream.write(typeof chunk === 'string' ? chunk : chunk.toString());
|
|
233
|
-
return true;
|
|
234
|
-
}) as any;
|
|
235
|
-
const origLog = (...args: unknown[]) => origStdoutWrite(args.join(' ') + '\n');
|
|
236
238
|
|
|
237
239
|
const { createHttpServer } = await import('../src/api/src/create-server.js');
|
|
238
240
|
const server = createHttpServer();
|
|
239
241
|
|
|
240
|
-
const host = process.env.HOST || '0.0.0.0';
|
|
241
|
-
|
|
242
242
|
await new Promise<void>((resolve) => {
|
|
243
|
-
server.listen(port,
|
|
243
|
+
server.listen(port, '0.0.0.0', () => resolve());
|
|
244
244
|
});
|
|
245
245
|
|
|
246
246
|
origLog(` API server started on port ${port}`);
|
|
@@ -254,7 +254,7 @@ async function startServerForTui(): Promise<void> {
|
|
|
254
254
|
process.on('SIGINT', shutdown);
|
|
255
255
|
process.on('SIGTERM', shutdown);
|
|
256
256
|
|
|
257
|
-
// Start TUI
|
|
257
|
+
// Start TUI — stdout.write is NOT intercepted, Ink has full control
|
|
258
258
|
const { startTui } = await import('../src/tui/index.tsx');
|
|
259
259
|
await startTui({ port });
|
|
260
260
|
}
|
|
@@ -272,24 +272,37 @@ export async function main(args: string[]): Promise<void> {
|
|
|
272
272
|
return;
|
|
273
273
|
}
|
|
274
274
|
|
|
275
|
-
//
|
|
275
|
+
// --classic: legacy pixel office web UI
|
|
276
|
+
if (command === '--classic' || args.includes('--classic')) {
|
|
277
|
+
if (command === '--classic' && args[1] && !args[1].startsWith('-')) {
|
|
278
|
+
process.env.COMPANY_ROOT = path.resolve(args[1]);
|
|
279
|
+
}
|
|
280
|
+
await startServer();
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// --attach: connect TUI to existing API server
|
|
285
|
+
if (command === '--attach' || args.includes('--attach')) {
|
|
286
|
+
const port = process.env.PORT ? Number(process.env.PORT) : 3000;
|
|
287
|
+
const { startTui } = await import('../src/tui/index.tsx');
|
|
288
|
+
await startTui({ port });
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// Legacy: `tui` subcommand still works
|
|
276
293
|
if (command === 'tui') {
|
|
277
|
-
|
|
278
|
-
// If --attach, skip server start — just connect to existing API
|
|
279
|
-
if (attachMode) {
|
|
294
|
+
if (args.includes('--attach')) {
|
|
280
295
|
const port = process.env.PORT ? Number(process.env.PORT) : 3000;
|
|
281
296
|
const { startTui } = await import('../src/tui/index.tsx');
|
|
282
297
|
await startTui({ port });
|
|
283
298
|
return;
|
|
284
299
|
}
|
|
285
|
-
|
|
286
|
-
// Start API server, then TUI
|
|
287
300
|
await startServerForTui();
|
|
288
301
|
return;
|
|
289
302
|
}
|
|
290
303
|
|
|
304
|
+
// Path argument: treat as company directory
|
|
291
305
|
if (command && !command.startsWith('-')) {
|
|
292
|
-
// Treat as path to company directory
|
|
293
306
|
const resolved = path.resolve(command);
|
|
294
307
|
if (!fs.existsSync(resolved)) {
|
|
295
308
|
console.error(` Path not found: ${resolved}`);
|
|
@@ -298,5 +311,27 @@ export async function main(args: string[]): Promise<void> {
|
|
|
298
311
|
process.env.COMPANY_ROOT = resolved;
|
|
299
312
|
}
|
|
300
313
|
|
|
301
|
-
|
|
314
|
+
// Show first-run notice (once only)
|
|
315
|
+
const prefsPath = path.resolve(process.env.COMPANY_ROOT || process.cwd(), '.tycono', 'preferences.json');
|
|
316
|
+
let prefs: Record<string, unknown> = {};
|
|
317
|
+
try { prefs = JSON.parse(fs.readFileSync(prefsPath, 'utf-8')); } catch {}
|
|
318
|
+
if (!prefs.tuiNoticeShown) {
|
|
319
|
+
console.log('');
|
|
320
|
+
console.log(' Tycono v' + VERSION + ' — AI Company OS');
|
|
321
|
+
console.log('');
|
|
322
|
+
console.log(' New: Terminal mode is now the default.');
|
|
323
|
+
console.log(' Faster, scriptable, built for work.');
|
|
324
|
+
console.log('');
|
|
325
|
+
console.log(' Looking for the pixel office?');
|
|
326
|
+
console.log(' → npx tycono --classic');
|
|
327
|
+
console.log('');
|
|
328
|
+
prefs.tuiNoticeShown = true;
|
|
329
|
+
try {
|
|
330
|
+
fs.mkdirSync(path.dirname(prefsPath), { recursive: true });
|
|
331
|
+
fs.writeFileSync(prefsPath, JSON.stringify(prefs, null, 2));
|
|
332
|
+
} catch {}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// Default: TUI mode
|
|
336
|
+
await startServerForTui();
|
|
302
337
|
}
|
package/package.json
CHANGED
|
@@ -227,8 +227,12 @@ export function createExpressApp(): express.Application {
|
|
|
227
227
|
}
|
|
228
228
|
|
|
229
229
|
app.use((err: Error, req: express.Request, res: express.Response, _next: express.NextFunction) => {
|
|
230
|
-
console.error(`[ERROR] ${req.method} ${req.url} — ${err.message}`);
|
|
231
230
|
const status = err.name === 'FileNotFoundError' ? 404 : 500;
|
|
231
|
+
// Log server errors via console.error (redirected to log file in TUI mode)
|
|
232
|
+
// 404 errors are expected (e.g. fresh install, no company.md) — skip
|
|
233
|
+
if (status >= 500) {
|
|
234
|
+
console.error(`[ERROR] ${req.method} ${req.url} — ${err.message}`);
|
|
235
|
+
}
|
|
232
236
|
res.status(status).json({ error: err.message });
|
|
233
237
|
});
|
|
234
238
|
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
import { Router } from 'express';
|
|
7
7
|
import { portRegistry } from '../services/port-registry.js';
|
|
8
8
|
import { executionManager } from '../services/execution-manager.js';
|
|
9
|
+
import { getSession } from '../services/session-store.js';
|
|
9
10
|
|
|
10
11
|
export const activeSessionsRouter = Router();
|
|
11
12
|
|
|
@@ -17,8 +18,10 @@ activeSessionsRouter.get('/', (_req, res) => {
|
|
|
17
18
|
|
|
18
19
|
const enriched = sessions.map(s => {
|
|
19
20
|
const exec = executionManager.getActiveExecution(s.sessionId);
|
|
21
|
+
const session = getSession(s.sessionId);
|
|
20
22
|
return {
|
|
21
23
|
...s,
|
|
24
|
+
waveId: session?.waveId ?? null,
|
|
22
25
|
messageStatus: exec?.status ?? null,
|
|
23
26
|
roleName: exec?.roleId ?? s.roleId,
|
|
24
27
|
alive: s.pid ? isAlive(s.pid) : null,
|
|
@@ -51,25 +51,21 @@ export function handleExecRequest(req: IncomingMessage, res: ServerResponse): vo
|
|
|
51
51
|
// ── /api/waves/active — restore active waves after refresh ──
|
|
52
52
|
if (method === 'GET' && url === '/api/waves/active') {
|
|
53
53
|
// Recovery: rebuild wave→session mapping from session-store if lost
|
|
54
|
+
// Only recover ACTIVE sessions to prevent OOM on large datasets (140+ sessions)
|
|
54
55
|
const waves = waveMultiplexer.getActiveWaves();
|
|
55
56
|
if (waves.length === 0) {
|
|
56
57
|
const allSessions = listSessions();
|
|
57
|
-
|
|
58
|
+
let recovered = 0;
|
|
58
59
|
for (const ses of allSessions) {
|
|
59
|
-
if (ses.waveId)
|
|
60
|
-
|
|
61
|
-
|
|
60
|
+
if (!ses.waveId || ses.status !== 'active') continue;
|
|
61
|
+
const exec = executionManager.getActiveExecution(ses.id);
|
|
62
|
+
if (exec) {
|
|
63
|
+
waveMultiplexer.registerSession(ses.waveId, exec);
|
|
64
|
+
recovered++;
|
|
62
65
|
}
|
|
63
66
|
}
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
for (const sid of sids) {
|
|
67
|
-
// getActiveExecution falls back to stream recovery for completed sessions
|
|
68
|
-
const exec = executionManager.getActiveExecution(sid);
|
|
69
|
-
if (exec) {
|
|
70
|
-
waveMultiplexer.registerSession(wid, exec);
|
|
71
|
-
}
|
|
72
|
-
}
|
|
67
|
+
if (recovered > 0) {
|
|
68
|
+
console.log(`[WaveRecovery] Recovered ${recovered} active sessions`);
|
|
73
69
|
}
|
|
74
70
|
}
|
|
75
71
|
jsonResponse(res, 200, { waves: waveMultiplexer.getActiveWaves() });
|
|
@@ -212,10 +208,8 @@ function handleStartJob(body: Record<string, unknown>, res: ServerResponse): voi
|
|
|
212
208
|
const attachments = body.attachments as ImageAttachment[] | undefined;
|
|
213
209
|
|
|
214
210
|
if (type === 'wave') {
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
return;
|
|
218
|
-
}
|
|
211
|
+
// directive가 없으면 idle 상태로 시작 (empty wave)
|
|
212
|
+
const actualDirective = directive || '';
|
|
219
213
|
|
|
220
214
|
const targetRoles = body.targetRoles as string[] | undefined;
|
|
221
215
|
const continuous = body.continuous === true;
|
|
@@ -224,7 +218,7 @@ function handleStartJob(body: Record<string, unknown>, res: ServerResponse): voi
|
|
|
224
218
|
{
|
|
225
219
|
const state = supervisorHeartbeat.start(
|
|
226
220
|
`wave-${Date.now()}`,
|
|
227
|
-
|
|
221
|
+
actualDirective,
|
|
228
222
|
targetRoles && targetRoles.length > 0 ? targetRoles : undefined,
|
|
229
223
|
continuous,
|
|
230
224
|
);
|
|
@@ -238,7 +232,7 @@ function handleStartJob(body: Record<string, unknown>, res: ServerResponse): voi
|
|
|
238
232
|
waveId: state.waveId,
|
|
239
233
|
supervisorSessionId: state.supervisorSessionId,
|
|
240
234
|
mode: 'supervisor',
|
|
241
|
-
directive,
|
|
235
|
+
directive: actualDirective,
|
|
242
236
|
});
|
|
243
237
|
return;
|
|
244
238
|
}
|
|
@@ -482,13 +476,11 @@ function handleWaveStream(waveId: string, url: string, res: ServerResponse, req:
|
|
|
482
476
|
|
|
483
477
|
let sessionIds = waveMultiplexer.getWaveSessionIds(waveId);
|
|
484
478
|
|
|
485
|
-
// Recovery:
|
|
486
|
-
// rebuild from session-store (sessions have waveId) + executionManager
|
|
479
|
+
// Recovery: only recover ACTIVE sessions for this wave (not all 140)
|
|
487
480
|
if (sessionIds.length === 0) {
|
|
488
481
|
const allSessions = listSessions();
|
|
489
|
-
const waveSessions = allSessions.filter(s => s.waveId === waveId);
|
|
482
|
+
const waveSessions = allSessions.filter(s => s.waveId === waveId && s.status === 'active');
|
|
490
483
|
for (const ses of waveSessions) {
|
|
491
|
-
// getActiveExecution recovers from stream files for completed sessions too
|
|
492
484
|
const exec = executionManager.getActiveExecution(ses.id);
|
|
493
485
|
if (exec) {
|
|
494
486
|
waveMultiplexer.registerSession(waveId, exec);
|
|
@@ -497,12 +489,8 @@ function handleWaveStream(waveId: string, url: string, res: ServerResponse, req:
|
|
|
497
489
|
sessionIds = waveMultiplexer.getWaveSessionIds(waveId);
|
|
498
490
|
}
|
|
499
491
|
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
res.end(JSON.stringify({ error: `No sessions found for wave: ${waveId}` }));
|
|
503
|
-
return;
|
|
504
|
-
}
|
|
505
|
-
|
|
492
|
+
// Don't 404 on empty waves — keep SSE alive, sessions will appear later
|
|
493
|
+
// (e.g. idle wave waiting for first directive, or supervisor restarting)
|
|
506
494
|
const client = waveMultiplexer.attach(waveId, res as any, fromWaveSeq);
|
|
507
495
|
|
|
508
496
|
req.on('close', () => {
|
|
@@ -769,25 +757,26 @@ function handleStatus(res: ServerResponse): void {
|
|
|
769
757
|
);
|
|
770
758
|
|
|
771
759
|
const recovered: typeof activeExecs = [];
|
|
772
|
-
|
|
773
|
-
|
|
760
|
+
// Limit recovery scan to prevent OOM on large session stores
|
|
761
|
+
const MAX_RECOVERY_SCAN = 20;
|
|
762
|
+
const recentActive = activeSessions.slice(-MAX_RECOVERY_SCAN);
|
|
763
|
+
|
|
764
|
+
for (const ses of recentActive) {
|
|
774
765
|
if (!ActivityStream.exists(ses.id)) continue;
|
|
766
|
+
// Only read last few events to check done/error (not entire stream)
|
|
775
767
|
const events = ActivityStream.readFrom(ses.id, 0);
|
|
776
768
|
if (events.length === 0) continue;
|
|
777
769
|
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
const errorEvent = events.find(e => e.type === 'msg:error');
|
|
783
|
-
|
|
784
|
-
// Only include sessions that haven't finished yet
|
|
785
|
-
if (doneEvent || errorEvent) continue;
|
|
770
|
+
// Check last 5 events for done/error (optimization: don't scan entire file)
|
|
771
|
+
const tail = events.slice(-5);
|
|
772
|
+
const isDone = tail.some(e => e.type === 'msg:done' || e.type === 'msg:error');
|
|
773
|
+
if (isDone) continue;
|
|
786
774
|
|
|
787
|
-
const
|
|
775
|
+
const startEvent = events.find(e => e.type === 'msg:start');
|
|
776
|
+
const task = (startEvent?.data?.task as string) ?? ses.title ?? '';
|
|
788
777
|
recovered.push({
|
|
789
778
|
id: `recovered-${ses.id}`,
|
|
790
|
-
type: (startEvent
|
|
779
|
+
type: (startEvent?.data?.type as string ?? 'assign') as 'assign' | 'wave' | 'consult',
|
|
791
780
|
roleId: ses.roleId,
|
|
792
781
|
task,
|
|
793
782
|
status: 'running',
|
package/src/api/src/server.ts
CHANGED
|
@@ -1,11 +1,40 @@
|
|
|
1
1
|
import { COMPANY_ROOT } from './services/file-reader.js';
|
|
2
2
|
import { applyConfig } from './services/company-config.js';
|
|
3
3
|
import { createHttpServer } from './create-server.js';
|
|
4
|
+
import { listSessions, updateSession } from './services/session-store.js';
|
|
5
|
+
import { ActivityStream } from './services/activity-stream.js';
|
|
4
6
|
|
|
5
7
|
// Load .tycono/config.json and apply to process.env
|
|
6
8
|
const config = applyConfig(COMPANY_ROOT);
|
|
7
9
|
console.log(`[STARTUP] Engine: ${config.engine}, API key: ${config.apiKey ? 'set' : 'none'}`);
|
|
8
10
|
|
|
11
|
+
// Startup: mark orphaned 'active' sessions as 'interrupted'
|
|
12
|
+
// These are sessions from a previous server that crashed or was killed
|
|
13
|
+
{
|
|
14
|
+
const allSessions = listSessions();
|
|
15
|
+
let orphaned = 0;
|
|
16
|
+
for (const ses of allSessions) {
|
|
17
|
+
if (ses.status !== 'active') continue;
|
|
18
|
+
// Check activity stream — if it has msg:done/msg:error, mark done
|
|
19
|
+
// If not, mark interrupted (previous server died mid-execution)
|
|
20
|
+
if (ActivityStream.exists(ses.id)) {
|
|
21
|
+
const events = ActivityStream.readFrom(ses.id, 0);
|
|
22
|
+
const tail = events.slice(-5);
|
|
23
|
+
const isDone = tail.some(e => e.type === 'msg:done' || e.type === 'msg:error');
|
|
24
|
+
if (isDone) {
|
|
25
|
+
updateSession(ses.id, { status: 'done' });
|
|
26
|
+
orphaned++;
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
updateSession(ses.id, { status: 'interrupted' as any });
|
|
31
|
+
orphaned++;
|
|
32
|
+
}
|
|
33
|
+
if (orphaned > 0) {
|
|
34
|
+
console.log(`[STARTUP] Cleaned ${orphaned} orphaned sessions (active → done/interrupted)`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
9
38
|
const PORT = Number(process.env.PORT) || 3001;
|
|
10
39
|
const server = createHttpServer();
|
|
11
40
|
|
|
@@ -13,3 +42,18 @@ server.listen(PORT, () => {
|
|
|
13
42
|
console.log(`[API] Server running on http://localhost:${PORT}`);
|
|
14
43
|
console.log(`[API] COMPANY_ROOT: ${COMPANY_ROOT}`);
|
|
15
44
|
});
|
|
45
|
+
|
|
46
|
+
// Graceful shutdown: mark running sessions as interrupted
|
|
47
|
+
function gracefulShutdown(signal: string) {
|
|
48
|
+
console.log(`[SHUTDOWN] ${signal} received, marking active sessions as interrupted...`);
|
|
49
|
+
const sessions = listSessions();
|
|
50
|
+
for (const ses of sessions) {
|
|
51
|
+
if (ses.status === 'active') {
|
|
52
|
+
updateSession(ses.id, { status: 'interrupted' as any });
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
process.exit(0);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
process.on('SIGINT', () => gracefulShutdown('SIGINT'));
|
|
59
|
+
process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));
|
|
@@ -10,7 +10,7 @@ import { estimateCost } from './pricing.js';
|
|
|
10
10
|
import { readConfig, getConversationLimits, resolveCodeRoot } from './company-config.js';
|
|
11
11
|
import { postKnowledgingCheck, type KnowledgeDebtItem } from '../engine/knowledge-gate.js';
|
|
12
12
|
import { earnCoinsInternal } from '../routes/coins.js';
|
|
13
|
-
import { getSession, createSession, addMessage, updateMessage as updateSessionMessage, appendMessageEvent, type Message, type ImageAttachment } from './session-store.js';
|
|
13
|
+
import { getSession, createSession, addMessage, updateMessage as updateSessionMessage, updateSession, appendMessageEvent, type Message, type ImageAttachment } from './session-store.js';
|
|
14
14
|
import { portRegistry, type PortAllocation } from './port-registry.js';
|
|
15
15
|
import { type MessageStatus, isMessageActive, canTransition, messageStatusToRoleStatus } from '../../../shared/types.js';
|
|
16
16
|
|
|
@@ -603,6 +603,12 @@ class ExecutionManager {
|
|
|
603
603
|
knowledgeDebt: execution.knowledgeDebt.map(d => ({ type: d.type, file: d.file, message: d.message })),
|
|
604
604
|
}),
|
|
605
605
|
});
|
|
606
|
+
|
|
607
|
+
// Mark session as done in session-store (persisted to file)
|
|
608
|
+
// Skip CEO supervisor sessions — they stay active for wave lifecycle
|
|
609
|
+
if (session.roleId !== 'ceo' || session.source !== 'wave') {
|
|
610
|
+
updateSession(execution.sessionId, { status: 'done' });
|
|
611
|
+
}
|
|
606
612
|
}
|
|
607
613
|
|
|
608
614
|
private cleanupOrphanedChildren(parentSessionId: string): void {
|