@aragon-agent/cli 0.6.2 → 0.6.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (167) hide show
  1. package/CHANGELOG.md +321 -0
  2. package/README.md +297 -2
  3. package/dist/agent/controller.d.ts +193 -1
  4. package/dist/agent/controller.js +475 -2
  5. package/dist/agent/headless.d.ts +8 -0
  6. package/dist/agent/headless.js +47 -0
  7. package/dist/agent/reducer.d.ts +189 -0
  8. package/dist/agent/reducer.js +301 -2
  9. package/dist/agent/system-prompt.d.ts +14 -0
  10. package/dist/agent/system-prompt.js +2 -0
  11. package/dist/cli.js +297 -16
  12. package/dist/commands/builtins.js +165 -24
  13. package/dist/commands/registry.d.ts +27 -0
  14. package/dist/compaction/archive.d.ts +175 -0
  15. package/dist/compaction/archive.js +267 -0
  16. package/dist/compaction/child.d.ts +97 -0
  17. package/dist/compaction/child.js +118 -0
  18. package/dist/compaction/command.d.ts +41 -0
  19. package/dist/compaction/command.js +355 -0
  20. package/dist/compaction/compactor.d.ts +302 -0
  21. package/dist/compaction/compactor.js +810 -0
  22. package/dist/compaction/digest.d.ts +62 -0
  23. package/dist/compaction/digest.js +164 -0
  24. package/dist/compaction/limits.d.ts +165 -0
  25. package/dist/compaction/limits.js +172 -0
  26. package/dist/compaction/pressure.d.ts +135 -0
  27. package/dist/compaction/pressure.js +167 -0
  28. package/dist/compaction/summarize-call.d.ts +70 -0
  29. package/dist/compaction/summarize-call.js +86 -0
  30. package/dist/compaction/summary-prompt.d.ts +85 -0
  31. package/dist/compaction/summary-prompt.js +197 -0
  32. package/dist/compaction/tail-budget.d.ts +52 -0
  33. package/dist/compaction/tail-budget.js +67 -0
  34. package/dist/compaction/types.d.ts +215 -0
  35. package/dist/compaction/types.js +22 -0
  36. package/dist/compaction/wiring.d.ts +264 -0
  37. package/dist/compaction/wiring.js +577 -0
  38. package/dist/config/app-paths.d.ts +14 -0
  39. package/dist/config/app-paths.js +16 -0
  40. package/dist/config/cli-commands.d.ts +41 -0
  41. package/dist/config/cli-commands.js +77 -1
  42. package/dist/config/env.d.ts +5 -2
  43. package/dist/config/env.js +140 -3
  44. package/dist/config/load.d.ts +42 -0
  45. package/dist/config/load.js +101 -1
  46. package/dist/config/schema.d.ts +244 -0
  47. package/dist/config/schema.js +170 -0
  48. package/dist/config/store.js +37 -1
  49. package/dist/config/ui-state.d.ts +28 -1
  50. package/dist/config/ui-state.js +9 -0
  51. package/dist/exec/events.d.ts +50 -1
  52. package/dist/exec/runner.js +42 -0
  53. package/dist/input/limits.d.ts +72 -0
  54. package/dist/input/limits.js +63 -0
  55. package/dist/input/mouse-events.d.ts +31 -5
  56. package/dist/input/mouse-events.js +86 -27
  57. package/dist/input/paste-parse.d.ts +71 -0
  58. package/dist/input/paste-parse.js +95 -0
  59. package/dist/input/stdin-filter.d.ts +81 -0
  60. package/dist/input/stdin-filter.js +379 -0
  61. package/dist/logging/install.d.ts +53 -1
  62. package/dist/logging/install.js +81 -1
  63. package/dist/logging/logger.d.ts +1 -1
  64. package/dist/proc/classify.d.ts +52 -0
  65. package/dist/proc/classify.js +143 -0
  66. package/dist/proc/kill-tree.d.ts +42 -0
  67. package/dist/proc/kill-tree.js +119 -0
  68. package/dist/proc/limits.d.ts +84 -0
  69. package/dist/proc/limits.js +84 -0
  70. package/dist/proc/log-ring.d.ts +68 -0
  71. package/dist/proc/log-ring.js +102 -0
  72. package/dist/proc/prompt.d.ts +24 -0
  73. package/dist/proc/prompt.js +49 -0
  74. package/dist/proc/readiness.d.ts +102 -0
  75. package/dist/proc/readiness.js +219 -0
  76. package/dist/proc/supervisor.d.ts +103 -0
  77. package/dist/proc/supervisor.js +465 -0
  78. package/dist/proc/types.d.ts +175 -0
  79. package/dist/proc/types.js +15 -0
  80. package/dist/session/persist.js +55 -1
  81. package/dist/team/report.js +7 -1
  82. package/dist/team/runtime.d.ts +10 -0
  83. package/dist/team/runtime.js +7 -0
  84. package/dist/team/subagent.d.ts +31 -1
  85. package/dist/team/subagent.js +42 -15
  86. package/dist/team/types.d.ts +8 -0
  87. package/dist/tools/bash-tool.d.ts +51 -2
  88. package/dist/tools/bash-tool.js +321 -51
  89. package/dist/tools/fs-tools.d.ts +17 -0
  90. package/dist/tools/index.d.ts +55 -2
  91. package/dist/tools/index.js +39 -1
  92. package/dist/tools/proc-tools.d.ts +25 -0
  93. package/dist/tools/proc-tools.js +102 -0
  94. package/dist/ui/ActivityLine.d.ts +19 -1
  95. package/dist/ui/ActivityLine.js +10 -2
  96. package/dist/ui/App.d.ts +62 -2
  97. package/dist/ui/App.js +546 -16
  98. package/dist/ui/Composer.d.ts +66 -1
  99. package/dist/ui/Composer.js +50 -6
  100. package/dist/ui/PromptInput.d.ts +82 -3
  101. package/dist/ui/PromptInput.js +232 -49
  102. package/dist/ui/StatusBar.d.ts +59 -0
  103. package/dist/ui/StatusBar.js +16 -4
  104. package/dist/ui/TeamPanel.js +7 -1
  105. package/dist/ui/Transcript.d.ts +21 -1
  106. package/dist/ui/Transcript.js +61 -3
  107. package/dist/ui/clipboard.d.ts +52 -0
  108. package/dist/ui/clipboard.js +91 -0
  109. package/dist/ui/composer-limits.d.ts +31 -0
  110. package/dist/ui/composer-limits.js +50 -0
  111. package/dist/ui/composer-rows.d.ts +73 -0
  112. package/dist/ui/composer-rows.js +201 -0
  113. package/dist/ui/editor-reducer.d.ts +37 -0
  114. package/dist/ui/editor-reducer.js +88 -12
  115. package/dist/ui/entries/CompactionCard.d.ts +75 -0
  116. package/dist/ui/entries/CompactionCard.js +158 -0
  117. package/dist/ui/entries/ServiceCard.d.ts +66 -0
  118. package/dist/ui/entries/ServiceCard.js +116 -0
  119. package/dist/ui/entries/UserEntry.d.ts +17 -3
  120. package/dist/ui/entries/UserEntry.js +34 -5
  121. package/dist/ui/frame-differ.d.ts +46 -0
  122. package/dist/ui/frame-differ.js +94 -10
  123. package/dist/ui/gauge.d.ts +16 -1
  124. package/dist/ui/gauge.js +25 -6
  125. package/dist/ui/glyphs.d.ts +11 -0
  126. package/dist/ui/glyphs.js +2 -0
  127. package/dist/ui/layout/ScrollViewport.d.ts +43 -6
  128. package/dist/ui/layout/ScrollViewport.js +128 -21
  129. package/dist/ui/layout/budget.d.ts +44 -9
  130. package/dist/ui/layout/budget.js +42 -10
  131. package/dist/ui/layout/follow-state.d.ts +137 -0
  132. package/dist/ui/layout/follow-state.js +150 -0
  133. package/dist/ui/layout/virtual-window.d.ts +11 -0
  134. package/dist/ui/layout/virtual-window.js +85 -1
  135. package/dist/ui/overlays/HelpOverlay.js +9 -1
  136. package/dist/ui/overlays/PlanReviewOverlay.js +10 -2
  137. package/dist/ui/overlays/QuestionOverlay.js +5 -1
  138. package/dist/ui/overlays/SettingsScreen.d.ts +47 -1
  139. package/dist/ui/overlays/SettingsScreen.js +74 -2
  140. package/dist/ui/palettes.d.ts +16 -0
  141. package/dist/ui/palettes.js +6 -0
  142. package/dist/ui/paste-frames.d.ts +87 -0
  143. package/dist/ui/paste-frames.js +112 -0
  144. package/dist/ui/paste-tokens.d.ts +90 -0
  145. package/dist/ui/paste-tokens.js +147 -0
  146. package/dist/ui/screen.d.ts +38 -0
  147. package/dist/ui/screen.js +113 -8
  148. package/dist/ui/selection/highlight.d.ts +39 -0
  149. package/dist/ui/selection/highlight.js +64 -0
  150. package/dist/ui/selection/screen-mirror.d.ts +24 -0
  151. package/dist/ui/selection/screen-mirror.js +35 -0
  152. package/dist/ui/selection/selection-controller.d.ts +88 -0
  153. package/dist/ui/selection/selection-controller.js +282 -0
  154. package/dist/ui/selection/selection.d.ts +90 -0
  155. package/dist/ui/selection/selection.js +223 -0
  156. package/dist/ui/stdout-frame-writer.d.ts +33 -0
  157. package/dist/ui/stdout-frame-writer.js +15 -0
  158. package/dist/ui/theme.d.ts +11 -0
  159. package/dist/ui/theme.js +2 -0
  160. package/dist/ui/transcript-text.js +72 -0
  161. package/dist/ui/use-startup-notices.d.ts +27 -1
  162. package/dist/ui/use-startup-notices.js +43 -11
  163. package/dist/ui/use-wheel-routing.d.ts +1 -1
  164. package/dist/ui/use-wheel-routing.js +8 -0
  165. package/package.json +4 -2
  166. package/dist/input/stdin-mouse-filter.d.ts +0 -42
  167. package/dist/input/stdin-mouse-filter.js +0 -153
