@technomoron/agent-run 0.99.43 → 0.99.44

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,15 @@
1
1
  CHANGES
2
2
  =======
3
3
 
4
+ Version 0.99.44 (2026-10-14)
5
+ ----------------------------
6
+ * Remove local agent sessions last modified more than seven days ago before
7
+ startup, including Codex SQLite history, using native deletion where needed.
8
+ * Add --clean to delete all saved sessions for selected agents while preserving
9
+ authentication, settings, skills and brain knowledge.
10
+ * Check the configured brain MCP connection before launching an agent, and
11
+ explain socket ownership and permission failures.
12
+
4
13
  Version 0.99.43 (2026-09-25)
5
14
  ----------------------------
6
15
  * Report startup errors without a stack trace and include file ownership and
package/README.md CHANGED
@@ -281,7 +281,7 @@ Config root:
281
281
 
282
282
  ## Commands
283
283
 
284
- Global flag:
284
+ Global flags:
285
285
 
286
286
  - `-h`, `--help`: show wrapper help for `agent-run` and the built-in commands
287
287
  - `-v`, `--verbose`: print path resolution, file creation, include expansion,
@@ -290,6 +290,26 @@ Global flag:
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.
297
+
298
+ Delete all saved sessions for selected agents without launching an agent:
299
+
300
+ ```sh
301
+ agent-run --clean all
302
+ agent-run --clean codex,claude
303
+ agent-run --clean grok gemini
304
+ ```
305
+
306
+ `--configdir PATH` selects the configuration tree to scan. The native user homes
307
+ are also included. Codex and Grok deletion requires an installed CLI with its
308
+ native deletion command; Codex SQLite history is included. Failed or unsupported
309
+ deletions are reported and retained; explicit cleanup exits with status 1 if any
310
+ operation fails. A current session identified in the environment is skipped.
311
+ Symlinks and paths owned by a different user are not cleaned.
312
+
293
313
  For any native agent command, use `--generate` to generate profile files
294
314
  without launching the underlying tool.
295
315
 
@@ -1067,6 +1087,8 @@ The supplied service uses `serve --pull`. On startup this pulls the configuratio
1067
1087
  repository with `--ff-only --no-rebase` when it is clean and has an upstream.
1068
1088
  Dirty checkouts and missing upstreams are skipped. Divergence or network failures
1069
1089
  are reported in the service journal and the service continues with local files.
1090
+ Before launching any configured agent, agent-run initializes its enabled brain MCP connection and calls `brain_status`. If this fails, launch stops with the underlying error. A disabled brain MCP connection also blocks launch when brain is enabled. Run the agent and brain service as the same user; private socket ownership is enforced. `--none` remains an explicit configuration bypass.
1091
+
1070
1092
  Startup never commits, pushes, stashes, or rebases local work. Omit `--pull` to
1071
1093
  disable this behavior for a manually launched service.
1072
1094
 
