@technomoron/agent-run 0.99.44 → 0.99.45

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/CHANGES CHANGED
@@ -1,6 +1,16 @@
1
1
  CHANGES
2
2
  =======
3
3
 
4
+ Version 0.99.45 (2026-10-14)
5
+ ----------------------------
6
+ * Move automatic session cleanup out of interactive startup and exit into
7
+ scheduled maintenance, using the hourly AI tools timer on Linux.
8
+ * Add --clean-old and per-user cleanup.env retention settings, defaulting to
9
+ seven days; set AGENT_RUN_SESSION_RETENTION_DAYS=0 to disable retention cleanup.
10
+ * Add hourly Windows session cleanup as a hidden, unelevated scheduled task.
11
+ * Run Linux cleanup as explicitly selected normal users and preserve native
12
+ session deletion and history maintenance.
13
+
4
14
  Version 0.99.44 (2026-10-14)
5
15
  ----------------------------
6
16
  * Remove local agent sessions last modified more than seven days ago before
package/README.md CHANGED
@@ -290,10 +290,19 @@ Global flags:
290
290
  `agent-run codex --help`, `agent-run claude --help`, `agent-run gemini --help`,
291
291
  and `agent-run grok --help` pass `--help` through to the underlying tool.
292
292
 
293
- Agent launches purge local sessions last modified more than seven days ago for all
294
- four agents. This covers native user homes and the profiles under the selected
295
- configuration root. Sessions exactly seven days old are retained. Authentication,
296
- settings, skills and brain knowledge are preserved.
293
+ Session cleanup runs separately from interactive agent startup and exit. The
294
+ hourly AI tool timer cleans sessions older than seven days by default for selected
295
+ users. Set retention in `~/.agent-run/cleanup.env`:
296
+
297
+ ```sh
298
+ AGENT_RUN_SESSION_RETENTION_DAYS=7
299
+ ```
300
+
301
+ Use a non-negative whole number of days; `0` disables retention cleanup.
302
+ An environment value overrides the file. `--configdir PATH` selects another
303
+ configuration root and its `cleanup.env`. Sessions exactly at the retention
304
+ boundary are retained. Authentication, settings, skills and brain knowledge are
305
+ preserved. Run retention cleanup manually with `agent-run --clean-old all`.
297
306
 
298
307
  Delete all saved sessions for selected agents without launching an agent:
299
308
 
@@ -1155,6 +1164,25 @@ starts at boot without an interactive login, and restarts it to load the update.
1155
1164
  No accounts are selected by default. The service remains owned by the selected
1156
1165
  user and listens on that user's private Unix socket.
1157
1166
 
1167
+ After a successful timer update, session cleanup runs as each selected user.
1168
+ Set `AI_TOOLS_CLEANUP_USERS="bjorn"` in `/etc/default/ai-tools-update` to select
1169
+ accounts; if unset, it uses `AI_TOOLS_BRAIN_USERS`. An explicitly empty value
1170
+ disables scheduled cleanup. Each user's `~/.agent-run/cleanup.env` controls
1171
+ retention, defaulting to seven days. Failures appear in the systemd journal.
1172
+
1173
+ On Windows, install hourly cleanup for the current user from PowerShell:
1174
+
1175
+ ```powershell
1176
+ $prefix = npm.cmd prefix -g
1177
+ & "$prefix\node_modules\@technomoron\agent-run\scripts\install-windows-cleanup.ps1"
1178
+ ```
1179
+
1180
+ The `Agent Session Cleanup` task runs hidden without elevation while the user is
1181
+ signed in, starting ten minutes after installation and repeating hourly. It uses
1182
+ the same `cleanup.env` and seven-day default. Pass `-ConfigRoot PATH` to select
1183
+ another configuration tree. Its latest output is in
1184
+ `<config-root>/runtime/cleanup.log`. The task skips overlapping runs.
1185
+
1158
1186
  Node 24 or newer must be installed at `/usr/local/bin/node` before running the
1159
1187
  updater. `AI_TOOLS_AGENT_RUN_PACKAGE` can select a package version or a
1160
1188
  root-owned local package archive while testing an unpublished build; it defaults