@@ -3,20 +3,20 @@
3
3
  * /help /model /settings /thinking /max-tokens /tools /clear /reset /cwd /save
4
4
  * /resume /copy /exit (/quit), plus the /team, /fast and /todo subsystems.
5
5
  */
6
- import { spawn } from 'node:child_process';
7
6
  import { existsSync, statSync } from 'node:fs';
8
7
  import { isAbsolute, resolve } from 'node:path';
9
- import process from 'node:process';
10
8
  import { resolveOutputTokens } from '@aragon-agent/core';
11
9
  import { clampThinkingLevel, clampTheme, isAdapterProvider, isThemeName, parseMaxTokensInput, ADAPTER_PROVIDERS, DEFAULT_MAX_TOKENS, HARD_MAX_RETRIES, LEGACY_DARK_THEME, THEME_NAMES, THINKING_LEVELS, } from '../config/schema.js';
12
- import { computeCost, formatCost, formatTokens } from '../agent/usage.js';
10
+ import { computeCost, formatCost, formatDuration, formatTokens } from '../agent/usage.js';
13
11
  import { TODO_FOLLOW_LIMITS } from '../todo/limits.js';
14
12
  import { buildContinuationMessage } from '../todo/follow-through.js';
15
13
  import { MODE_LABEL, nextMode } from '../agent/agent-mode.js';
