heyio 3.0.2 → 3.0.4

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 (65) hide show
  1. package/dist/api/server.js +1 -1
  2. package/dist/api/server.js.map +1 -1
  3. package/dist/index.js +8 -1
  4. package/dist/index.js.map +1 -1
  5. package/dist/logging/logger.d.ts.map +1 -1
  6. package/dist/logging/logger.js +13 -1
  7. package/dist/logging/logger.js.map +1 -1
  8. package/node_modules/@io/shared/package.json +1 -1
  9. package/package.json +7 -2
  10. package/public/assets/index-2RY89H3W.js +336 -0
  11. package/public/assets/index-2RY89H3W.js.map +1 -0
  12. package/public/assets/index-D3cGfBsj.css +1 -0
  13. package/public/index.html +14 -0
  14. package/src/api/middleware/auth.ts +0 -76
  15. package/src/api/notifications.ts +0 -122
  16. package/src/api/routes/activity.ts +0 -29
  17. package/src/api/routes/attachments.ts +0 -93
  18. package/src/api/routes/config.ts +0 -115
  19. package/src/api/routes/conversations.ts +0 -87
  20. package/src/api/routes/health.ts +0 -18
  21. package/src/api/routes/inbox.ts +0 -98
  22. package/src/api/routes/schedules.ts +0 -121
  23. package/src/api/routes/skills.ts +0 -105
  24. package/src/api/routes/squads.ts +0 -145
  25. package/src/api/routes/usage.ts +0 -57
  26. package/src/api/routes/wiki.ts +0 -49
  27. package/src/api/server.ts +0 -186
  28. package/src/config.ts +0 -3
  29. package/src/copilot/client.ts +0 -42
  30. package/src/copilot/health-monitor.ts +0 -85
  31. package/src/copilot/orchestrator.ts +0 -222
  32. package/src/copilot/tools.ts +0 -707
  33. package/src/index.ts +0 -113
  34. package/src/logging/logger.ts +0 -26
  35. package/src/models/index.ts +0 -11
  36. package/src/models/pricing.ts +0 -121
  37. package/src/models/registry.ts +0 -131
  38. package/src/models/token-tracker.ts +0 -151
  39. package/src/scheduler/engine.ts +0 -146
  40. package/src/skills/index.ts +0 -13
  41. package/src/skills/store.ts +0 -188
  42. package/src/squad/agent.ts +0 -326
  43. package/src/squad/autonomy.ts +0 -78
  44. package/src/squad/event-bus.ts +0 -71
  45. package/src/squad/execution/index.ts +0 -17
  46. package/src/squad/execution/instance.ts +0 -186
  47. package/src/squad/execution/meeting.ts +0 -191
  48. package/src/squad/execution/pr.ts +0 -127
  49. package/src/squad/execution/runner.ts +0 -97
  50. package/src/squad/execution/tasks.ts +0 -111
  51. package/src/squad/execution/worktree.ts +0 -138
  52. package/src/squad/hiring.ts +0 -222
  53. package/src/squad/index.ts +0 -17
  54. package/src/squad/manager.ts +0 -337
  55. package/src/squad/name-generator.ts +0 -135
  56. package/src/squad/roles/templates.ts +0 -104
  57. package/src/squad/skill-parser.ts +0 -120
  58. package/src/squad/source-resolver.ts +0 -57
  59. package/src/store/activity.ts +0 -176
  60. package/src/store/db.ts +0 -237
  61. package/src/store/inbox.ts +0 -199
  62. package/src/store/schedules.ts +0 -199
  63. package/src/wiki/index.ts +0 -12
  64. package/src/wiki/store.ts +0 -139
  65. package/tsconfig.json +0 -9