@@ -116,7 +116,7 @@ function createBrainServer(store) {
116
116
  function requirePrivateSocketDirectory(socketPath) {
117
117
  const stat = fs.lstatSync(path.dirname(socketPath));
118
118
  if (!stat.isDirectory() || stat.uid !== process.getuid() || (stat.mode & 0o077) !== 0)
119
- throw new Error('The socket directory must be owned by the current user with mode 0700.');
119
+ throw new Error(`The socket directory ${path.dirname(socketPath)} must be owned by the current user with mode 0700 (current uid ${process.getuid()}, owner uid ${stat.uid}, mode ${(stat.mode & 0o777).toString(8)}). Run agent-run and agent-brain as the same user.`);
120
120
  }
121
121
  async function serveApiCore(configRoot, socketPath) {
122
122
  if (process.platform !== 'win32') {
package/dist/cli.js CHANGED
@@ -12,11 +12,26 @@ 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='))) {
16
+ return parseCleanCommand(options.args);
17
+ }
15
18
  if (command === null) {
16
19
  command = extractCommand(options.args);
17
20
  }
18
21
  return parseCommand(command, options.args);
19
22
  }
23
+ function parseCleanCommand(args) {
24
+ const flag = args[0];
25
+ const values = [
26
+ ...(flag.includes('=') ? [flag.slice('--clean='.length)] : []),
27
+ ...args.slice(1)
28
+ ].flatMap((value) => value.split(','));
29
+ if (!values.length || values.some((value) => value !== 'all' && !types_1.AGENT_IDS.includes(value))
30
+ || (values.includes('all') && values.length !== 1)) {
31
+ (0, utils_1.fail)('--clean requires all or a list of codex, claude, gemini, grok');
32
+ }
33
+ return { command: 'clean', agents: values[0] === 'all' ? [...types_1.AGENT_IDS] : [...new Set(values)] };
34
+ }
20
35
  function applyGlobalOptions(options) {
21
36
  if (options.verbose) {
22
37
  process.env[constants_1.VERBOSE_ENV] = '1';
@@ -52,6 +67,8 @@ function parseCommand(command, args) {
52
67
  case 'create':
53
68
  case 'project':
54
69
  return { command, args };
70
+ case 'clean':
71
+ (0, utils_1.fail)('Use agent-run --clean all|codex,claude,gemini,grok');
55
72
  case 'compress':
56
73
  return parseCompressCommand(args);
57
74
  case 'check':
@@ -156,6 +173,8 @@ function parseRunCommand(command, inputArgs) {
156
173
  const args = [];
157
174
  let passthrough = false;
158
175
  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');
159
178
  if (passthrough) {
160
179
  args.push(arg);
161
180
  continue;
@@ -380,6 +399,7 @@ function renderHelp(topic) {
380
399
  ' Run gemini with generated private config',
381
400
  ' grok [--none] [--create] [--local] [--show] [--generate] [--yolo|--sandboxed] [args...]',
382
401
  ' Run grok with generated private config',
402
+ ' --clean all|AGENTS Delete saved sessions for selected agents',
383
403
  ' compress [--agent NAME] [--scope SCOPE] [--dry-run] Consolidate brain knowledge',
384
404
  ' setup [org/repo] Install the default tree and selected profile',
385
405
  ' check [--all] [path] Validate generated profile output',
package/dist/commands.js CHANGED
@@ -13,6 +13,8 @@ const process_1 = require("./process");
13
13
  const project_1 = require("./project");
14
14
  const renderer_1 = require("./renderer");
15
15
  const spawn_agent_1 = require("./runtime/spawn-agent");
16
+ const brain_preflight_1 = require("./runtime/brain-preflight");
17
+ const clean_sessions_1 = require("./runtime/clean-sessions");
16
18
  const utils_1 = require("./utils");
17
19
  function main(invokedTool, argv) {
18
20
  try {
@@ -25,6 +27,9 @@ function main(invokedTool, argv) {
25
27
  }
26
28
  function dispatch(command) {
27
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);
32
+ return;
28
33
  case 'compress':
29
34
  void runCompress(command).catch((error) => { process.stderr.write(`agent-run: ${(0, utils_1.formatError)(error)}\n`); process.exitCode = 1; });
30
35
  return;
@@ -57,7 +62,7 @@ function dispatch(command) {
57
62
  runMigrateConfig(command);
58
63
  return;
59
64
  default:
60
- runTool(command);
65
+ void runTool(command).catch((error) => { process.stderr.write(`agent-run: ${(0, utils_1.formatError)(error)}\n`); process.exitCode = 1; });
61
66
  }
62
67
  }
63
68
  async function runCompress(command) {
@@ -96,11 +101,11 @@ async function runCompress(command) {
96
101
  if (process.platform === 'win32')
97
102
  prompt = prompt.replace(/\r?\n/g, ' ');
98
103
  // Use the normal adapters, configuration and permission modes. The prompt is one argv value.
99
- runTool({ command: agent, args: agent === 'gemini' ? ['-i', prompt] : [prompt], wrapperArgs: {
104
+ await runTool({ command: agent, args: agent === 'gemini' ? ['-i', prompt] : [prompt], wrapperArgs: {
100
105
  none: false, create: false, local: false, show: false, generate: false, sandboxMode: null, codexNetwork: false
101
106
  } });
102
107
  }
103
- function runTool(parsed) {
108
+ async function runTool(parsed) {
104
109
  const { args, command, wrapperArgs } = parsed;
105
110
  const adapter = (0, registry_1.getAgentAdapter)(command);
106
111
  const projectRoot = (0, project_1.findProjectRoot)(process.cwd());
@@ -144,11 +149,13 @@ function runTool(parsed) {
144
149
  }
145
150
  checkLocalAiFiles(command, projectRoot, agentDir, preview.context, wrapperArgs.local);
146
151
  const realBinary = (0, process_1.findRealBinary)(command);
152
+ await (0, brain_preflight_1.requireBrain)(preview.context);
147
153
  const rendered = (0, renderer_1.syncRenderedProfile)(preview);
148
154
  const runtime = rendered.runtimes[command];
149
155
  if (!runtime) {
150
156
  (0, utils_1.fail)(`no generated ${command} runtime found for profile ${rendered.profile}`);
151
157
  }
158
+ (0, clean_sessions_1.reportCleanup)((0, clean_sessions_1.cleanSessions)(configRoot));
152
159
  (0, spawn_agent_1.spawnAgent)(adapter.spawn(runtime, { binary: realBinary, passthroughArgs: args, wrapperArgs }));
153
160
  }
154
161
  function runStatus() {
package/dist/process.js CHANGED
@@ -9,8 +9,8 @@ const path = require("path");
9
9
  const constants_1 = require("./constants");
10
10
  const utils_1 = require("./utils");
11
11
  const crossSpawn = require('cross-spawn');
12
- function findRealBinary(tool) {
13
- const pathDirs = (process.env.PATH || '').split(path.delimiter).filter(Boolean);
12
+ function findRealBinary(tool, searchPath = process.env.PATH ?? '') {
13
+ const pathDirs = searchPath.split(path.delimiter).filter(Boolean);
14
14
  const currentScriptArg = process.argv[1];
15
15
  const currentScript = currentScriptArg ? fs.realpathSync(currentScriptArg) : '';
16
16
  const wrappers = wrapperCandidates(tool);
@@ -31,7 +31,7 @@ function findRealBinary(tool) {
31
31
  return candidate;
32
32
  }
33
33
  }
34
- (0, utils_1.fail)(`no ${tool} binary found in PATH`);
34
+ throw new Error(`no ${tool} binary found in PATH`);
35
35
  }
36
36
  function wrapperCandidates(tool) {
37
37
  const names = [
@@ -0,0 +1,40 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.requireBrain = requireBrain;
4
+ const index_js_1 = require("@modelcontextprotocol/sdk/client/index.js");
5
+ const stdio_js_1 = require("@modelcontextprotocol/sdk/client/stdio.js");
6
+ const streamableHttp_js_1 = require("@modelcontextprotocol/sdk/client/streamableHttp.js");
7
+ const sse_js_1 = require("@modelcontextprotocol/sdk/client/sse.js");
8
+ const config_1 = require("../brain/config");
9
+ const environment_1 = require("./environment");
10
+ async function requireBrain(context) {
11
+ const server = context.mcpServers['agent-brain'];
12
+ if (!server?.enabled) {
13
+ if ((0, config_1.readBrainConfig)(context.configRoot)?.enabled)
14
+ throw new Error('Cannot start agent: agent-brain is enabled but its MCP connection is disabled or missing.');
15
+ return;
16
+ }
17
+ const client = new index_js_1.Client({ name: 'agent-run-preflight', version: '1.0.0' });
18
+ let diagnostics = '';
19
+ const transport = server.transport === 'stdio'
20
+ ? new stdio_js_1.StdioClientTransport({ command: server.command ?? '', args: server.args, cwd: server.cwd ?? context.projectRoot,
21
+ env: Object.fromEntries(Object.entries({ ...(0, environment_1.buildAgentEnvironment)(context), ...server.env }).filter((entry) => entry[1] !== undefined)), stderr: 'pipe' })
22
+ : server.transport === 'http'
23
+ ? new streamableHttp_js_1.StreamableHTTPClientTransport(new URL(server.url ?? ''), { requestInit: { headers: server.headers } })
24
+ : new sse_js_1.SSEClientTransport(new URL(server.url ?? ''), { requestInit: { headers: server.headers } });
25
+ if (transport instanceof stdio_js_1.StdioClientTransport)
26
+ transport.stderr?.on('data', (chunk) => { diagnostics = (diagnostics + chunk.toString()).slice(-8192); });
27
+ try {
28
+ await client.connect(transport, { timeout: 10000 });
29
+ const status = await client.callTool({ name: 'brain_status', arguments: {} }, undefined, { timeout: 10000 });
30
+ if (status.isError)
31
+ throw new Error(JSON.stringify(status.content));
32
+ }
33
+ catch (error) {
34
+ throw new Error(`Cannot start agent: agent-brain MCP check failed: ${error instanceof Error ? error.message : String(error)}${diagnostics.trim() ? `\n${diagnostics.trim()}` : ''}`);
35
+ }
36
+ finally {
37
+ await client.close();
38
+ await transport.close();
39
+ }
40
+ }
@@ -0,0 +1,230 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.cleanSessions = cleanSessions;
4
+ exports.reportCleanup = reportCleanup;
5
+ const fs = require("node:fs");
6
+ const os = require("node:os");
7
+ const path = require("node:path");
8
+ const node_sqlite_1 = require("node:sqlite");
9
+ const cross_spawn_1 = require("cross-spawn");
10
+ const process_1 = require("../process");
11
+ const types_1 = require("../agents/types");
12
+ const config_tree_1 = require("../config-tree");
13
+ const manifest_1 = require("../manifest");
14
+ const templates_1 = require("../templates");
15
+ const project_1 = require("../project");
16
+ const WEEK = 7 * 24 * 60 * 60 * 1000;
17
+ const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
18
+ // Do not follow symlinks, including symlinked parent directories or another user's home.
19
+ function owned(file) {
20
+ const stat = fs.lstatSync(file, { throwIfNoEntry: false });
21
+ if (!stat || stat.isSymbolicLink())
22
+ return undefined;
23
+ if (process.getuid && stat.uid !== process.getuid())
24
+ throw new Error(`Session path belongs to uid ${stat.uid}, current uid is ${process.getuid()}: ${file}`);
25
+ const parent = path.dirname(file);
26
+ if (parent !== file) {
27
+ const parentStat = fs.lstatSync(parent);
28
+ if (parentStat.isSymbolicLink())
29
+ return undefined;
30
+ // Shared ancestors such as /tmp need not belong to the user.
31
+ for (let dir = path.dirname(parent); path.dirname(dir) !== dir; dir = path.dirname(dir)) {
32
+ if (fs.lstatSync(dir).isSymbolicLink())
33
+ return undefined;
34
+ }
35
+ }
36
+ return stat;
37
+ }
38
+ function entries(dir) {
39
+ return owned(dir)?.isDirectory() ? fs.readdirSync(dir, { withFileTypes: true }) : [];
40
+ }
41
+ function files(dir) {
42
+ return entries(dir).flatMap((entry) => {
43
+ const file = path.join(dir, entry.name);
44
+ return entry.isDirectory() ? files(file) : entry.isFile() && owned(file)?.isFile() ? [file] : [];
45
+ });
46
+ }
47
+ function modified(paths) {
48
+ return Math.max(...paths.map((file) => owned(file)?.mtimeMs ?? Infinity));
49
+ }
50
+ function homes(configRoot, agent) {
51
+ const native = process.env[agent === 'claude' ? 'CLAUDE_CONFIG_DIR' : agent === 'gemini' ? 'GEMINI_CLI_HOME' : agent === 'grok' ? 'GROK_HOME' : 'CODEX_HOME'];
52
+ const result = [path.join(os.homedir(), `.${agent}`)];
53
+ if (native)
54
+ result.push(agent === 'gemini' ? path.join(native, '.gemini') : native);
55
+ if (agent === 'codex')
56
+ result.push(path.join(configRoot, 'runtime', 'codex'));
57
+ for (const profile of (0, config_tree_1.findProfileDirs)(configRoot)) {
58
+ const live = path.join(profile, 'live');
59
+ if (agent === 'codex') {
60
+ const profileName = path.relative(configRoot, profile).split(path.sep).join('/');
61
+ const manifest = (0, manifest_1.normalizeManifest)((0, manifest_1.loadManifest)(configRoot, profile, profileName), profileName);
62
+ const context = (0, templates_1.buildRenderContext)((0, project_1.projectRootForProfile)(profileName, configRoot), profile, configRoot, manifest, (0, templates_1.createNunjucksEnv)(configRoot));
63
+ result.push(context.paths.codexHomeDir);
64
+ }
65
+ if (agent === 'gemini')
66
+ result.push(path.join(live, 'gemini', 'home', '.gemini'));
67
+ if (agent === 'grok')
68
+ result.push(path.join(live, 'grok'));
69
+ }
70
+ return [...new Set(result.map((home) => {
71
+ const absolute = path.resolve(home);
72
+ const stat = fs.lstatSync(absolute, { throwIfNoEntry: false });
73
+ // Resolve system aliases such as macOS /var, but never follow a home symlink.
74
+ return stat && !stat.isSymbolicLink() ? fs.realpathSync(absolute) : absolute;
75
+ }))];
76
+ }
77
+ function fileSessions(home, agent) {
78
+ if (agent === 'grok') {
79
+ return entries(path.join(home, 'sessions')).filter((entry) => entry.isDirectory()).flatMap((group) => entries(path.join(home, 'sessions', group.name)).filter((entry) => entry.isDirectory() && UUID.test(entry.name)).map((entry) => {
80
+ const dir = path.join(home, 'sessions', group.name, entry.name);
81
+ return { id: entry.name, files: [dir], modified: modified([dir, ...files(dir)]) };
82
+ }));
83
+ }
84
+ if (agent === 'claude') {
85
+ return entries(path.join(home, 'projects')).filter((entry) => entry.isDirectory()).flatMap((group) => {
86
+ const dir = path.join(home, 'projects', group.name);
87
+ return entries(dir).filter((entry) => entry.isFile() && UUID.test(path.basename(entry.name, '.jsonl')) && entry.name.endsWith('.jsonl')).map((entry) => {
88
+ const id = path.basename(entry.name, '.jsonl');
89
+ const transcript = path.join(dir, entry.name);
90
+ const companions = [path.join(dir, id), path.join(home, 'file-history', id)].filter((file) => owned(file)?.isDirectory());
91
+ return { id, files: [transcript, ...companions], modified: modified([transcript, ...companions.flatMap(files)]) };
92
+ });
93
+ });
94
+ }
95
+ if (agent === 'gemini') {
96
+ return entries(path.join(home, 'tmp')).filter((entry) => entry.isDirectory()).flatMap((group) => {
97
+ const dir = path.join(home, 'tmp', group.name, 'chats');
98
+ return entries(dir).filter((entry) => entry.isFile() && entry.name.endsWith('.json')).map((entry) => {
99
+ const file = path.join(dir, entry.name);
100
+ return { id: entry.name, files: [file], modified: modified([file]) };
101
+ });
102
+ });
103
+ }
104
+ const sessions = new Map();
105
+ for (const dir of ['sessions', 'archived_sessions']) {
106
+ for (const file of files(path.join(home, dir)).filter((file) => file.endsWith('.jsonl'))) {
107
+ const id = path.basename(file, '.jsonl').slice(-36);
108
+ if (!UUID.test(id))
109
+ continue;
110
+ const previous = sessions.get(id);
111
+ sessions.set(id, { id, files: [...previous?.files ?? [], file], modified: Math.max(previous?.modified ?? 0, modified([file])) });
112
+ }
113
+ }
114
+ // Recent Codex keeps paginated history in SQLite; rollout files alone are incomplete.
115
+ for (const entry of entries(home).filter((entry) => entry.isFile() && /^state_\d+\.sqlite$/.test(entry.name))) {
116
+ const db = new node_sqlite_1.DatabaseSync(path.join(home, entry.name), { readOnly: true });
117
+ try {
118
+ for (const row of db.prepare('SELECT id, updated_at FROM threads').all()) {
119
+ if (typeof row.id !== 'string' || !UUID.test(row.id))
120
+ continue;
121
+ const previous = sessions.get(row.id);
122
+ sessions.set(row.id, { id: row.id, files: previous?.files ?? [], modified: Math.max(previous?.modified ?? 0, Number(row.updated_at) * 1000) });
123
+ }
124
+ }
125
+ finally {
126
+ db.close();
127
+ }
128
+ }
129
+ return [...sessions.values()];
130
+ }
131
+ function pruneIndexes(home, agent, ids) {
132
+ const jsonl = agent === 'claude' ? ['history.jsonl'] : agent === 'codex' ? ['history.jsonl', 'session_index.jsonl'] : [];
133
+ for (const name of jsonl) {
134
+ const file = path.join(home, name);
135
+ if (!owned(file)?.isFile())
136
+ continue;
137
+ const original = fs.readFileSync(file, 'utf8');
138
+ const lines = original.split(/(?<=\n)/).filter((line) => {
139
+ try {
140
+ const row = JSON.parse(line);
141
+ return !ids.has(row.sessionId ?? row.session_id ?? row.id);
142
+ }
143
+ catch {
144
+ return true;
145
+ }
146
+ });
147
+ if (lines.join('') !== original)
148
+ fs.writeFileSync(file, lines.join(''));
149
+ }
150
+ if (agent === 'claude') {
151
+ for (const group of entries(path.join(home, 'projects')).filter((entry) => entry.isDirectory())) {
152
+ const file = path.join(home, 'projects', group.name, 'sessions-index.json');
153
+ if (!owned(file)?.isFile())
154
+ continue;
155
+ const data = JSON.parse(fs.readFileSync(file, 'utf8'));
156
+ if (Array.isArray(data.entries)) {
157
+ data.entries = data.entries.filter((entry) => !ids.has(entry.sessionId ?? ''));
158
+ fs.writeFileSync(file, `${JSON.stringify(data, null, 2)}\n`);
159
+ }
160
+ }
161
+ }
162
+ }
163
+ function cleanSessions(configRoot, agents = types_1.AGENT_IDS, all = false, now = Date.now()) {
164
+ return agents.map((agent) => {
165
+ const result = { agent, removed: 0, errors: [] };
166
+ let directories;
167
+ try {
168
+ directories = homes(configRoot, agent);
169
+ }
170
+ catch (error) {
171
+ result.errors.push(error instanceof Error ? error.message : String(error));
172
+ return result;
173
+ }
174
+ for (const home of directories) {
175
+ try {
176
+ if (!owned(home)?.isDirectory())
177
+ continue;
178
+ const removed = new Set();
179
+ for (const session of fileSessions(home, agent)) {
180
+ if (!all && session.modified >= now - WEEK)
181
+ continue;
182
+ 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
+ if (all)
184
+ result.errors.push(`Skipped current session ${session.id}`);
185
+ continue;
186
+ }
187
+ try {
188
+ if (agent === 'codex' || agent === 'grok') {
189
+ // Let the native CLI remove history and update its own databases/indexes.
190
+ const binary = (0, process_1.findRealBinary)(agent, process.env.AGENT_RUN_REAL_PATH ?? process.env.PATH ?? '');
191
+ const response = (0, cross_spawn_1.sync)(binary, agent === 'codex' ? ['delete', '--force', session.id] : ['sessions', 'delete', session.id], {
192
+ env: { ...process.env, PATH: process.env.AGENT_RUN_REAL_PATH ?? process.env.PATH, [agent === 'codex' ? 'CODEX_HOME' : 'GROK_HOME']: home }, shell: false, encoding: 'utf8', timeout: 30000, maxBuffer: 1024 * 1024
193
+ });
194
+ if (response.error || response.status !== 0)
195
+ throw new Error(response.error?.message ?? (response.stderr || response.stdout || `delete exited ${response.status}`).trim());
196
+ }
197
+ else {
198
+ for (const file of session.files)
199
+ if (owned(file))
200
+ fs.rmSync(file, { recursive: true, force: true });
201
+ }
202
+ if (session.files.some((file) => fs.existsSync(file)))
203
+ throw new Error('Native deletion left session files in place');
204
+ removed.add(session.id);
205
+ result.removed++;
206
+ }
207
+ catch (error) {
208
+ result.errors.push(`${home}: ${session.id}: ${error instanceof Error ? error.message : String(error)}`);
209
+ }
210
+ }
211
+ if (removed.size)
212
+ pruneIndexes(home, agent, removed);
213
+ }
214
+ catch (error) {
215
+ result.errors.push(`${home}: ${error instanceof Error ? error.message : String(error)}`);
216
+ }
217
+ }
218
+ return result;
219
+ });
220
+ }
221
+ function reportCleanup(results, explicit = false) {
222
+ for (const result of results) {
223
+ if (explicit || result.removed)
224
+ process.stdout.write(`${result.agent}: removed ${result.removed} session(s)\n`);
225
+ for (const error of result.errors)
226
+ process.stderr.write(`agent-run: ${result.agent} session cleanup: ${error}\n`);
227
+ }
228
+ if (explicit && results.some((result) => result.errors.length))
229
+ process.exitCode = 1;
230
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@technomoron/agent-run",
3
- "version": "0.99.43",
3
+ "version": "0.99.44",
4
4
  "packageManager": "pnpm@10.34.5",
5
5
  "bin": {
6
6
  "agent-run": "dist/agent-run.js",
@@ -36,7 +36,7 @@
36
36
  "cleanbuild": "run-s clean:dist build",
37
37
  "prepack": "run-s build",
38
38
  "test": "run-s build test:codex-profiles test:integration test:release",
39
- "test:codex-profiles": "node --test scripts/test-codex-profiles.cjs",
39
+ "test:codex-profiles": "node --test scripts/test-codex-profiles.cjs scripts/test-session-cleanup.cjs",
40
40
  "test:brain": "node --test scripts/test-brain.cjs",
41
41
  "test:integration": "bash ./scripts/test.sh",
42
42
  "test:release": "bash ./scripts/test-release.sh",