package/dist/cli.js CHANGED
@@ -12,7 +12,7 @@ function parseInvocation(invokedTool, argv) {
12
12
  let command = normalizeCommandName(invokedTool);
13
13
  const options = extractGlobalOptions(argv);
14
14
  applyGlobalOptions(options);
15
- if (command === null && (options.args[0] === '--clean' || options.args[0]?.startsWith('--clean='))) {
15
+ if (command === null && options.args[0] && /^(--clean|--clean-old)(=|$)/.test(options.args[0])) {
16
16
  return parseCleanCommand(options.args);
17
17
  }
18
18
  if (command === null) {
@@ -22,15 +22,17 @@ function parseInvocation(invokedTool, argv) {
22
22
  }
23
23
  function parseCleanCommand(args) {
24
24
  const flag = args[0];
25
+ const olderOnly = flag === '--clean-old' || flag.startsWith('--clean-old=');
26
+ const name = olderOnly ? '--clean-old' : '--clean';
25
27
  const values = [
26
- ...(flag.includes('=') ? [flag.slice('--clean='.length)] : []),
28
+ ...(flag.includes('=') ? [flag.slice(name.length + 1)] : []),
27
29
  ...args.slice(1)
28
30
  ].flatMap((value) => value.split(','));
29
31
  if (!values.length || values.some((value) => value !== 'all' && !types_1.AGENT_IDS.includes(value))
30
32
  || (values.includes('all') && values.length !== 1)) {
31
- (0, utils_1.fail)('--clean requires all or a list of codex, claude, gemini, grok');
33
+ (0, utils_1.fail)(`${name} requires all or a list of codex, claude, gemini, grok`);
32
34
  }
33
- return { command: 'clean', agents: values[0] === 'all' ? [...types_1.AGENT_IDS] : [...new Set(values)] };
35
+ return { command: 'clean', agents: values[0] === 'all' ? [...types_1.AGENT_IDS] : [...new Set(values)], ...(olderOnly ? { olderOnly: true } : {}) };
34
36
  }
35
37
  function applyGlobalOptions(options) {
36
38
  if (options.verbose) {
@@ -173,8 +175,8 @@ function parseRunCommand(command, inputArgs) {
173
175
  const args = [];
174
176
  let passthrough = false;
175
177
  for (const arg of inputArgs) {
176
- if (!passthrough && (arg === '--clean' || arg.startsWith('--clean=')))
177
- (0, utils_1.fail)('Use --clean on its own: agent-run --clean all|codex,claude,gemini,grok');
178
+ if (!passthrough && /^(--clean|--clean-old)(=|$)/.test(arg))
179
+ (0, utils_1.fail)('Use --clean or --clean-old on its own: agent-run --clean-old all|codex,claude,gemini,grok');
178
180
  if (passthrough) {
179
181
  args.push(arg);
180
182
  continue;
@@ -400,6 +402,7 @@ function renderHelp(topic) {
400
402
  ' grok [--none] [--create] [--local] [--show] [--generate] [--yolo|--sandboxed] [args...]',
401
403
  ' Run grok with generated private config',
402
404
  ' --clean all|AGENTS Delete saved sessions for selected agents',
405
+ ' --clean-old all|AGENTS Delete sessions older than configured retention (7 days)',
403
406
  ' compress [--agent NAME] [--scope SCOPE] [--dry-run] Consolidate brain knowledge',
404
407
  ' setup [org/repo] Install the default tree and selected profile',
405
408
  ' check [--all] [path] Validate generated profile output',
package/dist/commands.js CHANGED
@@ -27,9 +27,11 @@ function main(invokedTool, argv) {
27
27
  }
28
28
  function dispatch(command) {
29
29
  switch (command.command) {
30
- case 'clean':
31
- (0, clean_sessions_1.reportCleanup)((0, clean_sessions_1.cleanSessions)((0, project_1.defaultConfigRoot)(process.cwd()), command.agents, true), true);
30
+ case 'clean': {
31
+ const root = (0, project_1.defaultConfigRoot)(process.cwd());
32
+ (0, clean_sessions_1.reportCleanup)((0, clean_sessions_1.cleanSessions)(root, command.agents, !command.olderOnly, Date.now(), command.olderOnly ? (0, clean_sessions_1.sessionRetentionDays)(root) : 7), true);
32
33
  return;
34
+ }
33
35
  case 'compress':
34
36
  void runCompress(command).catch((error) => { process.stderr.write(`agent-run: ${(0, utils_1.formatError)(error)}\n`); process.exitCode = 1; });
35
37
  return;
@@ -155,7 +157,6 @@ async function runTool(parsed) {
155
157
  if (!runtime) {
156
158
  (0, utils_1.fail)(`no generated ${command} runtime found for profile ${rendered.profile}`);
157
159
  }
158
- (0, clean_sessions_1.reportCleanup)((0, clean_sessions_1.cleanSessions)(configRoot));
159
160
  (0, spawn_agent_1.spawnAgent)(adapter.spawn(runtime, { binary: realBinary, passthroughArgs: args, wrapperArgs }));
160
161
  }
161
162
  function runStatus() {
package/dist/project.js CHANGED
@@ -12,6 +12,7 @@ exports.resolveProfile = resolveProfile;
12
12
  exports.resolveProfileResult = resolveProfileResult;
13
13
  exports.parseProfile = parseProfile;
14
14
  exports.projectRootForProfile = projectRootForProfile;
15
+ exports.parseEnvFile = parseEnvFile;
15
16
  exports.defaultConfigRoot = defaultConfigRoot;
16
17
  const childProcess = require("child_process");
17
18
  const fs = require("fs");
@@ -1,5 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.sessionRetentionDays = sessionRetentionDays;
3
4
  exports.cleanSessions = cleanSessions;
4
5
  exports.reportCleanup = reportCleanup;
5
6
  const fs = require("node:fs");
@@ -14,6 +15,17 @@ const manifest_1 = require("../manifest");
14
15
  const templates_1 = require("../templates");
15
16
  const project_1 = require("../project");
16
17
  const WEEK = 7 * 24 * 60 * 60 * 1000;
18
+ const DAY = WEEK / 7;
19
+ function sessionRetentionDays(configRoot) {
20
+ const file = path.join(configRoot, 'cleanup.env');
21
+ const settings = fs.existsSync(file) ? (0, project_1.parseEnvFile)(fs.readFileSync(file, 'utf8')) : {};
22
+ const value = process.env.AGENT_RUN_SESSION_RETENTION_DAYS ?? settings.AGENT_RUN_SESSION_RETENTION_DAYS ?? '7';
23
+ const days = Number(value);
24
+ if (!/^\d+$/.test(value) || !Number.isSafeInteger(days * DAY)) {
25
+ throw new Error('AGENT_RUN_SESSION_RETENTION_DAYS must be a non-negative whole number of days (0 disables retention cleanup).');
26
+ }
27
+ return days;
28
+ }
17
29
  const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
18
30
  // Do not follow symlinks, including symlinked parent directories or another user's home.
19
31
  function owned(file) {
@@ -160,9 +172,11 @@ function pruneIndexes(home, agent, ids) {
160
172
  }
161
173
  }
162
174
  }
163
- function cleanSessions(configRoot, agents = types_1.AGENT_IDS, all = false, now = Date.now()) {
175
+ function cleanSessions(configRoot, agents = types_1.AGENT_IDS, all = false, now = Date.now(), retentionDays = 7) {
164
176
  return agents.map((agent) => {
165
177
  const result = { agent, removed: 0, errors: [] };
178
+ if (!all && retentionDays === 0)
179
+ return result;
166
180
  let directories;
167
181
  try {
168
182
  directories = homes(configRoot, agent);
@@ -177,7 +191,7 @@ function cleanSessions(configRoot, agents = types_1.AGENT_IDS, all = false, now
177
191
  continue;
178
192
  const removed = new Set();
179
193
  for (const session of fileSessions(home, agent)) {
180
- if (!all && session.modified >= now - WEEK)
194
+ if (!all && session.modified >= now - retentionDays * DAY)
181
195
  continue;
182
196
  if ([process.env.CODEX_THREAD_ID, process.env.CODEX_SESSION_ID, process.env.CLAUDE_SESSION_ID, process.env.GROK_SESSION_ID].includes(session.id)) {
183
197
  if (all)
@@ -15,3 +15,4 @@ Environment=PNPM_HOME=/usr/local
15
15
  Environment=PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
16
16
  EnvironmentFile=-/etc/default/ai-tools-update
17
17
  ExecStart=/usr/local/libexec/agent-run/update-ai-tools
18
+ ExecStartPost=/usr/local/libexec/agent-run/cleanup-agent-sessions
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@technomoron/agent-run",
3
- "version": "0.99.44",
3
+ "version": "0.99.45",
4
4
  "packageManager": "pnpm@10.34.5",
5
5
  "bin": {
6
6
  "agent-run": "dist/agent-run.js",
@@ -17,8 +17,11 @@
17
17
  "scripts/install-systemd-jobs.sh",
18
18
  "scripts/update-ai-tools.sh",
19
19
  "scripts/ensure-agent-brain.sh",
20
+ "scripts/cleanup-agent-sessions.sh",
20
21
  "scripts/brain-pipe-security.ps1",
21
22
  "scripts/install-windows-brain.ps1",
23
+ "scripts/install-windows-cleanup.ps1",
24
+ "scripts/run-windows-cleanup.ps1",
22
25
  "scripts/run-windows-brain.ps1",
23
26
  "scripts/run-hidden.vbs",
24
27
  "README.md",
@@ -36,7 +39,7 @@
36
39
  "cleanbuild": "run-s clean:dist build",
37
40
  "prepack": "run-s build",
38
41
  "test": "run-s build test:codex-profiles test:integration test:release",
39
- "test:codex-profiles": "node --test scripts/test-codex-profiles.cjs scripts/test-session-cleanup.cjs",
42
+ "test:codex-profiles": "node --test scripts/test-codex-profiles.cjs scripts/test-session-cleanup.cjs scripts/test-windows-cleanup.cjs",
40
43
  "test:brain": "node --test scripts/test-brain.cjs",
41
44
  "test:integration": "bash ./scripts/test.sh",
42
45
  "test:release": "bash ./scripts/test-release.sh",
@@ -0,0 +1,27 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ if [ "$(id -u)" -ne 0 ]; then
5
+ echo "cleanup-agent-sessions.sh must run as root" >&2
6
+ exit 1
7
+ fi
8
+
9
+ # Select accounts explicitly; never scan every home or run native CLIs as root.
10
+ read -r -a accounts <<<"${AI_TOOLS_CLEANUP_USERS-${AI_TOOLS_BRAIN_USERS:-}}"
11
+ status=0
12
+ for account in "${accounts[@]}"; do
13
+ entry="$(getent passwd "$account")" || { echo "Unknown cleanup account: $account" >&2; status=1; continue; }
14
+ IFS=: read -r username password uid gid gecos account_home login_shell <<<"$entry"
15
+ if [ "$uid" -lt 1000 ] || [ "$uid" -eq 65534 ]; then
16
+ echo "Refusing to clean sessions for system account: $account" >&2
17
+ status=1
18
+ continue
19
+ fi
20
+ # A clean environment avoids leaking root's home, session IDs or native settings.
21
+ if ! runuser -u "$username" -- env -i "HOME=$account_home" "USER=$username" "LOGNAME=$username" \
22
+ PATH=/usr/local/bin:/usr/bin:/bin /bin/sh -c 'cd "$HOME" && exec "$@"' cleanup-agent-sessions /usr/local/bin/agent-run \
23
+ --configdir "$account_home/.agent-run" --clean-old all; then
24
+ status=1
25
+ fi
26
+ done
27
+ exit "$status"
@@ -29,6 +29,7 @@ SERVICE_FILE="$ROOT/ops/systemd/ai-tools-update.service"
29
29
  TIMER_FILE="$ROOT/ops/systemd/ai-tools-update.timer"
30
30
  BRAIN_HELPER="$ROOT/scripts/ensure-agent-brain.sh"
31
31
  BRAIN_SERVICE="$ROOT/ops/systemd/agent-brain.service"
32
+ CLEANUP_HELPER="$ROOT/scripts/cleanup-agent-sessions.sh"
32
33
 
33
34
 
34
35
  INSTALL_AI_TOOLS=0
@@ -82,6 +83,7 @@ run() {
82
83
  run install -d -m 0755 "$INSTALL_LIBEXEC_DIR" "$SYSTEMD_DIR" "$SYSTEMD_USER_DIR"
83
84
  run install -m 0755 "$UPDATE_SCRIPT" "$INSTALL_LIBEXEC_DIR/update-ai-tools"
84
85
  run install -m 0755 "$BRAIN_HELPER" "$INSTALL_LIBEXEC_DIR/ensure-agent-brain"
86
+ run install -m 0755 "$CLEANUP_HELPER" "$INSTALL_LIBEXEC_DIR/cleanup-agent-sessions"
85
87
  run install -m 0644 "$BRAIN_SERVICE" "$SYSTEMD_USER_DIR/agent-brain.service"
86
88
  run install -m 0644 "$SERVICE_FILE" "$SYSTEMD_DIR/ai-tools-update.service"
87
89
  run install -m 0644 "$TIMER_FILE" "$SYSTEMD_DIR/ai-tools-update.timer"
@@ -0,0 +1,14 @@
1
+ param([string]$ConfigRoot = (Join-Path $env:USERPROFILE '.agent-run'))
2
+ $ErrorActionPreference = 'Stop'
3
+ $ConfigRoot = [System.IO.Path]::GetFullPath($ConfigRoot)
4
+ $runner = Join-Path $PSScriptRoot 'run-windows-cleanup.ps1'
5
+ $launcher = Join-Path $PSScriptRoot 'run-hidden.vbs'
6
+ if ($ConfigRoot.Contains('"') -or $runner.Contains('"') -or $launcher.Contains('"')) { throw 'Paths cannot contain quotes.' }
7
+ $wscript = Join-Path $env:SystemRoot 'System32\wscript.exe'
8
+ $account = [System.Security.Principal.WindowsIdentity]::GetCurrent().Name
9
+ $action = New-ScheduledTaskAction -Execute $wscript -Argument "//B //NoLogo `"$launcher`" `"$runner`" -ConfigRoot `"$ConfigRoot`"" -WorkingDirectory $env:USERPROFILE
10
+ $principal = New-ScheduledTaskPrincipal -UserId $account -LogonType Interactive -RunLevel Limited
11
+ $trigger = New-ScheduledTaskTrigger -Once -At (Get-Date).AddMinutes(10) -RepetitionInterval (New-TimeSpan -Hours 1)
12
+ $settings = New-ScheduledTaskSettingsSet -StartWhenAvailable -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -ExecutionTimeLimit (New-TimeSpan -Minutes 30) -MultipleInstances IgnoreNew
13
+ Register-ScheduledTask -TaskName 'Agent Session Cleanup' -Action $action -Principal $principal -Trigger $trigger -Settings $settings -Description 'Hourly agent-run session retention cleanup for the signed-in user.' -Force | Out-Null
14
+ Write-Host "Agent Session Cleanup registered hourly for $account. Retention: $ConfigRoot\cleanup.env (default 7 days). Log: $ConfigRoot\runtime\cleanup.log"
@@ -0,0 +1,23 @@
1
+ param([Parameter(Mandatory = $true)][string]$ConfigRoot)
2
+ $ErrorActionPreference = 'Stop'
3
+ $ConfigRoot = [System.IO.Path]::GetFullPath($ConfigRoot)
4
+ $env:Path = [Environment]::GetEnvironmentVariable('Path', 'Machine') + ';' + [Environment]::GetEnvironmentVariable('Path', 'User')
5
+ # Do not inherit a wrapper's guarded PATH or a particular interactive session.
6
+ foreach ($name in @('AGENT_RUN_REAL_PATH', 'CODEX_THREAD_ID', 'CODEX_SESSION_ID', 'CLAUDE_SESSION_ID', 'GROK_SESSION_ID')) {
7
+ [Environment]::SetEnvironmentVariable($name, $null, 'Process')
8
+ }
9
+ Set-Location $env:USERPROFILE
10
+ $runtime = Join-Path $ConfigRoot 'runtime'
11
+ New-Item -ItemType Directory -Path $runtime -Force | Out-Null
12
+ $log = Join-Path $runtime 'cleanup.log'
13
+ $agentRun = Join-Path $PSScriptRoot '..\dist\agent-run.js'
14
+ try {
15
+ $node = (Get-Command node.exe -ErrorAction Stop).Source
16
+ # Native stderr is diagnostic output; the native exit status decides failure.
17
+ $ErrorActionPreference = 'Continue'
18
+ & $node $agentRun --configdir $ConfigRoot --clean-old all *> $log
19
+ exit $LASTEXITCODE
20
+ } catch {
21
+ $_ | Out-File -FilePath $log
22
+ exit 1
23
+ }