@@ -1132,6 +1132,8 @@ func newScopedStatusNotifyHandler(controlDir string, paths ralph.Paths, scope st
11321132 }
11331133}
11341134
1135+ const telegramInputRequiredReminderInterval = 30 * time .Minute
1136+
11351137func hasFleetProjects (controlDir string ) (bool , error ) {
11361138 cfg , err := ralph .LoadFleetConfig (controlDir )
11371139 if err != nil {
@@ -1143,6 +1145,7 @@ func hasFleetProjects(controlDir string) (bool, error) {
11431145func newFleetStatusNotifyHandler (controlDir string , defaultPaths ralph.Paths , retryThreshold , permThreshold int ) ralph.TelegramNotifyHandler {
11441146 initialized := false
11451147 prevByProject := map [string ]ralph.Status {}
1148+ lastInputRequiredAlertAt := map [string ]time.Time {}
11461149 return func (ctx context.Context ) ([]string , error ) {
11471150 _ = ctx
11481151
@@ -1189,14 +1192,24 @@ func newFleetStatusNotifyHandler(controlDir string, defaultPaths ralph.Paths, re
11891192 if ! initialized {
11901193 continue
11911194 }
1192- prev , ok := prevByProject [target .ID ]
1193- if ! ok {
1194- continue
1195- }
1195+ prev := prevByProject [target .ID ]
11961196 alerts = append (alerts , buildStatusAlerts (prev , current , retryThreshold , permThreshold )... )
1197+ now := time .Now ().UTC ()
1198+ lastAt := lastInputRequiredAlertAt [target .ID ]
1199+ if shouldSendInputRequiredAlert (prev , current , lastAt , now ) {
1200+ alerts = append (alerts , buildInputRequiredAlert (current .ProjectDir ))
1201+ lastInputRequiredAlertAt [target .ID ] = now
1202+ } else if ! ralph .IsInputRequiredStatus (current ) {
1203+ delete (lastInputRequiredAlertAt , target .ID )
1204+ }
11971205 }
11981206
11991207 prevByProject = currByProject
1208+ for projectID := range lastInputRequiredAlertAt {
1209+ if _ , ok := currByProject [projectID ]; ! ok {
1210+ delete (lastInputRequiredAlertAt , projectID )
1211+ }
1212+ }
12001213 if ! initialized {
12011214 initialized = true
12021215 return nil , nil
@@ -1208,6 +1221,7 @@ func newFleetStatusNotifyHandler(controlDir string, defaultPaths ralph.Paths, re
12081221func newStatusNotifyHandler (paths ralph.Paths , retryThreshold , permThreshold int ) ralph.TelegramNotifyHandler {
12091222 initialized := false
12101223 prev := ralph.Status {}
1224+ lastInputRequiredAlertAt := time.Time {}
12111225 return func (ctx context.Context ) ([]string , error ) {
12121226 _ = ctx
12131227 current , err := ralph .GetStatus (paths )
@@ -1220,6 +1234,13 @@ func newStatusNotifyHandler(paths ralph.Paths, retryThreshold, permThreshold int
12201234 return nil , nil
12211235 }
12221236 alerts := buildStatusAlerts (prev , current , retryThreshold , permThreshold )
1237+ now := time .Now ().UTC ()
1238+ if shouldSendInputRequiredAlert (prev , current , lastInputRequiredAlertAt , now ) {
1239+ alerts = append (alerts , buildInputRequiredAlert (current .ProjectDir ))
1240+ lastInputRequiredAlertAt = now
1241+ } else if ! ralph .IsInputRequiredStatus (current ) {
1242+ lastInputRequiredAlertAt = time.Time {}
1243+ }
12231244 prev = current
12241245 return alerts , nil
12251246 }
@@ -1280,16 +1301,34 @@ func buildStatusAlerts(prev, current ralph.Status, retryThreshold, permThreshold
12801301 valueOrDash (compactSingleLine (current .LastFailureCause , 160 )),
12811302 ))
12821303 }
1283- if ralph .IsInputRequiredStatus (current ) && ! ralph .IsInputRequiredStatus (prev ) {
1284- out = append (out , fmt .Sprintf (
1285- "[ralph alert][input_required]\n - project: %s\n - message: no queued work. add issue (`./ralph new ...`) or run PRD wizard (`/prd start -> /prd refine -> /prd apply`)" ,
1286- project ,
1287- ))
1288- }
12891304
12901305 return out
12911306}
12921307
1308+ func shouldSendInputRequiredAlert (prev , current ralph.Status , lastSentAt , now time.Time ) bool {
1309+ if ! ralph .IsInputRequiredStatus (current ) {
1310+ return false
1311+ }
1312+ if ! ralph .IsInputRequiredStatus (prev ) {
1313+ return true
1314+ }
1315+ if lastSentAt .IsZero () {
1316+ return true
1317+ }
1318+ return now .Sub (lastSentAt ) >= telegramInputRequiredReminderInterval
1319+ }
1320+
1321+ func buildInputRequiredAlert (project string ) string {
1322+ p := strings .TrimSpace (project )
1323+ if p == "" {
1324+ p = "(unknown-project)"
1325+ }
1326+ return fmt .Sprintf (
1327+ "[ralph alert][input_required]\n - project: %s\n - message: no queued work. add issue (`./ralph new ...`) or run PRD wizard (`/prd start -> /prd refine -> /prd apply`)" ,
1328+ p ,
1329+ )
1330+ }
1331+
12931332func startTelegramDaemon (paths ralph.Paths , runArgs []string ) (string , error ) {
12941333 if err := ralph .EnsureLayout (paths ); err != nil {
12951334 return "" , err
0 commit comments