aoaoe 1.3.0 → 2.0.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.
@@ -0,0 +1,42 @@
1
+ import type { ReasonerResult, Action } from "./types.js";
2
+ export interface ABTrialResult {
3
+ timestamp: number;
4
+ backendA: string;
5
+ backendB: string;
6
+ actionsA: Action[];
7
+ actionsB: Action[];
8
+ confidenceA?: string;
9
+ confidenceB?: string;
10
+ winner: "a" | "b" | "tie";
11
+ reason: string;
12
+ }
13
+ export interface ABStats {
14
+ totalTrials: number;
15
+ winsA: number;
16
+ winsB: number;
17
+ ties: number;
18
+ backendA: string;
19
+ backendB: string;
20
+ }
21
+ /**
22
+ * Compare two reasoner results and determine which is better.
23
+ * Heuristic: more specific actions > wait, higher confidence > lower,
24
+ * fewer redundant actions = better.
25
+ */
26
+ export declare function compareResults(resultA: ReasonerResult, resultB: ReasonerResult, backendA: string, backendB: string, now?: number): ABTrialResult;
27
+ /**
28
+ * Track A/B trial results over time.
29
+ */
30
+ export declare class ABReasoningTracker {
31
+ private trials;
32
+ private backendA;
33
+ private backendB;
34
+ constructor(backendA: string, backendB: string);
35
+ /** Record a trial result. */
36
+ recordTrial(result: ABTrialResult): void;
37
+ /** Get aggregate stats. */
38
+ getStats(): ABStats;
39
+ /** Format stats for TUI display. */
40
+ formatStats(): string[];
41
+ }
42
+ //# sourceMappingURL=ab-reasoning.d.ts.map
@@ -0,0 +1,91 @@
1
+ // ab-reasoning.ts — run two reasoner backends on the same observation,
2
+ // compare their outputs, and track which performs better over time.
3
+ /**
4
+ * Compare two reasoner results and determine which is better.
5
+ * Heuristic: more specific actions > wait, higher confidence > lower,
6
+ * fewer redundant actions = better.
7
+ */
8
+ export function compareResults(resultA, resultB, backendA, backendB, now = Date.now()) {
9
+ let scoreA = 0;
10
+ let scoreB = 0;
11
+ // prefer non-wait actions over wait
12
+ const nonWaitA = resultA.actions.filter((a) => a.action !== "wait").length;
13
+ const nonWaitB = resultB.actions.filter((a) => a.action !== "wait").length;
14
+ if (nonWaitA > nonWaitB)
15
+ scoreA += 2;
16
+ else if (nonWaitB > nonWaitA)
17
+ scoreB += 2;
18
+ // prefer higher confidence
19
+ const confOrder = { high: 3, medium: 2, low: 1 };
20
+ const confA = confOrder[resultA.confidence ?? "medium"] ?? 2;
21
+ const confB = confOrder[resultB.confidence ?? "medium"] ?? 2;
22
+ if (confA > confB)
23
+ scoreA += 1;
24
+ else if (confB > confA)
25
+ scoreB += 1;
26
+ // prefer fewer total actions (more focused)
27
+ if (resultA.actions.length > 0 && resultB.actions.length > 0) {
28
+ if (resultA.actions.length < resultB.actions.length)
29
+ scoreA += 1;
30
+ else if (resultB.actions.length < resultA.actions.length)
31
+ scoreB += 1;
32
+ }
33
+ const winner = scoreA > scoreB ? "a" : scoreB > scoreA ? "b" : "tie";
34
+ const reason = `A(${nonWaitA} actions, ${resultA.confidence ?? "?"}) vs B(${nonWaitB} actions, ${resultB.confidence ?? "?"})`;
35
+ return {
36
+ timestamp: now,
37
+ backendA,
38
+ backendB,
39
+ actionsA: resultA.actions,
40
+ actionsB: resultB.actions,
41
+ confidenceA: resultA.confidence,
42
+ confidenceB: resultB.confidence,
43
+ winner,
44
+ reason,
45
+ };
46
+ }
47
+ /**
48
+ * Track A/B trial results over time.
49
+ */
50
+ export class ABReasoningTracker {
51
+ trials = [];
52
+ backendA;
53
+ backendB;
54
+ constructor(backendA, backendB) {
55
+ this.backendA = backendA;
56
+ this.backendB = backendB;
57
+ }
58
+ /** Record a trial result. */
59
+ recordTrial(result) {
60
+ this.trials.push(result);
61
+ }
62
+ /** Get aggregate stats. */
63
+ getStats() {
64
+ return {
65
+ totalTrials: this.trials.length,
66
+ winsA: this.trials.filter((t) => t.winner === "a").length,
67
+ winsB: this.trials.filter((t) => t.winner === "b").length,
68
+ ties: this.trials.filter((t) => t.winner === "tie").length,
69
+ backendA: this.backendA,
70
+ backendB: this.backendB,
71
+ };
72
+ }
73
+ /** Format stats for TUI display. */
74
+ formatStats() {
75
+ const s = this.getStats();
76
+ if (s.totalTrials === 0)
77
+ return [" (no A/B trials recorded yet)"];
78
+ const pctA = s.totalTrials > 0 ? Math.round((s.winsA / s.totalTrials) * 100) : 0;
79
+ const pctB = s.totalTrials > 0 ? Math.round((s.winsB / s.totalTrials) * 100) : 0;
80
+ return [
81
+ ` A/B Reasoning: ${s.totalTrials} trials`,
82
+ ` ${s.backendA}: ${s.winsA} wins (${pctA}%)`,
83
+ ` ${s.backendB}: ${s.winsB} wins (${pctB}%)`,
84
+ ` Ties: ${s.ties}`,
85
+ s.winsA > s.winsB ? ` → ${s.backendA} is performing better` :
86
+ s.winsB > s.winsA ? ` → ${s.backendB} is performing better` :
87
+ ` → Both backends performing equally`,
88
+ ];
89
+ }
90
+ }
91
+ //# sourceMappingURL=ab-reasoning.js.map
@@ -0,0 +1,42 @@
1
+ export type AlertSeverity = "info" | "warning" | "critical";
2
+ export type AlertCondition = (ctx: AlertContext) => boolean;
3
+ export interface AlertContext {
4
+ fleetHealth: number;
5
+ activeSessions: number;
6
+ errorSessions: number;
7
+ totalCostUsd: number;
8
+ hourlyCostRate: number;
9
+ stuckSessions: number;
10
+ idleMinutes: Map<string, number>;
11
+ }
12
+ export interface AlertRule {
13
+ name: string;
14
+ description: string;
15
+ severity: AlertSeverity;
16
+ condition: AlertCondition;
17
+ cooldownMs: number;
18
+ lastFiredAt: number;
19
+ }
20
+ export interface FiredAlert {
21
+ ruleName: string;
22
+ severity: AlertSeverity;
23
+ message: string;
24
+ timestamp: number;
25
+ }
26
+ /**
27
+ * Built-in alert rules.
28
+ */
29
+ export declare function defaultAlertRules(): AlertRule[];
30
+ /**
31
+ * Evaluate all alert rules against current fleet state.
32
+ */
33
+ export declare function evaluateAlertRules(rules: AlertRule[], ctx: AlertContext, now?: number): FiredAlert[];
34
+ /**
35
+ * Format fired alerts for TUI display.
36
+ */
37
+ export declare function formatFiredAlerts(alerts: FiredAlert[]): string[];
38
+ /**
39
+ * Format all rules and their status for TUI display.
40
+ */
41
+ export declare function formatAlertRules(rules: AlertRule[], now?: number): string[];
42
+ //# sourceMappingURL=alert-rules.d.ts.map
@@ -0,0 +1,94 @@
1
+ // alert-rules.ts — custom fleet health alerting rules beyond SLA threshold.
2
+ // define conditions that trigger alerts when met, with configurable
3
+ // severity, cooldown, and notification routing.
4
+ /**
5
+ * Built-in alert rules.
6
+ */
7
+ export function defaultAlertRules() {
8
+ return [
9
+ {
10
+ name: "fleet-health-critical",
11
+ description: "Fleet health dropped below 30",
12
+ severity: "critical",
13
+ condition: (ctx) => ctx.fleetHealth < 30,
14
+ cooldownMs: 10 * 60_000,
15
+ lastFiredAt: 0,
16
+ },
17
+ {
18
+ name: "high-error-rate",
19
+ description: "More than 50% of sessions in error state",
20
+ severity: "critical",
21
+ condition: (ctx) => ctx.activeSessions > 0 && (ctx.errorSessions / ctx.activeSessions) > 0.5,
22
+ cooldownMs: 5 * 60_000,
23
+ lastFiredAt: 0,
24
+ },
25
+ {
26
+ name: "cost-spike",
27
+ description: "Hourly cost rate exceeds $5",
28
+ severity: "warning",
29
+ condition: (ctx) => ctx.hourlyCostRate > 5,
30
+ cooldownMs: 15 * 60_000,
31
+ lastFiredAt: 0,
32
+ },
33
+ {
34
+ name: "all-stuck",
35
+ description: "All active sessions are stuck",
36
+ severity: "critical",
37
+ condition: (ctx) => ctx.activeSessions > 0 && ctx.stuckSessions === ctx.activeSessions,
38
+ cooldownMs: 10 * 60_000,
39
+ lastFiredAt: 0,
40
+ },
41
+ {
42
+ name: "no-active-sessions",
43
+ description: "No active sessions running",
44
+ severity: "info",
45
+ condition: (ctx) => ctx.activeSessions === 0,
46
+ cooldownMs: 30 * 60_000,
47
+ lastFiredAt: 0,
48
+ },
49
+ ];
50
+ }
51
+ /**
52
+ * Evaluate all alert rules against current fleet state.
53
+ */
54
+ export function evaluateAlertRules(rules, ctx, now = Date.now()) {
55
+ const fired = [];
56
+ for (const rule of rules) {
57
+ if (now - rule.lastFiredAt < rule.cooldownMs)
58
+ continue;
59
+ if (rule.condition(ctx)) {
60
+ rule.lastFiredAt = now;
61
+ fired.push({
62
+ ruleName: rule.name,
63
+ severity: rule.severity,
64
+ message: rule.description,
65
+ timestamp: now,
66
+ });
67
+ }
68
+ }
69
+ return fired;
70
+ }
71
+ /**
72
+ * Format fired alerts for TUI display.
73
+ */
74
+ export function formatFiredAlerts(alerts) {
75
+ if (alerts.length === 0)
76
+ return [" ✅ no alerts fired"];
77
+ const icons = { info: "ℹ", warning: "⚠", critical: "🚨" };
78
+ return alerts.map((a) => ` ${icons[a.severity]} [${a.severity}] ${a.ruleName}: ${a.message}`);
79
+ }
80
+ /**
81
+ * Format all rules and their status for TUI display.
82
+ */
83
+ export function formatAlertRules(rules, now = Date.now()) {
84
+ const lines = [];
85
+ lines.push(` Alert rules (${rules.length}):`);
86
+ for (const r of rules) {
87
+ const cooldownRemaining = Math.max(0, r.cooldownMs - (now - r.lastFiredAt));
88
+ const cooldownStr = cooldownRemaining > 0 ? ` (cooldown: ${Math.round(cooldownRemaining / 60_000)}m)` : "";
89
+ const icon = r.severity === "critical" ? "🚨" : r.severity === "warning" ? "⚠" : "ℹ";
90
+ lines.push(` ${icon} ${r.name}: ${r.description}${cooldownStr}`);
91
+ }
92
+ return lines;
93
+ }
94
+ //# sourceMappingURL=alert-rules.js.map
@@ -0,0 +1,34 @@
1
+ export interface FederationPeer {
2
+ name: string;
3
+ url: string;
4
+ lastSeenAt?: number;
5
+ status: "online" | "offline" | "unknown";
6
+ }
7
+ export interface FederatedFleetState {
8
+ peer: string;
9
+ sessions: number;
10
+ activeTasks: number;
11
+ fleetHealth: number;
12
+ totalCostUsd: number;
13
+ lastUpdatedAt: number;
14
+ }
15
+ export interface FederationOverview {
16
+ peers: FederatedFleetState[];
17
+ totalSessions: number;
18
+ totalActiveTasks: number;
19
+ averageHealth: number;
20
+ totalCostUsd: number;
21
+ }
22
+ /**
23
+ * Fetch fleet state from a peer daemon's health endpoint.
24
+ */
25
+ export declare function fetchPeerState(peer: FederationPeer, timeoutMs?: number): Promise<FederatedFleetState | null>;
26
+ /**
27
+ * Aggregate fleet state from all peers into an overview.
28
+ */
29
+ export declare function aggregateFederation(states: FederatedFleetState[]): FederationOverview;
30
+ /**
31
+ * Format federation overview for TUI display.
32
+ */
33
+ export declare function formatFederationOverview(overview: FederationOverview): string[];
34
+ //# sourceMappingURL=fleet-federation.d.ts.map
@@ -0,0 +1,55 @@
1
+ // fleet-federation.ts — coordinate across multiple aoaoe daemons via HTTP.
2
+ // each daemon exposes a lightweight status endpoint; the federation client
3
+ // aggregates fleet state across hosts for unified monitoring.
4
+ /**
5
+ * Fetch fleet state from a peer daemon's health endpoint.
6
+ */
7
+ export async function fetchPeerState(peer, timeoutMs = 5000) {
8
+ try {
9
+ const controller = new AbortController();
10
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
11
+ const response = await fetch(`${peer.url}/health`, { signal: controller.signal });
12
+ clearTimeout(timer);
13
+ if (!response.ok)
14
+ return null;
15
+ const data = await response.json();
16
+ return {
17
+ peer: peer.name,
18
+ sessions: data.sessions ?? 0,
19
+ activeTasks: data.activeTasks ?? 0,
20
+ fleetHealth: data.fleetHealth ?? 0,
21
+ totalCostUsd: data.totalCostUsd ?? 0,
22
+ lastUpdatedAt: Date.now(),
23
+ };
24
+ }
25
+ catch {
26
+ return null;
27
+ }
28
+ }
29
+ /**
30
+ * Aggregate fleet state from all peers into an overview.
31
+ */
32
+ export function aggregateFederation(states) {
33
+ const totalSessions = states.reduce((s, p) => s + p.sessions, 0);
34
+ const totalActiveTasks = states.reduce((s, p) => s + p.activeTasks, 0);
35
+ const totalCost = states.reduce((s, p) => s + p.totalCostUsd, 0);
36
+ const avgHealth = states.length > 0
37
+ ? Math.round(states.reduce((s, p) => s + p.fleetHealth, 0) / states.length)
38
+ : 0;
39
+ return { peers: states, totalSessions, totalActiveTasks, averageHealth: avgHealth, totalCostUsd: totalCost };
40
+ }
41
+ /**
42
+ * Format federation overview for TUI display.
43
+ */
44
+ export function formatFederationOverview(overview) {
45
+ if (overview.peers.length === 0)
46
+ return [" (no federation peers configured)"];
47
+ const lines = [];
48
+ lines.push(` Federation: ${overview.peers.length} peers, ${overview.totalSessions} sessions, health ${overview.averageHealth}/100, $${overview.totalCostUsd.toFixed(2)} total`);
49
+ for (const p of overview.peers) {
50
+ const age = Math.round((Date.now() - p.lastUpdatedAt) / 60_000);
51
+ lines.push(` ${p.peer}: ${p.sessions} sessions, ${p.activeTasks} active, health ${p.fleetHealth}/100, $${p.totalCostUsd.toFixed(2)} (${age}m ago)`);
52
+ }
53
+ return lines;
54
+ }
55
+ //# sourceMappingURL=fleet-federation.js.map
package/dist/index.js CHANGED
@@ -61,7 +61,18 @@ import { analyzeCompletedTasks, refineGoal, formatGoalRefinement } from "./goal-
61
61
  import { generateHtmlReport, buildReportData } from "./fleet-export.js";
