aoaoe 1.0.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.
- package/README.md +23 -1
- package/dist/ab-reasoning.d.ts +42 -0
- package/dist/ab-reasoning.js +91 -0
- package/dist/alert-rules.d.ts +42 -0
- package/dist/alert-rules.js +94 -0
- package/dist/cli-completions.d.ts +24 -0
- package/dist/cli-completions.js +114 -0
- package/dist/config.d.ts +3 -0
- package/dist/config.js +8 -1
- package/dist/fleet-federation.d.ts +34 -0
- package/dist/fleet-federation.js +55 -0
- package/dist/index.js +281 -1
- package/dist/input.d.ts +42 -0
- package/dist/input.js +130 -0
- package/dist/multi-reasoner.d.ts +36 -0
- package/dist/multi-reasoner.js +87 -0
- package/dist/output-archival.d.ts +23 -0
- package/dist/output-archival.js +72 -0
- package/dist/runbook-generator.d.ts +21 -0
- package/dist/runbook-generator.js +104 -0
- package/dist/service-generator.d.ts +32 -0
- package/dist/service-generator.js +132 -0
- package/dist/session-checkpoint.d.ts +55 -0
- package/dist/session-checkpoint.js +69 -0
- package/dist/session-replay.d.ts +25 -0
- package/dist/session-replay.js +103 -0
- package/dist/token-quota.d.ts +45 -0
- package/dist/token-quota.js +76 -0
- package/dist/workflow-chain.d.ts +33 -0
- package/dist/workflow-chain.js +69 -0
- package/dist/workflow-cost-forecast.d.ts +22 -0
- package/dist/workflow-cost-forecast.js +55 -0
- package/dist/workflow-engine.d.ts +48 -0
- package/dist/workflow-engine.js +91 -0
- package/dist/workflow-templates.d.ts +25 -0
- package/dist/workflow-templates.js +92 -0
- package/package.json +1 -1
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
// multi-reasoner.ts — assign different LLM backends per session.
|
|
2
|
+
// routes observations to the appropriate reasoner based on session config,
|
|
3
|
+
// template, or difficulty. supports reasoner pools with load balancing.
|
|
4
|
+
const DEFAULT_CONFIG = {
|
|
5
|
+
defaultBackend: "opencode",
|
|
6
|
+
sessionOverrides: {},
|
|
7
|
+
templateMappings: {},
|
|
8
|
+
difficultyThreshold: 7,
|
|
9
|
+
premiumBackend: "opencode",
|
|
10
|
+
economyBackend: "claude-code",
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* Determine which reasoner backend to use for each session.
|
|
14
|
+
*/
|
|
15
|
+
export function assignReasonerBackends(sessions, config = {}) {
|
|
16
|
+
const c = { ...DEFAULT_CONFIG, ...config };
|
|
17
|
+
return sessions.map((s) => {
|
|
18
|
+
// explicit override wins
|
|
19
|
+
if (c.sessionOverrides[s.title]) {
|
|
20
|
+
return { sessionTitle: s.title, backend: c.sessionOverrides[s.title], reason: "explicit override" };
|
|
21
|
+
}
|
|
22
|
+
// template mapping
|
|
23
|
+
if (s.template && c.templateMappings[s.template]) {
|
|
24
|
+
return { sessionTitle: s.title, backend: c.templateMappings[s.template], reason: `template: ${s.template}` };
|
|
25
|
+
}
|
|
26
|
+
// difficulty-based routing
|
|
27
|
+
if (s.difficultyScore !== undefined) {
|
|
28
|
+
if (s.difficultyScore >= c.difficultyThreshold) {
|
|
29
|
+
return { sessionTitle: s.title, backend: c.premiumBackend, reason: `high difficulty (${s.difficultyScore}/10)` };
|
|
30
|
+
}
|
|
31
|
+
return { sessionTitle: s.title, backend: c.economyBackend, reason: `low difficulty (${s.difficultyScore}/10)` };
|
|
32
|
+
}
|
|
33
|
+
return { sessionTitle: s.title, backend: c.defaultBackend, reason: "default" };
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Split an observation into per-backend groups for routing.
|
|
38
|
+
*/
|
|
39
|
+
export function routeObservation(observation, assignments) {
|
|
40
|
+
const assignMap = new Map(assignments.map((a) => [a.sessionTitle, a.backend]));
|
|
41
|
+
const groups = new Map();
|
|
42
|
+
for (const snap of observation.sessions) {
|
|
43
|
+
const backend = assignMap.get(snap.session.title) ?? "opencode";
|
|
44
|
+
if (!groups.has(backend))
|
|
45
|
+
groups.set(backend, { sessions: [], changes: [] });
|
|
46
|
+
groups.get(backend).sessions.push(snap);
|
|
47
|
+
}
|
|
48
|
+
for (const change of observation.changes) {
|
|
49
|
+
const backend = assignMap.get(change.title) ?? "opencode";
|
|
50
|
+
if (groups.has(backend))
|
|
51
|
+
groups.get(backend).changes.push(change);
|
|
52
|
+
}
|
|
53
|
+
const result = new Map();
|
|
54
|
+
for (const [backend, data] of groups) {
|
|
55
|
+
result.set(backend, { timestamp: observation.timestamp, sessions: data.sessions, changes: data.changes });
|
|
56
|
+
}
|
|
57
|
+
return result;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Merge results from multiple reasoner backends into a single result.
|
|
61
|
+
*/
|
|
62
|
+
export function mergeReasonerResults(results) {
|
|
63
|
+
const allActions = results.flatMap((r) => r.actions);
|
|
64
|
+
// use lowest confidence across all results
|
|
65
|
+
const confidences = results.map((r) => r.confidence).filter(Boolean);
|
|
66
|
+
const confidence = confidences.length > 0
|
|
67
|
+
? (confidences.includes("low") ? "low" : confidences.includes("medium") ? "medium" : "high")
|
|
68
|
+
: undefined;
|
|
69
|
+
return { actions: allActions, confidence: confidence };
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Format assignments for TUI display.
|
|
73
|
+
*/
|
|
74
|
+
export function formatAssignments(assignments) {
|
|
75
|
+
if (assignments.length === 0)
|
|
76
|
+
return [" (no sessions to assign)"];
|
|
77
|
+
const lines = [];
|
|
78
|
+
const backendCounts = new Map();
|
|
79
|
+
for (const a of assignments) {
|
|
80
|
+
backendCounts.set(a.backend, (backendCounts.get(a.backend) ?? 0) + 1);
|
|
81
|
+
lines.push(` ${a.sessionTitle}: ${a.backend} (${a.reason})`);
|
|
82
|
+
}
|
|
83
|
+
const summary = [...backendCounts.entries()].map(([b, c]) => `${b}: ${c}`).join(", ");
|
|
84
|
+
lines.unshift(` Multi-reasoner assignments (${summary}):`);
|
|
85
|
+
return lines;
|
|
86
|
+
}
|
|
87
|
+
//# sourceMappingURL=multi-reasoner.js.map
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export interface ArchiveResult {
|
|
2
|
+
sessionTitle: string;
|
|
3
|
+
filepath: string;
|
|
4
|
+
originalLines: number;
|
|
5
|
+
compressedBytes: number;
|
|
6
|
+
archivedAt: number;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Archive session output to a gzipped file on disk.
|
|
10
|
+
*/
|
|
11
|
+
export declare function archiveSessionOutput(sessionTitle: string, output: string[], now?: number): ArchiveResult;
|
|
12
|
+
/**
|
|
13
|
+
* List available archives.
|
|
14
|
+
*/
|
|
15
|
+
export declare function listArchives(): Array<{
|
|
16
|
+
filename: string;
|
|
17
|
+
sessionTitle: string;
|
|
18
|
+
}>;
|
|
19
|
+
/**
|
|
20
|
+
* Format archive list for TUI display.
|
|
21
|
+
*/
|
|
22
|
+
export declare function formatArchiveList(): string[];
|
|
23
|
+
//# sourceMappingURL=output-archival.d.ts.map
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// output-archival.ts — compress and archive old session outputs to disk.
|
|
2
|
+
// keeps the daemon's memory footprint manageable over long runs by
|
|
3
|
+
// offloading old output to gzipped files.
|
|
4
|
+
import { writeFileSync, mkdirSync, existsSync, readdirSync, unlinkSync } from "node:fs";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
import { homedir } from "node:os";
|
|
7
|
+
import { gzipSync } from "node:zlib";
|
|
8
|
+
const ARCHIVE_DIR = join(homedir(), ".aoaoe", "output-archive");
|
|
9
|
+
const MAX_ARCHIVES = 200;
|
|
10
|
+
/**
|
|
11
|
+
* Archive session output to a gzipped file on disk.
|
|
12
|
+
*/
|
|
13
|
+
export function archiveSessionOutput(sessionTitle, output, now = Date.now()) {
|
|
14
|
+
mkdirSync(ARCHIVE_DIR, { recursive: true });
|
|
15
|
+
const safe = sessionTitle.replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
16
|
+
const timestamp = new Date(now).toISOString().replace(/[:.]/g, "-").slice(0, 19);
|
|
17
|
+
const filename = `${safe}_${timestamp}.txt.gz`;
|
|
18
|
+
const filepath = join(ARCHIVE_DIR, filename);
|
|
19
|
+
const content = output.join("\n");
|
|
20
|
+
const compressed = gzipSync(Buffer.from(content, "utf-8"));
|
|
21
|
+
writeFileSync(filepath, compressed);
|
|
22
|
+
pruneOldArchives();
|
|
23
|
+
return {
|
|
24
|
+
sessionTitle,
|
|
25
|
+
filepath,
|
|
26
|
+
originalLines: output.length,
|
|
27
|
+
compressedBytes: compressed.length,
|
|
28
|
+
archivedAt: now,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* List available archives.
|
|
33
|
+
*/
|
|
34
|
+
export function listArchives() {
|
|
35
|
+
if (!existsSync(ARCHIVE_DIR))
|
|
36
|
+
return [];
|
|
37
|
+
return readdirSync(ARCHIVE_DIR)
|
|
38
|
+
.filter((f) => f.endsWith(".txt.gz"))
|
|
39
|
+
.sort()
|
|
40
|
+
.reverse()
|
|
41
|
+
.map((f) => ({
|
|
42
|
+
filename: f,
|
|
43
|
+
sessionTitle: f.split("_").slice(0, -2).join("_") || f,
|
|
44
|
+
}));
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Format archive list for TUI display.
|
|
48
|
+
*/
|
|
49
|
+
export function formatArchiveList() {
|
|
50
|
+
const archives = listArchives();
|
|
51
|
+
if (archives.length === 0)
|
|
52
|
+
return [" (no archived outputs)"];
|
|
53
|
+
const lines = [];
|
|
54
|
+
lines.push(` Output archives: ${archives.length} files`);
|
|
55
|
+
for (const a of archives.slice(0, 10)) {
|
|
56
|
+
lines.push(` ${a.filename}`);
|
|
57
|
+
}
|
|
58
|
+
if (archives.length > 10)
|
|
59
|
+
lines.push(` ... and ${archives.length - 10} more`);
|
|
60
|
+
return lines;
|
|
61
|
+
}
|
|
62
|
+
function pruneOldArchives() {
|
|
63
|
+
try {
|
|
64
|
+
const files = readdirSync(ARCHIVE_DIR).filter((f) => f.endsWith(".txt.gz")).sort();
|
|
65
|
+
while (files.length > MAX_ARCHIVES) {
|
|
66
|
+
const oldest = files.shift();
|
|
67
|
+
unlinkSync(join(ARCHIVE_DIR, oldest));
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
catch { /* best-effort */ }
|
|
71
|
+
}
|
|
72
|
+
//# sourceMappingURL=output-archival.js.map
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export interface RunbookStep {
|
|
2
|
+
action: string;
|
|
3
|
+
detail: string;
|
|
4
|
+
frequency: number;
|
|
5
|
+
}
|
|
6
|
+
export interface GeneratedRunbook {
|
|
7
|
+
title: string;
|
|
8
|
+
scenario: string;
|
|
9
|
+
steps: RunbookStep[];
|
|
10
|
+
basedOnEvents: number;
|
|
11
|
+
confidence: "low" | "medium" | "high";
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Analyze audit trail and generate runbooks for common patterns.
|
|
15
|
+
*/
|
|
16
|
+
export declare function generateRunbooks(): GeneratedRunbook[];
|
|
17
|
+
/**
|
|
18
|
+
* Format generated runbooks for TUI display.
|
|
19
|
+
*/
|
|
20
|
+
export declare function formatGeneratedRunbooks(runbooks: GeneratedRunbook[]): string[];
|
|
21
|
+
//# sourceMappingURL=runbook-generator.d.ts.map
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
// runbook-generator.ts — auto-generate operator runbooks from audit trail patterns.
|
|
2
|
+
// analyzes recurring event sequences and produces step-by-step playbooks
|
|
3
|
+
// for common scenarios (stuck sessions, budget overruns, error recovery).
|
|
4
|
+
import { readRecentAuditEntries } from "./audit-trail.js";
|
|
5
|
+
/**
|
|
6
|
+
* Analyze audit trail and generate runbooks for common patterns.
|
|
7
|
+
*/
|
|
8
|
+
export function generateRunbooks() {
|
|
9
|
+
const entries = readRecentAuditEntries(5_000);
|
|
10
|
+
if (entries.length < 10)
|
|
11
|
+
return [];
|
|
12
|
+
const runbooks = [];
|
|
13
|
+
// pattern 1: stuck session recovery
|
|
14
|
+
const stuckEntries = entries.filter((e) => e.type === "stuck_nudge" || e.type === "session_restart");
|
|
15
|
+
if (stuckEntries.length >= 3) {
|
|
16
|
+
const actions = countActions(stuckEntries);
|
|
17
|
+
runbooks.push({
|
|
18
|
+
title: "Stuck Session Recovery",
|
|
19
|
+
scenario: "Session has not made progress for >30 minutes",
|
|
20
|
+
steps: actions,
|
|
21
|
+
basedOnEvents: stuckEntries.length,
|
|
22
|
+
confidence: stuckEntries.length >= 10 ? "high" : stuckEntries.length >= 5 ? "medium" : "low",
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
// pattern 2: budget management
|
|
26
|
+
const budgetEntries = entries.filter((e) => e.type === "budget_pause");
|
|
27
|
+
if (budgetEntries.length >= 2) {
|
|
28
|
+
runbooks.push({
|
|
29
|
+
title: "Budget Overrun Response",
|
|
30
|
+
scenario: "Session cost exceeds configured budget",
|
|
31
|
+
steps: [
|
|
32
|
+
{ action: "Review cost attribution", detail: "Use /cost-report to identify top spenders", frequency: budgetEntries.length },
|
|
33
|
+
{ action: "Adjust budget or pause", detail: "Use /budget-predict to estimate remaining runway", frequency: budgetEntries.length },
|
|
34
|
+
{ action: "Check for runaway loops", detail: "Use /drift to verify session is on-task", frequency: Math.ceil(budgetEntries.length * 0.5) },
|
|
35
|
+
],
|
|
36
|
+
basedOnEvents: budgetEntries.length,
|
|
37
|
+
confidence: budgetEntries.length >= 5 ? "high" : "medium",
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
// pattern 3: error recovery
|
|
41
|
+
const errorEntries = entries.filter((e) => e.type === "session_error");
|
|
42
|
+
if (errorEntries.length >= 3) {
|
|
43
|
+
runbooks.push({
|
|
44
|
+
title: "Session Error Recovery",
|
|
45
|
+
scenario: "Session enters error state",
|
|
46
|
+
steps: [
|
|
47
|
+
{ action: "Check session output", detail: "Use /session-replay to review recent activity", frequency: errorEntries.length },
|
|
48
|
+
{ action: "Review recovery playbook", detail: "Use /recovery to see auto-recovery status", frequency: errorEntries.length },
|
|
49
|
+
{ action: "Restart if needed", detail: "Recovery playbook auto-restarts at health <40", frequency: Math.ceil(errorEntries.length * 0.3) },
|
|
50
|
+
],
|
|
51
|
+
basedOnEvents: errorEntries.length,
|
|
52
|
+
confidence: errorEntries.length >= 10 ? "high" : "medium",
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
// pattern 4: goal completion
|
|
56
|
+
const completionEntries = entries.filter((e) => e.type === "auto_complete" || e.type === "task_completed");
|
|
57
|
+
if (completionEntries.length >= 3) {
|
|
58
|
+
runbooks.push({
|
|
59
|
+
title: "Task Completion Workflow",
|
|
60
|
+
scenario: "Task auto-detected as complete",
|
|
61
|
+
steps: [
|
|
62
|
+
{ action: "Verify completion", detail: "Check /goal-progress and /velocity for confirmation", frequency: completionEntries.length },
|
|
63
|
+
{ action: "Review output", detail: "Use /session-replay to verify work quality", frequency: completionEntries.length },
|
|
64
|
+
{ action: "Advance dependencies", detail: "Use /schedule to activate dependent tasks", frequency: Math.ceil(completionEntries.length * 0.5) },
|
|
65
|
+
],
|
|
66
|
+
basedOnEvents: completionEntries.length,
|
|
67
|
+
confidence: completionEntries.length >= 10 ? "high" : "medium",
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
return runbooks;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Format generated runbooks for TUI display.
|
|
74
|
+
*/
|
|
75
|
+
export function formatGeneratedRunbooks(runbooks) {
|
|
76
|
+
if (runbooks.length === 0)
|
|
77
|
+
return [" (insufficient audit data to generate runbooks — need 10+ events)"];
|
|
78
|
+
const lines = [];
|
|
79
|
+
for (const rb of runbooks) {
|
|
80
|
+
const conf = rb.confidence === "high" ? "●" : rb.confidence === "medium" ? "◐" : "○";
|
|
81
|
+
lines.push(` ${conf} ${rb.title} (based on ${rb.basedOnEvents} events)`);
|
|
82
|
+
lines.push(` Scenario: ${rb.scenario}`);
|
|
83
|
+
for (let i = 0; i < rb.steps.length; i++) {
|
|
84
|
+
lines.push(` ${i + 1}. ${rb.steps[i].action} — ${rb.steps[i].detail}`);
|
|
85
|
+
}
|
|
86
|
+
lines.push("");
|
|
87
|
+
}
|
|
88
|
+
return lines;
|
|
89
|
+
}
|
|
90
|
+
function countActions(entries) {
|
|
91
|
+
const counts = new Map();
|
|
92
|
+
for (const e of entries) {
|
|
93
|
+
const key = e.type;
|
|
94
|
+
const existing = counts.get(key);
|
|
95
|
+
if (existing)
|
|
96
|
+
existing.count++;
|
|
97
|
+
else
|
|
98
|
+
counts.set(key, { detail: e.detail.slice(0, 80), count: 1 });
|
|
99
|
+
}
|
|
100
|
+
return [...counts.entries()].map(([action, { detail, count }]) => ({
|
|
101
|
+
action, detail, frequency: count,
|
|
102
|
+
})).sort((a, b) => b.frequency - a.frequency);
|
|
103
|
+
}
|
|
104
|
+
//# sourceMappingURL=runbook-generator.js.map
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export interface ServiceConfig {
|
|
2
|
+
name: string;
|
|
3
|
+
description: string;
|
|
4
|
+
execPath: string;
|
|
5
|
+
workingDir: string;
|
|
6
|
+
configPath?: string;
|
|
7
|
+
user?: string;
|
|
8
|
+
restartSec: number;
|
|
9
|
+
logPath?: string;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Generate a systemd unit file for Linux.
|
|
13
|
+
*/
|
|
14
|
+
export declare function generateSystemdUnit(config?: Partial<ServiceConfig>): string;
|
|
15
|
+
/**
|
|
16
|
+
* Generate a launchd plist file for macOS.
|
|
17
|
+
*/
|
|
18
|
+
export declare function generateLaunchdPlist(config?: Partial<ServiceConfig>): string;
|
|
19
|
+
/**
|
|
20
|
+
* Detect the current platform and generate the appropriate service file.
|
|
21
|
+
*/
|
|
22
|
+
export declare function generateServiceFile(config?: Partial<ServiceConfig>): {
|
|
23
|
+
content: string;
|
|
24
|
+
filename: string;
|
|
25
|
+
installPath: string;
|
|
26
|
+
platform: string;
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* Write the service file and return install instructions.
|
|
30
|
+
*/
|
|
31
|
+
export declare function installService(config?: Partial<ServiceConfig>): string[];
|
|
32
|
+
//# sourceMappingURL=service-generator.d.ts.map
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
// service-generator.ts — generate systemd/launchd service files for daemon
|
|
2
|
+
// auto-start on boot and crash restart. supports both Linux (systemd) and
|
|
3
|
+
// macOS (launchd) platforms.
|
|
4
|
+
import { writeFileSync, mkdirSync } from "node:fs";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
import { homedir, platform } from "node:os";
|
|
7
|
+
const DEFAULT_CONFIG = {
|
|
8
|
+
name: "aoaoe",
|
|
9
|
+
description: "aoaoe — autonomous supervisor daemon for agent-of-empires",
|
|
10
|
+
execPath: "aoaoe",
|
|
11
|
+
workingDir: process.cwd(),
|
|
12
|
+
restartSec: 5,
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* Generate a systemd unit file for Linux.
|
|
16
|
+
*/
|
|
17
|
+
export function generateSystemdUnit(config = {}) {
|
|
18
|
+
const c = { ...DEFAULT_CONFIG, ...config };
|
|
19
|
+
const execStart = c.configPath
|
|
20
|
+
? `${c.execPath} --config ${c.configPath}`
|
|
21
|
+
: c.execPath;
|
|
22
|
+
return `[Unit]
|
|
23
|
+
Description=${c.description}
|
|
24
|
+
After=network.target
|
|
25
|
+
|
|
26
|
+
[Service]
|
|
27
|
+
Type=simple
|
|
28
|
+
ExecStart=${execStart}
|
|
29
|
+
WorkingDirectory=${c.workingDir}
|
|
30
|
+
Restart=on-failure
|
|
31
|
+
RestartSec=${c.restartSec}
|
|
32
|
+
${c.user ? `User=${c.user}` : ""}
|
|
33
|
+
${c.logPath ? `StandardOutput=append:${c.logPath}\nStandardError=append:${c.logPath}` : "StandardOutput=journal\nStandardError=journal"}
|
|
34
|
+
Environment=NODE_ENV=production
|
|
35
|
+
|
|
36
|
+
[Install]
|
|
37
|
+
WantedBy=multi-user.target
|
|
38
|
+
`.replace(/\n{3,}/g, "\n\n").trim() + "\n";
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Generate a launchd plist file for macOS.
|
|
42
|
+
*/
|
|
43
|
+
export function generateLaunchdPlist(config = {}) {
|
|
44
|
+
const c = { ...DEFAULT_CONFIG, ...config };
|
|
45
|
+
const logPath = c.logPath ?? join(homedir(), "Library", "Logs", "aoaoe.log");
|
|
46
|
+
const errPath = c.logPath ? c.logPath.replace(".log", ".err.log") : join(homedir(), "Library", "Logs", "aoaoe.err.log");
|
|
47
|
+
const args = c.configPath
|
|
48
|
+
? [`${c.execPath}`, "--config", c.configPath]
|
|
49
|
+
: [c.execPath];
|
|
50
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
51
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
52
|
+
<plist version="1.0">
|
|
53
|
+
<dict>
|
|
54
|
+
<key>Label</key>
|
|
55
|
+
<string>com.aoaoe.daemon</string>
|
|
56
|
+
<key>ProgramArguments</key>
|
|
57
|
+
<array>
|
|
58
|
+
${args.map((a) => ` <string>${escXml(a)}</string>`).join("\n")}
|
|
59
|
+
</array>
|
|
60
|
+
<key>WorkingDirectory</key>
|
|
61
|
+
<string>${escXml(c.workingDir)}</string>
|
|
62
|
+
<key>RunAtLoad</key>
|
|
63
|
+
<true/>
|
|
64
|
+
<key>KeepAlive</key>
|
|
65
|
+
<dict>
|
|
66
|
+
<key>SuccessfulExit</key>
|
|
67
|
+
<false/>
|
|
68
|
+
</dict>
|
|
69
|
+
<key>ThrottleInterval</key>
|
|
70
|
+
<integer>${c.restartSec}</integer>
|
|
71
|
+
<key>StandardOutPath</key>
|
|
72
|
+
<string>${escXml(logPath)}</string>
|
|
73
|
+
<key>StandardErrorPath</key>
|
|
74
|
+
<string>${escXml(errPath)}</string>
|
|
75
|
+
<key>EnvironmentVariables</key>
|
|
76
|
+
<dict>
|
|
77
|
+
<key>NODE_ENV</key>
|
|
78
|
+
<string>production</string>
|
|
79
|
+
</dict>
|
|
80
|
+
</dict>
|
|
81
|
+
</plist>
|
|
82
|
+
`;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Detect the current platform and generate the appropriate service file.
|
|
86
|
+
*/
|
|
87
|
+
export function generateServiceFile(config = {}) {
|
|
88
|
+
const os = platform();
|
|
89
|
+
if (os === "darwin") {
|
|
90
|
+
const filename = "com.aoaoe.daemon.plist";
|
|
91
|
+
const installPath = join(homedir(), "Library", "LaunchAgents", filename);
|
|
92
|
+
return { content: generateLaunchdPlist(config), filename, installPath, platform: "launchd" };
|
|
93
|
+
}
|
|
94
|
+
// Linux + fallback
|
|
95
|
+
const filename = `${config.name ?? "aoaoe"}.service`;
|
|
96
|
+
const installPath = join("/etc", "systemd", "system", filename);
|
|
97
|
+
return { content: generateSystemdUnit(config), filename, installPath, platform: "systemd" };
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Write the service file and return install instructions.
|
|
101
|
+
*/
|
|
102
|
+
export function installService(config = {}) {
|
|
103
|
+
const { content, filename, installPath, platform: plat } = generateServiceFile(config);
|
|
104
|
+
const outDir = join(homedir(), ".aoaoe");
|
|
105
|
+
mkdirSync(outDir, { recursive: true });
|
|
106
|
+
const outPath = join(outDir, filename);
|
|
107
|
+
writeFileSync(outPath, content);
|
|
108
|
+
const lines = [];
|
|
109
|
+
lines.push(`Generated ${plat} service file: ${outPath}`);
|
|
110
|
+
lines.push("");
|
|
111
|
+
if (plat === "systemd") {
|
|
112
|
+
lines.push("Install with:");
|
|
113
|
+
lines.push(` sudo cp ${outPath} ${installPath}`);
|
|
114
|
+
lines.push(" sudo systemctl daemon-reload");
|
|
115
|
+
lines.push(` sudo systemctl enable ${filename}`);
|
|
116
|
+
lines.push(` sudo systemctl start ${filename.replace(".service", "")}`);
|
|
117
|
+
}
|
|
118
|
+
else {
|
|
119
|
+
lines.push("Install with:");
|
|
120
|
+
lines.push(` cp ${outPath} ${installPath}`);
|
|
121
|
+
lines.push(` launchctl load ${installPath}`);
|
|
122
|
+
lines.push("");
|
|
123
|
+
lines.push("Uninstall with:");
|
|
124
|
+
lines.push(` launchctl unload ${installPath}`);
|
|
125
|
+
lines.push(` rm ${installPath}`);
|
|
126
|
+
}
|
|
127
|
+
return lines;
|
|
128
|
+
}
|
|
129
|
+
function escXml(s) {
|
|
130
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
131
|
+
}
|
|
132
|
+
//# sourceMappingURL=service-generator.js.map
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
export interface DaemonCheckpoint {
|
|
2
|
+
version: number;
|
|
3
|
+
savedAt: number;
|
|
4
|
+
graduation: Record<string, {
|
|
5
|
+
mode: string;
|
|
6
|
+
successes: number;
|
|
7
|
+
failures: number;
|
|
8
|
+
rate: number;
|
|
9
|
+
}>;
|
|
10
|
+
escalation: Record<string, {
|
|
11
|
+
level: string;
|
|
12
|
+
notifyCount: number;
|
|
13
|
+
}>;
|
|
14
|
+
velocitySamples: Record<string, Array<{
|
|
15
|
+
timestamp: number;
|
|
16
|
+
percent: number;
|
|
17
|
+
}>>;
|
|
18
|
+
nudgeRecords: Array<{
|
|
19
|
+
session: string;
|
|
20
|
+
sentAt: number;
|
|
21
|
+
effective: boolean;
|
|
22
|
+
}>;
|
|
23
|
+
budgetSamples: Record<string, Array<{
|
|
24
|
+
timestamp: number;
|
|
25
|
+
costUsd: number;
|
|
26
|
+
}>>;
|
|
27
|
+
cacheStats: {
|
|
28
|
+
hits: number;
|
|
29
|
+
misses: number;
|
|
30
|
+
};
|
|
31
|
+
slaHistory: number[];
|
|
32
|
+
pollInterval: number;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Save daemon state to a checkpoint file.
|
|
36
|
+
*/
|
|
37
|
+
export declare function saveCheckpoint(checkpoint: DaemonCheckpoint): string;
|
|
38
|
+
/**
|
|
39
|
+
* Load the most recent checkpoint. Returns null if none exists.
|
|
40
|
+
*/
|
|
41
|
+
export declare function loadCheckpoint(): DaemonCheckpoint | null;
|
|
42
|
+
/**
|
|
43
|
+
* Build a checkpoint from current module states.
|
|
44
|
+
* This is a generic serialization — callers extract state from each module.
|
|
45
|
+
*/
|
|
46
|
+
export declare function buildCheckpoint(state: Omit<DaemonCheckpoint, "version" | "savedAt">): DaemonCheckpoint;
|
|
47
|
+
/**
|
|
48
|
+
* Format checkpoint info for TUI display.
|
|
49
|
+
*/
|
|
50
|
+
export declare function formatCheckpointInfo(): string[];
|
|
51
|
+
/**
|
|
52
|
+
* Check if a checkpoint exists and is recent enough to restore.
|
|
53
|
+
*/
|
|
54
|
+
export declare function shouldRestoreCheckpoint(maxAgeMs?: number): boolean;
|
|
55
|
+
//# sourceMappingURL=session-checkpoint.d.ts.map
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// session-checkpoint.ts — save + resume session state across daemon restarts.
|
|
2
|
+
// serializes all transient module state (graduation, escalation, velocity,
|
|
3
|
+
// nudge tracker, etc.) to disk so the daemon can pick up where it left off.
|
|
4
|
+
import { writeFileSync, readFileSync, existsSync, mkdirSync } from "node:fs";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
import { homedir } from "node:os";
|
|
7
|
+
const CHECKPOINT_DIR = join(homedir(), ".aoaoe", "checkpoints");
|
|
8
|
+
const CHECKPOINT_FILE = join(CHECKPOINT_DIR, "daemon-state.json");
|
|
9
|
+
/**
|
|
10
|
+
* Save daemon state to a checkpoint file.
|
|
11
|
+
*/
|
|
12
|
+
export function saveCheckpoint(checkpoint) {
|
|
13
|
+
mkdirSync(CHECKPOINT_DIR, { recursive: true });
|
|
14
|
+
checkpoint.savedAt = Date.now();
|
|
15
|
+
checkpoint.version = 1;
|
|
16
|
+
writeFileSync(CHECKPOINT_FILE, JSON.stringify(checkpoint, null, 2) + "\n");
|
|
17
|
+
return CHECKPOINT_FILE;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Load the most recent checkpoint. Returns null if none exists.
|
|
21
|
+
*/
|
|
22
|
+
export function loadCheckpoint() {
|
|
23
|
+
if (!existsSync(CHECKPOINT_FILE))
|
|
24
|
+
return null;
|
|
25
|
+
try {
|
|
26
|
+
const data = JSON.parse(readFileSync(CHECKPOINT_FILE, "utf-8"));
|
|
27
|
+
if (data.version !== 1)
|
|
28
|
+
return null; // incompatible version
|
|
29
|
+
return data;
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Build a checkpoint from current module states.
|
|
37
|
+
* This is a generic serialization — callers extract state from each module.
|
|
38
|
+
*/
|
|
39
|
+
export function buildCheckpoint(state) {
|
|
40
|
+
return { version: 1, savedAt: Date.now(), ...state };
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Format checkpoint info for TUI display.
|
|
44
|
+
*/
|
|
45
|
+
export function formatCheckpointInfo() {
|
|
46
|
+
const cp = loadCheckpoint();
|
|
47
|
+
if (!cp)
|
|
48
|
+
return [" (no checkpoint found)"];
|
|
49
|
+
const age = Math.round((Date.now() - cp.savedAt) / 60_000);
|
|
50
|
+
const lines = [];
|
|
51
|
+
lines.push(` Checkpoint: saved ${age}min ago`);
|
|
52
|
+
lines.push(` Graduation: ${Object.keys(cp.graduation).length} sessions`);
|
|
53
|
+
lines.push(` Escalation: ${Object.keys(cp.escalation).length} active`);
|
|
54
|
+
lines.push(` Velocity: ${Object.keys(cp.velocitySamples).length} tracked`);
|
|
55
|
+
lines.push(` Nudges: ${cp.nudgeRecords.length} records`);
|
|
56
|
+
lines.push(` Cache: ${cp.cacheStats.hits} hits / ${cp.cacheStats.misses} misses`);
|
|
57
|
+
lines.push(` SLA history: ${cp.slaHistory.length} ticks`);
|
|
58
|
+
return lines;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Check if a checkpoint exists and is recent enough to restore.
|
|
62
|
+
*/
|
|
63
|
+
export function shouldRestoreCheckpoint(maxAgeMs = 30 * 60_000) {
|
|
64
|
+
const cp = loadCheckpoint();
|
|
65
|
+
if (!cp)
|
|
66
|
+
return false;
|
|
67
|
+
return (Date.now() - cp.savedAt) <= maxAgeMs;
|
|
68
|
+
}
|
|
69
|
+
//# sourceMappingURL=session-checkpoint.js.map
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export interface ReplayEvent {
|
|
2
|
+
timestamp: number;
|
|
3
|
+
timeLabel: string;
|
|
4
|
+
type: string;
|
|
5
|
+
detail: string;
|
|
6
|
+
}
|
|
7
|
+
export interface SessionReplay {
|
|
8
|
+
sessionTitle: string;
|
|
9
|
+
events: ReplayEvent[];
|
|
10
|
+
totalDurationMs: number;
|
|
11
|
+
eventCount: number;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Build a replay timeline for a session from the audit trail.
|
|
15
|
+
*/
|
|
16
|
+
export declare function buildSessionReplay(sessionTitle: string, maxEvents?: number): SessionReplay;
|
|
17
|
+
/**
|
|
18
|
+
* Format replay as a chronological narrative.
|
|
19
|
+
*/
|
|
20
|
+
export declare function formatReplay(replay: SessionReplay): string[];
|
|
21
|
+
/**
|
|
22
|
+
* Generate a summary of the replay (for quick review).
|
|
23
|
+
*/
|
|
24
|
+
export declare function summarizeReplay(replay: SessionReplay): string[];
|
|
25
|
+
//# sourceMappingURL=session-replay.d.ts.map
|