Tells the model the time after a long gap, and how long its own turns really take. A few dozen tokens, only when useful.

A Claude Code mod that gives the agent a sense of time, for a few dozen tokens.
The problem. An agent has no clock. Come back after six hours and it acts as if no time passed. Ask how long a task will take and it answers in human workdays ("1–2 days") for work it finishes in ten minutes.
Situations it is meant for. (What changes is what the model sees; how it reacts is up to the model. These are the cases I wrote it for, not measured results.)
this conversation's last message 9h05m ago.--resume a session from three weeks ago. The agent sees last message 21d3h ago, a cue that plans and "what's running" may be stale.What it does.
[time] now 2026-10-17 Sat 21:40 · this conversation's last message 6h12m ago The gap is tracked per conversation, so resuming an old session shows that session's own gap. Back-to-back chatting adds nothing.[pace] your last 40 turns took median 4m, longest 38m of real time. Estimate durations in these terms, not human workdays.Cost: ~20 tokens per gap, ~30 tokens once per session. No model calls, no system-prompt changes (so prompt caching is untouched). If the mod fails, the prompt goes through unchanged.
/plugin install time-sense --marketplace u6vu06Ou/time-sense
(Answer y to add the marketplace, then pick the user scope so it works in every project. Pick project scope if you want it in one project only.)
讓 Claude Code 有時間感:隔很久回來時,偷偷告訴 AI 現在幾點、這個對話上一則訊息是多久以前;每個對話第一次發言時,也告訴它自己平常一回合實際花多久。這些情境是我設計它要解決的(例如「我去睡了」隔天回來、resume 幾週前的對話、把 10 分鐘的事估成 1~2 天),AI 看到後怎麼反應由它自己決定,不是保證的效果。每次只加幾十個 token,連續聊天時完全不加。安裝時選 user 範圍=所有專案都有效。
hooks/register.ts 84 lines1import type { Register } from 'claude-code'
2
3const GAP_MS = 20 * 60_000 // only speak up after 20 min of silence
4const KEEP = 40 // turn durations remembered
5const MIN_SAMPLES = 5
6
7let sessionNoted = false
8let turnStartedAt: number | undefined
9
10function pad(n: number) {
11 return String(n).padStart(2, '0')
12}
13
14function stamp(ms: number) {
15 const d = new Date(ms)
16 const day = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'][d.getDay()]
17 return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${day} ${pad(d.getHours())}:${pad(d.getMinutes())}`
18}
19
20function span(ms: number) {
21 const m = Math.round(ms / 60_000)
22 if (m < 60) return `${m}m`
23 const h = Math.floor(m / 60)
24 return h < 48 ? `${h}h${pad(m % 60)}m` : `${Math.floor(h / 24)}d${h % 24}h`
25}
26
27async function paceLine($: any) {
28 const list = ((await $.store.get('durations')) as number[] | undefined) ?? []
29 if (list.length < MIN_SAMPLES) return undefined
30 const s = [...list].sort((a, b) => a - b)
31 const median = s[Math.floor(s.length / 2)]
32 return `[pace] your last ${s.length} turns took median ${span(median)}, longest ${span(s[s.length - 1])} of real time. Estimate durations in these terms, not human workdays.`
33}
34
35// Per-conversation last-active time, so a resumed or overnight conversation sees its own gap.
36async function markActive($: any, id: string, now: number) {
37 const map = ((await $.store.get('lastBySession')) as Record<string, number> | undefined) ?? {}
38 map[id] = now
39 const keep = Object.entries(map).sort((a, b) => b[1] - a[1]).slice(0, 200)
40 await $.store.set('lastBySession', Object.fromEntries(keep))
41 await $.store.set('lastActive', now)
42}
43
44export const register: Register = on => {
45 on('prompt.submit', async ($, e, next) => {
46 const now = await $.clock.now()
47 const id = await $.session.id()
48 const bySession = ((await $.store.get('lastBySession')) as Record<string, number> | undefined) ?? {}
49 const mine = bySession[id]
50 const any = (await $.store.get('lastActive')) as number | undefined
51 await markActive($, id, now)
52 const last = mine ?? any
53 const notes: string[] = []
54 if (last === undefined || now - last >= GAP_MS || !sessionNoted) {
55 const ago = last === undefined ? '' : mine !== undefined
56 ? ` · this conversation's last message ${span(now - mine)} ago`
57 : ` · last activity ${span(now - last)} ago`
58 notes.push(`[time] now ${stamp(now)}` + ago)
59 }
60 if (!sessionNoted) {
61 sessionNoted = true
62 const pace = await paceLine($)
63 if (pace) notes.push(pace)
64 }
65 return notes.length ? next({ ...e, context: [...(e.context ?? []), notes.join(String.fromCharCode(10))] }) : next(e)
66 }).catch(($, e, next) => next(e))
67
68 on('turn.start', async ($, e, next) => {
69 if (!(e as any).agentId) turnStartedAt = await $.clock.now()
70 return next(e)
71 })
72
73 on('turn.complete', async ($, e, next) => {
74 if (!(e as any).agentId && turnStartedAt !== undefined) {
75 const now = await $.clock.now()
76 const list = ((await $.store.get('durations')) as number[] | undefined) ?? []
77 await $.store.set('durations', [...list, now - turnStartedAt].slice(-KEEP))
78 await markActive($, await $.session.id(), now)
79 turnStartedAt = undefined
80 }
81 return next(e)
82 })
83}
84