62
62
  import { installService } from "./service-generator.js";
63
63
  import { buildSessionReplay, formatReplay, summarizeReplay } from "./session-replay.js";
64
- import { advanceWorkflow, formatWorkflow } from "./workflow-engine.js";
64
+ import { createWorkflowState, advanceWorkflow, formatWorkflow } from "./workflow-engine.js";
65
+ import { assignReasonerBackends, formatAssignments } from "./multi-reasoner.js";
66
+ import { TokenQuotaManager } from "./token-quota.js";
67
+ import { saveCheckpoint, loadCheckpoint, buildCheckpoint, formatCheckpointInfo, shouldRestoreCheckpoint } from "./session-checkpoint.js";
68
+ import { findWorkflowTemplate, instantiateWorkflow, formatWorkflowTemplateList } from "./workflow-templates.js";
69
+ import { ABReasoningTracker } from "./ab-reasoning.js";
70
+ import { forecastWorkflowCost, formatWorkflowCostForecast } from "./workflow-cost-forecast.js";
71
+ import { advanceChain, formatWorkflowChain } from "./workflow-chain.js";
72
+ import { aggregateFederation, formatFederationOverview } from "./fleet-federation.js";
73
+ import { formatArchiveList } from "./output-archival.js";
74
+ import { generateRunbooks, formatGeneratedRunbooks } from "./runbook-generator.js";
75
+ import { defaultAlertRules, evaluateAlertRules, formatAlertRules } from "./alert-rules.js";
65
76
  import { buildLifecycleRecords, computeLifecycleStats, formatLifecycleStats } from "./lifecycle-analytics.js";
