@phnx-labs/agents-cli 1.22.11 → 1.22.13
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/CHANGELOG.md +44 -0
- package/README.md +5 -1
- package/dist/bin/agents +0 -0
- package/dist/commands/cli.js +12 -12
- package/dist/commands/events.d.ts +20 -1
- package/dist/commands/events.js +71 -55
- package/dist/commands/logs.js +14 -138
- package/dist/commands/repo.d.ts +1 -1
- package/dist/commands/repo.js +3 -3
- package/dist/commands/sessions.d.ts +15 -0
- package/dist/commands/sessions.js +79 -10
- package/dist/commands/view.d.ts +1 -1
- package/dist/commands/view.js +7 -7
- package/dist/commands/workflows.js +1 -0
- package/dist/lib/cli-resources.js +2 -2
- package/dist/lib/event-stream.d.ts +1 -1
- package/dist/lib/event-stream.js +1 -1
- package/dist/lib/events.d.ts +21 -12
- package/dist/lib/events.js +312 -65
- package/dist/lib/exec.js +1 -2
- package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
- package/dist/lib/migrate.d.ts +8 -0
- package/dist/lib/migrate.js +26 -62
- package/dist/lib/resources/workflows.js +41 -21
- package/dist/lib/resources.d.ts +1 -1
- package/dist/lib/runner.js +1 -2
- package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
- package/dist/lib/secrets/audit.js +1 -1
- package/dist/lib/startup/command-registry.js +1 -1
- package/dist/lib/state.d.ts +1 -1
- package/dist/lib/state.js +60 -2
- package/dist/lib/watchdog/runner.d.ts +13 -0
- package/dist/lib/watchdog/runner.js +75 -6
- package/dist/lib/workflows.d.ts +22 -4
- package/dist/lib/workflows.js +79 -29
- package/package.json +1 -1
package/dist/lib/events.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Centralized event logging for agents-cli.
|
|
3
3
|
*
|
|
4
|
-
* Structured JSONL audit
|
|
5
|
-
* gzip rotation at 10
|
|
4
|
+
* Structured JSONL audit logs at ~/.agents/.history/events/YYYY-MM-DD/events.jsonl with
|
|
5
|
+
* lossless numbered gzip rotation at 10 MiB and bounded retention.
|
|
6
6
|
*
|
|
7
7
|
* Features:
|
|
8
8
|
* - Rich metadata: hostname, platform, arch, pid, timezone
|
|
@@ -17,7 +17,7 @@ import * as os from 'os';
|
|
|
17
17
|
import { createHash } from 'node:crypto';
|
|
18
18
|
import { gzipSync, gunzipSync } from 'node:zlib';
|
|
19
19
|
import { ensureLockTarget, withFileLock } from './fs-atomic.js';
|
|
20
|
-
import {
|
|
20
|
+
import { getUserAgentsDir } from './state.js';
|
|
21
21
|
import { stampProvenance, resetEventProvenanceForTest } from './event-provenance.js';
|
|
22
22
|
/** Lazy perf warehouse write — avoids a hard cycle at module load. */
|
|
23
23
|
function recordPerfTiming(payload) {
|
|
@@ -47,14 +47,42 @@ function recordPerfTiming(payload) {
|
|
|
47
47
|
// unlike _resetForTest it survives a bare reset AND propagates to CLI subprocesses
|
|
48
48
|
// a test spawns, so fixture events can never land in the user's real log (#910).
|
|
49
49
|
let _eventsPath;
|
|
50
|
-
|
|
51
|
-
|
|
50
|
+
let _eventsPathOverride = false;
|
|
51
|
+
let _legacyMigrationChecked = false;
|
|
52
|
+
let _userAgentsDirOverride;
|
|
53
|
+
function userAgentsDir() {
|
|
54
|
+
return _userAgentsDirOverride ?? getUserAgentsDir();
|
|
52
55
|
}
|
|
53
|
-
function
|
|
54
|
-
|
|
56
|
+
function localDateKey(date = new Date()) {
|
|
57
|
+
const year = date.getFullYear();
|
|
58
|
+
const month = String(date.getMonth() + 1).padStart(2, '0');
|
|
59
|
+
const day = String(date.getDate()).padStart(2, '0');
|
|
60
|
+
return `${year}-${month}-${day}`;
|
|
61
|
+
}
|
|
62
|
+
function eventsRoot() {
|
|
63
|
+
if (_eventsPathOverride && _eventsPath)
|
|
64
|
+
return path.dirname(_eventsPath);
|
|
65
|
+
return path.join(userAgentsDir(), '.history', 'events');
|
|
66
|
+
}
|
|
67
|
+
function eventsPath(date = new Date()) {
|
|
68
|
+
if (_eventsPathOverride && _eventsPath)
|
|
69
|
+
return _eventsPath;
|
|
70
|
+
const override = _userAgentsDirOverride ? undefined : process.env.AGENTS_EVENTS_PATH;
|
|
71
|
+
if (override) {
|
|
72
|
+
_eventsPathOverride = true;
|
|
73
|
+
return (_eventsPath = override);
|
|
74
|
+
}
|
|
75
|
+
return path.join(eventsRoot(), localDateKey(date), 'events.jsonl');
|
|
76
|
+
}
|
|
77
|
+
function eventsDir(date = new Date()) {
|
|
78
|
+
return path.dirname(eventsPath(date));
|
|
55
79
|
}
|
|
56
80
|
/** Default retention period in days. */
|
|
57
81
|
const DEFAULT_RETENTION_DAYS = 7;
|
|
82
|
+
/** Default total footprint for the active log plus gzip archives (50 MiB). */
|
|
83
|
+
const DEFAULT_MAX_STORAGE_BYTES = 50 * 1024 * 1024;
|
|
84
|
+
/** Cross-process marker used to avoid a full archive scan on every append. */
|
|
85
|
+
const PRUNE_MARKER = '.last-prune';
|
|
58
86
|
/** Default max length for truncated strings. */
|
|
59
87
|
const DEFAULT_TRUNCATE_LENGTH = 500;
|
|
60
88
|
/** Gzip rotation threshold in bytes (10 MB). */
|
|
@@ -154,6 +182,8 @@ function getTimezoneName() {
|
|
|
154
182
|
}
|
|
155
183
|
}
|
|
156
184
|
function ensureLogsDir() {
|
|
185
|
+
eventsPath(); // Resolve an explicit AGENTS_EVENTS_PATH before migration checks.
|
|
186
|
+
migrateLegacyEventLogs();
|
|
157
187
|
if (!fs.existsSync(eventsDir())) {
|
|
158
188
|
fs.mkdirSync(eventsDir(), { recursive: true, mode: DIR_MODE });
|
|
159
189
|
}
|
|
@@ -167,6 +197,102 @@ function ensureLogsDir() {
|
|
|
167
197
|
}
|
|
168
198
|
}
|
|
169
199
|
}
|
|
200
|
+
/**
|
|
201
|
+
* Move root-level and interim flat-history event families into dated directories.
|
|
202
|
+
*
|
|
203
|
+
* The common case is a whole-family rename into an empty destination. A
|
|
204
|
+
* Each segment is assigned to the local calendar day of its filesystem mtime;
|
|
205
|
+
* new writes are split by day at source. A partially completed migration keeps
|
|
206
|
+
* the destination active file authoritative and assigns a fresh archive number,
|
|
207
|
+
* so no record is overwritten or silently discarded. The legacy active-file
|
|
208
|
+
* lock serializes this with older installed processes that still append there.
|
|
209
|
+
*/
|
|
210
|
+
export function migrateLegacyEventLogs(userDir = userAgentsDir()) {
|
|
211
|
+
if (_eventsPathOverride || _legacyMigrationChecked)
|
|
212
|
+
return 0;
|
|
213
|
+
_legacyMigrationChecked = true;
|
|
214
|
+
const destinationRoot = path.join(userDir, '.history', 'events');
|
|
215
|
+
const legacyActive = path.join(userDir, 'events.jsonl');
|
|
216
|
+
const interimActive = path.join(destinationRoot, 'events.jsonl');
|
|
217
|
+
const listArchives = (dir) => {
|
|
218
|
+
try {
|
|
219
|
+
return fs.readdirSync(dir)
|
|
220
|
+
.map((file) => ({ file, match: file.match(/^events\.(\d+)\.jsonl\.gz$/) }))
|
|
221
|
+
.filter((entry) => entry.match !== null)
|
|
222
|
+
.map((entry) => ({ file: entry.file, number: Number(entry.match[1]) }))
|
|
223
|
+
.sort((a, b) => a.number - b.number);
|
|
224
|
+
}
|
|
225
|
+
catch {
|
|
226
|
+
return [];
|
|
227
|
+
}
|
|
228
|
+
};
|
|
229
|
+
const hasSourceFiles = fs.existsSync(legacyActive) || fs.existsSync(interimActive) ||
|
|
230
|
+
listArchives(userDir).length > 0 || listArchives(destinationRoot).length > 0;
|
|
231
|
+
if (!hasSourceFiles)
|
|
232
|
+
return 0;
|
|
233
|
+
try {
|
|
234
|
+
fs.mkdirSync(destinationRoot, { recursive: true, mode: DIR_MODE });
|
|
235
|
+
ensureLockTarget(legacyActive, '', DIR_MODE);
|
|
236
|
+
return withFileLock(legacyActive, () => {
|
|
237
|
+
ensureLockTarget(interimActive, '', DIR_MODE);
|
|
238
|
+
return withFileLock(interimActive, () => {
|
|
239
|
+
const sourceFamilies = [
|
|
240
|
+
{ dir: userDir, active: legacyActive, archives: listArchives(userDir) },
|
|
241
|
+
{ dir: destinationRoot, active: interimActive, archives: listArchives(destinationRoot) },
|
|
242
|
+
];
|
|
243
|
+
let moved = 0;
|
|
244
|
+
const nextArchiveNumber = (dir) => {
|
|
245
|
+
const numbers = fs.readdirSync(dir)
|
|
246
|
+
.map((file) => file.match(/^events\.(\d+)\.jsonl\.gz$/))
|
|
247
|
+
.filter((match) => match !== null)
|
|
248
|
+
.map((match) => Number(match[1]));
|
|
249
|
+
return (numbers.length ? Math.max(...numbers) : 0) + 1;
|
|
250
|
+
};
|
|
251
|
+
const withDestinationLock = (dayDir, fn) => {
|
|
252
|
+
const destinationActive = path.join(dayDir, 'events.jsonl');
|
|
253
|
+
fs.mkdirSync(dayDir, { recursive: true, mode: DIR_MODE });
|
|
254
|
+
ensureLockTarget(destinationActive, '', DIR_MODE);
|
|
255
|
+
return withFileLock(destinationActive, fn);
|
|
256
|
+
};
|
|
257
|
+
for (const family of sourceFamilies) {
|
|
258
|
+
const activeBytes = fs.existsSync(family.active) ? fs.statSync(family.active).size : 0;
|
|
259
|
+
if (activeBytes > 0) {
|
|
260
|
+
const activeStat = fs.statSync(family.active);
|
|
261
|
+
const dayDir = path.join(destinationRoot, localDateKey(activeStat.mtime));
|
|
262
|
+
withDestinationLock(dayDir, () => {
|
|
263
|
+
const archivePath = path.join(dayDir, `events.${nextArchiveNumber(dayDir)}.jsonl.gz`);
|
|
264
|
+
fs.writeFileSync(archivePath, gzipSync(fs.readFileSync(family.active)), { mode: FILE_MODE });
|
|
265
|
+
fs.utimesSync(archivePath, activeStat.atime, activeStat.mtime);
|
|
266
|
+
fs.truncateSync(family.active, 0);
|
|
267
|
+
});
|
|
268
|
+
moved++;
|
|
269
|
+
}
|
|
270
|
+
for (const archive of family.archives) {
|
|
271
|
+
const source = path.join(family.dir, archive.file);
|
|
272
|
+
const dayDir = path.join(destinationRoot, localDateKey(fs.statSync(source).mtime));
|
|
273
|
+
withDestinationLock(dayDir, () => {
|
|
274
|
+
const destination = path.join(dayDir, `events.${nextArchiveNumber(dayDir)}.jsonl.gz`);
|
|
275
|
+
fs.renameSync(source, destination);
|
|
276
|
+
});
|
|
277
|
+
moved++;
|
|
278
|
+
}
|
|
279
|
+
try {
|
|
280
|
+
if (fs.existsSync(family.active) && fs.statSync(family.active).size === 0)
|
|
281
|
+
fs.unlinkSync(family.active);
|
|
282
|
+
}
|
|
283
|
+
catch { /* an older process may have reopened it */ }
|
|
284
|
+
}
|
|
285
|
+
return moved;
|
|
286
|
+
});
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
catch {
|
|
290
|
+
// Audit logging must remain fail-soft. Files stay at the legacy path and a
|
|
291
|
+
// later process retries because this guard is process-local.
|
|
292
|
+
_legacyMigrationChecked = false;
|
|
293
|
+
return 0;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
170
296
|
// ─── Redaction ────────────────────────────────────────────────────────────────
|
|
171
297
|
/**
|
|
172
298
|
* Replace a prompt string with length + short SHA so we can correlate runs
|
|
@@ -380,7 +506,8 @@ export function emit(event, payload = {}, overrides = {}) {
|
|
|
380
506
|
// May fail if not owner
|
|
381
507
|
}
|
|
382
508
|
}
|
|
383
|
-
maybeGzipRotateLocked(logPath);
|
|
509
|
+
const rotated = maybeGzipRotateLocked(logPath);
|
|
510
|
+
maybePruneLocked(rotated);
|
|
384
511
|
});
|
|
385
512
|
}
|
|
386
513
|
catch {
|
|
@@ -657,7 +784,7 @@ export function emitFriction(surface, failureId, payload = {}) {
|
|
|
657
784
|
function maybeGzipRotateLocked(logPath) {
|
|
658
785
|
const stat = fs.statSync(logPath);
|
|
659
786
|
if (stat.size < GZIP_ROTATION_BYTES)
|
|
660
|
-
return;
|
|
787
|
+
return false;
|
|
661
788
|
const raw = fs.readFileSync(logPath);
|
|
662
789
|
const tmpArchive = path.join(eventsDir(), `.events.1.jsonl.gz.${process.pid}.tmp`);
|
|
663
790
|
fs.writeFileSync(tmpArchive, gzipSync(raw), { mode: FILE_MODE });
|
|
@@ -672,6 +799,7 @@ function maybeGzipRotateLocked(logPath) {
|
|
|
672
799
|
}
|
|
673
800
|
fs.renameSync(tmpArchive, path.join(eventsDir(), 'events.1.jsonl.gz'));
|
|
674
801
|
fs.truncateSync(logPath, 0);
|
|
802
|
+
return true;
|
|
675
803
|
}
|
|
676
804
|
catch (err) {
|
|
677
805
|
try {
|
|
@@ -681,45 +809,179 @@ function maybeGzipRotateLocked(logPath) {
|
|
|
681
809
|
throw err;
|
|
682
810
|
}
|
|
683
811
|
}
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
812
|
+
function listEventLogFiles() {
|
|
813
|
+
const current = eventsPath();
|
|
814
|
+
const files = [];
|
|
815
|
+
const addFile = (filePath, gzip) => {
|
|
816
|
+
try {
|
|
817
|
+
const stat = fs.statSync(filePath);
|
|
818
|
+
files.push({
|
|
819
|
+
path: filePath,
|
|
820
|
+
gzip,
|
|
821
|
+
currentActive: filePath === current,
|
|
822
|
+
mtimeMs: stat.mtimeMs,
|
|
823
|
+
size: stat.size,
|
|
824
|
+
});
|
|
825
|
+
}
|
|
826
|
+
catch { /* file may rotate while an unlocked reader enumerates */ }
|
|
827
|
+
};
|
|
828
|
+
const addDirectory = (dir) => {
|
|
829
|
+
let names = [];
|
|
830
|
+
try {
|
|
831
|
+
names = fs.readdirSync(dir);
|
|
832
|
+
}
|
|
833
|
+
catch {
|
|
834
|
+
return;
|
|
835
|
+
}
|
|
836
|
+
for (const name of names) {
|
|
837
|
+
if (name !== 'events.jsonl' && !/^events\.\d+\.jsonl\.gz$/.test(name))
|
|
838
|
+
continue;
|
|
839
|
+
addFile(path.join(dir, name), name.endsWith('.gz'));
|
|
840
|
+
}
|
|
841
|
+
};
|
|
842
|
+
if (_eventsPathOverride) {
|
|
843
|
+
if (path.basename(current) !== 'events.jsonl') {
|
|
844
|
+
addFile(current, false);
|
|
845
|
+
let names = [];
|
|
846
|
+
try {
|
|
847
|
+
names = fs.readdirSync(eventsDir());
|
|
848
|
+
}
|
|
849
|
+
catch {
|
|
850
|
+
return files;
|
|
851
|
+
}
|
|
852
|
+
for (const name of names) {
|
|
853
|
+
if (/^events\.\d+\.jsonl\.gz$/.test(name))
|
|
854
|
+
addFile(path.join(eventsDir(), name), true);
|
|
704
855
|
}
|
|
856
|
+
return files;
|
|
705
857
|
}
|
|
706
|
-
|
|
858
|
+
addDirectory(eventsDir());
|
|
859
|
+
return files;
|
|
860
|
+
}
|
|
861
|
+
let days = [];
|
|
862
|
+
try {
|
|
863
|
+
days = fs.readdirSync(eventsRoot()).filter((name) => /^\d{4}-\d{2}-\d{2}$/.test(name));
|
|
707
864
|
}
|
|
708
865
|
catch {
|
|
709
|
-
return
|
|
866
|
+
return files;
|
|
710
867
|
}
|
|
868
|
+
for (const day of days)
|
|
869
|
+
addDirectory(path.join(eventsRoot(), day));
|
|
870
|
+
return files;
|
|
711
871
|
}
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
let
|
|
716
|
-
|
|
717
|
-
|
|
872
|
+
function removeEmptyDayDirectories() {
|
|
873
|
+
if (_eventsPathOverride)
|
|
874
|
+
return;
|
|
875
|
+
let days = [];
|
|
876
|
+
try {
|
|
877
|
+
days = fs.readdirSync(eventsRoot()).filter((name) => /^\d{4}-\d{2}-\d{2}$/.test(name));
|
|
878
|
+
}
|
|
879
|
+
catch {
|
|
880
|
+
return;
|
|
881
|
+
}
|
|
882
|
+
for (const day of days) {
|
|
883
|
+
const dir = path.join(eventsRoot(), day);
|
|
884
|
+
try {
|
|
885
|
+
if (fs.readdirSync(dir).length === 0)
|
|
886
|
+
fs.rmdirSync(dir);
|
|
887
|
+
}
|
|
888
|
+
catch { /* best effort */ }
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
function finalizePastDayLogs() {
|
|
892
|
+
if (_eventsPathOverride)
|
|
893
|
+
return;
|
|
894
|
+
const currentDay = localDateKey();
|
|
895
|
+
for (const file of listEventLogFiles()) {
|
|
896
|
+
if (path.basename(file.path) !== 'events.jsonl')
|
|
897
|
+
continue;
|
|
898
|
+
if (path.basename(path.dirname(file.path)) === currentDay)
|
|
899
|
+
continue;
|
|
900
|
+
try {
|
|
901
|
+
withFileLock(file.path, () => {
|
|
902
|
+
const dir = path.dirname(file.path);
|
|
903
|
+
const numbers = fs.readdirSync(dir)
|
|
904
|
+
.map((name) => name.match(/^events\.(\d+)\.jsonl\.gz$/))
|
|
905
|
+
.filter((match) => match !== null)
|
|
906
|
+
.map((match) => Number(match[1]));
|
|
907
|
+
const next = (numbers.length ? Math.max(...numbers) : 0) + 1;
|
|
908
|
+
const target = path.join(dir, `events.${next}.jsonl.gz`);
|
|
909
|
+
const sourceStat = fs.statSync(file.path);
|
|
910
|
+
fs.writeFileSync(target, gzipSync(fs.readFileSync(file.path)), { mode: FILE_MODE });
|
|
911
|
+
fs.utimesSync(target, sourceStat.atime, sourceStat.mtime);
|
|
912
|
+
fs.unlinkSync(file.path);
|
|
913
|
+
});
|
|
914
|
+
}
|
|
915
|
+
catch { /* retry on the next prune */ }
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
function pruneEventLogsLocked(retentionDays = DEFAULT_RETENTION_DAYS, maxStorageBytes = DEFAULT_MAX_STORAGE_BYTES) {
|
|
919
|
+
finalizePastDayLogs();
|
|
920
|
+
const result = { removedByAge: 0, removedBySize: 0, bytesReclaimed: 0 };
|
|
921
|
+
const cutoff = Date.now() - retentionDays * 24 * 60 * 60 * 1000;
|
|
922
|
+
let files = listEventLogFiles();
|
|
923
|
+
for (const file of files) {
|
|
924
|
+
if (file.currentActive || file.mtimeMs >= cutoff)
|
|
925
|
+
continue;
|
|
926
|
+
try {
|
|
927
|
+
fs.unlinkSync(file.path);
|
|
928
|
+
result.removedByAge++;
|
|
929
|
+
result.bytesReclaimed += file.size;
|
|
930
|
+
}
|
|
931
|
+
catch { /* another process may already have removed it */ }
|
|
932
|
+
}
|
|
933
|
+
files = listEventLogFiles();
|
|
934
|
+
let totalBytes = files.reduce((sum, file) => sum + file.size, 0);
|
|
935
|
+
const oldestFirst = files
|
|
936
|
+
.filter((file) => !file.currentActive)
|
|
937
|
+
.sort((a, b) => a.mtimeMs - b.mtimeMs || a.path.localeCompare(b.path));
|
|
938
|
+
for (const file of oldestFirst) {
|
|
939
|
+
if (totalBytes <= maxStorageBytes)
|
|
940
|
+
break;
|
|
941
|
+
try {
|
|
942
|
+
fs.unlinkSync(file.path);
|
|
943
|
+
totalBytes -= file.size;
|
|
944
|
+
result.removedBySize++;
|
|
945
|
+
result.bytesReclaimed += file.size;
|
|
946
|
+
}
|
|
947
|
+
catch { /* another process may already have removed it */ }
|
|
948
|
+
}
|
|
949
|
+
removeEmptyDayDirectories();
|
|
950
|
+
return result;
|
|
951
|
+
}
|
|
952
|
+
/** Apply age retention and the total-size ceiling immediately. */
|
|
953
|
+
export function rotate(retentionDays = DEFAULT_RETENTION_DAYS, maxStorageBytes = DEFAULT_MAX_STORAGE_BYTES) {
|
|
954
|
+
try {
|
|
955
|
+
ensureLogsDir();
|
|
956
|
+
const active = eventsPath();
|
|
957
|
+
ensureLockTarget(active, '', DIR_MODE);
|
|
958
|
+
return withFileLock(active, () => pruneEventLogsLocked(retentionDays, maxStorageBytes));
|
|
959
|
+
}
|
|
960
|
+
catch {
|
|
961
|
+
return { removedByAge: 0, removedBySize: 0, bytesReclaimed: 0 };
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
/** Prune daily across processes, and immediately after a size rotation. */
|
|
965
|
+
function maybePruneLocked(force) {
|
|
966
|
+
const marker = path.join(eventsRoot(), PRUNE_MARKER);
|
|
718
967
|
const oneDayMs = 24 * 60 * 60 * 1000;
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
968
|
+
let due = force;
|
|
969
|
+
try {
|
|
970
|
+
due ||= Date.now() - fs.statSync(marker).mtimeMs > oneDayMs;
|
|
722
971
|
}
|
|
972
|
+
catch {
|
|
973
|
+
due = true;
|
|
974
|
+
}
|
|
975
|
+
if (!due)
|
|
976
|
+
return;
|
|
977
|
+
const maxBytes = force
|
|
978
|
+
? DEFAULT_MAX_STORAGE_BYTES - GZIP_ROTATION_BYTES
|
|
979
|
+
: DEFAULT_MAX_STORAGE_BYTES;
|
|
980
|
+
pruneEventLogsLocked(DEFAULT_RETENTION_DAYS, maxBytes);
|
|
981
|
+
try {
|
|
982
|
+
fs.writeFileSync(marker, '', { mode: FILE_MODE });
|
|
983
|
+
}
|
|
984
|
+
catch { /* retry next append */ }
|
|
723
985
|
}
|
|
724
986
|
// ─── Query ────────────────────────────────────────────────────────────────────
|
|
725
987
|
/**
|
|
@@ -731,19 +993,9 @@ export function maybeRotate() {
|
|
|
731
993
|
export function query(options) {
|
|
732
994
|
const { startDate, endDate = new Date(), eventTypes, level, agent, sessionId, caller, command, module, bundle, limit } = options;
|
|
733
995
|
const results = [];
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
const files =
|
|
737
|
-
if (fs.existsSync(eventsPath()))
|
|
738
|
-
files.push({ path: eventsPath(), gzip: false });
|
|
739
|
-
const archives = fs.readdirSync(eventsDir())
|
|
740
|
-
.map((file) => ({ file, match: file.match(/^events\.(\d+)\.jsonl\.gz$/) }))
|
|
741
|
-
.filter((entry) => entry.match !== null)
|
|
742
|
-
.map((entry) => ({ file: entry.file, number: Number(entry.match[1]) }))
|
|
743
|
-
.sort((a, b) => a.number - b.number);
|
|
744
|
-
for (const archive of archives) {
|
|
745
|
-
files.push({ path: path.join(eventsDir(), archive.file), gzip: true });
|
|
746
|
-
}
|
|
996
|
+
eventsPath(); // Resolve AGENTS_EVENTS_PATH before deciding whether to migrate.
|
|
997
|
+
migrateLegacyEventLogs();
|
|
998
|
+
const files = listEventLogFiles().sort((a, b) => Number(b.currentActive) - Number(a.currentActive) || b.mtimeMs - a.mtimeMs || b.path.localeCompare(a.path));
|
|
747
999
|
const startMs = startDate?.getTime();
|
|
748
1000
|
const endMs = endDate?.getTime();
|
|
749
1001
|
for (const file of files) {
|
|
@@ -849,16 +1101,9 @@ export function stats(options = {}) {
|
|
|
849
1101
|
let fileCount = 0;
|
|
850
1102
|
let totalBytes = 0;
|
|
851
1103
|
try {
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
for (const f of files) {
|
|
856
|
-
try {
|
|
857
|
-
totalBytes += fs.statSync(path.join(eventsDir(), f)).size;
|
|
858
|
-
}
|
|
859
|
-
catch { /* skip */ }
|
|
860
|
-
}
|
|
861
|
-
}
|
|
1104
|
+
const files = listEventLogFiles();
|
|
1105
|
+
fileCount = files.length;
|
|
1106
|
+
totalBytes = files.reduce((sum, file) => sum + file.size, 0);
|
|
862
1107
|
}
|
|
863
1108
|
catch { /* skip */ }
|
|
864
1109
|
return {
|
|
@@ -876,9 +1121,11 @@ export function stats(options = {}) {
|
|
|
876
1121
|
export function getLogsPath() {
|
|
877
1122
|
return eventsPath();
|
|
878
1123
|
}
|
|
879
|
-
export function _resetForTest(overrideEventsPath) {
|
|
1124
|
+
export function _resetForTest(overrideEventsPath, overrideUserAgentsDir) {
|
|
880
1125
|
_eventsPath = overrideEventsPath;
|
|
1126
|
+
_eventsPathOverride = Boolean(overrideEventsPath);
|
|
1127
|
+
_userAgentsDirOverride = overrideUserAgentsDir;
|
|
1128
|
+
_legacyMigrationChecked = false;
|
|
881
1129
|
resetEventProvenanceForTest();
|
|
882
1130
|
_chmoddedPath = undefined;
|
|
883
|
-
lastRotationCheck = 0;
|
|
884
1131
|
}
|
package/dist/lib/exec.js
CHANGED
|
@@ -14,7 +14,7 @@ import { parseTimeout } from './routines.js';
|
|
|
14
14
|
import { getBinaryPath, getVersionHomePath, isVersionInstalled, resolveVersion } from './versions.js';
|
|
15
15
|
import { resolveModel, buildReasoningFlags } from './models.js';
|
|
16
16
|
import { isTierToken, resolveTier } from './model-tiers.js';
|
|
17
|
-
import {
|
|
17
|
+
import { createTimer, redactPrompt, redactArgs } from './events.js';
|
|
18
18
|
import { sanitizeProcessEnv } from './secrets/bundles.js';
|
|
19
19
|
import { resolveActor, actorEnv } from './actor.js';
|
|
20
20
|
import { getShimsDir, getHistoryDir, getUserAgentsDir } from './state.js';
|
|
@@ -1399,7 +1399,6 @@ async function spawnAgent(options) {
|
|
|
1399
1399
|
const runId = launchId;
|
|
1400
1400
|
options = { ...options, env: { ...options.env, AGENT_LAUNCH_ID: launchId } };
|
|
1401
1401
|
const watcherState = await setupBudgetWatcher(options, cwd, runId);
|
|
1402
|
-
maybeRotate();
|
|
1403
1402
|
const timer = createTimer('agent.run', {
|
|
1404
1403
|
agent: options.agent,
|
|
1405
1404
|
version: options.version,
|
|
Binary file
|
|
Binary file
|
package/dist/lib/migrate.d.ts
CHANGED
|
@@ -133,5 +133,13 @@ export declare function migrateRoutineDeviceToDevices(routinesDir?: string): voi
|
|
|
133
133
|
* real routines dir (and without racing the routine module's own tests).
|
|
134
134
|
*/
|
|
135
135
|
export declare function migrateWatchdogSentinelToRoutine(sentinelPath?: string, ensure?: (enabled: boolean) => void): void;
|
|
136
|
+
/**
|
|
137
|
+
* Rename cli/ → clis/ in each of the given `.agents/` directories.
|
|
138
|
+
*
|
|
139
|
+
* One-way, idempotent: no-op when src is absent. Throws when both
|
|
140
|
+
* `<dir>/cli` and `<dir>/clis` exist — the user must resolve the conflict
|
|
141
|
+
* manually before proceeding. Exported for unit-testing with temp dirs.
|
|
142
|
+
*/
|
|
143
|
+
export declare function migrateCliDirToClis(agentsDirs: string[]): void;
|
|
136
144
|
/** Run all idempotent migrations. Safe to call multiple times. */
|
|
137
145
|
export declare function runMigration(): Promise<void>;
|
package/dist/lib/migrate.js
CHANGED
|
@@ -998,7 +998,6 @@ function migrateRuntimeToHistory() {
|
|
|
998
998
|
moveDirOnce(path.join(USER_DIR, '.backups'), path.join(HISTORY_DIR, 'backups'));
|
|
999
999
|
moveDirOnce(path.join(USER_DIR, 'routines', 'runs'), path.join(HISTORY_DIR, 'runs'));
|
|
1000
1000
|
moveDirOnce(path.join(USER_DIR, 'teams', 'agents'), path.join(HISTORY_DIR, 'teams', 'agents'));
|
|
1001
|
-
migrateEventLogsToHistory();
|
|
1002
1001
|
// Drop any empty leftover skeletons created mid-rename (e.g. `versions/<agent>/<v>/home/`
|
|
1003
1002
|
// recreated by a concurrent process). The real data is already under .history/.
|
|
1004
1003
|
rmEmptyDirTree(path.join(USER_DIR, 'versions'));
|
|
@@ -1014,66 +1013,6 @@ function migrateRuntimeToHistory() {
|
|
|
1014
1013
|
catch { /* best-effort */ }
|
|
1015
1014
|
}
|
|
1016
1015
|
}
|
|
1017
|
-
/** Move the operational event stream out of the git-backed user-repo root. */
|
|
1018
|
-
function migrateEventLogsToHistory() {
|
|
1019
|
-
const destination = path.join(HISTORY_DIR, 'events');
|
|
1020
|
-
let files = [];
|
|
1021
|
-
try {
|
|
1022
|
-
files = fs.readdirSync(USER_DIR).filter((file) => file === 'events.jsonl' || /^events\.\d+\.jsonl\.gz$/.test(file));
|
|
1023
|
-
}
|
|
1024
|
-
catch {
|
|
1025
|
-
return;
|
|
1026
|
-
}
|
|
1027
|
-
try {
|
|
1028
|
-
fs.mkdirSync(destination, { recursive: true, mode: 0o700 });
|
|
1029
|
-
}
|
|
1030
|
-
catch {
|
|
1031
|
-
return;
|
|
1032
|
-
}
|
|
1033
|
-
const active = files.find((file) => file === 'events.jsonl');
|
|
1034
|
-
if (active) {
|
|
1035
|
-
const src = path.join(USER_DIR, active);
|
|
1036
|
-
const dest = path.join(destination, active);
|
|
1037
|
-
if (!fs.existsSync(dest)) {
|
|
1038
|
-
moveFileOnce(src, dest);
|
|
1039
|
-
}
|
|
1040
|
-
else {
|
|
1041
|
-
try {
|
|
1042
|
-
const lines = [fs.readFileSync(src, 'utf-8'), fs.readFileSync(dest, 'utf-8')]
|
|
1043
|
-
.flatMap((content) => content.split('\n').filter(Boolean))
|
|
1044
|
-
.map((line, index) => {
|
|
1045
|
-
try {
|
|
1046
|
-
const timestamp = Date.parse(JSON.parse(line).ts ?? '');
|
|
1047
|
-
return { line, index, timestamp: Number.isNaN(timestamp) ? Number.MAX_SAFE_INTEGER : timestamp };
|
|
1048
|
-
}
|
|
1049
|
-
catch {
|
|
1050
|
-
return { line, index, timestamp: Number.MAX_SAFE_INTEGER };
|
|
1051
|
-
}
|
|
1052
|
-
})
|
|
1053
|
-
.sort((a, b) => a.timestamp - b.timestamp || a.index - b.index)
|
|
1054
|
-
.map(({ line }) => line);
|
|
1055
|
-
atomicWriteFileSync(dest, `${lines.join('\n')}\n`, { mode: 0o600 });
|
|
1056
|
-
fs.unlinkSync(src);
|
|
1057
|
-
}
|
|
1058
|
-
catch { /* preserve the source for a later retry */ }
|
|
1059
|
-
}
|
|
1060
|
-
}
|
|
1061
|
-
const archives = files
|
|
1062
|
-
.map((file) => ({ file, match: file.match(/^events\.(\d+)\.jsonl\.gz$/) }))
|
|
1063
|
-
.filter((entry) => entry.match !== null)
|
|
1064
|
-
.sort((a, b) => Number(a.match[1]) - Number(b.match[1]));
|
|
1065
|
-
let nextArchive = fs.readdirSync(destination).reduce((max, file) => {
|
|
1066
|
-
const match = file.match(/^events\.(\d+)\.jsonl\.gz$/);
|
|
1067
|
-
return match ? Math.max(max, Number(match[1])) : max;
|
|
1068
|
-
}, 0) + 1;
|
|
1069
|
-
for (const archive of archives) {
|
|
1070
|
-
const preferred = path.join(destination, archive.file);
|
|
1071
|
-
const dest = fs.existsSync(preferred)
|
|
1072
|
-
? path.join(destination, `events.${nextArchive++}.jsonl.gz`)
|
|
1073
|
-
: preferred;
|
|
1074
|
-
moveFileOnce(path.join(USER_DIR, archive.file), dest);
|
|
1075
|
-
}
|
|
1076
|
-
}
|
|
1077
1016
|
/**
|
|
1078
1017
|
* Restore plugins from the cache bucket back to the user-root.
|
|
1079
1018
|
*
|
|
@@ -1694,7 +1633,7 @@ function containsOnlyDsStore(dir) {
|
|
|
1694
1633
|
function warnSystemOrphans() {
|
|
1695
1634
|
const SHIPPED_ALLOWLIST = new Set([
|
|
1696
1635
|
// resource directories shipped by the npm package
|
|
1697
|
-
'commands', 'hooks', 'skills', 'rules', 'mcp', '
|
|
1636
|
+
'commands', 'hooks', 'skills', 'rules', 'mcp', 'clis', 'permissions', 'subagents', 'profiles', 'agents', 'routines',
|
|
1698
1637
|
// top-level metadata files
|
|
1699
1638
|
'agents.yaml', 'hooks.yaml', 'README.md', 'CHANGELOG.md',
|
|
1700
1639
|
// git + repo metadata
|
|
@@ -2074,10 +2013,35 @@ export function migrateWatchdogSentinelToRoutine(sentinelPath = path.join(CACHE_
|
|
|
2074
2013
|
catch { /* already gone */ }
|
|
2075
2014
|
console.error('Migrated watchdog: legacy enable sentinel → watchdog routine (kept enabled)');
|
|
2076
2015
|
}
|
|
2016
|
+
/**
|
|
2017
|
+
* Rename cli/ → clis/ in each of the given `.agents/` directories.
|
|
2018
|
+
*
|
|
2019
|
+
* One-way, idempotent: no-op when src is absent. Throws when both
|
|
2020
|
+
* `<dir>/cli` and `<dir>/clis` exist — the user must resolve the conflict
|
|
2021
|
+
* manually before proceeding. Exported for unit-testing with temp dirs.
|
|
2022
|
+
*/
|
|
2023
|
+
export function migrateCliDirToClis(agentsDirs) {
|
|
2024
|
+
for (const agentsDir of agentsDirs) {
|
|
2025
|
+
const src = path.join(agentsDir, 'cli');
|
|
2026
|
+
const dest = path.join(agentsDir, 'clis');
|
|
2027
|
+
if (!fs.existsSync(src))
|
|
2028
|
+
continue;
|
|
2029
|
+
if (fs.existsSync(dest)) {
|
|
2030
|
+
throw new Error(`Migration conflict: both ${src} and ${dest} exist. ` +
|
|
2031
|
+
`Remove or merge the old cli/ directory into clis/ manually.`);
|
|
2032
|
+
}
|
|
2033
|
+
fs.renameSync(src, dest);
|
|
2034
|
+
}
|
|
2035
|
+
}
|
|
2077
2036
|
/** Run all idempotent migrations. Safe to call multiple times. */
|
|
2078
2037
|
export async function runMigration() {
|
|
2079
2038
|
// MUST run first: every other migrator reads SYSTEM_DIR (the new path).
|
|
2080
2039
|
foldLegacySystemRepo();
|
|
2040
|
+
const cliMigrateDirs = [USER_DIR, SYSTEM_DIR];
|
|
2041
|
+
const projectDotAgents = path.join(process.cwd(), '.agents');
|
|
2042
|
+
if (fs.existsSync(projectDotAgents))
|
|
2043
|
+
cliMigrateDirs.push(projectDotAgents);
|
|
2044
|
+
migrateCliDirToClis(cliMigrateDirs);
|
|
2081
2045
|
migrateAgentsYaml();
|
|
2082
2046
|
deleteSystemPromptsJson();
|
|
2083
2047
|
migrateSystemConfigJson();
|