@@ -7,9 +7,20 @@ const fs = require('fs');
77const path = require ( 'path' ) ;
88const os = require ( 'os' ) ;
99
10- const { findEvolverRoot, findMemoryGraph, resolveProjectDir, resolveWorkspaceId } = require ( './_runtimePaths' ) ;
10+ const { findEvolverRoot, findMemoryGraph, resolveProjectDir, resolveWorkspaceId, isGitWorkspace } = require ( './_runtimePaths' ) ;
1111const { filterRelevantOutcomes } = require ( './_memoryFiltering' ) ;
1212
13+ // One-line notice shown (throttled) when the workspace is not a git repo.
14+ // Evolver derives every outcome from the git diff, so in a non-git folder the
15+ // session-end hook records nothing — silently, unless we say so here. We surface
16+ // it in session-start's additionalContext (injected as opening context, which
17+ // does NOT trigger an extra inference round, unlike a stop-hook systemMessage).
18+ const NON_GIT_NOTICE =
19+ '[Evolver] This folder is not a git repository, so evolution memory is inactive ' +
20+ '(outcomes are derived from git diffs). Run `git init` here, or open a git project, ' +
21+ 'to enable recall and recording.' ;
22+ const NON_GIT_NOTICE_TTL_MS = 30 * 60 * 1000 ; // once per 30 min per folder
23+
1324// Return up to `n` of the current workspace's most-recent entries, in
1425// chronological (oldest-first) order.
1526//
@@ -94,18 +105,14 @@ function getDedupStatePath() {
94105 return path . join ( dir , 'session-start-state.json' ) ;
95106}
96107
97- function shouldSkipInjection ( ) {
98- // Only apply dedup when explicitly enabled (set by Kiro adapter) OR when
99- // we detect a per-prompt-firing platform via PROMPT_SUBMIT heuristic in
100- // stdin. The stdin is drained in main(), so we rely on env flag here.
101- const dedupEnabled = String ( process . env . EVOLVER_SESSION_START_DEDUP || '' ) . toLowerCase ( ) === '1'
102- || String ( process . env . EVOLVER_SESSION_START_DEDUP || '' ) . toLowerCase ( ) === 'true' ;
103- if ( ! dedupEnabled ) return false ;
104-
105- const ttlMs = Number ( process . env . EVOLVER_SESSION_START_DEDUP_TTL_MS ) || ( 30 * 60 * 1000 ) ;
106- const key = process . cwd ( ) ;
108+ // TTL throttle keyed by an arbitrary string, persisted in session-start-state
109+ // .json. Returns true if `key` fired within the last `ttlMs` (caller should
110+ // suppress); otherwise records "now" for `key` and returns false. Best-effort:
111+ // a state read/write failure just means no throttling (fail open). Shared by
112+ // the Kiro per-prompt dedup and the non-git notice so both age out of the same
113+ // file (entries older than 24h are pruned on write).
114+ function throttled ( key , ttlMs ) {
107115 const statePath = getDedupStatePath ( ) ;
108-
109116 let state = { } ;
110117 try {
111118 if ( fs . existsSync ( statePath ) ) {
@@ -115,9 +122,7 @@ function shouldSkipInjection() {
115122
116123 const now = Date . now ( ) ;
117124 const last = state [ key ] ;
118- if ( typeof last === 'number' && now - last < ttlMs ) {
119- return true ;
120- }
125+ if ( typeof last === 'number' && now - last < ttlMs ) return true ;
121126
122127 state [ key ] = now ;
123128 try {
@@ -130,53 +135,70 @@ function shouldSkipInjection() {
130135 fs . writeFileSync ( tmp , JSON . stringify ( state ) , 'utf8' ) ;
131136 fs . renameSync ( tmp , statePath ) ;
132137 } catch { /* best-effort */ }
133-
134138 return false ;
135139}
136140
141+ function shouldSkipInjection ( ) {
142+ // Only apply dedup when explicitly enabled (set by Kiro adapter) OR when
143+ // we detect a per-prompt-firing platform via PROMPT_SUBMIT heuristic in
144+ // stdin. The stdin is drained in main(), so we rely on env flag here.
145+ const dedupEnabled = String ( process . env . EVOLVER_SESSION_START_DEDUP || '' ) . toLowerCase ( ) === '1'
146+ || String ( process . env . EVOLVER_SESSION_START_DEDUP || '' ) . toLowerCase ( ) === 'true' ;
147+ if ( ! dedupEnabled ) return false ;
148+
149+ const ttlMs = Number ( process . env . EVOLVER_SESSION_START_DEDUP_TTL_MS ) || ( 30 * 60 * 1000 ) ;
150+ return throttled ( process . cwd ( ) , ttlMs ) ;
151+ }
152+
137153function main ( ) {
138154 if ( shouldSkipInjection ( ) ) {
139155 process . stdout . write ( JSON . stringify ( { } ) ) ;
140156 return ;
141157 }
142158
143- const evolverRoot = findEvolverRoot ( ) ;
144- const graphPath = findMemoryGraph ( evolverRoot ) ;
159+ const currentDir = resolveProjectDir ( ) ;
145160
146- if ( ! graphPath ) {
147- process . stdout . write ( JSON . stringify ( { } ) ) ;
148- return ;
161+ // Non-git notice: evolver records nothing in a non-git folder (outcomes come
162+ // from git diffs), so tell the user — once per folder per TTL — instead of
163+ // failing silently. Emitted regardless of whether any memory exists below.
164+ const parts = [ ] ;
165+ if ( ! isGitWorkspace ( currentDir ) && ! throttled ( 'nongit:' + currentDir , NON_GIT_NOTICE_TTL_MS ) ) {
166+ parts . push ( NON_GIT_NOTICE ) ;
149167 }
150168
169+ const evolverRoot = findEvolverRoot ( ) ;
170+ const graphPath = findMemoryGraph ( evolverRoot ) ;
171+
151172 // Scope to the current workspace BEFORE trimming to the most-recent window,
152173 // so other projects sharing the user-level fallback graph can't crowd this
153174 // workspace's outcomes out of view. When the workspace id can't be resolved,
154175 // belongsToWorkspace() falls back to "show it" — no regression vs. the old
155176 // unscoped behavior.
156- const currentDir = resolveProjectDir ( ) ;
157- const currentId = resolveWorkspaceId ( evolverRoot , currentDir ) ;
158- const recent = readRecentWorkspaceEntries ( graphPath , currentId , currentDir , 5 ) ;
159- const filtered = filterRelevantOutcomes ( recent ) ;
177+ if ( graphPath ) {
178+ const currentId = resolveWorkspaceId ( evolverRoot , currentDir ) ;
179+ const recent = readRecentWorkspaceEntries ( graphPath , currentId , currentDir , 5 ) ;
180+ const filtered = filterRelevantOutcomes ( recent ) ;
181+ if ( filtered . length > 0 ) {
182+ const successCount = filtered . filter ( e => e . outcome && e . outcome . status === 'success' ) . length ;
183+ const failCount = filtered . filter ( e => e . outcome && e . outcome . status === 'failed' ) . length ;
184+ parts . push ( [
185+ `[Evolution Memory] Recent ${ filtered . length } outcomes (${ successCount } success, ${ failCount } failed):` ,
186+ ...filtered . map ( formatOutcome ) ,
187+ '' ,
188+ 'Use successful approaches. Avoid repeating failed patterns.' ,
189+ ] . join ( '\n' ) ) ;
190+ }
191+ }
160192
161- if ( filtered . length === 0 ) {
193+ if ( parts . length === 0 ) {
162194 process . stdout . write ( JSON . stringify ( { } ) ) ;
163195 return ;
164196 }
165197
166- const successCount = filtered . filter ( e => e . outcome && e . outcome . status === 'success' ) . length ;
167- const failCount = filtered . filter ( e => e . outcome && e . outcome . status === 'failed' ) . length ;
168-
169- const lines = filtered . map ( formatOutcome ) ;
170- const summary = [
171- `[Evolution Memory] Recent ${ filtered . length } outcomes (${ successCount } success, ${ failCount } failed):` ,
172- ...lines ,
173- '' ,
174- 'Use successful approaches. Avoid repeating failed patterns.' ,
175- ] . join ( '\n' ) ;
176-
198+ const out = parts . join ( '\n\n' ) ;
177199 process . stdout . write ( JSON . stringify ( {
178- agent_message : summary ,
179- additionalContext : summary ,
200+ agent_message : out ,
201+ additionalContext : out ,
180202 } ) ) ;
181203}
182204
0 commit comments