66
77
  import { buildCostAttributions, computeCostReport, formatCostReport } from "./cost-attribution.js";
67
78
  import { decomposeGoal, formatDecomposition } from "./goal-decomposer.js";
@@ -567,6 +578,24 @@ async function main() {
567
578
  const approvalQueue = new ApprovalQueue();
568
579
  const graduationManager = new GraduationManager();
569
580
  let activeWorkflow = null;
581
+ let activeWorkflowChain = null;
582
+ const tokenQuotaManager = new TokenQuotaManager();
583
+ const abReasoningTracker = new ABReasoningTracker(config.reasoner, "claude-code");
584
+ const alertRules = defaultAlertRules();
585
+ // checkpoint restore: load previous daemon state if available
586
+ if (shouldRestoreCheckpoint()) {
587
+ const cp = loadCheckpoint();
588
+ if (cp) {
589
+ // restore adaptive poll interval
590
+ if (cp.pollInterval && cp.pollInterval !== config.pollIntervalMs) {
591
+ // poll controller will naturally adjust, but log what was saved
592
+ }
593
+ const restoredSessions = Object.keys(cp.graduation).length;
594
+ if (restoredSessions > 0) {
595
+ audit("daemon_start", `restored checkpoint: ${restoredSessions} graduation states, cache ${cp.cacheStats.hits}/${cp.cacheStats.misses}`);
596
+ }
597
+ }
598
+ }
570
599
  // audit: log daemon start
571
600
  audit("daemon_start", `daemon started (v${pkg ?? "dev"}, reasoner=${config.reasoner})`);
572
601
  const refreshTaskSupervisorState = (reason) => {
@@ -2467,6 +2496,120 @@ async function main() {
2467
2496
  for (const l of lines)
2468
2497
  tui.log("system", l);
2469
2498
  });
2499
+ // wire /multi-reasoner — show reasoner assignments
2500
+ input.onMultiReasoner(() => {
2501
+ const sessions = tui.getSessions();
2502
+ const sessionInfos = sessions.map((s) => ({ title: s.title, template: undefined, difficultyScore: undefined }));
2503
+ const assignments = assignReasonerBackends(sessionInfos, { defaultBackend: config.reasoner });
2504
+ const lines = formatAssignments(assignments);
2505
+ for (const l of lines)
2506
+ tui.log("system", l);
2507
+ });
2508
+ // wire /token-quota — per-model token quotas
2509
+ input.onTokenQuota(() => {
2510
+ const lines = tokenQuotaManager.formatAll();
2511
+ for (const l of lines)
2512
+ tui.log("system", l);
2513
+ });
2514
+ // wire /checkpoint — show checkpoint info
2515
+ input.onCheckpoint(() => {
2516
+ const lines = formatCheckpointInfo();
2517
+ for (const l of lines)
2518
+ tui.log("system", l);
2519
+ });
2520
+ // wire /workflow-new — create workflow from template
2521
+ input.onWorkflowNew((args) => {
2522
+ const parts = args.split(/\s+/);
2523
+ const templateName = parts[0];
2524
+ const prefix = parts[1] ?? "wf";
2525
+ const template = findWorkflowTemplate(templateName);
2526
+ if (!template) {
2527
+ tui.log("system", `workflow-new: template "${templateName}" not found`);
2528
+ const lines = formatWorkflowTemplateList();
2529
+ for (const l of lines)
2530
+ tui.log("system", l);
2531
+ return;
2532
+ }
2533
+ const def = instantiateWorkflow(template, prefix);
2534
+ // show cost forecast before creating
2535
+ const forecast = forecastWorkflowCost(def);
2536
+ const forecastLines = formatWorkflowCostForecast(forecast);
2537
+ for (const l of forecastLines)
2538
+ tui.log("system", l);
2539
+ activeWorkflow = createWorkflowState(def);
2540
+ tui.log("+ action", `workflow "${def.name}" created from template "${templateName}" (${def.stages.length} stages)`);
2541
+ audit("task_created", `workflow created: ${def.name} from ${templateName}`, undefined, { stages: def.stages.length });
2542
+ const lines = formatWorkflow(activeWorkflow);
2543
+ for (const l of lines)
2544
+ tui.log("system", l);
2545
+ });
2546
+ // wire /ab-stats — A/B reasoning statistics
2547
+ input.onABStats(() => {
2548
+ const lines = abReasoningTracker.formatStats();
2549
+ for (const l of lines)
2550
+ tui.log("system", l);
2551
+ });
2552
+ // wire /workflow-chain — show active workflow chain
2553
+ input.onWorkflowChain(() => {
2554
+ if (!activeWorkflowChain) {
2555
+ tui.log("system", "workflow-chain: no active chain");
2556
+ return;
2557
+ }
2558
+ const lines = formatWorkflowChain(activeWorkflowChain);
2559
+ for (const l of lines)
2560
+ tui.log("system", l);
2561
+ });
2562
+ // wire /workflow-forecast — preview cost estimate for a template
2563
+ input.onWorkflowForecast((templateName) => {
2564
+ const template = findWorkflowTemplate(templateName);
2565
+ if (!template) {
2566
+ tui.log("system", `workflow-forecast: template "${templateName}" not found`);
2567
+ return;
2568
+ }
2569
+ const def = instantiateWorkflow(template, "preview");
2570
+ const forecast = forecastWorkflowCost(def);
2571
+ const lines = formatWorkflowCostForecast(forecast);
2572
+ for (const l of lines)
2573
+ tui.log("system", l);
2574
+ });
2575
+ // wire /federation — multi-host fleet overview
2576
+ input.onFederation(() => {
2577
+ // in practice, would fetch from configured peers; show local state as single peer
2578
+ const sessions = tui.getSessions();
2579
+ const tasks = taskManager?.tasks ?? [];
2580
+ const scores = sessions.map((s) => s.status === "working" || s.status === "running" ? 80 : s.status === "error" ? 20 : 50);
2581
+ const health = scores.length > 0 ? Math.round(scores.reduce((a, b) => a + b, 0) / scores.length) : 100;
2582
+ let cost = 0;
2583
+ for (const s of sessions) {
2584
+ const m = s.costStr?.match(/\$(\d+(?:\.\d+)?)/);
2585
+ if (m)
2586
+ cost += parseFloat(m[1]);
2587
+ }
2588
+ const localState = { peer: "local", sessions: sessions.length, activeTasks: tasks.filter((t) => t.status === "active").length, fleetHealth: health, totalCostUsd: cost, lastUpdatedAt: Date.now() };
2589
+ const overview = aggregateFederation([localState]);
2590
+ const lines = formatFederationOverview(overview);
2591
+ for (const l of lines)
2592
+ tui.log("system", l);
2593
+ });
2594
+ // wire /archives — show output archive list
2595
+ input.onArchives(() => {
2596
+ const lines = formatArchiveList();
2597
+ for (const l of lines)
2598
+ tui.log("system", l);
2599
+ });
2600
+ // wire /runbook-gen — generate runbooks from audit trail
2601
+ input.onRunbookGen(() => {
2602
+ const runbooks = generateRunbooks();
2603
+ const lines = formatGeneratedRunbooks(runbooks);
2604
+ for (const l of lines)
2605
+ tui.log("system", l);
2606
+ });
2607
+ // wire /alert-rules — show alert rules and their status
2608
+ input.onAlertRules(() => {
2609
+ const lines = formatAlertRules(alertRules);
2610
+ for (const l of lines)
2611
+ tui.log("system", l);
2612
+ });
2470
2613
  input.onCostSummary(() => {
2471
2614
  const sessions = tui.getSessions();
2472
2615
  const summary = computeCostSummary(sessions, tui.getAllSessionCosts());
@@ -2928,6 +3071,27 @@ async function main() {
2928
3071
  .then(() => reasoner?.shutdown())
2929
3072
  .catch((err) => console.error(`[shutdown] error during cleanup: ${err}`))
2930
3073
  .finally(() => {
3074
+ // save daemon state checkpoint before exit
3075
+ try {
3076
+ const cp = buildCheckpoint({
3077
+ graduation: Object.fromEntries([...(tui?.getSessions() ?? [])].map((s) => [s.title, {
3078
+ mode: graduationManager.getState(s.title)?.currentMode ?? "confirm",
3079
+ successes: graduationManager.getState(s.title)?.successfulActions ?? 0,
3080
+ failures: graduationManager.getState(s.title)?.failedActions ?? 0,
3081
+ rate: graduationManager.getState(s.title)?.successRate ?? 0,
3082
+ }])),
3083
+ escalation: {},
3084
+ velocitySamples: {},
3085
+ nudgeRecords: [],
3086
+ budgetSamples: {},
3087
+ cacheStats: { hits: observationCache.getStats().totalHits, misses: observationCache.getStats().totalMisses },
3088
+ slaHistory: [],
3089
+ pollInterval: adaptivePollController.intervalMs,
3090
+ });
3091
+ saveCheckpoint(cp);
3092
+ audit("daemon_stop", "daemon stopped, checkpoint saved");
3093
+ }
3094
+ catch { /* best-effort */ }
2931
3095
  cleanupState();
2932
3096
  process.exit(0);
2933
3097
  });
@@ -4006,6 +4170,7 @@ async function main() {
4006
4170
  escalationManager,
4007
4171
  graduationManager,
4008
4172
  approvalQueue,
4173
+ tokenQuotaManager,
4009
4174
  pushSupervisorEvent,
4010
4175
  refreshTaskSupervisorState,
4011
4176
  });
@@ -4111,6 +4276,45 @@ async function main() {
4111
4276
  activeWorkflow = null;
4112
4277
  }
4113
4278
  }
4279
+ // workflow chain: advance cross-workflow dependencies
4280
+ if (activeWorkflowChain && tui) {
4281
+ const chain = activeWorkflowChain;
4282
+ const wfStates = new Map();
4283
+ const { activate, completed: chainDone, failed: chainFailed } = advanceChain(chain, wfStates);
4284
+ for (const name of activate) {
4285
+ tui.log("status", `workflow-chain: activating workflow "${name}"`);
4286
+ audit("task_created", `chain activated: ${name}`, name);
4287
+ }
4288
+ if (chainDone) {
4289
+ tui.log("+ action", `workflow chain "${chain.name}" completed`);
4290
+ activeWorkflowChain = null;
4291
+ }
4292
+ if (chainFailed) {
4293
+ tui.log("status", `workflow chain "${chain.name}" has failures`);
4294
+ }
4295
+ }
4296
+ // custom alert rules: evaluate fleet conditions per tick
4297
+ if (tui) {
4298
+ const sessions = tui.getSessions();
4299
+ const tasks = taskManager?.tasks ?? [];
4300
+ const scores = sessions.map((s) => s.status === "working" || s.status === "running" ? 80 : s.status === "error" ? 20 : 50);
4301
+ const fleetHealth = scores.length > 0 ? Math.round(scores.reduce((a, b) => a + b, 0) / scores.length) : 100;
4302
+ const activeSessions = sessions.filter((s) => s.status === "working" || s.status === "running").length;
4303
+ const errorSessions = sessions.filter((s) => s.status === "error").length;
4304
+ const stuckSessions = tasks.filter((t) => t.status === "active" && (t.stuckNudgeCount ?? 0) > 0).length;
4305
+ let hourlyCost = 0;
4306
+ for (const s of sessions) {
4307
+ const m = s.costStr?.match(/\$(\d+(?:\.\d+)?)/);
4308
+ if (m)
4309
+ hourlyCost += parseFloat(m[1]);
4310
+ }
4311
+ const alertCtx = { fleetHealth, activeSessions, errorSessions, totalCostUsd: hourlyCost, hourlyCostRate: hourlyCost, stuckSessions, idleMinutes: new Map() };
4312
+ const firedAlerts = evaluateAlertRules(alertRules, alertCtx);
4313
+ for (const alert of firedAlerts) {
4314
+ tui.log("status", `${alert.severity === "critical" ? "🚨" : alert.severity === "warning" ? "⚠" : "ℹ"} ALERT: ${alert.message}`);
4315
+ audit("session_error", `alert fired: ${alert.ruleName} — ${alert.message}`, undefined, { severity: alert.severity });
4316
+ }
4317
+ }
4114
4318
  // trust ladder: record stable tick or failure, sync mode if escalated
4115
4319
  if (tui && decisionsThisTick > 0) {
4116
4320
  if (actionsFail > 0) {
@@ -4248,6 +4452,14 @@ async function daemonTick(config, poller, reasoner, executor, reasonerConsole, p
4248
4452
  init: () => reasoner.init(),
4249
4453
  shutdown: () => reasoner.shutdown(),
4250
4454
  decide: async (obs) => {
4455
+ // ── gate 0: per-model token quota — block if model quota exceeded ──
4456
+ if (intelligence?.tokenQuotaManager.isBlocked(config.reasoner)) {
4457
+ const status = intelligence.tokenQuotaManager.getStatus(config.reasoner);
4458
+ if (tui)
4459
+ tui.log("status", `⏸ token quota exceeded for ${config.reasoner}: ${status.reason}`);
4460
+ audit("reasoner_action", `token quota blocked: ${config.reasoner} — ${status.reason}`);
4461
+ return { actions: [{ action: "wait", reason: `token quota: ${status.reason}` }] };
4462
+ }
4251
4463
  // ── gate 1: fleet rate limiter — block if over API spend limits ──
4252
4464
  if (intelligence?.fleetRateLimiter.isBlocked()) {
4253
4465
  const status = intelligence.fleetRateLimiter.getStatus();
@@ -4326,6 +4538,8 @@ async function daemonTick(config, poller, reasoner, executor, reasonerConsole, p
4326
4538
  intelligence.reasonerCostTracker.recordCall("fleet", tokenEstimate, outputEstimate, reasonerDurationMs);
4327
4539
  intelligence.fleetRateLimiter.recordCost(estimateCallCost(tokenEstimate, outputEstimate));
4328
4540
  intelligence.observationCache.set(obsJson, r);
4541
+ // per-model token quota tracking
4542
+ intelligence.tokenQuotaManager.recordUsage(config.reasoner, tokenEstimate, outputEstimate);
4329
4543
  // approval workflow: gate risky/low-confidence actions through approval queue
4330
4544
  if (config.confirm || r.confidence === "low") {
4331
4545
  const { immediate, queued } = filterThroughApproval(r, intelligence.approvalQueue);
package/dist/input.d.ts CHANGED
@@ -134,6 +134,17 @@ export type ExportHandler = () => void;
134
134
  export type ServiceHandler = () => void;
135
135
  export type SessionReplayHandler = (target: string) => void;
136
136
  export type WorkflowHandler = () => void;
137
+ export type MultiReasonerHandler = () => void;
138
+ export type TokenQuotaHandler = () => void;
139
+ export type CheckpointHandler = () => void;
140
+ export type WorkflowNewHandler = (template: string) => void;
141
+ export type ABStatsHandler = () => void;
142
+ export type WorkflowChainHandler = () => void;
143
+ export type WorkflowForecastHandler = (template: string) => void;
144
+ export type FederationHandler = () => void;
145
+ export type ArchivesHandler = () => void;
146
+ export type RunbookGenHandler = () => void;
147
+ export type AlertRulesHandler = () => void;
137
148
  export interface MouseEvent {
138
149
  button: number;
139
150
  col: number;
@@ -452,6 +463,28 @@ export declare class InputReader {
452
463
  onService(handler: ServiceHandler): void;
453
464
  onSessionReplay(handler: SessionReplayHandler): void;
454
465
  onWorkflow(handler: WorkflowHandler): void;
466
+ private multiReasonerHandler;
467
+ private tokenQuotaHandler;
468
+ private checkpointHandler;
469
+ private workflowNewHandler;
470
+ onMultiReasoner(handler: MultiReasonerHandler): void;
471
+ onTokenQuota(handler: TokenQuotaHandler): void;
472
+ onCheckpoint(handler: CheckpointHandler): void;
473
+ onWorkflowNew(handler: WorkflowNewHandler): void;
474
+ private abStatsHandler;
475
+ private workflowChainHandler;
476
+ private workflowForecastHandler;
477
+ onABStats(handler: ABStatsHandler): void;
478
+ onWorkflowChain(handler: WorkflowChainHandler): void;
479
+ onWorkflowForecast(handler: WorkflowForecastHandler): void;
480
+ private federationHandler;
481
+ private archivesHandler;
482
+ private runbookGenHandler;
483
+ private alertRulesHandler;
484
+ onFederation(handler: FederationHandler): void;
485
+ onArchives(handler: ArchivesHandler): void;
486
+ onRunbookGen(handler: RunbookGenHandler): void;
487
+ onAlertRules(handler: AlertRulesHandler): void;
455
488
  onFleetSearch(handler: FleetSearchHandler): void;
456
489
  onNudgeStats(handler: NudgeStatsHandler): void;
457
490
  onAllocation(handler: AllocationHandler): void;