中文EN

dsh-time-reminder

tools装不上npmMIT

为 agent 提供它自己的时钟:按会话注入时间读数且精度可由 agent 运行时调整,并附带累计计时器与跨宿主重启可靠的一次性倒计时。

下载量 / Downloads
1,116
测试时间 / Tested
2026-11-06
DSH 版本 / Version
0.2.2-rc.1

安装

dsh plugin --profile web add dsh-time-reminder

沙箱测试结果

阶段结果
1 安装✗
2 列装—
3 挂载—
4 启动—
第一条错误
dsh: initialized profile web at /home/node/.dsh/profiles/web

四阶段在隔离沙箱里跑:安装(有网)→ 列装 → 挂载 → 启动(后三步断网)。

README

dsh-time-reminder

给 DSH 的模型一只可调精度的时钟,外加会话内计时器。

解决什么

模型默认不知道"现在几点",也不知道"距上一轮结束过了多久"。长时间干活时这让它无法判断

"这次等了 20 秒"还是"等了 20 分钟"。官方 @deepseek-ai/dsh-time-context 能做基础注入,但

不提供工具、且刷新间隔在加载时被闭包固定,会话中改不了。本插件把控制权交给模型和你。

四种时间模式(每会话独立,随会话日志持久化)

模式 · 行为 · 用途

cron(默认) · 跨过 cron 触发时刻注入一次;默认 0 0 *(每天 00:00,按你的浏览器时区) · 日常:知道今天几号、大概几点

off · 不自动注入;工具照常可查时间与计时器 · 上下文要干净时

turn · 每轮恰好一次,在该轮第一步(即你发消息之后那一步);给出距上一条模型可见消息的间隔 · 想每轮都知道"距上轮结束多久",又不想每步都注入

fine · 每个步骤都注入(含每次工具调用之后) · 精确计时、要步间耗时

粒度谱:off < cron(按时间)< turn(每轮 1 条)< fine(每步约 19 条,实测均值)。

工具

time_mode       mode: off | cron | turn | fine   可选 cron: "<表达式>"
                切换精度;cron 表达式按会话时区解释

time_counter    action: start | lapse | list | note | scope | reset | remove
                name, note, inject, toChildren, toParents, depth
                建/查/改计时器;可见性属性在调用时设置

time_countdown  action: start | snooze | cancel | list
                name, in("10m"/"1h30m"/"90s"), at("15:30"), onFire, note
                一次性倒计时:到点把自己叫醒(或只投一条通知)

time_counter 的属性:

属性 · 默认 · 含义

inject · false · 是否加入每次自动读数

toChildren · true · 是否让子代理读到

toParents · false · 是否让父代理读到

depth · 2 · 可见性沿谱系穿透几代(1 = 直接父子,2 = 也含祖孙)

计数器只存在于自己会话的状态里(随会话日志持久化),不跨会话共享。

cron 表达式同样是每会话状态:用 time_mode 改过之后,恢复会话仍然沿用。

倒计时(time_countdown)

一次性定时器,in 给相对时长(90s / 10m / 1h30m / 2d),at 给会话时区里的钟点

(15:30,今天已过就顺延到明天);两者必须给且只给一个。snooze 用 in 把它往后挪

(会清掉"已触发"标记,重新变回待触发),cancel 删掉。

已触发的不是"删掉"而是"收起来":list 默认只列待触发的(触发记录留着,

includeFired: true 才显示,最近 10 条、新的在前)。这样 list 始终是"还欠什么"的视图,

而不会随响过的提醒越积越长;同时"顺延刚响过的那条"(snooze 会清 firedAt 重新武装)

和"我刚才那条到底响了没"仍然可查 —— 记录一旦真删,这两种用法就都没了。

到点做什么由 onFire 决定:

onFire · 默认 · 行为

wake · ✅ · 投一条通知并唤醒该会话(空闲的会话会被拉起来开一轮)——这样"到点执行动作"才真的发生

inject · · 只把通知排进该会话的下一步,不唤醒(省 token,但空闲会话可能很久之后才看到)

持久化与重启:倒计时折进同一份会话状态(stateVersion: 3),会话日志仍是唯一真值;

另有一份待办索引表(<DSH_HOME>/time-reminder/countdowns.json,可用 pendingTable / pendingPath 配置)

只回答一个问题——"哪些会话还欠一条倒计时"。它存在的理由:插件看不见没被恢复的会话

(sessions.list() 只列活会话、get() 不恢复、没有枚举已存会话的 API),

宿主重启后若没人打开那个会话,光靠折叠根本不知道有这么条倒计时在等。

于是重启后的路径是:**读表 → 对每个待办会话 resolveAgent(resolve-or-resume)→ 折叠日志判断是否还欠 →

该武装的武装、该丢的行丢掉。表里的行不是真值**:倒计时被取消、已触发、或时间被改过,都会在对账时

按折叠结果修正,所以一行陈旧数据不可能凭空造出提醒。宿主当时没运行的倒计时会在启动后立刻补发

(通知里注明迟了多久);失败方向是至少一次:极窄的窗口里可能重复提醒一次,但不会静默丢掉。

它不会带偏读数节奏:触发通知来源带 countdown 标记且 form: 'notice',折叠把它当作

"这个倒计时已触发"的凭据,而不当作读数——所以触发不会重置 cron / turn 的节奏。

待触发的倒计时会出现在读数里(见下),让模型知道自己排了什么。

通知会说明"为什么不是准点到的",而且原因是触发那一刻观察到的,不是猜的:

观察到的情况 · 通知里补的那句

定时器在触发点之后才被武装(装载/扫描补发) · It came due before this session's countdown timers were running again, so this is the first chance to deliver it (12m0s late).

