@@ -19,6 +19,7 @@ const logger = winston.createLogger({
1919const DEFAULT_DIR = path . join ( os . homedir ( ) , '.orchestrator' ) ;
2020const DEFAULT_FILE = path . join ( DEFAULT_DIR , 'activity.jsonl' ) ;
2121const DEFAULT_MAX_EVENTS = 500 ;
22+ const DEFAULT_LOAD_MAX_BYTES = 1024 * 1024 ; // 1MB tail read
2223
2324function safeJsonLine ( obj ) {
2425 try {
@@ -28,12 +29,34 @@ function safeJsonLine(obj) {
2829 }
2930}
3031
32+ function safeJsonParse ( line ) {
33+ try {
34+ return JSON . parse ( line ) ;
35+ } catch {
36+ return null ;
37+ }
38+ }
39+
3140function clampInt ( n , { min, max, fallback } ) {
3241 const v = Number . parseInt ( String ( n || '' ) , 10 ) ;
3342 if ( ! Number . isFinite ( v ) ) return fallback ;
3443 return Math . max ( min , Math . min ( max , v ) ) ;
3544}
3645
46+ function dedupeByIdKeepLast ( items ) {
47+ const seen = new Set ( ) ;
48+ const out = [ ] ;
49+ for ( let i = items . length - 1 ; i >= 0 ; i -= 1 ) {
50+ const ev = items [ i ] ;
51+ const id = ev ?. id ;
52+ if ( ! id || seen . has ( id ) ) continue ;
53+ seen . add ( id ) ;
54+ out . push ( ev ) ;
55+ }
56+ out . reverse ( ) ;
57+ return out ;
58+ }
59+
3760class ActivityFeedService {
3861 constructor ( { filePath = DEFAULT_FILE , maxEvents = DEFAULT_MAX_EVENTS } = { } ) {
3962 this . filePath = filePath ;
@@ -59,13 +82,55 @@ class ActivityFeedService {
5982 this . _loaded = true ;
6083 try {
6184 await fs . mkdir ( path . dirname ( this . filePath ) , { recursive : true } ) ;
62- // Do not load entire history into memory; keep v1 in-memory only.
63- // Existence is enough to ensure appends won't throw due to missing dir.
85+ await this . loadRecentFromDisk ( ) ;
6486 } catch ( error ) {
6587 logger . warn ( 'Failed to ensure activity dir exists' , { error : error . message } ) ;
6688 }
6789 }
6890
91+ async loadRecentFromDisk ( { maxBytes = DEFAULT_LOAD_MAX_BYTES } = { } ) {
92+ const byteBudget = clampInt ( maxBytes , { min : 16 * 1024 , max : 10 * 1024 * 1024 , fallback : DEFAULT_LOAD_MAX_BYTES } ) ;
93+ try {
94+ const stat = await fs . stat ( this . filePath ) ;
95+ const size = Number ( stat . size ) || 0 ;
96+ if ( ! size ) return ;
97+
98+ const start = Math . max ( 0 , size - byteBudget ) ;
99+ const length = Math . max ( 0 , size - start ) ;
100+ if ( ! length ) return ;
101+
102+ const handle = await fs . open ( this . filePath , 'r' ) ;
103+ try {
104+ const buf = Buffer . alloc ( length ) ;
105+ const { bytesRead } = await handle . read ( buf , 0 , length , start ) ;
106+ const text = buf . toString ( 'utf8' , 0 , bytesRead ) ;
107+ let lines = text . split ( '\n' ) . filter ( Boolean ) ;
108+ // If we started mid-file, first line may be partial; drop it.
109+ if ( start > 0 && lines . length > 0 ) {
110+ lines = lines . slice ( 1 ) ;
111+ }
112+
113+ const parsed = [ ] ;
114+ for ( const line of lines ) {
115+ const ev = safeJsonParse ( line ) ;
116+ if ( ! ev || typeof ev !== 'object' ) continue ;
117+ if ( ! ev . id || ! ev . kind || ! ev . ts ) continue ;
118+ parsed . push ( ev ) ;
119+ }
120+
121+ const merged = dedupeByIdKeepLast ( [ ...parsed , ...this . events ] ) ;
122+ this . events = merged . slice ( - this . maxEvents ) ;
123+ } finally {
124+ await handle . close ( ) ;
125+ }
126+ } catch ( error ) {
127+ // If the file doesn't exist, that's fine. Any other errors should be logged but not fatal.
128+ if ( String ( error ?. code || '' ) !== 'ENOENT' ) {
129+ logger . warn ( 'Failed to load activity history from disk' , { error : error . message } ) ;
130+ }
131+ }
132+ }
133+
69134 list ( { since = 0 , limit = 200 } = { } ) {
70135 const sinceMs = Number . isFinite ( Number ( since ) ) ? Number ( since ) : 0 ;
71136 const lim = clampInt ( limit , { min : 1 , max : 1000 , fallback : 200 } ) ;
@@ -114,4 +179,3 @@ class ActivityFeedService {
114179}
115180
116181module . exports = { ActivityFeedService } ;
117-
0 commit comments