tycono-server 0.1.0-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.
Files changed (84) hide show
  1. package/bin/cli.js +35 -0
  2. package/bin/server.ts +160 -0
  3. package/package.json +50 -0
  4. package/src/api/package.json +31 -0
  5. package/src/api/src/create-app.ts +90 -0
  6. package/src/api/src/create-server.ts +251 -0
  7. package/src/api/src/engine/agent-loop.ts +738 -0
  8. package/src/api/src/engine/authority-validator.ts +149 -0
  9. package/src/api/src/engine/context-assembler.ts +912 -0
  10. package/src/api/src/engine/index.ts +27 -0
  11. package/src/api/src/engine/knowledge-gate.ts +365 -0
  12. package/src/api/src/engine/llm-adapter.ts +304 -0
  13. package/src/api/src/engine/org-tree.ts +270 -0
  14. package/src/api/src/engine/role-lifecycle.ts +369 -0
  15. package/src/api/src/engine/runners/claude-cli.ts +796 -0
  16. package/src/api/src/engine/runners/direct-api.ts +66 -0
  17. package/src/api/src/engine/runners/index.ts +30 -0
  18. package/src/api/src/engine/runners/types.ts +95 -0
  19. package/src/api/src/engine/skill-template.ts +134 -0
  20. package/src/api/src/engine/tools/definitions.ts +201 -0
  21. package/src/api/src/engine/tools/executor.ts +611 -0
  22. package/src/api/src/routes/active-sessions.ts +134 -0
  23. package/src/api/src/routes/coins.ts +153 -0
  24. package/src/api/src/routes/company.ts +57 -0
  25. package/src/api/src/routes/cost.ts +141 -0
  26. package/src/api/src/routes/engine.ts +220 -0
  27. package/src/api/src/routes/execute.ts +1075 -0
  28. package/src/api/src/routes/git.ts +211 -0
  29. package/src/api/src/routes/knowledge.ts +378 -0
  30. package/src/api/src/routes/operations.ts +309 -0
  31. package/src/api/src/routes/preferences.ts +63 -0
  32. package/src/api/src/routes/presets.ts +123 -0
  33. package/src/api/src/routes/projects.ts +82 -0
  34. package/src/api/src/routes/quests.ts +41 -0
  35. package/src/api/src/routes/roles.ts +112 -0
  36. package/src/api/src/routes/save.ts +152 -0
  37. package/src/api/src/routes/sessions.ts +288 -0
  38. package/src/api/src/routes/setup.ts +437 -0
  39. package/src/api/src/routes/skills.ts +357 -0
  40. package/src/api/src/routes/speech.ts +959 -0
  41. package/src/api/src/routes/supervision.ts +136 -0
  42. package/src/api/src/routes/sync.ts +165 -0
  43. package/src/api/src/server.ts +59 -0
  44. package/src/api/src/services/activity-stream.ts +184 -0
  45. package/src/api/src/services/activity-tracker.ts +115 -0
  46. package/src/api/src/services/claude-md-manager.ts +94 -0
  47. package/src/api/src/services/company-config.ts +115 -0
  48. package/src/api/src/services/database.ts +77 -0
  49. package/src/api/src/services/digest-engine.ts +313 -0
  50. package/src/api/src/services/execution-manager.ts +1036 -0
  51. package/src/api/src/services/file-reader.ts +77 -0
  52. package/src/api/src/services/git-save.ts +614 -0
  53. package/src/api/src/services/job-manager.ts +16 -0
  54. package/src/api/src/services/knowledge-importer.ts +466 -0
  55. package/src/api/src/services/markdown-parser.ts +173 -0
  56. package/src/api/src/services/port-registry.ts +222 -0
  57. package/src/api/src/services/preferences.ts +150 -0
  58. package/src/api/src/services/preset-loader.ts +149 -0
  59. package/src/api/src/services/pricing.ts +34 -0
  60. package/src/api/src/services/scaffold.ts +546 -0
  61. package/src/api/src/services/session-store.ts +340 -0
  62. package/src/api/src/services/supervisor-heartbeat.ts +897 -0
  63. package/src/api/src/services/team-recommender.ts +382 -0
  64. package/src/api/src/services/token-ledger.ts +127 -0
  65. package/src/api/src/services/wave-messages.ts +194 -0
  66. package/src/api/src/services/wave-multiplexer.ts +356 -0
  67. package/src/api/src/services/wave-tracker.ts +359 -0
  68. package/src/api/src/utils/role-level.ts +31 -0
  69. package/src/core/scaffolder.ts +620 -0
  70. package/src/shared/types.ts +224 -0
  71. package/templates/CLAUDE.md.tmpl +239 -0
  72. package/templates/company.md.tmpl +17 -0
  73. package/templates/gitignore.tmpl +28 -0
  74. package/templates/roles.md.tmpl +8 -0
  75. package/templates/skills/_manifest.json +23 -0
  76. package/templates/skills/agent-browser/SKILL.md +159 -0
  77. package/templates/skills/agent-browser/meta.json +19 -0
  78. package/templates/skills/akb-linter/SKILL.md +125 -0
  79. package/templates/skills/akb-linter/meta.json +12 -0
  80. package/templates/skills/knowledge-gate/SKILL.md +120 -0
  81. package/templates/skills/knowledge-gate/meta.json +12 -0
  82. package/templates/teams/agency.json +58 -0
  83. package/templates/teams/research.json +58 -0
  84. package/templates/teams/startup.json +58 -0