16
14
  import { loadConfig } from '../config/load.js';
17
15
  import { loadSession, resolveSessionPath, saveSession } from '../session/persist.js';
18
16
  import { getLogger } from '../logging/logger.js';
17
+ import { copyText } from '../ui/clipboard.js';
19
18
  import { perfCommand } from './perf.js';
19
+ import { runCompactCommand } from '../compaction/command.js';
20
20
  /**
21
21
  * `/fast status` — THE GUARANTEED REPORTING SURFACE for the tier (§4.4 / R-8).
22
22
  *
@@ -801,8 +801,21 @@ const COMMANDS = [
801
801
  ctx.notify('warn', `Unknown argument "${verb}" - use /retry [show|on|off|max <n>].`);
802
802
  },
803
803
  },
804
+ {
805
+ // REGISTRATION ONLY. The body lives in `compaction/command.ts` because this
806
+ // file is already past the 1000-line guideline and `/compact` has six forms
807
+ // plus the longest status report in the package (C-16 / P1-12).
808
+ name: 'compact',
809
+ description: 'Context compaction: status | on | off | threshold <n> | keep <n> | <instructions>',
810
+ run: (ctx) => runCompactCommand(ctx),
811
+ },
804
812
  {
805
813
  name: 'expand',
814
+ // DELIBERATELY LEFT TOOL-ONLY, and the asymmetry with Ctrl+O is a decision
815
+ // rather than an oversight (§6.3). Its description says "a recent tool card",
816
+ // it takes an N-from-last index over tool cards specifically, and widening it
817
+ // would change what `/expand 3` means for every existing user. Ctrl+O is the
818
+ // general expansion surface — that is where `'compaction'` was added.
806
819
  description: 'Expand / collapse a recent tool card (default: last)',
807
820
  run: (ctx) => {
808
821
  const toolEntries = ctx.state.entries.filter((e) => e.kind === 'tool');
@@ -942,8 +955,60 @@ const COMMANDS = [
942
955
  ctx.notify('warn', 'No assistant message to copy.');
943
956
  return;
944
957
  }
945
- const ok = copyToClipboard(last.text);
946
- ctx.toast(ok ? 'success' : 'warn', ok ? 'Copied to clipboard.' : 'Clipboard not available.');
958
+ // ROUTED THROUGH `ui/clipboard.ts` (§4.4.5). The duplicate spawn helper this
959
+ // command used to own is gone, and the command gained SSH support on the
960
+ // way: OSC 52 reaches the clipboard of the machine the USER is sitting at,
961
+ // which the local `pbcopy` never could.
962
+ //
963
+ // `ctx.writeForeign` and not `stdout.write` — see the field's comment.
964
+ const via = copyText(last.text, ctx.writeForeign);
965
+ if (via === 'none') {
966
+ ctx.toast('warn', 'Clipboard not available.');
967
+ return;
968
+ }
969
+ // NAMES THE MECHANISM RATHER THAN CLAIMING SUCCESS: neither path is
970
+ // detectable, and tmux without `set -g set-clipboard on` swallows OSC 52
971
+ // silently (R-7).
972
+ ctx.toast('success', via === 'osc52' ? 'Sent to the terminal clipboard.' : 'Sent to the system clipboard.');
973
+ },
974
+ },
975
+ {
976
+ name: 'mouse',
977
+ description: 'Release the mouse to the terminal, or take it back',
978
+ run: (ctx) => {
979
+ const port = ctx.mouse;
980
+ if (!port) {
981
+ // Inline mode, `--no-mouse`, a non-TTY, or a console that cannot deliver
982
+ // reports. Saying so beats pretending to toggle something that is not on.
983
+ ctx.notify('info', 'The mouse is not captured in this session, so there is nothing to release.');
984
+ return;
985
+ }
986
+ const arg = ctx.args.trim().toLowerCase();
987
+ if (arg === 'off') {
988
+ port.setCapture(false);
989
+ // NAMING THE COST IS THE HONEST THING TO DO, and it is one clause. The
990
+ // wheel goes INERT rather than back to scrolling: alternate scroll is
991
+ // saved and disabled alongside the capture, because otherwise every notch
992
+ // becomes a burst of arrow keys that `PromptInput` reads as prompt-history
993
+ // recall (I-10 / P1-1).
994
+ ctx.toast('info', 'Mouse released to the terminal - the wheel will not scroll. /mouse on gives it back.');
995
+ return;
996
+ }
997
+ if (arg === 'on') {
998
+ port.setCapture(true);
999
+ ctx.toast('info', 'Mouse captured - the wheel scrolls the transcript again.');
1000
+ return;
1001
+ }
1002
+ const captured = port.captured();
1003
+ const lines = [
1004
+ `Mouse capture: ${captured ? 'on' : 'off'}`,
1005
+ ` drag-select ${port.selectEnabled() ? 'on' : 'off (mouseSelect is false)'}`,
1006
+ ' /mouse off hand the mouse to your terminal for this session',
1007
+ ' /mouse on take it back',
1008
+ ' --no-mouse never capture it (also ARAGON_MOUSE=0)',
1009
+ ' aragon config set mouseSelect false keep the wheel, drop drag-select',
1010
+ ];
1011
+ ctx.notify('info', lines.join('\n'));
947
1012
  },
948
1013
  },
949
1014
  {
@@ -983,6 +1048,95 @@ const COMMANDS = [
983
1048
  // render layer, and threading `ui/render-cache.js` into this file would make
984
1049
  // every other command's import graph pull the renderer in with it.
985
1050
  perfCommand,
1051
+ {
1052
+ name: 'bg',
1053
+ description: 'Background services: list | logs <id> [n] | stop <id|all> | status',
1054
+ /**
1055
+ * THE USER'S HANDLE ON WHAT THE AGENT LEFT RUNNING (§5.3).
1056
+ *
1057
+ * It is the LIVE view, and that is why it exists at all: the transcript
1058
+ * records EVENTS about a service (D-11) — a card printed into `<Static>`
1059
+ * cannot be rewritten — so "what is running right now" lives here, in the
1060
+ * status chip, and in `bash_output`. Without this command a user whose
1061
+ * server card scrolled away an hour ago has no way to name it.
1062
+ */
1063
+ run: async (ctx) => {
1064
+ const arg = ctx.args.trim();
1065
+ const [verb = 'list', value, count] = arg.split(/\s+/);
1066
+ const controller = ctx.controller;
1067
+ if (!controller.isBackgroundRegistered()) {
1068
+ ctx.notify('info', 'Background services are off for this session (bash.background).');
1069
+ return;
1070
+ }
1071
+ const services = controller.listServices();
1072
+ if (verb === 'status') {
1073
+ const live = controller.liveServiceCount();
1074
+ ctx.notify('info', live === 0
1075
+ ? 'No background services running.'
1076
+ : `${live} background service${live === 1 ? '' : 's'} running.`);
1077
+ return;
1078
+ }
1079
+ if (verb === 'logs') {
1080
+ if (!value) {
1081
+ ctx.notify('warn', 'Usage: /bg logs <id> [n]');
1082
+ return;
1083
+ }
1084
+ const service = controller.getServiceSnapshot(value);
1085
+ if (!service) {
1086
+ ctx.notify('warn', `No service "${value}".`);
1087
+ return;
1088
+ }
1089
+ const page = controller.readServiceLog(value);
1090
+ const n = count ? Number.parseInt(count, 10) : DEFAULT_BG_LOG_ROWS;
1091
+ const rows = page?.rows ?? [];
1092
+ const take = Number.isFinite(n) && n > 0 ? n : DEFAULT_BG_LOG_ROWS;
1093
+ const shown = rows.slice(-take);
1094
+ ctx.notify('info', shown.length === 0
1095
+ ? `service ${service.id}: no output yet.`
1096
+ : `service ${service.id} (last ${shown.length} rows)\n${shown.join('\n')}`);
1097
+ return;
1098
+ }
1099
+ if (verb === 'stop') {
1100
+ if (!value) {
1101
+ ctx.notify('warn', 'Usage: /bg stop <id|all>');
1102
+ return;
1103
+ }
1104
+ // AWAITED, unlike `doExit`'s force path: a slash command runs on a live
1105
+ // event loop, so the graceful SIGTERM -> SIGKILL ladder can run and a
1106
+ // dev server gets its chance to release its port cleanly.
1107
+ const stopped = await controller.stopService(value);
1108
+ if (stopped.length === 0) {
1109
+ ctx.notify('info', 'Nothing to stop.');
1110
+ return;
1111
+ }
1112
+ const leaked = stopped.filter((svc) => svc.killIncomplete);
1113
+ ctx.toast('success', `Stopped ${stopped.length} service${stopped.length === 1 ? '' : 's'}.`);
1114
+ if (leaked.length > 0) {
1115
+ // AN HONEST WARNING BEATS A SILENT LEAK (R-2 / P2-6). `taskkill /t`
1116
+ // cannot reach a grandchild that re-parented itself, and saying so is
1117
+ // the difference between a puzzle and a `netstat` the user knows to run.
1118
+ ctx.notify('warn', `${leaked.map((svc) => svc.id).join(', ')} may have left a detached child - ` +
1119
+ 'check with netstat/lsof if a port is still held.');
1120
+ }
1121
+ return;
1122
+ }
1123
+ if (verb !== 'list' && verb !== '') {
1124
+ ctx.notify('warn', `Unknown argument "${verb}" - use /bg [list|logs|stop|status].`);
1125
+ return;
1126
+ }
1127
+ if (services.length === 0) {
1128
+ ctx.notify('info', 'No background services this session.');
1129
+ return;
1130
+ }
1131
+ const now = Date.now();
1132
+ const lines = services.map((svc) => {
1133
+ const uptime = formatDuration((svc.endedAt ?? now) - svc.startedAt);
1134
+ const where = svc.url ? ` ${svc.url}` : '';
1135
+ return `${svc.id} ${svc.status} ${uptime}${where} $ ${svc.command}`;
1136
+ });
1137
+ ctx.notify('info', `Background services:\n${lines.join('\n')}`);
1138
+ },
1139
+ },
986
1140
  {
987
1141
  name: 'exit',
988
1142
  aliases: ['quit'],
@@ -990,6 +1144,8 @@ const COMMANDS = [
990
1144
  run: (ctx) => ctx.exit(),
991
1145
  },
992
1146
  ];
