-
Notifications
You must be signed in to change notification settings - Fork 16.5k
Expand file tree
/
Copy pathtaskAnchorReminder.ts
More file actions
95 lines (84 loc) · 4.05 KB
/
Copy pathtaskAnchorReminder.ts
File metadata and controls
95 lines (84 loc) · 4.05 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
import type { Task } from '../../utils/tasks.js'
// Short, generic "keep going" nudges that carry no task anchor. In a long
// session these dilute the task anchor and the model drifts off the task
// (see the long-session drift investigation, 2026-07-23). When one arrives and
// the task list still has unfinished items, we re-surface them once — only in
// this exact situation, so there is no per-turn token cost.
const ANCHORLESS_NUDGE_PATTERN =
/(继续|接着|后续|往下|完成剩余|自动执行|自动跑|接着做|keep going|carry on|go on|continue|proceed|next step|move on)/i
// Guard against false positives on substantive messages that merely contain a
// nudge word (e.g. "继续用 X 方案重构 Y 模块…"). Real anchorless nudges are short
// — the ones seen in the investigation were all ≤12 CJK chars ("继续执行后续的工作").
const MAX_NUDGE_CHARS = 20
/**
* True when `text` is a short "keep going" style nudge that carries no specific
* task anchor. Long messages never match, even if they contain a nudge word.
*/
export function isAnchorlessNudge(text: string): boolean {
const trimmed = text.trim()
if (!trimmed || trimmed.length > MAX_NUDGE_CHARS) {
return false
}
return ANCHORLESS_NUDGE_PATTERN.test(trimmed)
}
// Cap the injected list so a large backlog can't balloon the reminder.
const MAX_TASKS_IN_REMINDER = 20
const STATUS_ORDER: Record<string, number> = {
in_progress: 0,
pending: 1,
}
/**
* Builds a `<system-reminder>` re-surfacing the unfinished tasks, or null when
* there are none. in_progress tasks come first, then pending, ordered by id.
*/
export function buildTaskAnchorReminder(tasks: Task[]): string | null {
const unfinished = tasks.filter(t => t.status !== 'completed')
if (unfinished.length === 0) {
return null
}
const sorted = [...unfinished].sort((a, b) => {
const byStatus =
(STATUS_ORDER[a.status] ?? 2) - (STATUS_ORDER[b.status] ?? 2)
if (byStatus !== 0) {
return byStatus
}
return Number(a.id) - Number(b.id)
})
const shown = sorted.slice(0, MAX_TASKS_IN_REMINDER)
const lines = shown.map(t => `- #${t.id} [${t.status}] ${t.subject}`)
const overflow = sorted.length - shown.length
if (overflow > 0) {
lines.push(`- …and ${overflow} more`)
}
return [
'<system-reminder>',
`You still have ${unfinished.length} unfinished task(s) in the current task list. The user's last message was a short "keep going" nudge without a specific anchor. Re-align to these tasks and continue the highest-priority unfinished one. Do not end your turn until the tasks are done or you genuinely need user input:`,
...lines,
'Use TaskUpdate to mark progress as you go. This reminder is generated by the client, not sent by the user.',
'</system-reminder>',
].join('\n')
}
// How many in_progress task names to spell out in the local notice.
const MAX_NOTICE_TASK_NAMES = 3
/**
* Builds a display-only, user-facing notice for when a turn ends naturally while
* tasks are still in_progress — a strong signal the model stopped mid-work. Only
* in_progress tasks trigger it (a pending backlog the user is pacing does not).
* Returns null when there is nothing worth surfacing. This text is shown in the
* REPL and never sent to the model (zero token cost).
*/
export function buildUnfinishedTaskNotice(tasks: Task[]): string | null {
const inProgress = tasks.filter(t => t.status === 'in_progress')
if (inProgress.length === 0) {
return null
}
const pendingCount = tasks.filter(t => t.status === 'pending').length
const names = inProgress
.slice(0, MAX_NOTICE_TASK_NAMES)
.map(t => `#${t.id} ${t.subject}`)
.join('、')
const overflow = inProgress.length - MAX_NOTICE_TASK_NAMES
const namesSuffix = overflow > 0 ? ` 等 ${inProgress.length} 个` : ''
const pendingSuffix = pendingCount > 0 ? `,另有 ${pendingCount} 个待办` : ''
return `还有进行中的任务未完成:${names}${namesSuffix}${pendingSuffix}。可直接回车继续,或用「按计划继续 Task #N」这类带锚点的话催促,避免长任务跑偏。`
}