@@ -0,0 +1,356 @@
1
+ import { ActivityStream, type ActivityEvent, type ActivitySubscriber } from './activity-stream.js';
2
+ import type { Execution } from './execution-manager.js';
3
+ import { getSession } from './session-store.js';
4
+ import type { Response } from 'express';
5
+
6
+ /* ─── Types ──────────────────────────────── */
7
+
8
+ export interface WaveStreamEnvelope {
9
+ waveId: string;
10
+ waveSeq: number;
11
+ sessionId: string;
12
+ roleId: string;
13
+ event: ActivityEvent;
14
+ }
15
+
16
+ interface AttachedSession {
17
+ sessionId: string;
18
+ roleId: string;
19
+ executionId: string;
20
+ unsubscribe: () => void;
21
+ }
22
+
23
+ interface WaveStreamClient {
24
+ res: Response;
25
+ waveSeq: number;
26
+ attachedSessions: Map<string, AttachedSession>; // sessionId → attachment
27
+ sentEvents: Set<string>;
28
+ heartbeat: ReturnType<typeof setInterval>;
29
+ closed: boolean;
30
+ }
31
+
32
+ /* ─── WaveMultiplexer ────────────────────── */
33
+
34
+ class WaveMultiplexer {
35
+ private clients = new Map<string, Set<WaveStreamClient>>();
36
+ private waveSessions = new Map<string, Map<string, Execution>>();
37
+
38
+ registerSession(waveId: string, execution: Execution): void {
39
+ if (!this.waveSessions.has(waveId)) {
40
+ this.waveSessions.set(waveId, new Map());
41
+ }
42
+ this.waveSessions.get(waveId)!.set(execution.sessionId, execution);
43
+
44
+ console.log(`[WaveMux] registerSession wave=${waveId} session=${execution.sessionId} role=${execution.roleId}`);
45
+
46
+ const clients = this.clients.get(waveId);
47
+ if (clients) {
48
+ for (const client of clients) {
49
+ if (!client.closed) {
50
+ this.subscribeSessionToClient(waveId, client, execution, true);
51
+ }
52
+ }
53
+ }
54
+ }
55
+
56
+ attach(waveId: string, res: Response, fromWaveSeq: number): WaveStreamClient {
57
+ res.writeHead(200, {
58
+ 'Content-Type': 'text/event-stream',
59
+ 'Cache-Control': 'no-cache',
60
+ 'Connection': 'keep-alive',
61
+ 'X-Accel-Buffering': 'no',
62
+ });
63
+ res.flushHeaders();
64
+
65
+ const client: WaveStreamClient = {
66
+ res,
67
+ waveSeq: 0,
68
+ attachedSessions: new Map(),
69
+ sentEvents: new Set(),
70
+ heartbeat: setInterval(() => {
71
+ if (client.closed || res.destroyed || res.writableEnded) {
72
+ clearInterval(client.heartbeat);
73
+ return;
74
+ }
75
+ try { res.write(': heartbeat\n\n'); } catch { /* ignore */ }
76
+ }, 15_000),
77
+ closed: false,
78
+ };
79
+
80
+ if (!this.clients.has(waveId)) {
81
+ this.clients.set(waveId, new Set());
82
+ }
83
+ this.clients.get(waveId)!.add(client);
84
+
85
+ const sessions = this.waveSessions.get(waveId);
86
+ if (sessions) {
87
+ // Phase 1: Replay recent historical events (capped to prevent OOM)
88
+ // Only replay from recent sessions (last 5) to avoid 120-session waves killing memory
89
+ const MAX_REPLAY_SESSIONS = 5;
90
+ const MAX_REPLAY_EVENTS = 200;
91
+
92
+ const sessionList = Array.from(sessions.values());
93
+ const recentSessions = sessionList.slice(-MAX_REPLAY_SESSIONS);
94
+
95
+ const allEvents: { event: ActivityEvent; sessionId: string }[] = [];
96
+ for (const exec of recentSessions) {
97
+ const events = ActivityStream.readFrom(exec.sessionId, 0);
98
+ const recent = events.slice(-50);
99
+ for (const event of recent) {
100
+ allEvents.push({ event, sessionId: exec.sessionId });
101
+ }
102
+ }
103
+
104
+ allEvents.sort((a, b) => a.event.ts.localeCompare(b.event.ts));
105
+
106
+ // Cap total replay events
107
+ const replayEvents = allEvents.slice(-MAX_REPLAY_EVENTS);
108
+
109
+ for (const item of replayEvents) {
110
+ const waveSeq = client.waveSeq++;
111
+ if (waveSeq < fromWaveSeq) continue;
112
+
113
+ const key = `${item.event.roleId}:${item.event.seq}`;
114
+ client.sentEvents.add(key);
115
+
116
+ sendSSE(client, 'wave:event', {
117
+ waveId,
118
+ waveSeq,
119
+ sessionId: item.sessionId,
120
+ roleId: item.event.roleId,
121
+ event: item.event,
122
+ } as WaveStreamEnvelope);
123
+ }
124
+
125
+ console.log(`[WaveMux] Replayed ${replayEvents.length} events (${sessionList.length} total sessions, ${recentSessions.length} replayed)`);
126
+
127
+ // Phase 2: Subscribe to live events for active sessions
128
+ for (const exec of sessionList) {
129
+ if (exec.status === 'running' || exec.status === 'awaiting_input') {
130
+ this.subscribeSessionToClient(waveId, client, exec, true);
131
+ }
132
+ }
133
+ }
134
+
135
+ console.log(`[WaveMux] attach wave=${waveId} sessions=${sessions?.size ?? 0} from=${fromWaveSeq}`);
136
+ return client;
137
+ }
138
+
139
+ private subscribeSessionToClient(waveId: string, client: WaveStreamClient, execution: Execution, sendNotification: boolean): void {
140
+ // If already attached to this session with a DIFFERENT execution, re-subscribe
141
+ // This handles the resume case where a new execution reuses the same sessionId
142
+ const existing = client.attachedSessions.get(execution.sessionId);
143
+ if (existing) {
144
+ if (existing.executionId === execution.id) return; // Same execution, skip
145
+ existing.unsubscribe();
146
+ client.attachedSessions.delete(execution.sessionId);
147
+ console.log(`[WaveMux] re-subscribing session=${execution.sessionId} (new exec=${execution.id})`);
148
+ }
149
+
150
+ const sessionId = execution.sessionId;
151
+ const roleId = execution.roleId;
152
+
153
+ if (sendNotification) {
154
+ sendSSE(client, 'wave:role-attached', {
155
+ sessionId,
156
+ roleId,
157
+ parentSessionId: execution.parentSessionId,
158
+ });
159
+
160
+ const events = ActivityStream.readFrom(execution.sessionId, 0);
161
+ const recentEvents = events.slice(-50);
162
+ for (const event of recentEvents) {
163
+ const key = `${event.roleId}:${event.seq}`;
164
+ if (client.sentEvents.has(key)) continue;
165
+ client.sentEvents.add(key);
166
+
167
+ const waveSeq = client.waveSeq++;
168
+ sendSSE(client, 'wave:event', {
169
+ waveId,
170
+ waveSeq,
171
+ sessionId,
172
+ roleId: event.roleId,
173
+ event,
174
+ } as WaveStreamEnvelope);
175
+ }
176
+ }
177
+
178
+ const subscriber: ActivitySubscriber = (event: ActivityEvent) => {
179
+ if (client.closed) return;
180
+
181
+ const key = `${event.roleId}:${event.seq}`;
182
+ if (client.sentEvents.has(key)) return;
183
+ client.sentEvents.add(key);
184
+ // OOM prevention: cap sentEvents to prevent unbounded Set growth
185
+ if (client.sentEvents.size > 5000) {
186
+ const entries = Array.from(client.sentEvents);
187
+ client.sentEvents.clear();
188
+ for (const e of entries.slice(-2000)) client.sentEvents.add(e);
189
+ }
190
+
191
+ const waveSeq = client.waveSeq++;
192
+ sendSSE(client, 'wave:event', {
193
+ waveId,
194
+ waveSeq,
195
+ sessionId,
196
+ roleId: event.roleId,
197
+ event,
198
+ } as WaveStreamEnvelope);
199
+
200
+ if (event.type === 'msg:done' || event.type === 'msg:error') {
201
+ sendSSE(client, 'wave:role-detached', {
202
+ sessionId,
203
+ roleId,
204
+ reason: event.type === 'msg:done' ? 'done' : 'error',
205
+ });
206
+ }
207
+ };
208
+
209
+ execution.stream.subscribe(subscriber);
210
+
211
+ client.attachedSessions.set(sessionId, {
212
+ sessionId,
213
+ roleId,
214
+ executionId: execution.id,
215
+ unsubscribe: () => execution.stream.unsubscribe(subscriber),
216
+ });
217
+
218
+ console.log(`[WaveMux] subscribed session=${sessionId} role=${roleId} notify=${sendNotification}`);
219
+ }
220
+
221
+ onExecutionCreated(execution: Execution): void {
222
+ // Check multiplexer's in-memory map first
223
+ let waveId = this.findWaveIdForSession(execution.sessionId) ?? this.findWaveIdForSession(execution.parentSessionId ?? '');
224
+
225
+ // BUG-W02 fix: also check session-store for waveId (propagated from parent)
226
+ if (!waveId) {
227
+ const session = getSession(execution.sessionId);
228
+ if (session?.waveId) waveId = session.waveId;
229
+ }
230
+ if (!waveId && execution.parentSessionId) {
231
+ const parentSession = getSession(execution.parentSessionId);
232
+ if (parentSession?.waveId) waveId = parentSession.waveId;
233
+ }
234
+
235
+ if (!waveId) return;
236
+
237
+ this.registerSession(waveId, execution);
238
+ }
239
+
240
+ /** Remove completed wave sessions from memory.
241
+ * Keep SSE clients registered — they persist until connection closes.
242
+ * When the wave restarts (new directive), registerSession will resubscribe them. */
243
+ cleanupWave(waveId: string): void {
244
+ this.waveSessions.delete(waveId);
245
+
246
+ // Don't delete clients — TUI SSE connections stay open across directives.
247
+ // Just unsubscribe dead session listeners to prevent stale callbacks.
248
+ const clients = this.clients.get(waveId);
249
+ if (clients) {
250
+ // Remove disconnected clients
251
+ for (const client of clients) {
252
+ if (client.closed || client.res.destroyed || client.res.writableEnded) {
253
+ clearInterval(client.heartbeat);
254
+ clients.delete(client);
255
+ continue;
256
+ }
257
+ // Unsubscribe old session listeners (execution is done)
258
+ for (const [, attached] of client.attachedSessions) {
259
+ attached.unsubscribe();
260
+ }
261
+ client.attachedSessions.clear();
262
+ }
263
+ if (clients.size === 0) {
264
+ this.clients.delete(waveId);
265
+ }
266
+ }
267
+ }
268
+
269
+ detach(waveId: string, client: WaveStreamClient): void {
270
+ client.closed = true;
271
+ clearInterval(client.heartbeat);
272
+
273
+ for (const [, attached] of client.attachedSessions) {
274
+ attached.unsubscribe();
275
+ }
276
+ client.attachedSessions.clear();
277
+ client.sentEvents.clear();
278
+
279
+ const clientSet = this.clients.get(waveId);
280
+ if (clientSet) {
281
+ clientSet.delete(client);
282
+ if (clientSet.size === 0) {
283
+ this.clients.delete(waveId);
284
+ }
285
+ }
286
+ }
287
+
288
+ private findWaveIdForSession(sessionId: string): string | undefined {
289
+ for (const [waveId, sessions] of this.waveSessions) {
290
+ if (sessions.has(sessionId)) return waveId;
291
+ }
292
+ return undefined;
293
+ }
294
+
295
+ getWaveSessionIds(waveId: string): string[] {
296
+ const sessions = this.waveSessions.get(waveId);
297
+ return sessions ? Array.from(sessions.keys()) : [];
298
+ }
299
+
300
+ getActiveWaves(): Array<{
301
+ id: string;
302
+ directive: string;
303
+ dispatches: Array<{ sessionId: string; roleId: string; roleName: string }>;
304
+ startedAt: number;
305
+ sessionIds: string[];
306
+ }> {
307
+ const result: Array<{
308
+ id: string;
309
+ directive: string;
310
+ dispatches: Array<{ sessionId: string; roleId: string; roleName: string }>;
311
+ startedAt: number;
312
+ sessionIds: string[];
313
+ }> = [];
314
+
315
+ for (const [waveId, sessions] of this.waveSessions) {
316
+ const hasActive = Array.from(sessions.values()).some(e => e.status === 'running' || e.status === 'awaiting_input');
317
+ if (!hasActive) continue;
318
+
319
+ const rootSessions = Array.from(sessions.values())
320
+ .filter(e => !e.parentSessionId || !sessions.has(e.parentSessionId))
321
+ .map(e => ({
322
+ sessionId: e.sessionId,
323
+ roleId: e.roleId,
324
+ roleName: e.roleId.toUpperCase(),
325
+ }));
326
+
327
+ const firstExec = rootSessions.length > 0
328
+ ? Array.from(sessions.values()).find(e => e.sessionId === rootSessions[0].sessionId)
329
+ : undefined;
330
+ const directive = firstExec?.task.replace(/^\[CEO Wave\]\s*/, '') ?? '';
331
+
332
+ const startedAt = Math.min(
333
+ ...Array.from(sessions.values()).map(e => new Date(e.createdAt).getTime())
334
+ );
335
+
336
+ const sessionIds = Array.from(sessions.values()).map(e => e.sessionId);
337
+
338
+ result.push({ id: waveId, directive, dispatches: rootSessions, startedAt, sessionIds });
339
+ }
340
+
341
+ return result;
342
+ }
343
+ }
344
+
345
+ /* ─── Helpers ────────────────────────────── */
346
+
347
+ function sendSSE(client: WaveStreamClient, event: string, data: unknown): void {
348
+ if (client.closed || client.res.destroyed || client.res.writableEnded) return;
349
+ try {
350
+ client.res.write(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`);
351
+ } catch { /* ignore write errors */ }
352
+ }
353
+
354
+ /* ─── Export singleton ───────────────────── */
355
+
356
+ export const waveMultiplexer = new WaveMultiplexer();