aoaoe 0.156.0 → 0.160.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/dist/index.js CHANGED
@@ -16,7 +16,7 @@ import { wakeableSleep } from "./wake.js";
16
16
  import { classifyMessages, formatUserMessages, buildReceipts, shouldSkipSleep, hasPendingFile, isInsistMessage, stripInsistPrefix } from "./message.js";
17
17
  import { TaskManager, loadTaskDefinitions, loadTaskState, formatTaskTable, importAoeSessionsToTasks } from "./task-manager.js";
18
18
  import { runTaskCli, handleTaskSlashCommand, quickTaskUpdate } from "./task-cli.js";
19
- import { TUI, hitTestSession, nextSortMode, SORT_MODES, formatUptime, formatClipText, loadTuiPrefs, saveTuiPrefs, validateGroupName, CONTEXT_BURN_THRESHOLD, buildSnapshotData, formatSnapshotJson, formatSnapshotMarkdown, formatBroadcastSummary, rankSessions, TOP_SORT_MODES, formatIdleSince, CONTEXT_CEILING_THRESHOLD, buildSessionStats, formatSessionStatsLines, formatStatsJson, validateSessionTag, validateColorName, computeErrorTrend, parseQuietHoursRange, computeCostSummary, formatSessionReport, formatQuietStatus, formatSessionAge, formatHealthTrendChart, isOverBudget, DRAIN_ICON } from "./tui.js";
19
+ import { TUI, hitTestSession, nextSortMode, SORT_MODES, formatUptime, formatClipText, loadTuiPrefs, saveTuiPrefs, validateGroupName, CONTEXT_BURN_THRESHOLD, buildSnapshotData, formatSnapshotJson, formatSnapshotMarkdown, formatBroadcastSummary, rankSessions, TOP_SORT_MODES, formatIdleSince, CONTEXT_CEILING_THRESHOLD, buildSessionStats, formatSessionStatsLines, formatStatsJson, validateSessionTag, validateColorName, computeErrorTrend, parseQuietHoursRange, computeCostSummary, formatSessionReport, formatQuietStatus, formatSessionAge, formatHealthTrendChart, isOverBudget, DRAIN_ICON, formatSessionsTable } from "./tui.js";
20
20
  import { isDaemonRunningFromState } from "./chat.js";
21
21
  import { sendNotification, sendTestNotification } from "./notify.js";
22
22
  import { startHealthServer } from "./health.js";
@@ -1023,6 +1023,57 @@ async function main() {
1023
1023
  tui.log("system", `quiet hours: ${specs.join(", ")} — watchdog+burn alerts suppressed`);
1024
1024
  persistPrefs();
1025
1025
  });
1026
+ // wire /note-history
1027
+ input.onNoteHistory((target) => {
1028
+ const num = /^\d+$/.test(target) ? parseInt(target, 10) : undefined;
1029
+ const sessions = tui.getSessions();
1030
+ const session = num !== undefined
1031
+ ? sessions[num - 1]
1032
+ : sessions.find((s) => s.title.toLowerCase() === target.toLowerCase() || s.id.startsWith(target));
1033
+ if (!session) {
1034
+ tui.log("system", `session not found: ${target}`);
1035
+ return;
1036
+ }
1037
+ const hist = tui.getNoteHistory(session.id);
1038
+ if (hist.length === 0) {
1039
+ tui.log("system", `note-history: no previous notes for ${session.title}`);
1040
+ }
1041
+ else {
1042
+ tui.log("system", `note-history for ${session.title} (${hist.length}):`);
1043
+ for (const n of hist)
1044
+ tui.log("system", ` "${n}"`);
1045
+ }
1046
+ });
1047
+ // wire /label
1048
+ input.onLabel((target, label) => {
1049
+ const num = /^\d+$/.test(target) ? parseInt(target, 10) : undefined;
1050
+ const ok = tui.setLabel(num ?? target, label || null);
1051
+ if (ok) {
1052
+ tui.log("system", label ? `label set for ${target}: "${label}"` : `label cleared for ${target}`);
1053
+ }
1054
+ else {
1055
+ tui.log("system", `session not found: ${target}`);
1056
+ }
1057
+ });
1058
+ // wire /sessions enhanced table
1059
+ input.onSessionsTable(() => {
1060
+ const sessions = tui.getSessions();
1061
+ const now = Date.now();
1062
+ const lines = formatSessionsTable(sessions, {
1063
+ groups: tui.getAllGroups(),
1064
+ tags: tui.getAllSessionTags(),
1065
+ colors: tui.getAllSessionColors(),
1066
+ notes: tui.getAllNotes(),
1067
+ labels: tui.getAllLabels(),
1068
+ aliases: tui.getAllSessionAliases(),
1069
+ drainingIds: tui.getDrainingIds(),
1070
+ healthScores: tui.getAllHealthScores(now),
1071
+ costs: tui.getAllSessionCosts(),
1072
+ firstSeen: tui.getAllFirstSeen(),
1073
+ }, now);
1074
+ for (const l of lines)
1075
+ tui.log("system", l);
1076
+ });
1026
1077
  // wire /flap-log