回调本身迟了 ≥2 秒 · The timer went off 7s late, so the process was suspended or blocked at that instant.

触发时 agent.status === 'running'(会话正卡在某一步里) · This session was in the middle of a step, so the notice waited for that step to end.

都不成立(准点 + 会话空闲) · 什么都不加——没有延迟需要解释

后两条可以同时成立,那时两句都会出现;2 秒以内的抖动不算迟到(否则会写成误导性的 0s late,

并把原因错记到宿主头上——实测两次的真相都是"定时器准点,只是我当时卡在工具调用里")。

冷会话会被自动恢复:触发时若会话不在驻留中(ctx.agents 里没有 Agent),插件走宿主会话控制器的

resolveAgent —— 文档原话是"Resolve or resume one ordinary Session for another Host API domain"

(api/session-controller/src/index.ts),官方 dsh-schedule 投递时用的同一个办法(schedule/src/runtime.ts:101)。

这是可选服务(web profile 才有),所以用 ctx.get('sessionController') 读。

⚠️ 实测到的细节:这个服务可能比本插件挂得更晚——冷启动时插件加载那一刻它还是 undefined

(调试日志里的 reconcile.row … verdict:"no-controller"),20 秒后会话才由客户端重连恢复

(deliver.done … resumedHere:false)。所以除启动对账外还注册了

ctx.inject(['sessionController'], …):服务一出现就再对账一次——那才是"恢复成为可能"的时刻。

没有这个控制器的组合(headless / 纯 SDK)里,表仍能"找到"会话,但无法把它恢复成活会话,

倒计时只能等会话自己回来(见下面的已知限制)。

计数器变动会推送给"看得见它"的会话

start(含重启)/ reset / note / scope / remove 都会给相关会话送一条

<system-reminder> 通知(lapse / list 这类只读动作不送)。收件人是变更前后可见集合的并集——

所以把 scope 收窄、或直接 remove 时,原来看得见的那些会话也会收到"它走了"。

通知的来源是 form: 'relay' + senderSessionId,不会被当成时间读数(否则会把对方的 cron/turn 节奏带偏)。

投递方式由配置决定(见下):inject(默认,投到对方下一个步骤、不唤醒)、

steer(投到最近一步并唤醒 idle 的对方)、off(不送)。对方会话不在驻留中(例如已收工的子代理)时投不进去,

但它的状态本来就从日志折叠,等它下次醒来照样能看到。

注入读数长这样

读数以 <system-reminder> 块注入(与 AGENTS.md 同一套框法,模型按"系统上下文"读,而不是"用户说的话"):

<system-reminder>
Time sampled while preparing turn 12, step 1: 2026-10-09T01:23:45+08:00[Asia/Shanghai]
Time mode: cron (daily 00:00 Asia/Shanghai; previous reading 2026-10-08T00:00:03+08:00[Asia/Shanghai])
Elapsed since the preceding model-visible message: 3m7s
[counters] deploy=20m43s (开始部署)
[countdowns] build in 4m12s (看 GH Actions)
</system-reminder>

第一行是时刻;第二行说明当前精度与"上次读数"(可追溯);第三行是距上一条模型可见消息的间隔

(第 1 步)或距上一条读数的间隔(后续步)。第四行只在有计数器开了 inject 时出现;

第五行只在有待触发倒计时时出现(最近的优先,最多列 5 条)。

用户也看得见:来源声明为 form: 'notice' 并带一行摘要(如 turn reading · 2026-10-09T01:23:45+08:00),

GUI 里那条折叠的"上下文注入"行不展开就能读到"什么读数、什么时候",展开是完整读数。

(官方 @deepseek-ai/dsh-time-context 用 form: 'snapshot',它不记录摘要,折叠行只剩生产者名——读数在,但一眼看不见。)

顶栏小钟

浏览器半边:会话头部工具区一枚小钟图标(挂在 conversation.session.header.utilities 槽位,

order: 118,即 SSH 胶囊右侧),鼠标悬停展开卡片。卡片是一排三块,并锚定小钟右缘、向左生长

(小钟在头部靠右,向右长会顶出视口):

[计时器][倒计时][时钟 + 模式]

左块 · 计时器:本会话的计数器,每格是名字 + 已过时长(每秒走动)。3×3 网格、每页 9 个,右上角翻页。

故意只读:不给暂停/修改,免得模型对"基线悄悄变了"感到困惑(这是设计决定,不是没做完)。右键格子可删除,

块头 +新建 可建(默认 inject: true,即跟读数一起进模型上下文)。

右块 · 倒计时:本会话待触发的倒计时,每格是一个 SVG 圆环——弧长 = 已过 / (触发点 - 设定点),

环内是剩余时间(12m / 3m20s),快到时变警示色;环下是名字,·静 表示这个倒计时 onFire: inject。

同样每页 9 个、右键删除、+新建(名字 + 时长,如 15m)。

最右 · 时钟 + 模式:

  • 当前时间(每秒走动)、日期星期、浏览器时区;
  • 本会话的时间提醒推送模式:四个按钮(关闭 / 定时 / 每轮 / 每步),当前模式高亮,下面一行说明它的效果;

选到 定时 时多一个计划表达式输入框(回车或点"保存"提交,非法表达式由宿主回 400 bad-cron 并显示原因);

改完显示"已请求,将在下一步生效"。

这三块里的每一项都是每会话的,所以卡片读写都带 sessionId(由会话作用域的槽位从框架那里拿到)。宿主侧路由:

路由 · 作用

…(truncated)

以上内容来自 npm 上的 README,我们只做格式化,不改内容。

版本

npm 上共发布:4,最后更新 2026-10-19

链接

GitHub · Homepage · npm

同类插件