Skip to content

Commit aff4722

Browse files
authored
Merge pull request #188 from Dzarlax-AI/codex/185-proactive-context-prompts
Add proactive context prompt MVP
2 parents 0b46d1e + 4f229ff commit aff4722

17 files changed

Lines changed: 1983 additions & 24 deletions

cmd/server/main.go

Lines changed: 52 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ func main() {
103103
legacyDB.EnsureEnergySnapshotsTable()
104104
legacyDB.EnsureReadinessRedesignTables()
105105
legacyDB.EnsureSubjectiveCheckinsTable()
106+
legacyDB.EnsureContextPromptInteractionsTable()
106107
legacyDB.EnsureAuthSessionsTable()
107108

108109
passwordHash := ""
@@ -154,7 +155,7 @@ func main() {
154155
// One-time backfill of installation-wide Gemini config for installs
155156
// that pre-date PR #16 (where AI settings were per-tenant). When the
156157
// global table is empty but an admin tenant already has gemini_* rows,
157-
// copy them up so non-admin tenants (Maria-style accounts) inherit.
158+
// copy them up so non-admin tenants inherit.
158159
if err := migrateGlobalAIIfNeeded(ctx, reg, mgr, users); err != nil {
159160
log.Printf("global AI migration: %v", err)
160161
}
@@ -174,6 +175,7 @@ func main() {
174175
db.EnsureEnergySnapshotsTable()
175176
db.EnsureReadinessRedesignTables()
176177
db.EnsureSubjectiveCheckinsTable()
178+
db.EnsureContextPromptInteractionsTable()
177179
db.EnsureAuthSessionsTable()
178180
startTenant(ctx, mgr, reg, db, u.SchemaName, envNotifyDefaults, envAIDefaults, baseURL)
179181
}
@@ -229,6 +231,7 @@ func main() {
229231
db.EnsureEnergySnapshotsTable()
230232
db.EnsureReadinessRedesignTables()
231233
db.EnsureSubjectiveCheckinsTable()
234+
db.EnsureContextPromptInteractionsTable()
232235
db.EnsureAuthSessionsTable()
233236
startTenant(ctx, mgr, reg, db, schema, envNotifyDefaults, envAIDefaults, baseURL)
234237
})
@@ -667,6 +670,9 @@ type liveCheckinRouter struct {
667670
func (r *liveCheckinRouter) SaveAnswer(date, source, answer string, answeredAt time.Time) (string, error) {
668671
return r.db.SaveCheckinAnswer(date, source, answer, answeredAt)
669672
}
673+
func (r *liveCheckinRouter) SaveContextPromptAnswer(promptID, category, source string, answeredAt time.Time) (string, error) {
674+
return r.db.SaveContextPromptAnswer(promptID, category, source, answeredAt)
675+
}
670676
func (r *liveCheckinRouter) AnswerCallbackQuery(qid, text string) error {
671677
return r.bot.AnswerCallbackQuery(qid, text)
672678
}
@@ -706,20 +712,33 @@ func makeReportTrigger(mgr *tenants.Manager, schema string, defaults storage.Not
706712
sendMu := mgr.MorningSendMuFor(schema)
707713
if sendMu != nil {
708714
sendMu.Lock()
709-
defer sendMu.Unlock()
710715
}
716+
sentReport := false
711717
if db.HasSentMorningReport(today) {
718+
if sendMu != nil {
719+
sendMu.Unlock()
720+
}
712721
return
713722
}
714723
sent, reason, err := notify.SendMorningSmart(bot, db, ncfg, false)
715724
if err != nil {
725+
if sendMu != nil {
726+
sendMu.Unlock()
727+
}
716728
log.Printf("checkin-trigger: send: %v", err)
717729
return
718730
}
719731
if sent {
720732
if perr := db.MarkMorningReportSent(today); perr != nil {
721733
log.Printf("checkin-trigger: mark sent: %v", perr)
722734
}
735+
sentReport = true
736+
}
737+
if sendMu != nil {
738+
sendMu.Unlock()
739+
}
740+
if sentReport {
741+
trySendContextPromptAfterMorning(bot, db, ncfg, today, time.Now().In(loc))
723742
log.Printf("checkin-trigger: sent (reason=%s) for %s", reason, today)
724743
}
725744
}
@@ -832,25 +851,29 @@ func makeMorningTrigger(db *storage.DB, sendMu *sync.Mutex, mgr *tenants.Manager
832851
// both observe HasSent=false in the narrow window between
833852
// the outer check and the actual Telegram POST.
834853
sendMu.Lock()
835-
defer sendMu.Unlock()
836854
if db.HasSentMorningReport(today) {
855+
sendMu.Unlock()
837856
return
838857
}
839858
sent, reason, err := notify.SendMorningSmartOpts(bot, db, ncfg, notify.MorningSendOpts{
840859
Force: force,
841860
CheckinExpired: action == notify.MorningActionExpireAndForce,
842861
})
843862
if err != nil {
863+
sendMu.Unlock()
844864
log.Printf("morning trigger: send telegram: %v", err)
845865
return
846866
}
847867
if !sent {
868+
sendMu.Unlock()
848869
log.Printf("morning trigger: deferring — %s", reason)
849870
return
850871
}
851872
if err := db.MarkMorningReportSent(today); err != nil {
852873
log.Printf("morning trigger: mark sent: %v", err)
853874
}
875+
sendMu.Unlock()
876+
trySendContextPromptAfterMorning(bot, db, ncfg, today, now)
854877
log.Printf("morning trigger: sent (reason=%s, forced=%v, action=%s)", reason, force, action)
855878
}
856879
}
@@ -1090,6 +1113,7 @@ func runMorningSmartRetry(bot *notify.Bot, db *storage.DB, mgr *tenants.Manager,
10901113
}
10911114
if sent {
10921115
log.Printf("morning smart-retry: sent (reason=%s, forced=%v, action=%s)", reason, past, action)
1116+
trySendContextPromptAfterMorning(bot, db, ncfg, today, time.Now().In(loc))
10931117
return
10941118
}
10951119
if past {
@@ -1105,6 +1129,26 @@ func runMorningSmartRetry(bot *notify.Bot, db *storage.DB, mgr *tenants.Manager,
11051129
}
11061130
}
11071131

1132+
func trySendContextPromptAfterMorning(bot *notify.Bot, db *storage.DB, cfg notify.Config, signalDate string, now time.Time) {
1133+
if !storage.IsContextPromptsEnabled(db) {
1134+
return
1135+
}
1136+
promptDate := now.Format("2006-01-02")
1137+
prompt, reserved, err := db.ReserveLowSleepContextPrompt(signalDate, promptDate, now, now.Add(36*time.Hour))
1138+
if err != nil {
1139+
log.Printf("context prompt: reserve low_sleep date=%s: %v", signalDate, err)
1140+
return
1141+
}
1142+
if !reserved || prompt == nil {
1143+
return
1144+
}
1145+
if err := notify.SendContextPrompt(bot, db, cfg.Lang, prompt, now); err != nil {
1146+
log.Printf("context prompt: send low_sleep prompt=%s date=%s: %v", prompt.PromptID, signalDate, err)
1147+
return
1148+
}
1149+
log.Printf("context prompt: sent low_sleep prompt=%s date=%s", prompt.PromptID, signalDate)
1150+
}
1151+
11081152
// runDailyQualityScan ticks once per day at 03:00 (REPORT_TZ or system local)
11091153
// and calls MarkSuspectPoints to flag z-score outliers in the last 7 days of
11101154
// autonomic metrics. Cheap (~one query per metric) and idempotent — re-running
@@ -1146,6 +1190,11 @@ func runDailyQualityScan(db *storage.DB, schema string, defaults storage.NotifyC
11461190
now = time.Now().In(loc)
11471191
today := now.Format("2006-01-02")
11481192
db.RunReadinessRedesignBackfillForDatesAt([]string{today}, now)
1193+
if deleted, err := db.PruneContextPromptInteractions(now); err != nil {
1194+
log.Printf("[%s] context prompt retention: %v", schema, err)
1195+
} else if deleted > 0 {
1196+
log.Printf("[%s] context prompt retention: pruned %d rows", schema, deleted)
1197+
}
11491198
}
11501199
}
11511200