1147
+ /** Rows `/bg logs <id>` shows when the user names no count. */
1148
+ const DEFAULT_BG_LOG_ROWS = 40;
993
1149
  export function registerBuiltinCommands(registry) {
994
1150
  for (const command of COMMANDS)
995
1151
  registry.register(command);
@@ -1006,22 +1162,7 @@ export const BUILTIN_COMMAND_NAMES = COMMANDS.flatMap((c) => [
1006
1162
  c.name,
1007
1163
  ...(c.aliases ?? []),
1008
1164
  ]).sort();
1009
- /** Best-effort clipboard copy via the platform's clipboard CLI. */
1010
- function copyToClipboard(text) {
1011
- const cmd = process.platform === 'win32'
1012
- ? { file: 'clip', args: [] }
1013
- : process.platform === 'darwin'
1014
- ? { file: 'pbcopy', args: [] }
1015
- : { file: 'xclip', args: ['-selection', 'clipboard'] };
1016
- try {
1017
- const child = spawn(cmd.file, cmd.args, { stdio: ['pipe', 'ignore', 'ignore'] });
1018
- child.on('error', () => {
1019
- /* swallow — clipboard is best-effort */
1020
- });
1021
- child.stdin?.end(text);
1022
- return true;
1023
- }
1024
- catch {
1025
- return false;
1026
- }
1027
- }
1165
+ // The `copyToClipboard` helper that used to live here moved into
1166
+ // `ui/clipboard.ts` (tui-selection-and-scroll-follow §4.4.5), unchanged, so that
1167
+ // `/copy` and the drag-select release share ONE clipboard path instead of two
1168
+ // that can drift.
@@ -87,6 +87,33 @@ export interface CommandContext {
87
87
  effective: AgentMode;
88
88
  pending: AgentMode | null;
89
89
  };
90
+ /**
91
+ * Mouse capture, for `/mouse` (tui-selection-and-scroll-follow §6.2 / G3).
92
+ *
93
+ * OPTIONAL, and legitimately absent: inline mode, `--no-mouse`, a non-TTY, a
94
+ * Windows console that cannot deliver reports, and every test that builds this
95
+ * context by literal. `/mouse` has to have something honest to say when it is
96
+ * missing, which is the same reason `update` above it is optional.
97
+ */
98
+ mouse?: MouseCommandPort;
99
+ /**
100
+ * The foreign-write door, for `/copy`'s OSC 52 half (§4.4.5).
101
+ *
102
+ * NOT `process.stdout.write`. In full-screen mode stdout is a `Proxy` in front
103
+ * of the frame differ, and an unrecognised chunk there is counted as a foreign
104
+ * write and prints a diagnostic at the user the first time they copy anything
105
+ * (P1-6). Absent in inline mode and with `--no-diff-render`, where the real
106
+ * stream is handed over directly.
107
+ */
108
+ writeForeign?: (text: string) => void;
109
+ }
110
+ /** What `/mouse` can see and do. Read at command time, never captured. */
111
+ export interface MouseCommandPort {
112
+ /** Whether the CLI is holding the mouse right now. */
113
+ captured(): boolean;
114
+ /** Whether drag-select is on for this session (`mouseSelect`). */
115
+ selectEnabled(): boolean;
116
+ setCapture(on: boolean): void;
90
117
  }
