feat(assistant): 一輪算完就寫提醒佇列

助理算得出哪幾筆到期、哪幾筆逾期,但那些結果只留在監控頁上。這一批讓
每一輪把「要送到人面前」的那幾筆寫成一份佇列檔,位置固定在助理狀態目錄
底下,讀的那一端只印、不判。

判定不放在讀的那一邊,理由是漂移:讓讀的人自己拿 due 欄與 next_run 去跟
現在比,就是第二套到期判定,跟這裡那一套遲早對不上,而對不上的那一天
兩邊都說自己是對的。

這樣換來一個新的失效模式,所以檔頭寫足四個值:輪次代號、UTC 時間戳、
連續失敗筆數、待辦總筆數,再加同一刻的 epoch 秒。助理沒在跑的時候佇列
不會更新,而一份舊佇列讀起來跟新的一模一樣——讀的那一端要算得出它多舊,
「沒有提醒」與「沒有人算提醒」不可以長得一樣。epoch 是為了讓讀的那一端
不必自己解 ISO 字串:那等於在每一個讀取端各放一份日期解析。

同一筆待辦既到期又逾期時只列逾期那一行。同一批提醒裡出現兩行會被讀成
兩件事,而「逾期五天」比「排定點過了」講得更清楚。

逾期那幾行的「逾期多久」直接取判定那一支算好的寫法,不在這裡再寫一個
時間長度格式化——同一個秒數在兩個地方就會印出兩種說法。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-07 11:16:57 +08:00
co-authored by Claude Opus 5
parent 465bd9d40a
commit 932bfa826a
8 changed files with 73 additions and 7 deletions
+2 -1
View File
@@ -1140,7 +1140,8 @@ cmd_scan() {
if [ -n "$OVERDUE_LINES" ]; then
printf '%s' "$OVERDUE_LINES" | while IFS="$(printf '\t')" read -r _oi _os _od _oa _ot; do
[ -n "$_oi" ] || continue
printf 'overdue_row=%s state=%s due=%s overdue_secs=%s title=%s\n' "$_oi" "$_os" "$_od" "$_oa" "$_ot"
printf 'overdue_row=%s state=%s due=%s overdue_secs=%s human=%s title=%s\n' \
"$_oi" "$_os" "$_od" "$_oa" "$(dur_human "$_oa")" "$_ot"
done
fi
printf 'rows_file=%s\n' "$ROWS"
+63
View File
@@ -149,6 +149,7 @@
# 時,原因只寫在這裡
# tasks_total= tasks_failing= 待辦簿筆數與連續失敗筆數,供目錄頁那一個區塊與摘要用
# tasks_overdue= 逾期筆數(截止時間已經過了),取自到期判定那一支;判不出來時是減號
# reminders= reminders_file= 提醒佇列的筆數與路徑。工作階段開始那一支 hook 讀它
# tasks_due= 本輪到期的筆數;到期判定那一支失敗時為空
# events_new= 本輪偵測到的新事件種類數;到期判定那一支失敗時為空
# due_status= due_rc= 到期判定那一支的結果與結束碼
@@ -212,6 +213,8 @@ DUE_TASKS=''
DUE_EVENTS=''
DUE_OVERDUE=''
FAILING_LINES=''
REMINDERS=0
REMINDERS_FILE=''
# 這支腳本是不是從 $JSC_HOME/current 那一組路徑被叫起來的。不是就大聲警告,但照跑。
# 只警告、不中止是刻意的取捨:從工作樹直接跑腳本是開發時的正當用法,中止會把那條路擋掉;
@@ -903,6 +906,63 @@ count_tasks() {
return 0
}
# --- 提醒佇列 ---
#
# 一輪算完之後,把「要送到人面前」的那幾筆寫成一份佇列檔,位置固定在助理狀態目錄底下。
# 讀的那一端是工作階段開始那一支 hook:它只印,一個判定都不做。
#
# 為什麼判定不放在 hook 那一邊:hook 跑在每一個工作階段的開頭,它要快、而且絕對不能擋人。
# 更重要的是,讓 hook 自己拿 due 欄與 next_run 去比就是第二套到期判定,跟 due.sh 那一套
# 會漂移,而漂移的那一天兩邊都說自己是對的。所以判定只有一套,佇列是它的輸出。
#
# 這樣換來一個新的失效模式,要正面處理:助理沒在跑的時候,這份佇列不會更新,而一份舊佇列
# 讀起來跟新的一模一樣。所以檔頭寫一行 round,帶著這一輪的時間戳,讓讀的那一端算得出
# 它有多舊——「沒有提醒」與「沒有人算提醒」不可以長得一樣。
write_reminders() {
REMINDERS=0
REMINDERS_FILE="$STATE_DIR/reminders.tsv"
_rt="$REMINDERS_FILE.tmp.$$"
{
# 檔頭同時寫 ISO 時間與 epoch 秒。ISO 給人看,epoch 給讀的那一端算年紀——讓它自己解
# ISO 字串就是在每一個讀取端各放一份日期解析,而 date -d 不是每一台機器都認得那個格式。
# 最後那個待辦總筆數是給讀的那一端判「該不該吵」用的:佇列空又過期時,待辦簿有東西
# 才代表「有事沒人在算」,零筆就只是助理閒著,那時候安靜才對。
printf 'round\t%s\t%s\t%s\t%s\t%s\n' \
"$ROUND" "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$TASKS_FAILING" "$(date +%s)" "$TASKS_TOTAL"
# 逾期那幾筆照抄判定那一支印的行,連「逾期多久」那個給人看的寫法都取它算好的:
# 自己再寫一個時間長度格式化,同一個秒數在兩個地方就會印出兩種說法。
_ovids="$RD/overdue-ids"
: >"$_ovids"
if [ -f "$RD/due.out" ]; then
sed -n 's/^overdue_row=//p' "$RD/due.out" 2>/dev/null | while IFS= read -r _l; do
[ -n "$_l" ] || continue
_oi=${_l%% *}
_oh=$(printf '%s' "$_l" | sed -n 's/.* human=\(.*\) title=.*/\1/p')
_ot=$(printf '%s' "$_l" | sed -n 's/.* title=//p')
printf '%s\n' "$_oi" >>"$_ovids"
printf 'overdue\t%s\t%s\t%s\n' "$_oi" "${_oh:--}" "${_ot:--}"
done
fi
# 只提醒型而且到期的那幾筆。指令型不進佇列:它們由執行那一支真的跑掉了,人不必接手。
# 已經以逾期身分列過的那幾筆不再列第二次:同一筆待辦在同一批提醒裡出現兩行,讀的人會
# 當成兩件事,而且「逾期五天」比「排定點過了」講得更清楚——留強的那一行就好。
if [ -n "$DUE_ROWS" ] && [ -f "$DUE_ROWS" ]; then
awk -F'\t' -v idf="$_ovids" '
BEGIN { while ((getline _l < idf) > 0) seen[_l] = 1 }
NF >= 12 && $1 != "" && $2 == "due" && $5 == "remind" && !($1 in seen) {
printf "remind\t%s\t%s\t%s\n", $1, ($10 == "" ? "-" : $10), ($11 == "" ? "-" : $11)
}' "$DUE_ROWS" 2>/dev/null
fi
} >"$_rt" 2>/dev/null || { add_warn '提醒佇列寫不出來'; rm -f "$_rt"; return 0; }
if mv "$_rt" "$REMINDERS_FILE" 2>/dev/null; then
REMINDERS=$(awk -F'\t' '$1 == "remind" || $1 == "overdue" { n++ } END { print n + 0 }' "$REMINDERS_FILE")
else
add_warn '提醒佇列換不上去'
rm -f "$_rt"
fi
return 0
}
# --- 待辦簿的事件偵測與到期判定 ---
#
# 算到期的邏輯不在這一支,也不在 tools/tasks.sh,而在 tools/due.sh,理由寫在那一支的檔頭:
@@ -1167,6 +1227,7 @@ case "$CMD" in
if [ "$TASKS_FAILING" -gt 0 ]; then
add_pending "有 $TASKS_FAILING 筆待辦連續失敗,每一輪都在重試" '待辦簿到期與逾期' '/jsc-assist:assistant status'
fi
write_reminders
tally "$D01_STATUS"; tally "$D04_STATUS"; tally "$D07_STATUS"; tally "$D09_STATUS"
tally "$D11_STATUS"
@@ -1198,6 +1259,8 @@ case "$CMD" in
printf 'tasks_failing=%s\n' "$TASKS_FAILING"
printf 'tasks_due=%s\n' "$DUE_TASKS"
printf 'tasks_overdue=%s\n' "${DUE_OVERDUE:--}"
printf 'reminders=%s\n' "$REMINDERS"
printf 'reminders_file=%s\n' "$REMINDERS_FILE"
printf 'events_new=%s\n' "$DUE_EVENTS"
printf 'due_status=%s\n' "$DUE_STATUS"
printf 'due_rc=%s\n' "$DUE_RC"