@phnx-labs/agents-cli 1.22.10 → 1.22.12
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 +64 -0
- package/dist/bin/agents +0 -0
- package/dist/commands/beta.js +21 -9
- 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/projects.d.ts +1 -2
- package/dist/commands/projects.js +39 -47
- package/dist/commands/repo.d.ts +1 -1
- package/dist/commands/repo.js +3 -3
- package/dist/commands/view.d.ts +1 -1
- package/dist/commands/view.js +7 -7
- package/dist/index.js +1 -1
- package/dist/lib/beta.d.ts +1 -1
- package/dist/lib/beta.js +1 -1
- 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 +311 -64
- package/dist/lib/exec.js +1 -2
- package/dist/lib/feed-broadcast.d.ts +19 -5
- package/dist/lib/feed-broadcast.js +41 -8
- 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 -1
- package/dist/lib/project-probe.d.ts +1 -1
- package/dist/lib/project-probe.js +1 -1
- package/dist/lib/projects.d.ts +1 -1
- 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/types.d.ts +1 -1
- package/dist/lib/watchdog/runner.d.ts +13 -0
- package/dist/lib/watchdog/runner.js +75 -6
- 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
|
|
@@ -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,
|
|
@@ -48,8 +48,14 @@ export interface FeedBroadcastContext {
|
|
|
48
48
|
class?: string;
|
|
49
49
|
/** Block-only: cost-of-delay tag used by the urgency filter. */
|
|
50
50
|
cost?: string;
|
|
51
|
-
/** Block-only: the literal `agents focus <id>` command
|
|
51
|
+
/** Block-only: the literal `agents focus <id>` command (for a `{focus}` sink). */
|
|
52
52
|
focus?: string;
|
|
53
|
+
/** Block-only: the answer choices the operator can pick, in order. */
|
|
54
|
+
options?: string[];
|
|
55
|
+
/** Block-only: the fallback applied if nobody answers in time. */
|
|
56
|
+
safeDefault?: string;
|
|
57
|
+
/** Block-only: minutes before `safeDefault` applies. */
|
|
58
|
+
timeoutMinutes?: number;
|
|
53
59
|
}
|
|
54
60
|
/**
|
|
55
61
|
* Map an open block onto the broadcast context, so a block reaches the same sinks
|
|
@@ -69,9 +75,14 @@ export declare function blockBroadcastContext(block: {
|
|
|
69
75
|
host?: string;
|
|
70
76
|
questions?: Array<{
|
|
71
77
|
text?: string;
|
|
78
|
+
options?: Array<{
|
|
79
|
+
label?: string;
|
|
80
|
+
}>;
|
|
72
81
|
}>;
|
|
73
82
|
blockClass?: string;
|
|
74
83
|
costOfDelay?: string;
|
|
84
|
+
safeDefault?: string;
|
|
85
|
+
timeoutMinutes?: number;
|
|
75
86
|
ticket?: string;
|
|
76
87
|
pr?: string;
|
|
77
88
|
}, extras?: {
|
|
@@ -137,14 +148,17 @@ export declare function composeBroadcastFooter(ctx: FeedBroadcastContext): strin
|
|
|
137
148
|
* Title in a few words
|
|
138
149
|
*
|
|
139
150
|
* Body of what happened or the ask.
|
|
151
|
+
* Options: publish / wait (blocks with choices)
|
|
152
|
+
* Default in 15 min: wait (blocks with a safe default)
|
|
140
153
|
*
|
|
141
154
|
* Sent from grok/a02da0e2 on mac-mini
|
|
142
|
-
*
|
|
143
|
-
* https://… (optional attach URL)
|
|
155
|
+
* https://… (optional attach URL)
|
|
144
156
|
* ```
|
|
145
157
|
*
|
|
146
|
-
* Title first (scannable subject). Blank line. Body.
|
|
147
|
-
*
|
|
158
|
+
* Title first (scannable subject). Blank line. Body. Then the phone-actionable
|
|
159
|
+
* choices + default (a block that has stopped for the human), then footer
|
|
160
|
+
* provenance. No `agents focus <id>` line: a CLI command is unusable from a phone,
|
|
161
|
+
* so the safe default is the fallback and the message carries it. Prefer `{message}`
|
|
148
162
|
* over bare `{text}` in messaging sinks.
|
|
149
163
|
*/
|
|
150
164
|
export declare function composeBroadcastMessage(ctx: FeedBroadcastContext): string;
|
|
@@ -62,6 +62,12 @@ export function parseFeedPostLevel(raw) {
|
|
|
62
62
|
export function blockBroadcastContext(block, extras = {}) {
|
|
63
63
|
const ask = block.questions?.[0]?.text?.trim() || 'agent is blocked';
|
|
64
64
|
const links = [block.pr].filter((l) => !!l && /^https?:\/\//i.test(l));
|
|
65
|
+
// The answer choices + the safe default are what make the phone message
|
|
66
|
+
// actionable: the operator sees the options and what happens if they do not
|
|
67
|
+
// reply, instead of a `agents focus <id>` CLI command they cannot run from a phone.
|
|
68
|
+
const options = (block.questions?.[0]?.options ?? [])
|
|
69
|
+
.map((o) => o?.label?.trim())
|
|
70
|
+
.filter((l) => !!l);
|
|
65
71
|
// Prefer explicit title/body from the feed post; fall back to the ask as body.
|
|
66
72
|
const title = extras.title?.trim() || undefined;
|
|
67
73
|
const text = extras.body?.trim() || ask;
|
|
@@ -78,8 +84,12 @@ export function blockBroadcastContext(block, extras = {}) {
|
|
|
78
84
|
class: block.blockClass,
|
|
79
85
|
cost: block.costOfDelay,
|
|
80
86
|
// Short id: `agents focus` matches on a prefix, and a full uuid in a phone
|
|
81
|
-
// message is noise
|
|
87
|
+
// message is noise. Kept for a `{focus}` sink; the human message no longer
|
|
88
|
+
// shows it (a CLI command is unusable from a phone).
|
|
82
89
|
focus: `agents focus ${block.sessionId.slice(0, 8)}`,
|
|
90
|
+
...(options.length ? { options } : {}),
|
|
91
|
+
...(block.safeDefault ? { safeDefault: block.safeDefault } : {}),
|
|
92
|
+
...(block.timeoutMinutes ? { timeoutMinutes: block.timeoutMinutes } : {}),
|
|
83
93
|
...(links.length ? { links } : {}),
|
|
84
94
|
};
|
|
85
95
|
}
|
|
@@ -179,14 +189,17 @@ export function composeBroadcastFooter(ctx) {
|
|
|
179
189
|
* Title in a few words
|
|
180
190
|
*
|
|
181
191
|
* Body of what happened or the ask.
|
|
192
|
+
* Options: publish / wait (blocks with choices)
|
|
193
|
+
* Default in 15 min: wait (blocks with a safe default)
|
|
182
194
|
*
|
|
183
195
|
* Sent from grok/a02da0e2 on mac-mini
|
|
184
|
-
*
|
|
185
|
-
* https://… (optional attach URL)
|
|
196
|
+
* https://… (optional attach URL)
|
|
186
197
|
* ```
|
|
187
198
|
*
|
|
188
|
-
* Title first (scannable subject). Blank line. Body.
|
|
189
|
-
*
|
|
199
|
+
* Title first (scannable subject). Blank line. Body. Then the phone-actionable
|
|
200
|
+
* choices + default (a block that has stopped for the human), then footer
|
|
201
|
+
* provenance. No `agents focus <id>` line: a CLI command is unusable from a phone,
|
|
202
|
+
* so the safe default is the fallback and the message carries it. Prefer `{message}`
|
|
190
203
|
* over bare `{text}` in messaging sinks.
|
|
191
204
|
*/
|
|
192
205
|
export function composeBroadcastMessage(ctx) {
|
|
@@ -197,9 +210,21 @@ export function composeBroadcastMessage(ctx) {
|
|
|
197
210
|
const mid = title && body && title !== body ? body : undefined;
|
|
198
211
|
const footer = composeBroadcastFooter(ctx);
|
|
199
212
|
const link = ctx.links?.find((l) => /^https?:\/\//i.test(l));
|
|
200
|
-
//
|
|
201
|
-
//
|
|
202
|
-
|
|
213
|
+
// The action block: the one thing the operator can act on from a phone. Show the
|
|
214
|
+
// choices, then what happens if they do not answer. Deliberately NOT a CLI command
|
|
215
|
+
// (`agents focus <id>` is unusable from a phone) -- the safe default is the real
|
|
216
|
+
// fallback, so a block meant for a phone should always carry one.
|
|
217
|
+
const choices = ctx.options?.length
|
|
218
|
+
? `Options: ${ctx.options.map((o) => scrubOutboundDashes(o)).join(' / ')}`
|
|
219
|
+
: undefined;
|
|
220
|
+
const fallback = ctx.safeDefault
|
|
221
|
+
? (ctx.timeoutMinutes && ctx.timeoutMinutes > 0
|
|
222
|
+
? `Default in ${ctx.timeoutMinutes} min: ${scrubOutboundDashes(ctx.safeDefault)}`
|
|
223
|
+
: `Default: ${scrubOutboundDashes(ctx.safeDefault)}`)
|
|
224
|
+
: undefined;
|
|
225
|
+
const action = [choices, fallback].filter(Boolean).join('\n') || undefined;
|
|
226
|
+
// Link trail after the "Sent from" footer so the human sentence stays at the top.
|
|
227
|
+
const trail = [footer, link].filter(Boolean);
|
|
203
228
|
const parts = [];
|
|
204
229
|
if (head)
|
|
205
230
|
parts.push(head);
|
|
@@ -208,6 +233,12 @@ export function composeBroadcastMessage(ctx) {
|
|
|
208
233
|
parts.push('');
|
|
209
234
|
parts.push(mid);
|
|
210
235
|
}
|
|
236
|
+
if (action) {
|
|
237
|
+
// The choices/default hug the ask under a blank line so they read as the reply.
|
|
238
|
+
if (parts.length)
|
|
239
|
+
parts.push('');
|
|
240
|
+
parts.push(action);
|
|
241
|
+
}
|
|
211
242
|
if (trail.length) {
|
|
212
243
|
// Blank line before the footer block (iPhone "Sent from my iPhone" spacing).
|
|
213
244
|
if (parts.length)
|
|
@@ -233,6 +264,8 @@ function templateVars(ctx) {
|
|
|
233
264
|
class: ctx.class,
|
|
234
265
|
cost: ctx.cost,
|
|
235
266
|
focus: ctx.focus,
|
|
267
|
+
options: ctx.options?.length ? ctx.options.join(' / ') : undefined,
|
|
268
|
+
default: ctx.safeDefault,
|
|
236
269
|
};
|
|
237
270
|
}
|
|
238
271
|
/**
|
|
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>;
|