91
118
  export interface SlashCommand {
92
119
  name: string;
@@ -0,0 +1,175 @@
1
+ /**
2
+ * The compaction archive - a compaction stops being irreversible
3
+ * (context-auto-compaction-hardening §3.5 / W4).
4
+ *
5
+ * ASCII ONLY: `src/compaction/**` is inside the glyph scanner's scope
6
+ * (`glyphs.test.ts::inScope`), and that INCLUDES the field names below and every
7
+ * string `/compact history` and `/compact show` print.
8
+ *
9
+ * WHAT THIS IS FOR. Round 1's non-goal was stated plainly: "the dropped messages
10
+ * are gone". Combined with R-1 - "the summary is wrong or lossy, and the agent
11
+ * proceeds confidently on a false record... this is the feature's defining risk
12
+ * and it cannot be eliminated" - the product was asking the user to accept an
13
+ * unverifiable, unrecoverable transformation of their session. That is the wrong
14
+ * default for a tool that runs unattended. This does not add undo (§12: restoring
15
+ * an over-full history re-creates the condition that triggered compaction). It
16
+ * adds FIDELITY ON DISK, so R-1 becomes something a human can check.
17
+ *
18
+ * NO IMPORTS FROM `ui/`. This module writes files and formats two report blocks;
19
+ * it has no terminal, no theme and no glyphs.
20
+ */
21
+ import type { Message } from '@aragon-agent/core';
22
+ import type { CompactionMode, CompactionUiTrigger } from './types.js';
23
+ /** Bumped only when the SHAPE changes; the reader rejects anything else. */
24
+ export declare const ARCHIVE_FORMAT_VERSION = 1;
25
+ export interface CompactionArchive {
26
+ version: number;
27
+ /** `COMPACTION_BLOCK_VERSION` at write time, so a bad summary ties to a revision. */
28
+ blockVersion: string;
29
+ createdAt: number;
30
+ /**
31
+ * Which process wrote this.
32
+ *
33
+ * STORED INSIDE THE DOCUMENT as well as in the name, so a file renamed or
34
+ * copied by hand is still self-describing and the reader never has to parse a
35
+ * filename for meaning it cannot verify.
36
+ */
37
+ runId: string;
38
+ /** 1-based within this run. */
39
+ index: number;
40
+ trigger: CompactionUiTrigger;
41
+ mode: CompactionMode;
42
+ model: string;
43
+ generation: number;
44
+ tokensBefore: number;
45
+ tokensAfter: number;
46
+ messagesBefore: number;
47
+ messagesAfter: number;
48
+ /**
49
+ * THE NUMBER. Deliberately not called `droppedMessages`: that name already
50
+ * means a COUNT in three typed places (`CompactionPlan`, `finish`'s `applied`
51
+ * argument, and `AgentEvent.compaction_end`), and this is the document someone
52
+ * reads while diagnosing a bad summary (RV-3).
53
+ */
54
+ droppedCount: number;
55
+ summary?: string;
56
+ tailRelief?: {
57
+ messages: number;
58
+ charsRemoved: number;
59
+ };
60
+ /** True when `dropped` was shortened to fit `archiveMaxBytes`. */
61
+ clipped: boolean;
62
+ /** THE ARRAY. Verbatim, oldest first. */
63
+ dropped: Message[];
64
+ }
65
+ /** One row of `/compact history`, already resolved from disk. */
66
+ export interface ArchiveListing {
67
+ entries: ArchiveEntry[];
68
+ /** Archives in the directory belonging to OTHER runs. Counted, never listed. */
69
+ otherRuns: number;
70
+ dir: string;
71
+ }
72
+ export interface ArchiveEntry {
73
+ path: string;
74
+ fileName: string;
75
+ index: number;
76
+ createdAt: number;
77
+ trigger: CompactionUiTrigger;
78
+ mode: CompactionMode;
79
+ model: string;
80
+ tokensBefore: number;
81
+ tokensAfter: number;
82
+ droppedCount: number;
83
+ summary?: string;
84
+ tailRelief?: {
85
+ messages: number;
86
+ charsRemoved: number;
87
+ };
88
+ clipped: boolean;
89
+ }
90
+ export interface ArchiveWriteInput {
91
+ runId: string;
92
+ index: number;
93
+ trigger: CompactionUiTrigger;
94
+ mode: CompactionMode;
95
+ model: string;
96
+ generation: number;
97
+ tokensBefore: number;
98
+ tokensAfter: number;
99
+ messagesBefore: number;
100
+ messagesAfter: number;
101
+ summary?: string;
102
+ tailRelief?: {
103
+ messages: number;
104
+ charsRemoved: number;
105
+ };
106
+ dropped: Message[];
107
+ /** Injected by the tests; production passes nothing. */
108
+ dir?: string;
109
+ now?: number;
110
+ }
111
+ /**
112
+ * `compaction-<YYYY-MM-DD>-<hhmmss>-<runId>-<index>.json`.
113
+ *
114
+ * THE RUN ID IS IN THE NAME BECAUSE THE DIRECTORY IS SHARED (RV-6).
115
+ * `file-sink.ts` makes log retention best-effort precisely because "on Windows
116
+ * another `aragon` process may hold a handle on a file we would like to remove",
117
+ * so concurrent CLI processes are an acknowledged in-tree condition rather than
118
+ * an edge case. Without this segment `/compact history` presents another
119
+ * session's compactions as this one's and `/compact show 2` is ambiguous the
120
+ * moment two runs both reach their second compaction.
121
+ */
122
+ export declare function archiveFileName(runId: string, index: number, at: Date): string;
123
+ /**
124
+ * Mint this process's archive key.
125
+ *
126
+ * NOT `SessionMeta.id`: that exists only once a session has been SAVED, and the
127
+ * unattended long run this feature serves is exactly the one nobody saved. Short,
128
+ * filename-safe, and only ever compared for equality - a grouping key, not an
129
+ * identity.
130
+ */
131
+ export declare function mintRunId(pid?: number, now?: number): string;
132
+ /**
133
+ * Serialize, clipping the dropped bodies from the OLDEST end if the document
134
+ * exceeds `archiveMaxBytes`.
135
+ *
136
+ * THE METADATA AND THE SUMMARY ARE NEVER CLIPPED. A truncated archive that still
137
+ * says what happened, when, and with which model beats no archive at all - and
138
+ * the oldest dropped messages are the least likely to be the ones a human came
139
+ * looking for.
140
+ */
141
+ export declare function serializeArchive(doc: CompactionArchive, maxBytes: number): string;
142
+ /**
143
+ * Write one archive, then prune. Returns the path written, or `null`.
144
+ *
145
+ * NEVER THROWS. The caller runs inside an event handler on the compaction path;
146
+ * a filesystem is not allowed to affect whether a session survives its context
147
+ * window. Every failure is a `warn` with the path and nothing else - a toast for
148
+ * a best-effort audit file trains users to ignore toasts.
149
+ */
150
+ export declare function writeArchive(input: ArchiveWriteInput): string | null;
151
+ /**
152
+ * Two-tier retention, and the ORDER MATTERS (§3.5.2 / RV-6).
153
+ *
154
+ * 1. WITHIN THIS RUN: keep the newest `archiveMaxFiles` files carrying THIS
155
+ * `runId`. This is the bound that protects the user from their own long
156
+ * session, and it can never touch another run.
157
+ * 2. ACROSS RUNS: delete files older than `archiveMaxAgeMs`, regardless of
158
+ * `runId`, so an abandoned run's archives do not accumulate forever. AGE, not
159
+ * count - a count-based global prune is exactly the rule that deletes a live
160
+ * session's work while it is still running.
161
+ *
162
+ * Best-effort throughout, in the `enforceRetention` shape: every `rmSync` in its
163
+ * own `try`, a locked file skipped rather than fatal.
164
+ */
165
+ export declare function pruneArchives(runId: string, dir: string, now: number): void;
166
+ /**
167
+ * This run's archives, newest first, plus a COUNT of everyone else's.
168
+ *
169
+ * THE COUNT IS REPORTED RATHER THAN THE FILES, because the alternative is a user
170
+ * concluding their own compactions went missing when the directory holds a
171
+ * neighbour's.
172
+ */
173
+ export declare function listArchives(runId: string, dirOverride?: string): ArchiveListing;
174
+ /** One archive by its `#n`, or `null`. `n` is the index within THIS run. */
175
+ export declare function findArchive(runId: string, index: number, dirOverride?: string): ArchiveEntry | null;