#!/usr/bin/env sh # session-reminder.sh — 工作階段開始時,把助理算好的未讀提醒帶到前景。 # # 用法: # session-reminder.sh # SessionStart:印出未讀提醒,並記下這個工作階段提過了 # session-reminder.sh peek # 只印,不記。人要重看一次時用,也給檢核用 # # 結束碼:0=一律成功,只有這一種。這一支接在工作階段開始那個事件上,它的產出走 stdout, # 不走結束碼——那個事件的 stdout 會成為額外 context。佇列不在、讀不到、格式對不上, # 一律印一行說明然後 exit 0;一支在每個工作階段開頭都會跑的 hook 絕對不可以擋人。 # 唯一的非零來源是 `. lib.sh` 載入失敗,那時 sh 自己回 2。 # # --- 這一支不做判定 --- # # 提醒該不該送、哪幾筆該送,全由助理那一輪算完寫進佇列(jsc-assist 的 tools/patrol.sh # 每一輪重寫 $JSC_HOME/assistant/reminders.tsv)。這裡只把那份檔案印出來。 # 不自己判的理由有兩個。一是快:這一支跑在每一個工作階段的開頭,讀一個檔案就回來。 # 二是不漂移:自己拿 due 欄與 next_run 去跟現在比,就是第二套到期判定,跟助理那一套遲早 # 對不上,而對不上的那一天兩邊都說自己是對的。 # # --- 「沒有提醒」與「沒有人算提醒」不可以長得一樣 --- # # 只印佇列換來一個新的失效模式:助理停了,佇列就不再更新,而一份舊佇列讀起來跟新的一模一樣。 # 所以佇列檔頭帶著那一輪的時間戳,這裡算出它多舊,超過心跳門檻就明說「這份提醒是多久以前 # 算的、助理現在的心跳是什麼狀態」。門檻與狀態都取 heartbeat.sh 印的那一行,不自己定一套。 # # 助理的狀態目錄根本不存在時,這一支一個字都不印:那代表這台機器從沒啟動過助理, # 每個工作階段開頭都催一次「你要不要啟動助理」不是提醒,是噪音。 # # --- 一個工作階段只提一次 --- # # 記號檔是 $JSC_HOME/sessions/{工作階段}.reminded。工作階段開始那個事件在續接同一階段時 # 會再觸發,沒有記號就會每次都再提一次同一批。 # 接不到工作階段代號的 CLI(記號都落在 default 上)另有一條路:session-timer.sh 的 restart # 分支會把這個記號刪掉。判定「這是不是新的工作階段」只有那一支知道,所以刪除掛在那裡, # 這裡不自己再判一次。 HERE=$(dirname "$0"); . "$HERE/lib.sh" hook_trace "session-reminder ${1:-}" read_stdin sid=$(session_id) MODE="${1:-show}" STATE_DIR="$JSC_HOME/assistant" QUEUE="$STATE_DIR/reminders.tsv" MARK="$JSC_HOME/sessions/$sid.reminded" MAX_ROWS=8 # 助理沒啟動過就整支安靜退出。 [ -d "$STATE_DIR" ] || exit 0 # 這個工作階段提過了就不再提。peek 一律印,那是人自己要重看。 if [ "$MODE" != peek ] && [ -f "$MARK" ]; then exit 0 fi mark_done() { [ "$MODE" = peek ] && return 0 mkdir -p "$JSC_HOME/sessions" 2>/dev/null || return 0 now_epoch >"$MARK" 2>/dev/null || true return 0 } if [ ! -f "$QUEUE" ]; then echo "[jsc] 助理的狀態目錄在,但還沒有提醒佇列($QUEUE)。跑過一輪巡檢才會產生,所以這裡沒有提醒**不代表沒有事要做**——要現在看就跑 /jsc-assist:assistant status。" mark_done exit 0 fi TAB=$(printf '\t') # 檔頭。第一行不是 round 就是格式不對,或者檔案被別的東西蓋掉了,兩種都照實說。 _head=$(head -n1 "$QUEUE" 2>/dev/null) _kind=$(printf '%s' "$_head" | cut -f1) if [ "$_kind" != round ]; then echo "[jsc] 提醒佇列($QUEUE)的第一行不是輪次資訊,這一份讀不了。助理下一輪會重寫;在那之前要看待辦就跑 /jsc-assist:assistant status。" mark_done exit 0 fi _iso=$(printf '%s' "$_head" | cut -f3) _failing=$(printf '%s' "$_head" | cut -f4) _epoch=$(printf '%s' "$_head" | cut -f5) _tasks=$(printf '%s' "$_head" | cut -f6) case "${_failing:-}" in ''|*[!0-9]*) _failing=0 ;; esac case "${_tasks:-}" in ''|*[!0-9]*) _tasks=0 ;; esac # 佇列有多舊。門檻與心跳狀態一律取 heartbeat.sh 印的那一行:那是這台機器判定「助理還在跑」 # 的唯一一套規則,這裡再定一套就會出現兩個說法。 _age='' case "${_epoch:-}" in ''|*[!0-9]*) ;; *) _age=$(( $(now_epoch) - _epoch )) ;; esac _hb=$(sh "$HERE/heartbeat.sh" report /dev/null || true) _hbstate=$(printf '%s' "$_hb" | sed -n 's/^state=\([a-z]*\).*/\1/p') _ttl=$(printf '%s' "$_hb" | sed -n 's/.*[[:space:]]ttl=\([0-9]*\).*/\1/p') case "${_ttl:-}" in ''|*[!0-9]*) _ttl=300 ;; esac _rows=$(awk -F"$TAB" '$1 == "overdue" || $1 == "remind" { n++ } END { print n + 0 }' "$QUEUE" 2>/dev/null) case "${_rows:-}" in ''|*[!0-9]*) _rows=0 ;; esac _stale=0 if [ -n "$_age" ] && [ "$_age" -gt "$_ttl" ]; then _stale=1; fi [ "$_hbstate" = fresh ] || _stale=1 if [ "$_rows" -eq 0 ] && [ "$_failing" -eq 0 ]; then # 佇列是新的而且空的:這才是真的「沒有提醒」,安靜退出。 # 過期又空的那一種要分兩路。待辦簿有東西,就代表「有事而現在沒有人在算它到期沒到期」, # 那要說一句;零筆就只是助理閒著,人自己按停也算這一種——對著一個刻意的決定每個工作階段 # 催一次,那是噪音不是提醒。 if [ "$_stale" -eq 1 ] && [ "$_tasks" -gt 0 ]; then _agetxt='年紀算不出來' [ -n "$_age" ] && _agetxt="$(( _age / 60 )) 分鐘前" echo "[jsc] 助理的提醒清單是 ${_iso:-未知時間}($_agetxt)那一輪算的,那一輪沒有提醒;心跳現在是「${_hbstate:-讀不到}」。待辦簿還有 $_tasks 筆,**助理停著的時候沒有人再判它們到期了沒有**,所以這裡安靜不等於沒事。要現況就跑 /jsc-assist:assistant status。" fi # 記號一律記:不記的話同一個工作階段每次觸發都會再讀一次、再說一次同一句話。 mark_done exit 0 fi if [ "$_stale" -eq 1 ]; then _agetxt='年紀算不出來' [ -n "$_age" ] && _agetxt="$(( _age / 60 )) 分鐘前算的" echo "[jsc] 下面這批提醒是 ${_iso:-未知時間}($_agetxt)那一輪算出來的,助理現在的心跳是「${_hbstate:-讀不到}」,門檻 $_ttl 秒。**助理停著的時候這份清單不會更新**,所以它現在說什麼都只描述那一輪,不描述現在。要現況就跑 /jsc-assist:assistant status。" fi # 逾期排前面:那幾筆的截止時間已經過了,比「該做了」更急。 # # 先把兩種併成一份有序清單,再用 head 截筆數,不在迴圈裡自己數。 # 理由是那個迴圈接在管線後面,殼會把它放進子殼跑——在子殼裡加的計數,回到外面就沒了, # 於是那道「只列前幾筆」的上限看起來寫了,實際上一次都沒生效。 _ordered=$( awk -F"$TAB" '$1 == "overdue" { print }' "$QUEUE" 2>/dev/null awk -F"$TAB" '$1 == "remind" { print }' "$QUEUE" 2>/dev/null ) printf '%s\n' "$_ordered" | head -n "$MAX_ROWS" | while IFS="$TAB" read -r _k _id _why _title; do [ -n "$_id" ] || continue if [ "$_k" = overdue ]; then echo "[jsc] 逾期 $_id:${_title:--} —— 已經逾期 ${_why:--}" else echo "[jsc] 到期 $_id:${_title:--} —— ${_why:--}" fi done if [ "$_rows" -gt "$MAX_ROWS" ]; then echo "[jsc] 這一批共 $_rows 筆,上面只列了 $MAX_ROWS 筆。其餘的跑 /jsc-assist:assistant status 看得到全部。" fi if [ "$_failing" -gt 0 ]; then echo "[jsc] 另有 $_failing 筆待辦連續失敗,每一輪都在重試而且不會自動暫停。跑 /jsc-assist:assistant status 看是哪幾筆。" fi mark_done exit 0