package/src/index.ts DELETED
@@ -1,113 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- import { mkdirSync } from 'node:fs';
4
- import { initNotifications } from './api/notifications.js';
5
- import { createApiServer } from './api/server.js';
6
- import { loadConfig } from './config.js';
7
- import { stopClient } from './copilot/client.js';
8
- import { startHealthMonitor, stopHealthMonitor } from './copilot/health-monitor.js';
9
- import { destroyOrchestrator, initOrchestrator } from './copilot/orchestrator.js';
10
- import { getLogger, initLogger } from './logging/logger.js';
11
- import { seedPricing } from './models/index.js';
12
- import { startScheduler, stopScheduler } from './scheduler/engine.js';
13
- import { initSkills } from './skills/index.js';
14
- import { getEventBus } from './squad/event-bus.js';
15
- import { initActivityLogger } from './store/activity.js';
16
- import { closeDatabase, initDatabase } from './store/db.js';
17
- import { initWiki } from './wiki/index.js';
18
-
19
- const config = loadConfig();
20
-
21
- // Ensure data directory exists
22
- mkdirSync(config.dataDir, { recursive: true });
23
-
24
- // Initialize logger first — other modules depend on it
25
- const logger = initLogger(config);
26
-
27
- // Initialize wiki directory structure
28
- initWiki(config.dataDir);
29
-
30
- // Initialize skills directory
31
- initSkills(config.dataDir);
32
- logger.info({ config: { ...config, dataDir: config.dataDir } }, 'IO daemon starting');
33
-
34
- // Create API server
35
- const apiServer = createApiServer(config);
36
-
37
- async function start(): Promise<void> {
38
- // Initialize database
39
- await initDatabase(config.dataDir);
40
-
41
- // Seed model pricing defaults
42
- await seedPricing();
43
-
44
- // Initialize notification system (event bus → WebSocket broadcast)
45
- initNotifications();
46
-
47
- // Initialize activity logger (event bus → SQLite)
48
- initActivityLogger(getEventBus());
49
-
50
- // Initialize Copilot orchestrator
51
- await initOrchestrator(config);
52
-
53
- // Start health monitoring
54
- startHealthMonitor();
55
-
56
- // Start schedule engine (evaluates cron schedules every 60s)
57
- startScheduler();
58
-
59
- await apiServer.start();
60
- logger.info('IO daemon ready');
61
- }
62
-
63
- // Graceful shutdown
64
- let shuttingDown = false;
65
- async function shutdown(signal: string): Promise<void> {
66
- if (shuttingDown) return;
67
- shuttingDown = true;
68
-
69
- const log = getLogger();
70
- log.info({ signal }, 'Shutting down...');
71
-
72
- // Stop accepting new requests
73
- stopHealthMonitor();
74
- stopScheduler();
75
-
76
- // Stop API server (closes WebSocket connections)
77
- await apiServer.stop();
78
-
79
- // Destroy orchestrator session (drains queue)
80
- await destroyOrchestrator();
81
-
82
- // Stop Copilot SDK client
83
- await stopClient();
84
-
85
- // Clear event bus
86
- getEventBus().clear();
87
-
88
- // Close database
89
- closeDatabase();
90
-
91
- log.info('Shutdown complete');
92
- process.exit(0);
93
- }
94
-
95
- process.on('SIGTERM', () => shutdown('SIGTERM'));
96
- process.on('SIGINT', () => shutdown('SIGINT'));
97
-
98
- // Handle uncaught errors gracefully
99
- process.on('uncaughtException', (err) => {
100
- const log = getLogger();
101
- log.fatal({ err }, 'Uncaught exception');
102
- shutdown('uncaughtException');
103
- });
104
-
105
- process.on('unhandledRejection', (reason) => {
106
- const log = getLogger();
107
- log.error({ err: reason }, 'Unhandled rejection');
108
- });
109
-
110
- start().catch((err) => {
111
- logger.fatal({ err }, 'Failed to start IO daemon');
112
- process.exit(1);
113
- });
@@ -1,26 +0,0 @@
1
- import pino from 'pino';
2
- import type { IOConfig } from '../config.js';
3
-
4
- let rootLogger: pino.Logger;
5
-
6
- export function initLogger(config: IOConfig): pino.Logger {
7
- rootLogger = pino({
8
- level: config.logLevel,
9
- transport:
10
- process.env.NODE_ENV !== 'production'
11
- ? { target: 'pino-pretty', options: { colorize: true } }
12
- : undefined,
13
- });
14
- return rootLogger;
15
- }
16
-
17
- export function getLogger(): pino.Logger {
18
- if (!rootLogger) {
19
- throw new Error('Logger not initialized. Call initLogger() first.');
20
- }
21
- return rootLogger;
22
- }
23
-
24
- export function createChildLogger(name: string, meta?: Record<string, unknown>): pino.Logger {
25
- return getLogger().child({ component: name, ...meta });
26
- }
@@ -1,11 +0,0 @@
1
- export {
2
- MODEL_REGISTRY,
3
- DEFAULT_MODELS,
4
- getModel,
5
- getModelsByTier,
6
- selectModelForTask,
7
- } from './registry.js';
8
- export type { ModelInfo, ModelTier } from './registry.js';
9
- export { recordTokenUsage, queryUsage } from './token-tracker.js';
10
- export type { TokenUsageRecord } from './token-tracker.js';
11
- export { seedPricing, updatePricing, getAllPricing, refreshPricing } from './pricing.js';
@@ -1,121 +0,0 @@
1
- import { createChildLogger } from '../logging/logger.js';
2
- import { getDatabase } from '../store/db.js';
3
- import { MODEL_REGISTRY, type ModelInfo } from './registry.js';
4
-
5
- const logger = () => createChildLogger('pricing');
6
-
7
- /**
8
- * Default pricing estimates (USD per 1M tokens) when GitHub pricing is unavailable.
9
- * These are rough estimates based on publicly available pricing.
10
- */
11
- const DEFAULT_PRICING: Record<string, { input: number; output: number }> = {
12
- 'claude-haiku-4.5': { input: 0.8, output: 4.0 },
13
- 'gpt-5-mini': { input: 1.5, output: 6.0 },
14
- 'gpt-5.4-mini': { input: 1.5, output: 6.0 },
15
- 'claude-sonnet-4.5': { input: 3.0, output: 15.0 },
16
- 'claude-sonnet-4.6': { input: 3.0, output: 15.0 },
17
- 'gpt-5.2': { input: 5.0, output: 15.0 },
18
- 'gpt-5.4': { input: 5.0, output: 15.0 },
19
- 'gpt-5.5': { input: 5.0, output: 15.0 },
20
- 'gpt-4.1': { input: 2.0, output: 8.0 },
21
- 'claude-opus-4.5': { input: 15.0, output: 75.0 },
22
- 'claude-opus-4.6': { input: 15.0, output: 75.0 },
23
- 'claude-opus-4.7': { input: 15.0, output: 75.0 },
24
- 'claude-opus-4.8': { input: 15.0, output: 75.0 },
25
- 'gpt-5.2-codex': { input: 10.0, output: 30.0 },
26
- 'gpt-5.3-codex': { input: 10.0, output: 30.0 },
27
- };
28
-
29
- /**
30
- * Seed the model_pricing table with default values.
31
- * Only inserts if the table is empty.
32
- */
33
- export async function seedPricing(): Promise<void> {
34
- const log = logger();
35
- const db = getDatabase();
36
-
37
- const existing = await db.execute('SELECT COUNT(*) as cnt FROM model_pricing');
38
- if ((existing.rows[0]?.cnt as number) > 0) {
39
- return; // Already seeded
40
- }
41
-
42
- for (const [model, prices] of Object.entries(DEFAULT_PRICING)) {
43
- const info = MODEL_REGISTRY.find((m) => m.id === model);
44
- await db.execute({
45
- sql: `INSERT OR REPLACE INTO model_pricing (model, input_cost_per_1m, output_cost_per_1m, tier, last_updated)
46
- VALUES (?, ?, ?, ?, datetime('now'))`,
47
- args: [model, prices.input, prices.output, info?.tier ?? 'standard'],
48
- });
49
- }
50
-
51
- log.info('Model pricing seeded with defaults');
52
- }
53
-
54
- /**
55
- * Update pricing for a specific model.
56
- */
57
- export async function updatePricing(
58
- model: string,
59
- inputCostPer1M: number,
60
- outputCostPer1M: number,
61
- ): Promise<void> {
62
- const db = getDatabase();
63
- const info = MODEL_REGISTRY.find((m) => m.id === model);
64
-
65
- await db.execute({
66
- sql: `INSERT OR REPLACE INTO model_pricing (model, input_cost_per_1m, output_cost_per_1m, tier, last_updated)
67
- VALUES (?, ?, ?, ?, datetime('now'))`,
68
- args: [model, inputCostPer1M, outputCostPer1M, info?.tier ?? 'standard'],
69
- });
70
- }
71
-
72
- /**
73
- * Get all current pricing.
74
- */
75
- export async function getAllPricing(): Promise<
76
- Array<{
77
- model: string;
78
- inputCostPer1M: number;
79
- outputCostPer1M: number;
80
- tier: string;
81
- lastUpdated: string;
82
- }>
83
- > {
84
- const db = getDatabase();
85
- const result = await db.execute('SELECT * FROM model_pricing ORDER BY model');
86
-
87
- return result.rows.map((row) => ({
88
- model: row.model as string,
89
- inputCostPer1M: row.input_cost_per_1m as number,
90
- outputCostPer1M: row.output_cost_per_1m as number,
91
- tier: (row.tier as string) ?? 'standard',
92
- lastUpdated: row.last_updated as string,
93
- }));
94
- }
95
-
96
- /**
97
- * Refresh pricing — in future this could fetch from GitHub docs.
98
- * For now, re-seeds defaults for any missing models.
99
- */
100
- export async function refreshPricing(): Promise<void> {
101
- const log = logger();
102
- const db = getDatabase();
103
-
104
- for (const [model, prices] of Object.entries(DEFAULT_PRICING)) {
105
- const existing = await db.execute({
106
- sql: 'SELECT model FROM model_pricing WHERE model = ?',
107
- args: [model],
108
- });
109
-
110
- if (existing.rows.length === 0) {
111
- const info = MODEL_REGISTRY.find((m) => m.id === model);
112
- await db.execute({
113
- sql: `INSERT INTO model_pricing (model, input_cost_per_1m, output_cost_per_1m, tier, last_updated)
114
- VALUES (?, ?, ?, ?, datetime('now'))`,
115
- args: [model, prices.input, prices.output, info?.tier ?? 'standard'],
116
- });
117
- }
118
- }
119
-
120
- log.info('Pricing refreshed');
121
- }
@@ -1,131 +0,0 @@
1
- /**
2
- * Model registry — defines available models, tiers, and selection logic.
3
- * Models are sourced from the GitHub Copilot SDK's available model list.
4
- */
5
-
6
- export type ModelTier = 'fast' | 'standard' | 'reasoning';
7
-
8
- export interface ModelInfo {
9
- id: string;
10
- tier: ModelTier;
11
- description: string;
12
- maxContext: number; // approximate token window
13
- inputCostPer1M?: number; // USD per 1M input tokens
14
- outputCostPer1M?: number; // USD per 1M output tokens
15
- }
16
-
17
- /**
18
- * Known models available via GitHub Copilot SDK (as of 2026-05).
19
- */
20
- export const MODEL_REGISTRY: ModelInfo[] = [
21
- // Fast tier — low-latency, cost-effective
22
- {
23
- id: 'claude-haiku-4.5',
24
- tier: 'fast',
25
- description: 'Claude Haiku 4.5 — fast, cheap',
26
- maxContext: 200_000,
27
- },
28
- {
29
- id: 'gpt-5-mini',
30
- tier: 'fast',
31
- description: 'GPT-5 Mini — fast responses',
32
- maxContext: 128_000,
33
- },
34
- {
35
- id: 'gpt-5.4-mini',
36
- tier: 'fast',
37
- description: 'GPT-5.4 Mini — fast, latest',
38
- maxContext: 128_000,
39
- },
40
-
41
- // Standard tier — balanced capability/cost
42
- {
43
- id: 'claude-sonnet-4.5',
44
- tier: 'standard',
45
- description: 'Claude Sonnet 4.5',
46
- maxContext: 200_000,
47
- },
48
- {
49
- id: 'claude-sonnet-4.6',
50
- tier: 'standard',
51
- description: 'Claude Sonnet 4.6',
52
- maxContext: 200_000,
53
- },
54
- { id: 'gpt-5.2', tier: 'standard', description: 'GPT-5.2', maxContext: 128_000 },
55
- { id: 'gpt-5.4', tier: 'standard', description: 'GPT-5.4', maxContext: 128_000 },
56
- {
57
- id: 'gpt-5.5',
58
- tier: 'standard',
59
- description: 'GPT-5.5 — latest standard',
60
- maxContext: 128_000,
61
- },
62
- {
63
- id: 'gpt-4.1',
64
- tier: 'standard',
65
- description: 'GPT-4.1 — legacy standard',
66
- maxContext: 128_000,
67
- },
68
-
69
- // Reasoning tier — highest capability, most expensive
70
- { id: 'claude-opus-4.5', tier: 'reasoning', description: 'Claude Opus 4.5', maxContext: 200_000 },
71
- { id: 'claude-opus-4.6', tier: 'reasoning', description: 'Claude Opus 4.6', maxContext: 200_000 },
72
- { id: 'claude-opus-4.7', tier: 'reasoning', description: 'Claude Opus 4.7', maxContext: 200_000 },
73
- {
74
- id: 'claude-opus-4.8',
75
- tier: 'reasoning',
76
- description: 'Claude Opus 4.8 — latest',
77
- maxContext: 200_000,
78
- },
79
- {
80
- id: 'gpt-5.2-codex',
81
- tier: 'reasoning',
82
- description: 'GPT-5.2 Codex — code-focused reasoning',
83
- maxContext: 128_000,
84
- },
85
- {
86
- id: 'gpt-5.3-codex',
87
- tier: 'reasoning',
88
- description: 'GPT-5.3 Codex — code reasoning',
89
- maxContext: 128_000,
90
- },
91
- ];
92
-
93
- /** Get all models in a tier */
94
- export function getModelsByTier(tier: ModelTier): ModelInfo[] {
95
- return MODEL_REGISTRY.filter((m) => m.tier === tier);
96
- }
97
-
98
- /** Get a specific model by ID */
99
- export function getModel(modelId: string): ModelInfo | undefined {
100
- return MODEL_REGISTRY.find((m) => m.id === modelId);
101
- }
102
-
103
- /** Default model per tier */
104
- export const DEFAULT_MODELS: Record<ModelTier, string> = {
105
- fast: 'gpt-5.4-mini',
106
- standard: 'claude-sonnet-4.6',
107
- reasoning: 'claude-opus-4.6',
108
- };
109
-
110
- /**
111
- * Select the appropriate model tier for a task based on complexity.
112
- * Used by team leads to choose models for agent task execution.
113
- */
114
- export function selectModelForTask(params: {
115
- taskDescription: string;
116
- isCodeGeneration?: boolean;
117
- requiresReasoning?: boolean;
118
- preferFast?: boolean;
119
- }): string {
120
- if (params.preferFast) {
121
- return DEFAULT_MODELS.fast;
122
- }
123
- if (params.requiresReasoning) {
124
- return params.isCodeGeneration ? 'gpt-5.3-codex' : DEFAULT_MODELS.reasoning;
125
- }
126
- if (params.isCodeGeneration) {
127
- return DEFAULT_MODELS.standard;
128
- }
129
- // Default: standard tier
130
- return DEFAULT_MODELS.standard;
131
- }
@@ -1,151 +0,0 @@
1
- import { createChildLogger } from '../logging/logger.js';
2
- import { getDatabase } from '../store/db.js';
3
-
4
- const logger = () => createChildLogger('token-tracker');
5
-
6
- export interface TokenUsageRecord {
7
- squadId?: string;
8
- instanceId?: string;
9
- agentRole?: string;
10
- model: string;
11
- inputTokens: number;
12
- outputTokens: number;
13
- estimatedCostUsd?: number;
14
- }
15
-
16
- /**
17
- * Record token usage from an LLM call.
18
- */
19
- export async function recordTokenUsage(record: TokenUsageRecord): Promise<void> {
20
- const db = getDatabase();
21
-
22
- // Estimate cost if pricing is available
23
- let cost = record.estimatedCostUsd;
24
- if (cost === undefined) {
25
- cost = await estimateCost(record.model, record.inputTokens, record.outputTokens);
26
- }
27
-
28
- await db.execute({
29
- sql: `INSERT INTO token_usage (squad_id, instance_id, agent_role, model, input_tokens, output_tokens, estimated_cost_usd)
30
- VALUES (?, ?, ?, ?, ?, ?, ?)`,
31
- args: [
32
- record.squadId ?? null,
33
- record.instanceId ?? null,
34
- record.agentRole ?? null,
35
- record.model,
36
- record.inputTokens,
37
- record.outputTokens,
38
- cost ?? null,
39
- ],
40
- });
41
- }
42
-
43
- /**
44
- * Estimate cost for a call based on stored pricing.
45
- */
46
- async function estimateCost(
47
- model: string,
48
- inputTokens: number,
49
- outputTokens: number,
50
- ): Promise<number | undefined> {
51
- const db = getDatabase();
52
- const result = await db.execute({
53
- sql: 'SELECT input_cost_per_1m, output_cost_per_1m FROM model_pricing WHERE model = ?',
54
- args: [model],
55
- });
56
-
57
- if (result.rows.length === 0) return undefined;
58
-
59
- const row = result.rows[0];
60
- const inputCost = ((row.input_cost_per_1m as number) / 1_000_000) * inputTokens;
61
- const outputCost = ((row.output_cost_per_1m as number) / 1_000_000) * outputTokens;
62
- return inputCost + outputCost;
63
- }
64
-
65
- /**
66
- * Query token usage with filters.
67
- */
68
- export async function queryUsage(filters?: {
69
- squadId?: string;
70
- agentRole?: string;
71
- model?: string;
72
- since?: string; // ISO date
73
- until?: string; // ISO date
74
- }): Promise<{
75
- records: Array<{
76
- model: string;
77
- inputTokens: number;
78
- outputTokens: number;
79
- estimatedCostUsd: number | null;
80
- timestamp: string;
81
- squadId: string | null;
82
- agentRole: string | null;
83
- }>;
84
- totals: {
85
- totalInputTokens: number;
86
- totalOutputTokens: number;
87
- totalCostUsd: number;
88
- callCount: number;
89
- };
90
- }> {
91
- const db = getDatabase();
92
- const conditions: string[] = [];
93
- const args: (string | null)[] = [];
94
-
95
- if (filters?.squadId) {
96
- conditions.push('squad_id = ?');
97
- args.push(filters.squadId);
98
- }
99
- if (filters?.agentRole) {
100
- conditions.push('agent_role = ?');
101
- args.push(filters.agentRole);
102
- }
103
- if (filters?.model) {
104
- conditions.push('model = ?');
105
- args.push(filters.model);
106
- }
107
- if (filters?.since) {
108
- conditions.push('timestamp >= ?');
109
- args.push(filters.since);
110
- }
111
- if (filters?.until) {
112
- conditions.push('timestamp <= ?');
113
- args.push(filters.until);
114
- }
115
-
116
- const where = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
117
-
118
- const result = await db.execute({
119
- sql: `SELECT model, input_tokens, output_tokens, estimated_cost_usd, timestamp, squad_id, agent_role
120
- FROM token_usage ${where} ORDER BY timestamp DESC LIMIT 500`,
121
- args,
122
- });
123
-
124
- const records = result.rows.map((row) => ({
125
- model: row.model as string,
126
- inputTokens: row.input_tokens as number,
127
- outputTokens: row.output_tokens as number,
128
- estimatedCostUsd: row.estimated_cost_usd as number | null,
129
- timestamp: row.timestamp as string,
130
- squadId: row.squad_id as string | null,
131
- agentRole: row.agent_role as string | null,
132
- }));
133
-
134
- // Totals
135
- const totalsResult = await db.execute({
136
- sql: `SELECT COUNT(*) as cnt, COALESCE(SUM(input_tokens), 0) as total_in,
137
- COALESCE(SUM(output_tokens), 0) as total_out, COALESCE(SUM(estimated_cost_usd), 0) as total_cost
138
- FROM token_usage ${where}`,
139
- args,
140
- });
141
-
142
- const totalsRow = totalsResult.rows[0];
143
- const totals = {
144
- callCount: (totalsRow?.cnt as number) ?? 0,
145
- totalInputTokens: (totalsRow?.total_in as number) ?? 0,
146
- totalOutputTokens: (totalsRow?.total_out as number) ?? 0,
147
- totalCostUsd: (totalsRow?.total_cost as number) ?? 0,
148
- };
149
-
150
- return { records, totals };
151
- }
@@ -1,146 +0,0 @@
1
- import { sendMessage } from '../copilot/orchestrator.js';
2
- import { createChildLogger } from '../logging/logger.js';
3
- import { getEventBus } from '../squad/event-bus.js';
4
- import { runInstance } from '../squad/execution/runner.js';
5
- import { getSquadByName, listSquads } from '../squad/manager.js';
6
- import { addInboxEntry } from '../store/inbox.js';
7
- import { type Schedule, getDueSchedules, markScheduleFired } from '../store/schedules.js';
8
-
9
- const logger = () => createChildLogger('scheduler');
10
-
11
- let intervalHandle: ReturnType<typeof setInterval> | null = null;
12
- let running = false;
13
-
14
- const TICK_INTERVAL_MS = 60_000; // Check every minute
15
-
16
- /**
17
- * Start the schedule engine. Evaluates due schedules every 60 seconds.
18
- */
19
- export function startScheduler(): void {
20
- const log = logger();
21
- log.info('Scheduler started');
22
-
23
- // Initial tick
24
- tick();
25
-
26
- intervalHandle = setInterval(() => {
27
- tick();
28
- }, TICK_INTERVAL_MS);
29
- }
30
-
31
- /**
32
- * Stop the schedule engine.
33
- */
34
- export function stopScheduler(): void {
35
- if (intervalHandle) {
36
- clearInterval(intervalHandle);
37
- intervalHandle = null;
38
- }
39
- }
40
-
41
- async function tick(): Promise<void> {
42
- if (running) return; // Prevent overlapping ticks
43
- running = true;
44
-
45
- try {
46
- const due = await getDueSchedules();
47
- if (due.length === 0) {
48
- running = false;
49
- return;
50
- }
51
-
52
- const log = logger();
53
- log.info({ count: due.length }, 'Firing due schedules');
54
-
55
- for (const schedule of due) {
56
- await fireSchedule(schedule);
57
- }
58
- } catch (err) {
59
- logger().error({ err }, 'Scheduler tick error');
60
- } finally {
61
- running = false;
62
- }
63
- }
64
-
65
- async function fireSchedule(schedule: Schedule): Promise<void> {
66
- const log = logger();
67
- const bus = getEventBus();
68
-
69
- // Emit fired event
70
- bus.emit({
71
- type: 'schedule:fired',
72
- id: crypto.randomUUID(),
73
- timestamp: new Date(),
74
- data: { scheduleId: schedule.id, name: schedule.name },
75
- });
76
-
77
- log.info({ scheduleId: schedule.id, name: schedule.name }, 'Firing schedule');
78
-
79
- // Mark as fired immediately (update next_run)
80
- await markScheduleFired(schedule.id, schedule.cron);
81
-
82
- try {
83
- let result: string;
84
-
85
- if (schedule.targetType === 'orchestrator') {
86
- // Send as if user typed it
87
- result = await sendMessage(schedule.prompt, 'web', () => {});
88
- } else {
89
- // Squad target — run instance
90
- const squads = await listSquads();
91
- const squad = squads.find((s) => s.id === schedule.targetId || s.name === schedule.targetId);
92
-
93
- if (!squad) {
94
- throw new Error(`Target squad not found: ${schedule.targetId}`);
95
- }
96
-
97
- const runResult = await runInstance({
98
- squad,
99
- objective: schedule.prompt,
100
- });
101
-
102
- if (runResult.success) {
103
- result = runResult.pr
104
- ? `Completed successfully. PR: ${runResult.pr.url}`
105
- : 'Completed successfully (no PR created).';
106
- } else {
107
- result = `Failed: ${runResult.error ?? 'unknown error'}`;
108
- }
109
- }
110
-
111
- // Post result to inbox as deliverable
112
- const squadId =
113
- schedule.targetType === 'squad' && schedule.targetId ? schedule.targetId : 'orchestrator';
114
-
115
- // Only post to inbox if we have a real squad target
116
- if (schedule.targetType === 'squad' && schedule.targetId) {
117
- await addInboxEntry({
118
- squadId: schedule.targetId,
119
- kind: 'deliverable',
120
- title: `Schedule: ${schedule.name}`,
121
- content: result,
122
- });
123
- }
124
-
125
- // Emit completed event
126
- bus.emit({
127
- type: 'schedule:completed',
128
- id: crypto.randomUUID(),
129
- timestamp: new Date(),
130
- data: { scheduleId: schedule.id, name: schedule.name, result: result.slice(0, 200) },
131
- });
132
-
133
- log.info({ scheduleId: schedule.id }, 'Schedule completed');
134
- } catch (err) {
135
- const errorMsg = err instanceof Error ? err.message : String(err);
136
- log.error({ err, scheduleId: schedule.id }, 'Schedule execution failed');
137
-
138
- // Emit failed event
139
- bus.emit({
140
- type: 'schedule:failed',
141
- id: crypto.randomUUID(),
142
- timestamp: new Date(),
143
- data: { scheduleId: schedule.id, name: schedule.name, error: errorMsg },
144
- });
145
- }
146
- }