1027
1078
  input.onFlapLog(() => {
1028
1079
  const log = tui.getFlapLog();
@@ -2094,6 +2145,7 @@ async function daemonTick(config, poller, reasoner, executor, reasonerConsole, p
2094
2145
  try {
2095
2146
  tickResult = await loopTick({
2096
2147
  config, poller, reasoner: wrappedReasoner, executor, policyStates, pollCount, userMessage, taskContext, beforeExecute,
2148
+ drainingSessionIds: tui ? [...tui.getDrainingIds()] : undefined,
2097
2149
  });
2098
2150
  }
2099
2151
  catch (err) {
package/dist/input.d.ts CHANGED
@@ -65,6 +65,9 @@ export type BudgetStatusHandler = () => void;
65
65
  export type FlapLogHandler = () => void;
66
66
  export type DrainHandler = (target: string, drain: boolean) => void;
67
67
  export type ExportAllHandler = () => void;
68
+ export type NoteHistoryHandler = (target: string) => void;
69
+ export type LabelHandler = (target: string, label: string) => void;
70
+ export type SessionsTableHandler = () => void;
68
71
  export interface MouseEvent {
69
72
  button: number;
70
73
  col: number;
@@ -152,6 +155,9 @@ export declare class InputReader {
152
155
  private flapLogHandler;
153
156
  private drainHandler;
154
157
  private exportAllHandler;
158
+ private noteHistoryHandler;
159
+ private labelHandler;
160
+ private sessionsTableHandler;
155
161
  private aliases;
156
162
  private mouseDataListener;
157
163
  onScroll(handler: (dir: ScrollDirection) => void): void;
@@ -222,6 +228,9 @@ export declare class InputReader {
222
228
  onFlapLog(handler: FlapLogHandler): void;
223
229
  onDrain(handler: DrainHandler): void;
224
230
  onExportAll(handler: ExportAllHandler): void;
231
+ onNoteHistory(handler: NoteHistoryHandler): void;
232
+ onLabel(handler: LabelHandler): void;
233
+ onSessionsTable(handler: SessionsTableHandler): void;
225
234
  /** Set aliases from persisted prefs. */
226
235
  setAliases(aliases: Record<string, string>): void;
227
236
  /** Get current aliases as a plain object. */
package/dist/input.js CHANGED
@@ -98,6 +98,9 @@ export class InputReader {
98
98
  flapLogHandler = null;
99
99
  drainHandler = null;
100
100
  exportAllHandler = null;
101
+ noteHistoryHandler = null;
102
+ labelHandler = null;
103
+ sessionsTableHandler = null;
101
104
  aliases = new Map(); // /shortcut → /full command
102
105
  mouseDataListener = null;
103
106
  // register a callback for scroll key events (PgUp/PgDn/Home/End)
@@ -321,6 +324,9 @@ export class InputReader {
321
324
  onFlapLog(handler) { this.flapLogHandler = handler; }
322
325
  onDrain(handler) { this.drainHandler = handler; }
323
326
  onExportAll(handler) { this.exportAllHandler = handler; }
327
+ onNoteHistory(handler) { this.noteHistoryHandler = handler; }
328
+ onLabel(handler) { this.labelHandler = handler; }
329
+ onSessionsTable(handler) { this.sessionsTableHandler = handler; }
324
330
  /** Set aliases from persisted prefs. */
325
331
  setAliases(aliases) {
326
332
  this.aliases.clear();
@@ -595,6 +601,9 @@ ${BOLD}navigation:${RESET}
595
601
  /drain N mark session N as draining (supervisor will skip it)
596
602
  /undrain N remove drain mark from session N
597
603
  /export-all bulk export snapshot + stats JSON for all sessions
604
+ /note-history N show previous notes for a session (before they were cleared)
605
+ /label N [text] set a freeform label shown in the session card (no text = clear)
606
+ /sessions show rich session table (status, health, group, cost, flags)
598
607
  /history-stats show aggregate statistics from persisted activity history
599
608
  /cost-summary show total estimated spend across all sessions
600
609
  /session-report N generate full markdown report for a session → ~/.aoaoe/report-<name>-<ts>.md
@@ -1252,6 +1261,43 @@ ${BOLD}other:${RESET}
1252
1261
  console.error(`${DIM}undrain not available${RESET}`);
1253
1262
  break;
1254
1263
  }
1264
+ case "/note-history": {
1265
+ const nhArg = line.slice("/note-history".length).trim();
1266
+ if (!nhArg) {
1267
+ console.error(`${DIM}usage: /note-history <N|name>${RESET}`);
1268
+ break;
1269
+ }
1270
+ if (this.noteHistoryHandler)
1271
+ this.noteHistoryHandler(nhArg);
1272
+ else
1273
+ console.error(`${DIM}note-history not available${RESET}`);
1274
+ break;
1275
+ }
1276
+ case "/label": {
1277
+ const lblArgs = line.slice("/label".length).trim();
1278
+ if (!lblArgs) {
1279
+ console.error(`${DIM}usage: /label <N|name> [text]${RESET}`);
1280
+ break;
1281
+ }
1282
+ if (this.labelHandler) {
1283
+ const spaceIdx = lblArgs.indexOf(" ");
1284
+ if (spaceIdx > 0) {
1285
+ this.labelHandler(lblArgs.slice(0, spaceIdx), lblArgs.slice(spaceIdx + 1).trim());
1286
+ }
1287
+ else {
1288
+ this.labelHandler(lblArgs, ""); // clear
1289
+ }
1290
+ }
1291
+ else
1292
+ console.error(`${DIM}label not available${RESET}`);
1293
+ break;
1294
+ }
1295
+ case "/sessions":
1296
+ if (this.sessionsTableHandler)
1297
+ this.sessionsTableHandler();
1298
+ else
1299
+ console.error(`${DIM}sessions not available${RESET}`);
1300
+ break;
1255
1301
  case "/export-all":
1256
1302
  if (this.exportAllHandler)
1257
1303
  this.exportAllHandler();
package/dist/loop.d.ts CHANGED
@@ -32,5 +32,6 @@ export declare function tick(opts: {
32
32
  userMessage?: string;
33
33
  taskContext?: TaskState[];
34
34
  beforeExecute?: (action: Action) => Promise<boolean>;
35
+ drainingSessionIds?: string[];
35
36
  }): Promise<TickResult>;
36
37
  //# sourceMappingURL=loop.d.ts.map
package/dist/loop.js CHANGED
@@ -7,7 +7,7 @@ import { detectPermissionPrompt } from "./reasoner/prompt.js";
7
7
  * Returns a structured result for testing and inspection.
8
8
  */
9
9
  export async function tick(opts) {
10
- const { config, poller, reasoner, executor, policyStates, pollCount, userMessage, taskContext, beforeExecute } = opts;
10
+ const { config, poller, reasoner, executor, policyStates, pollCount, userMessage, taskContext, beforeExecute, drainingSessionIds } = opts;
11
11
  // 1. poll
12
12
  const observation = await poller.poll();
13
13
  const now = Date.now();
@@ -65,6 +65,10 @@ export async function tick(opts) {
65
65
  if (config.protectedSessions && config.protectedSessions.length > 0) {
66
66
  observation.protectedSessions = config.protectedSessions;
67
67
  }
68
+ // attach draining session IDs for the prompt formatter
69
+ if (drainingSessionIds && drainingSessionIds.length > 0) {
70
+ observation.drainingSessionIds = drainingSessionIds;
71
+ }
68
72
  // 2. reason
69
73
  const result = await reasoner.decide(observation);
70
74
  // 3. execute (skip wait-only results)
@@ -126,6 +126,7 @@ export function formatObservation(obs) {
126
126
  parts.push("");
127
127
  // resolve protected sessions list from config (attached by loop.ts)
128
128
  const protectedList = obs.protectedSessions ?? [];
129
+ const drainingList = new Set(obs.drainingSessionIds ?? []);
129
130
  // session summary table
130
131
  parts.push("Sessions:");
131
132
  const activeSessions = [];
@@ -133,7 +134,8 @@ export function formatObservation(obs) {
133
134
  const s = snap.session;
134
135
  const activeTag = snap.userActive ? " [USER ACTIVE]" : "";
135
136
  const protectedTag = protectedList.some((p) => p.toLowerCase() === s.title.toLowerCase()) ? " [PROTECTED]" : "";
136
- parts.push(` [${s.id.slice(0, 8)}] "${s.title}" tool=${s.tool} status=${s.status} path=${s.path}${activeTag}${protectedTag}`);
137
+ const drainingTag = drainingList.has(s.id) ? " [DRAINING — skip, do not send input]" : "";
138
+ parts.push(` [${s.id.slice(0, 8)}] "${s.title}" tool=${s.tool} status=${s.status} path=${s.path}${activeTag}${protectedTag}${drainingTag}`);
137
139
  if (snap.userActive)
138
140
  activeSessions.push(s.title);
139
141
  }
@@ -142,6 +144,15 @@ export function formatObservation(obs) {
142
144
  parts.push(`WARNING: A human user is currently interacting with: ${activeSessions.join(", ")}.`);
143
145
  parts.push("Do NOT send input to these sessions. The user is actively working and your input would interfere.");
144
146
  }
147
+ if (drainingList.size > 0) {
148
+ const drainingTitles = obs.sessions
149
+ .filter((s) => drainingList.has(s.session.id))
150
+ .map((s) => `"${s.session.title}"`);
151
+ if (drainingTitles.length > 0) {
152
+ parts.push("");
153
+ parts.push(`DRAINING: ${drainingTitles.join(", ")} — do NOT assign new tasks or send_input to these sessions.`);
154
+ }
155
+ }
145
156
  parts.push("");
146
157
  // task context (goals, progress) — injected if tasks are defined
147
158
  if (obs.taskContext && obs.taskContext.length > 0) {
package/dist/tui.d.ts CHANGED
@@ -160,6 +160,28 @@ export declare const FLAP_THRESHOLD = 5;
160
160
  * Returns true when there are >= FLAP_THRESHOLD status changes in FLAP_WINDOW_MS.
161
161
  */
162
162
  export declare function isFlapping(changes: readonly StatusChange[], now?: number, windowMs?: number, threshold?: number): boolean;
163
+ /**
164
+ * Format sessions as a rich table for /sessions command output.
165
+ * Returns array of lines (one per session + header).
166
+ */
167
+ export declare function formatSessionsTable(sessions: readonly DaemonSessionState[], opts: {
168
+ groups: ReadonlyMap<string, string>;
169
+ tags: ReadonlyMap<string, ReadonlySet<string>>;
170
+ colors: ReadonlyMap<string, string>;
171
+ notes: ReadonlyMap<string, string>;
172
+ labels: ReadonlyMap<string, string>;
173
+ aliases: ReadonlyMap<string, string>;
174
+ drainingIds: ReadonlySet<string>;
175
+ healthScores: ReadonlyMap<string, number>;
176
+ costs: ReadonlyMap<string, string>;
177
+ firstSeen: ReadonlyMap<string, number>;
178
+ }, now?: number): string[];
179
+ /** Max notes stored per session (in history before clear). */
180
+ export declare const MAX_NOTE_HISTORY = 5;
181
+ /** Max length of a session label (displayed below title in cards). */
182
+ export declare const MAX_LABEL_LEN = 40;
183
+ /** Truncate a label to the max length. */
184
+ export declare function truncateLabel(label: string): string;
163
185
  /** Drain icon shown in session cards for draining sessions. */
164
186
  export declare const DRAIN_ICON = "\u21E3";
165
187
  /**
@@ -442,6 +464,8 @@ export declare class TUI {
442
464
  private quietHoursRanges;
443
465
  private sessionHealthHistory;
444
466
  private alertLog;
467
+ private sessionNoteHistory;
468
+ private sessionLabels;
445
469
  private sessionBudgets;
446
470
  private globalBudget;
447
471
  private budgetAlerted;
@@ -531,6 +555,18 @@ export declare class TUI {
531
555
  getNoteCount(): number;
532
556
  /** Return all session notes (for /notes listing). */
533
557
  getAllNotes(): ReadonlyMap<string, string>;
558
+ /** Return note history for a session (oldest first). */
559
+ getNoteHistory(id: string): readonly string[];
560
+ /**
561
+ * Set or clear a label for a session (by 1-indexed number, ID, or title).
562
+ * Label is displayed below the session title in normal cards.
563
+ * Returns true if session found.
564
+ */
565
+ setLabel(sessionIdOrIndex: string | number, label: string | null): boolean;
566
+ /** Get the label for a session ID (or undefined). */
567
+ getLabel(id: string): string | undefined;
568
+ /** Return all session labels. */
569
+ getAllLabels(): ReadonlyMap<string, string>;
534
570
  /** Return the current sessions (read-only, for resolving IDs to titles in the UI). */
535
571
  getSessions(): readonly DaemonSessionState[];
536
572
  /** Return the uptime in ms for a session (0 if not tracked). */
@@ -771,7 +807,7 @@ export declare class TUI {
771
807
  private repaintDrilldownContent;
772
808
  private paintInputLine;
773
809
  }
774
- declare function formatSessionCard(s: DaemonSessionState, maxWidth: number, errorSparkline?: string, idleSinceMs?: number, healthBadge?: string, displayName?: string, ageStr?: string): string;
810
+ declare function formatSessionCard(s: DaemonSessionState, maxWidth: number, errorSparkline?: string, idleSinceMs?: number, healthBadge?: string, displayName?: string, ageStr?: string, label?: string): string;
775
811
  declare function formatActivity(entry: ActivityEntry, maxCols: number): string;
776
812
  declare function padBoxLine(line: string, totalWidth: number): string;
777
813
  declare function padBoxLineHover(line: string, totalWidth: number, hovered: boolean): string;
package/dist/tui.js CHANGED
@@ -481,6 +481,52 @@ export function isFlapping(changes, now, windowMs = FLAP_WINDOW_MS, threshold =
481
481
  const recent = changes.filter((c) => c.ts >= cutoff);
482
482
  return recent.length >= threshold;
483
483
  }
484
+ // ── Sessions table (pure, exported for testing) ────────────────────────────────
485
+ /**
486
+ * Format sessions as a rich table for /sessions command output.
487
+ * Returns array of lines (one per session + header).
488
+ */
489
+ export function formatSessionsTable(sessions, opts, now) {
490
+ if (sessions.length === 0)
491
+ return [" no sessions"];
492
+ const nowMs = now ?? Date.now();
493
+ const lines = [];
494
+ lines.push(` ${"#".padEnd(3)} ${"title".padEnd(20)} ${"status".padEnd(9)} ${"hlth".padEnd(5)} ${"group".padEnd(10)} ${"cost".padEnd(7)} ${"uptime".padEnd(8)} flags`);
495
+ lines.push(` ${"-".repeat(3)} ${"-".repeat(20)} ${"-".repeat(9)} ${"-".repeat(5)} ${"-".repeat(10)} ${"-".repeat(7)} ${"-".repeat(8)} -----`);
496
+ for (let i = 0; i < sessions.length; i++) {
497
+ const s = sessions[i];
498
+ const alias = opts.aliases.get(s.id);
499
+ const titleStr = (alias || s.title).slice(0, 20).padEnd(20);
500
+ const statusStr = s.status.slice(0, 9).padEnd(9);
501
+ const health = opts.healthScores.get(s.id) ?? 100;
502
+ const healthStr = String(health).padEnd(5);
503
+ const group = opts.groups.get(s.id) ?? "";
504
+ const groupStr = group.slice(0, 10).padEnd(10);
505
+ const cost = opts.costs.get(s.id) ?? "";
506
+ const costStr = cost.slice(0, 7).padEnd(7);
507
+ const fs = opts.firstSeen.get(s.id);
508
+ const uptimeStr = fs !== undefined ? formatUptime(nowMs - fs).padEnd(8) : "?".padEnd(8);
509
+ // flags: D=drain, ⊹=tag, ✎=note, ·=label
510
+ const flags = [
511
+ opts.drainingIds.has(s.id) ? "D" : "",
512
+ (opts.tags.get(s.id)?.size ?? 0) > 0 ? "T" : "",
513
+ opts.notes.has(s.id) ? "N" : "",
514
+ opts.labels.has(s.id) ? "L" : "",
515
+ ].filter(Boolean).join("") || "-";
516
+ lines.push(` ${String(i + 1).padEnd(3)} ${titleStr} ${statusStr} ${healthStr} ${groupStr} ${costStr} ${uptimeStr} ${flags}`);
517
+ }
518
+ return lines;
519
+ }
520
+ // ── Session note history ─────────────────────────────────────────────────────
521
+ /** Max notes stored per session (in history before clear). */
522
+ export const MAX_NOTE_HISTORY = 5;
523
+ // ── Session label ────────────────────────────────────────────────────────────
524
+ /** Max length of a session label (displayed below title in cards). */
525
+ export const MAX_LABEL_LEN = 40;
526
+ /** Truncate a label to the max length. */
527
+ export function truncateLabel(label) {
528
+ return label.length > MAX_LABEL_LEN ? label.slice(0, MAX_LABEL_LEN - 2) + ".." : label;
529
+ }
484
530
  // ── Session drain mode helpers ────────────────────────────────────────────────
485
531
  /** Drain icon shown in session cards for draining sessions. */
486
532
  export const DRAIN_ICON = "⇣";
@@ -811,6 +857,7 @@ export const BUILTIN_COMMANDS = new Set([
811
857
  "/duplicate", "/color-all", "/quiet-hours", "/quiet-status", "/history-stats", "/cost-summary", "/session-report", "/alert-log",
812
858
  "/budget", "/budgets", "/budget-status", "/pause-all", "/resume-all",
813
859
  "/health-trend", "/alert-mute", "/flap-log", "/drain", "/undrain", "/export-all",
860
+ "/note-history", "/label", "/sessions",
814
861
  ]);
815
862
  /** Resolve a slash command through the alias map. Returns the expanded command or the original. */
816
863
  export function resolveAlias(line, aliases) {
@@ -1084,6 +1131,8 @@ export class TUI {
1084
1131
  quietHoursRanges = []; // quiet-hour start/end pairs
1085
1132
  sessionHealthHistory = new Map(); // session ID → health snapshots
1086
1133
  alertLog = []; // recent auto-generated status alerts (ring buffer, max 100)
1134
+ sessionNoteHistory = new Map(); // session ID → last N notes before clear
1135
+ sessionLabels = new Map(); // session ID → freeform display label
1087
1136
  sessionBudgets = new Map(); // session ID → USD budget
1088
1137
  globalBudget = null; // global fallback budget in USD
1089
1138
  budgetAlerted = new Map(); // session ID → epoch ms of last budget alert
@@ -1355,6 +1404,15 @@ export class TUI {
1355
1404
  if (!sessionId)
1356
1405
  return false;
1357
1406
  if (text.trim() === "") {
1407
+ // push current note into history before clearing
1408
+ const current = this.sessionNotes.get(sessionId);
1409
+ if (current) {
1410
+ const hist = this.sessionNoteHistory.get(sessionId) ?? [];
1411
+ hist.push(current);
1412
+ if (hist.length > MAX_NOTE_HISTORY)
1413
+ hist.shift();
1414
+ this.sessionNoteHistory.set(sessionId, hist);
1415
+ }
1358
1416
  this.sessionNotes.delete(sessionId);
1359
1417
  }
1360
1418
  else {
@@ -1380,6 +1438,46 @@ export class TUI {
1380
1438
  getAllNotes() {
1381
1439
  return this.sessionNotes;
1382
1440
  }
1441
+ /** Return note history for a session (oldest first). */
1442
+ getNoteHistory(id) {
1443
+ return this.sessionNoteHistory.get(id) ?? [];
1444
+ }
1445
+ // ── Session labels ───────────────────────────────────────────────────────
1446
+ /**
1447
+ * Set or clear a label for a session (by 1-indexed number, ID, or title).
1448
+ * Label is displayed below the session title in normal cards.
1449
+ * Returns true if session found.
1450
+ */
1451
+ setLabel(sessionIdOrIndex, label) {
1452
+ let sessionId;
1453
+ if (typeof sessionIdOrIndex === "number") {
1454
+ sessionId = this.sessions[sessionIdOrIndex - 1]?.id;
1455
+ }
1456
+ else {
1457
+ const needle = sessionIdOrIndex.toLowerCase();
1458
+ const match = this.sessions.find((s) => s.id === sessionIdOrIndex || s.id.startsWith(needle) || s.title.toLowerCase() === needle);
1459
+ sessionId = match?.id;
1460
+ }
1461
+ if (!sessionId)
1462
+ return false;
1463
+ if (!label || label.trim() === "") {
1464
+ this.sessionLabels.delete(sessionId);
1465
+ }
1466
+ else {
1467
+ this.sessionLabels.set(sessionId, truncateLabel(label.trim()));
1468
+ }
1469
+ if (this.active)
1470
+ this.paintSessions();
1471
+ return true;
1472
+ }
1473
+ /** Get the label for a session ID (or undefined). */
1474
+ getLabel(id) {
1475
+ return this.sessionLabels.get(id);
1476
+ }
1477
+ /** Return all session labels. */
1478
+ getAllLabels() {
1479
+ return this.sessionLabels;
1480
+ }
1383
1481
  /** Return the current sessions (read-only, for resolving IDs to titles in the UI). */
1384
1482
  getSessions() {
1385
1483
  return this.sessions;
@@ -2636,7 +2734,8 @@ export class TUI {
2636
2734
  const iconsWidth = (pinned ? 2 : 0) + (muted ? 2 : 0) + (noted ? 2 : 0) + actualBadgeWidth + groupBadgeWidth + tagsBadgeWidth + colorDotWidth + (draining ? 2 : 0);
2637
2735
  const cardWidth = innerWidth - 1 - iconsWidth;
2638
2736
  const cardAge = s.createdAt ? formatSessionAge(s.createdAt, nowMs) : undefined;
2639
- const line = `${bg}${SLATE}${BOX.v}${RESET}${bg} ${pin}${mute}${badgeSuffix}${note}${groupBadge}${tagsBadge}${colorDot}${drainIcon}${formatSessionCard(s, cardWidth, errSparkline || undefined, idleSinceMs, healthBadge || undefined, displayName, cardAge || undefined)}`;
2737
+ const sessionLabel = this.sessionLabels.get(s.id);
2738
+ const line = `${bg}${SLATE}${BOX.v}${RESET}${bg} ${pin}${mute}${badgeSuffix}${note}${groupBadge}${tagsBadge}${colorDot}${drainIcon}${formatSessionCard(s, cardWidth, errSparkline || undefined, idleSinceMs, healthBadge || undefined, displayName, cardAge || undefined, sessionLabel)}`;
2640
2739
  const padded = padBoxLineHover(line, this.cols, isHovered);
2641
2740
  process.stderr.write(moveTo(startRow + 1 + i, 1) + CLEAR_LINE + padded);
2642
2741
  }
@@ -2709,7 +2808,8 @@ export class TUI {
2709
2808
  const iconsWidth = (pinned ? 2 : 0) + (muted ? 2 : 0) + (noted ? 2 : 0) + actualBadgeWidth + groupBadgeWidth + tagsBadgeWidth2 + colorDotWidth2 + (draining2 ? 2 : 0);
2710
2809
  const cardWidth = innerWidth - 1 - iconsWidth;
2711
2810
  const cardAge2 = s.createdAt ? formatSessionAge(s.createdAt, nowMs2) : undefined;
2712
- const line = `${bg}${SLATE}${BOX.v}${RESET}${bg} ${pin}${mute}${badgeSuffix}${note}${groupBadge}${tagsBadge2}${colorDot2}${drainIcon2}${formatSessionCard(s, cardWidth, errSparkline || undefined, idleSinceMs, healthBadge2 || undefined, displayName2, cardAge2 || undefined)}`;
2811
+ const sessionLabel2 = this.sessionLabels.get(s.id);
2812
+ const line = `${bg}${SLATE}${BOX.v}${RESET}${bg} ${pin}${mute}${badgeSuffix}${note}${groupBadge}${tagsBadge2}${colorDot2}${drainIcon2}${formatSessionCard(s, cardWidth, errSparkline || undefined, idleSinceMs, healthBadge2 || undefined, displayName2, cardAge2 || undefined, sessionLabel2)}`;
2713
2813
  const padded = padBoxLineHover(line, this.cols, isHovered);
2714
2814
  process.stderr.write(SAVE_CURSOR + moveTo(startRow + 1 + i, 1) + CLEAR_LINE + padded + RESTORE_CURSOR);
2715
2815
  }
@@ -2828,7 +2928,8 @@ export class TUI {
2828
2928
  // healthBadge: optional pre-formatted health score badge ("⬡83" colored)
2829
2929
  // displayName: optional custom name override (from /rename)
2830
2930
  // ageStr: optional session age string (from createdAt)
2831
- function formatSessionCard(s, maxWidth, errorSparkline, idleSinceMs, healthBadge, displayName, ageStr) {
2931
+ // label: optional freeform label shown as DIM subtitle
2932
+ function formatSessionCard(s, maxWidth, errorSparkline, idleSinceMs, healthBadge, displayName, ageStr, label) {
2832
2933
  const dot = STATUS_DOT[s.status] ?? `${AMBER}${DOT.filled}${RESET}`;
2833
2934
  const title = displayName ?? s.title;
2834
2935
  const name = displayName ? `${BOLD}${displayName}${DIM} (${s.title})${RESET}` : `${BOLD}${s.title}${RESET}`;
@@ -2869,7 +2970,8 @@ function formatSessionCard(s, maxWidth, errorSparkline, idleSinceMs, healthBadge
2869
2970
  desc = `${SLATE}${s.status}${RESET}`;
2870
2971
  }
2871
2972
  const ageSuffix = ageStr ? ` ${DIM}age:${ageStr}${RESET}` : "";
2872
- return truncateAnsi(`${dot} ${healthPrefix}${name} ${toolBadge}${contextBadge} ${SLATE}${BOX.h}${RESET} ${desc}${ageSuffix}${sparkSuffix}`, maxWidth);
2973
+ const labelSuffix = label ? ` ${DIM}· ${label}${RESET}` : "";
2974
+ return truncateAnsi(`${dot} ${healthPrefix}${name} ${toolBadge}${contextBadge} ${SLATE}${BOX.h}${RESET} ${desc}${ageSuffix}${labelSuffix}${sparkSuffix}`, maxWidth);
2873
2975
  }
2874
2976
  // colorize an activity entry based on its tag
2875
2977
  function formatActivity(entry, maxCols) {
package/dist/types.d.ts CHANGED
@@ -25,6 +25,7 @@ export interface Observation {
25
25
  userMessage?: string;
26
26
  taskContext?: TaskState[];
27
27
  protectedSessions?: string[];
28
+ drainingSessionIds?: string[];
28
29
  policyContext?: {
29
30
  policies: AoaoeConfig["policies"];
30
31
  sessionStates: Array<{
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aoaoe",
3
- "version": "0.156.0",
3
+ "version": "0.160.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",