docs/ai-plans/2026-06-18-checkin-calendar-sla.html

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -186,7 +186,7 @@ <h2>Implemented Result</h2>
186186
<section>
187187
<h2>Checks Run</h2>
188188
<ul>
189-
<li><code>gofmt -w internal\storage\subjective_checkin.go internal\storage\subjective_checkin_test.go internal\ui\handler.go internal\ui\admin_checkin_coverage_test.go internal\ui\i18n_en.go internal\ui\i18n_ru.go internal\ui\i18n_sr.go</code></li>
189+
<li><code>gofmt -w internal/storage/subjective_checkin.go internal/storage/subjective_checkin_test.go internal/ui/handler.go internal/ui/admin_checkin_coverage_test.go internal/ui/i18n_en.go internal/ui/i18n_ru.go internal/ui/i18n_sr.go</code></li>
190190
<li><code>go test ./internal/storage -run "Test.*Checkin|Test.*CheckinCoverage" -count=1</code> - passed.</li>
191191
<li><code>go test ./internal/ui -run TestAdminCheckinCoverage -count=1</code> - passed.</li>
192192
</ul>
@@ -202,10 +202,9 @@ <h2>Known Limitations</h2>
202202
</section>
203203

204204
<section class="approval">
205-
<h2>Approval Gate</h2>
205+
<h2>Approval Gate Status</h2>
206206
<p>
207-
Implementation must not start until the user explicitly approves this plan.
208-
Suggested approval phrase: <code>погнали</code>.
207+
This was the historical approval gate for the implementation plan. Approval was received and the implementation results above reflect the completed work.
209208
</p>
210209
</section>
211210
</main>

0 commit comments

Comments
 (0)