aoaoe 0.60.0 → 0.62.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 CHANGED
@@ -342,6 +342,7 @@ Config lives at `~/.aoaoe/aoaoe.config.json` (canonical, written by `aoaoe init`
342
342
  | `notifications.slackWebhookUrl` | Slack incoming webhook URL (block kit format) | (none) |
343
343
  | `notifications.events` | Filter which events fire (omit to send all). Valid: `session_error`, `session_done`, `action_executed`, `action_failed`, `daemon_started`, `daemon_stopped` | (all) |
344
344
  | `notifications.maxRetries` | Retry failed webhook deliveries with exponential backoff (1s, 2s, 4s, ...) | `0` (no retry) |
345
+ | `tuiHistoryRetentionDays` | How many days of TUI history to replay on startup (1-365) | `7` |
345
346
 
346
347
  Also reads `.aoaoe.json` as an alternative config filename.
347
348
 
@@ -497,6 +498,7 @@ src/
497
498
  dashboard.ts # periodic CLI status table with task column
498
499
  daemon-state.ts # shared IPC state file + interrupt flag
499
500
  tui.ts # in-place terminal UI (alternate screen, scroll regions)
501
+ tui-history.ts # persisted TUI history (JSONL file with rotation, replay on startup)
500
502
  input.ts # stdin readline listener with inject() for post-interrupt
501
503
  init.ts # `aoaoe init`: auto-discover tools, sessions, generate config
502
504
  notify.ts # webhook + Slack notification dispatcher for daemon events
package/dist/config.js CHANGED
@@ -83,7 +83,7 @@ export function loadConfig(overrides) {
83
83
  const KNOWN_KEYS = {
84
84
  reasoner: true, pollIntervalMs: true, captureLinesCount: true,
85
85
  verbose: true, dryRun: true, observe: true, confirm: true,
86
- contextFiles: true, sessionDirs: true, protectedSessions: true, healthPort: true,
86
+ contextFiles: true, sessionDirs: true, protectedSessions: true, healthPort: true, tuiHistoryRetentionDays: true,
87
87
  opencode: new Set(["port", "model"]),
88
88
  claudeCode: new Set(["model", "yolo", "resume"]),
89
89
  aoe: new Set(["profile"]),
@@ -133,6 +133,13 @@ export function validateConfig(config) {
133
133
  errors.push(`healthPort must be 1-65535, got ${config.healthPort}`);
134
134
  }
135
135
  }
136
+ // tuiHistoryRetentionDays: must be a positive integer, 1-365
137
+ if (config.tuiHistoryRetentionDays !== undefined) {
138
+ const d = config.tuiHistoryRetentionDays;
139
+ if (typeof d !== "number" || !isFinite(d) || !Number.isInteger(d) || d < 1 || d > 365) {
140
+ errors.push(`tuiHistoryRetentionDays must be an integer 1-365, got ${d}`);
141
+ }
142
+ }
136
143
  if (typeof config.policies?.maxErrorsBeforeRestart !== "number" || config.policies.maxErrorsBeforeRestart < 1) {
137
144
  errors.push(`policies.maxErrorsBeforeRestart must be >= 1, got ${config.policies?.maxErrorsBeforeRestart}`);
138
145
  }
@@ -540,6 +547,7 @@ example config:
540
547
  "other-repo": "/path/to/other-repo"
541
548
  },
542
549
  "healthPort": 4098,
550
+ "tuiHistoryRetentionDays": 7,
543
551
  "notifications": {
544
552
  "webhookUrl": "https://example.com/webhook",
545
553
  "slackWebhookUrl": "https://hooks.slack.com/services/T.../B.../xxx",
@@ -552,6 +560,9 @@ example config:
552
560
  aoaoe loads AGENTS.md, claude.md, and other AI instruction files
553
561
  from each project directory to give the reasoner per-session context.
554
562
 
563
+ tuiHistoryRetentionDays controls how many days of TUI history to replay
564
+ on daemon startup (default: 7, range: 1-365). History file rotates at 50MB.
565
+
555
566
  notifications sends webhook alerts for daemon events. Both webhookUrl
556
567
  and slackWebhookUrl are optional. events filters which events fire
557
568
  (omit to send all). maxRetries enables exponential backoff retry on
package/dist/index.js CHANGED
@@ -19,6 +19,7 @@ import { TUI } from "./tui.js";
19
19
  import { isDaemonRunningFromState } from "./chat.js";
20
20
  import { sendNotification, sendTestNotification } from "./notify.js";
21
21
  import { startHealthServer } from "./health.js";
22
+ import { loadTuiHistory } from "./tui-history.js";
22
23
  import { actionSession, actionDetail, toActionLogEntry } from "./types.js";
23
24
  import { YELLOW, GREEN, DIM, BOLD, RED, RESET } from "./colors.js";
24
25
  import { readFileSync, existsSync, statSync, mkdirSync, writeFileSync, chmodSync } from "node:fs";
@@ -230,6 +231,12 @@ async function main() {
230
231
  await reasonerConsole.start();
231
232
  // start TUI (alternate screen buffer) after input is ready
232
233
  if (tui) {
234
+ // replay persisted history from previous runs before entering alt screen
235
+ const retentionDays = config.tuiHistoryRetentionDays ?? 7;
236
+ const retentionMs = retentionDays * 24 * 60 * 60 * 1000;
237
+ const history = loadTuiHistory(200, undefined, retentionMs);
238
+ if (history.length > 0)
239
+ tui.replayHistory(history);
233
240
  tui.start(pkg || "dev");
234
241
  tui.updateState({ reasonerName: config.observe ? "observe-only" : config.reasoner });
235
242
  // welcome banner — plain-English explanation of what's happening
@@ -0,0 +1,28 @@
1
+ /** JSONL entry format — extends ActivityEntry with epoch timestamp for filtering */
2
+ export interface HistoryEntry {
3
+ ts: number;
4
+ time: string;
5
+ tag: string;
6
+ text: string;
7
+ }
8
+ /**
9
+ * Append a single history entry to the JSONL file.
10
+ * Fire-and-forget — errors are silently swallowed so they never block the TUI.
11
+ * Rotates the file if it exceeds MAX_FILE_SIZE before appending.
12
+ */
13
+ export declare function appendHistoryEntry(entry: HistoryEntry, filePath?: string, maxSize?: number): void;
14
+ /**
15
+ * Load recent TUI history entries from the JSONL file.
16
+ * Returns the last `maxEntries` entries (default 200), newest last.
17
+ * Filters out entries older than `maxAgeMs` (default: 7 days).
18
+ * Returns empty array if the file doesn't exist or is unreadable.
19
+ */
20
+ export declare function loadTuiHistory(maxEntries?: number, filePath?: string, maxAgeMs?: number): HistoryEntry[];
21
+ /**
22
+ * Rotate the history file if it exceeds the size threshold.
23
+ * Renames current file to .old (overwriting any previous .old) and starts fresh.
24
+ */
25
+ export declare function rotateTuiHistory(filePath?: string, maxSize?: number): boolean;
26
+ /** Default history file path (for wiring in index.ts) */
27
+ export declare const TUI_HISTORY_FILE: string;
28
+ //# sourceMappingURL=tui-history.d.ts.map
@@ -0,0 +1,91 @@
1
+ // tui-history.ts — persisted TUI activity history
2
+ // JSONL file at ~/.aoaoe/tui-history.jsonl with rotation at 50MB.
3
+ // pure exported functions for testability — no classes, no singletons.
4
+ import { appendFileSync, readFileSync, renameSync, statSync, mkdirSync, existsSync } from "node:fs";
5
+ import { join } from "node:path";
6
+ import { homedir } from "node:os";
7
+ const AOAOE_DIR = join(homedir(), ".aoaoe");
8
+ const HISTORY_FILE = join(AOAOE_DIR, "tui-history.jsonl");
9
+ const HISTORY_OLD = join(AOAOE_DIR, "tui-history.jsonl.old");
10
+ const MAX_FILE_SIZE = 50 * 1024 * 1024; // 50MB rotation threshold
11
+ /**
12
+ * Append a single history entry to the JSONL file.
13
+ * Fire-and-forget — errors are silently swallowed so they never block the TUI.
14
+ * Rotates the file if it exceeds MAX_FILE_SIZE before appending.
15
+ */
16
+ export function appendHistoryEntry(entry, filePath = HISTORY_FILE, maxSize = MAX_FILE_SIZE) {
17
+ try {
18
+ const dir = join(filePath, "..");
19
+ if (!existsSync(dir))
20
+ mkdirSync(dir, { recursive: true });
21
+ rotateTuiHistory(filePath, maxSize);
22
+ const line = JSON.stringify(entry) + "\n";
23
+ appendFileSync(filePath, line, "utf-8");
24
+ }
25
+ catch {
26
+ // fire-and-forget — never crash the daemon over history persistence
27
+ }
28
+ }
29
+ /**
30
+ * Load recent TUI history entries from the JSONL file.
31
+ * Returns the last `maxEntries` entries (default 200), newest last.
32
+ * Filters out entries older than `maxAgeMs` (default: 7 days).
33
+ * Returns empty array if the file doesn't exist or is unreadable.
34
+ */
35
+ export function loadTuiHistory(maxEntries = 200, filePath = HISTORY_FILE, maxAgeMs = 7 * 24 * 60 * 60 * 1000) {
36
+ try {
37
+ if (!existsSync(filePath))
38
+ return [];
39
+ const content = readFileSync(filePath, "utf-8");
40
+ const lines = content.split("\n").filter((l) => l.trim());
41
+ const cutoff = Date.now() - maxAgeMs;
42
+ const recent = lines.slice(-maxEntries * 2); // read extra to compensate for age filtering
43
+ const entries = [];
44
+ for (const line of recent) {
45
+ try {
46
+ const parsed = JSON.parse(line);
47
+ if (isValidEntry(parsed) && parsed.ts >= cutoff)
48
+ entries.push(parsed);
49
+ }
50
+ catch {
51
+ // skip malformed lines
52
+ }
53
+ }
54
+ return entries.slice(-maxEntries);
55
+ }
56
+ catch {
57
+ return [];
58
+ }
59
+ }
60
+ /**
61
+ * Rotate the history file if it exceeds the size threshold.
62
+ * Renames current file to .old (overwriting any previous .old) and starts fresh.
63
+ */
64
+ export function rotateTuiHistory(filePath = HISTORY_FILE, maxSize = MAX_FILE_SIZE) {
65
+ try {
66
+ if (!existsSync(filePath))
67
+ return false;
68
+ const size = statSync(filePath).size;
69
+ if (size < maxSize)
70
+ return false;
71
+ const oldPath = filePath + ".old";
72
+ renameSync(filePath, oldPath);
73
+ return true;
74
+ }
75
+ catch {
76
+ return false;
77
+ }
78
+ }
79
+ /** Validate that a parsed JSON value has the shape of a HistoryEntry */
80
+ function isValidEntry(val) {
81
+ if (typeof val !== "object" || val === null)
82
+ return false;
83
+ const obj = val;
84
+ return (typeof obj.ts === "number" &&
85
+ typeof obj.time === "string" &&
86
+ typeof obj.tag === "string" &&
87
+ typeof obj.text === "string");
88
+ }
89
+ /** Default history file path (for wiring in index.ts) */
90
+ export const TUI_HISTORY_FILE = HISTORY_FILE;
91
+ //# sourceMappingURL=tui-history.js.map
package/dist/tui.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import type { DaemonSessionState, DaemonPhase } from "./types.js";
2
+ import type { HistoryEntry } from "./tui-history.js";
2
3
  declare function phaseDisplay(phase: DaemonPhase, paused: boolean, spinnerFrame: number): string;
3
4
  export interface ActivityEntry {
4
5
  time: string;
@@ -38,6 +39,7 @@ export declare class TUI {
38
39
  nextTickAt?: number;
39
40
  }): void;
40
41
  log(tag: string, text: string): void;
42
+ replayHistory(entries: HistoryEntry[]): void;
41
43
  private updateDimensions;
42
44
  private computeLayout;
43
45
  private onResize;
package/dist/tui.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { BOLD, DIM, RESET, GREEN, CYAN, WHITE, BG_DARK, INDIGO, TEAL, AMBER, SLATE, ROSE, LIME, SKY, BOX, SPINNER, DOT, } from "./colors.js";
2
+ import { appendHistoryEntry } from "./tui-history.js";
2
3
  // ── ANSI helpers ────────────────────────────────────────────────────────────
3
4
  const ESC = "\x1b";
4
5
  const CSI = `${ESC}[`;
@@ -140,6 +141,19 @@ export class TUI {
140
141
  }
141
142
  if (this.active)
142
143
  this.writeActivityLine(entry);
144
+ // persist to disk (fire-and-forget, never blocks)
145
+ appendHistoryEntry({ ts: now.getTime(), time, tag, text });
146
+ }
147
+ // populate activity buffer from persisted history before start()
148
+ // entries are loaded from the JSONL file and added to the in-memory buffer
149
+ replayHistory(entries) {
150
+ for (const e of entries) {
151
+ this.activityBuffer.push({ time: e.time, tag: e.tag, text: e.text });
152
+ }
153
+ // trim to max
154
+ if (this.activityBuffer.length > this.maxActivity) {
155
+ this.activityBuffer = this.activityBuffer.slice(-this.maxActivity);
156
+ }
143
157
  }
144
158
  // ── Layout computation ──────────────────────────────────────────────────
145
159
  updateDimensions() {
package/dist/types.d.ts CHANGED
@@ -122,6 +122,7 @@ export interface AoaoeConfig {
122
122
  maxRetries?: number;
123
123
  };
124
124
  healthPort?: number;
125
+ tuiHistoryRetentionDays?: number;
125
126
  }
126
127
  export type NotificationEvent = "session_error" | "session_done" | "action_executed" | "action_failed" | "daemon_started" | "daemon_stopped";
127
128
  export type DaemonPhase = "sleeping" | "polling" | "reasoning" | "executing" | "interrupted";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aoaoe",
3
- "version": "0.60.0",
3
+ "version": "0.62.0",
4
4
  "description": "Autonomous supervisor for agent-of-empires sessions using OpenCode or Claude Code",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",