refactor: apply repository naming contract

Apply the accepted pre-release package, service, type, directory, and role renames as one repository-wide change.
This commit is contained in:
Tianyi Cui
2026-08-13 00:36:22 +08:00
parent 101df7cf58
commit a2d0f7f411
3281 changed files with 21730 additions and 21592 deletions

View File

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/schedule/README.md
README.md: 7fffe6efb91e92a5664ee30ef9bf7c77581fe346
README.zh.md: a819dcb0e57cae834813479713d598ef26ce4ed3
README.md: 2fac190cc19f40e5d5e87acacaddce970e4b8474
README.zh.md: 3db72e7e6650e38ab51fd75087d95cb2faf121d4

View File

@@ -6,7 +6,7 @@ The Schedule family owns reminders whose durable state lives in the original Ses
| Package | Role | ctx key |
|---|---|---|
| `tool-schedule/` | Versioned Schedule events and fold, model-facing create/list/delete tools, and a live root-Agent timer owner | — |
| `schedule/` | Versioned Schedule events and fold, model-facing create/list/delete tools, and a live root-Agent timer owner | — |
The package deliberately exposes no public Schedule service or mutable database. Tools and runtime append to the Session stream; due work enters the same conversation through the Agent's ordinary follow-up queue.

View File

@@ -6,7 +6,7 @@ Schedule 家族负责管理提醒,其持久状态保存在原 Session 日志
| 包 | 职责 | ctx 键 |
|---|---|---|
| `tool-schedule/` | 版本化 Schedule 事件与 fold、面向模型的创建列出删除工具以及 live 根 Agent timer owner | 无 |
| `schedule/` | 版本化 Schedule 事件与 fold、面向模型的创建列出删除工具以及 live 根 Agent timer owner | 无 |
本包有意不公开 Schedule service 或可变数据库。工具与 runtime 向 Session stream 追加事件;到期工作通过 Agent 的普通 follow-up 队列进入同一对话。

View File

@@ -1,6 +1,6 @@
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/schedule/tool-schedule/README.md
README.md: b4738ca54e6b3862c75a5d6a871f102a6e160c5f
README.zh.md: 12e05cf0644339f87800695916944418c7e73771
# pnpm run verify-translation-pairing --write packages/schedule/schedule/README.md
README.md: f56c689198b7e17a85f43a1ecb6e5f1527da6a91
README.zh.md: 75425c77d2b43c6511c400a55bb5f8f898f978cc

View File

@@ -1,8 +1,8 @@
# @deepseek-ai/dsh-tool-schedule
# @deepseek-ai/dsh-schedule
English | [中文](README.zh.md)
`dsh-tool-schedule` gives future live root Agents three Session-scoped tools for durable reminders. Version 1 accepts positive safe-integer `after_seconds` delays, explicit absolute `at` targets, and fixed-rate `every_seconds` intervals of at least five minutes. The Session event log owns reminder state; timers, tool values, and model follow-ups are disposable projections of that log.
`dsh-schedule` gives future live root Agents three Session-scoped tools for durable reminders. Version 1 accepts positive safe-integer `after_seconds` delays, explicit absolute `at` targets, and fixed-rate `every_seconds` intervals of at least five minutes. The Session event log owns reminder state; timers, tool values, and model follow-ups are disposable projections of that log.
## Composition

View File

@@ -1,8 +1,8 @@
# @deepseek-ai/dsh-tool-schedule
# @deepseek-ai/dsh-schedule
[English](README.md) | 中文
`dsh-tool-schedule` 为未来创建的 live 根 agent智能体提供 3 个会话范围内的工具,用于管理持久提醒。版本 1 接受正的安全整数 `after_seconds` 延时、显式绝对时间 `at` 目标,以及至少 5 分钟的固定速率 `every_seconds` 间隔。会话事件日志拥有提醒状态timer、工具值和模型 follow-up 都是该日志的可丢弃投影。
`dsh-schedule` 为未来创建的 live 根 agent智能体提供 3 个会话范围内的工具,用于管理持久提醒。版本 1 接受正的安全整数 `after_seconds` 延时、显式绝对时间 `at` 目标,以及至少 5 分钟的固定速率 `every_seconds` 间隔。会话事件日志拥有提醒状态timer、工具值和模型 follow-up 都是该日志的可丢弃投影。
## 组合

View File

@@ -1,5 +1,5 @@
{
"name": "@deepseek-ai/dsh-tool-schedule",
"name": "@deepseek-ai/dsh-schedule",
"description": "Agent-scoped durable after, at, and fixed-rate reminders over the session event log",
"version": "0.0.1-rc.2",
"publishConfig": {
@@ -8,7 +8,7 @@
"repository": {
"type": "git",
"url": "git+https://github.com/deepseek-ai/deepseek-harness.git",
"directory": "packages/schedule/tool-schedule"
"directory": "packages/schedule/schedule"
},
"type": "module",
"main": "lib/index.js",

View File

@@ -1,6 +1,6 @@
/**
* Strict Schedule decoding, replay, time validation, and framing.
* @module @deepseek-ai/dsh-tool-schedule
* @module @deepseek-ai/dsh-schedule
*/
import type { SessionEvent } from '@deepseek-ai/dsh-session'

View File

@@ -1,12 +1,12 @@
/**
* Agent-scoped durable one-shot and fixed-rate reminders over the session event log.
* @module @deepseek-ai/dsh-tool-schedule
* @module @deepseek-ai/dsh-schedule
*/
import type { Context } from '@deepseek-ai/cordis'
import type { Agent } from '@deepseek-ai/dsh-agent'
import type {} from '@deepseek-ai/dsh-session-persistence'
import { ScheduleOwner } from './runtime.ts'
import { ScheduleRuntime } from './runtime.ts'
import { registerScheduleTools } from './tools.ts'
export type * from './types.ts'
@@ -30,7 +30,7 @@ export {
export { registerScheduleTools } from './tools.ts'
/** Cordis function-plugin name. */
export const name = 'tool-schedule'
export const name = 'schedule'
/** Services required before future root agents can receive Schedule. */
export const inject = ['agents', 'sessions', 'tools', 'sessionPersistence']
@@ -38,40 +38,40 @@ type OwnerCleanup = () => void | Promise<void>
/** Install Schedule only for root agents published after this plugin loads. */
export function apply(ctx: Context): void {
const owners = new Map<Agent, OwnerCleanup>()
const runtimes = new Map<Agent, OwnerCleanup>()
let stopping = false
ctx.effect(() => {
const stopCreated = ctx.on('agent/created', ({ agent }) => {
if (stopping || owners.has(agent) || !ctx.agents.roots().includes(agent)) return
const owner = new ScheduleOwner(ctx, agent)
if (stopping || runtimes.has(agent) || !ctx.agents.roots().includes(agent)) return
const runtime = new ScheduleRuntime(ctx, agent)
const cleanup: OwnerCleanup = agent.ctx.effect(() => {
const disposeTools = registerScheduleTools(ctx, agent.ctx, agent, () => { owner.requestDrive() })
const disposeTools = registerScheduleTools(ctx, agent.ctx, agent, () => { runtime.requestDrive() })
const stopStatus = agent.ctx.on('agent/status', ({ status }) => {
if (status === 'idle' && agent.session.events.some(event => event.type === 'schedule/change')) {
owner.requestDrive()
runtime.requestDrive()
}
})
owner.start()
runtime.start()
return async () => {
stopStatus()
disposeTools()
try {
await owner.dispose()
await runtime.dispose()
} finally {
if (owners.get(agent) === cleanup) owners.delete(agent)
if (runtimes.get(agent) === cleanup) runtimes.delete(agent)
}
}
}, 'tool-schedule.owner()')
owners.set(agent, cleanup)
}, 'schedule.runtime()')
runtimes.set(agent, cleanup)
})
return async () => {
stopping = true
stopCreated()
const cleanups = [...owners.values()]
owners.clear()
const cleanups = [...runtimes.values()]
runtimes.clear()
await Promise.allSettled(cleanups.map(cleanup => Promise.resolve(cleanup())))
}
}, 'tool-schedule.lifecycle()')
}, 'schedule.lifecycle()')
}

View File

@@ -1,6 +1,6 @@
/**
* Package-owned strict Schedule stream invariant.
* @module @deepseek-ai/dsh-tool-schedule/invariant
* @module @deepseek-ai/dsh-schedule/invariant
*/
import type { Context } from '@deepseek-ai/cordis'
@@ -8,7 +8,7 @@ import type { Session, SessionEvent } from '@deepseek-ai/dsh-session'
import type { InvariantFailure, InvariantInstaller } from '@deepseek-ai/dsh-invariants'
import { foldScheduleEvents, ScheduleLogError } from './domain.ts'
const PACKAGE_NAME = '@deepseek-ai/dsh-tool-schedule'
const PACKAGE_NAME = '@deepseek-ai/dsh-schedule'
/** Cordis invariant-companion plugin name. */
export const name = 'tool-schedule-invariant'

View File

@@ -1,6 +1,6 @@
/**
* Disposable live timer projection for one exact root agent.
* @module @deepseek-ai/dsh-tool-schedule
* @module @deepseek-ai/dsh-schedule
*/
import type { Context } from '@deepseek-ai/cordis'
@@ -74,7 +74,7 @@ function renderThrown(value: unknown): string {
}
/** One process-local, disposable projection of an exact agent's durable schedules. */
export class ScheduleOwner {
export class ScheduleRuntime {
private readonly stop = Promise.withResolvers<void>()
private timer: ReturnType<typeof setTimeout> | undefined
private idleWait: Promise<void> | undefined
@@ -85,7 +85,7 @@ export class ScheduleOwner {
private disposal: Promise<void> | undefined
/**
* Construct an inactive owner; {@link start} begins the first preflight.
* Construct an inactive runtime; {@link start} begins the first preflight.
* @param ctx - Global service context.
* @param agent - Exact live root agent.
*/
@@ -110,7 +110,7 @@ export class ScheduleOwner {
run = this.ctx.agents.withoutInitiator(() => this.runRequested())
} catch (error: unknown) {
if (this.isLive()) {
this.ctx.logger.warn(`tool-schedule: could not start owner for agent "${this.agent.id}": ${renderThrown(error)}`)
this.ctx.logger.warn(`schedule: could not start runtime for agent "${this.agent.id}": ${renderThrown(error)}`)
}
return
}
@@ -119,7 +119,7 @@ export class ScheduleOwner {
() => { this.retire(run) },
(error: unknown) => {
if (this.isLive()) {
this.ctx.logger.warn(`tool-schedule: owner failed for agent "${this.agent.id}": ${renderThrown(error)}`)
this.ctx.logger.warn(`schedule: runtime failed for agent "${this.agent.id}": ${renderThrown(error)}`)
}
this.faulted = true
this.retire(run)
@@ -127,7 +127,7 @@ export class ScheduleOwner {
)
}
/** Stop future work, cancel timers, and await every outstanding owner promise. */
/** Stop future work, cancel timers, and await every outstanding runtime promise. */
dispose(): Promise<void> {
return (this.disposal ??= (async () => {
this.stopping = true
@@ -162,7 +162,7 @@ export class ScheduleOwner {
&& this.ctx.agents.roots().includes(this.agent)
}
/** Whether this owner may start or continue Schedule work. */
/** Whether this runtime may start or continue Schedule work. */
private isRunnable(): boolean {
return !this.stopping && this.isLive()
}
@@ -196,13 +196,13 @@ export class ScheduleOwner {
(error: unknown) => {
this.idleWait = undefined
if (this.isLive()) {
this.ctx.logger.warn(`tool-schedule: idle wait failed for agent "${this.agent.id}": ${renderThrown(error)}`)
this.ctx.logger.warn(`schedule: idle wait failed for agent "${this.agent.id}": ${renderThrown(error)}`)
}
},
)
}
/** Fold the current exact owner suffix and contain a corrupt durable stream. */
/** Fold the current exact runtime suffix and contain a corrupt durable stream. */
private readFolded(): FoldedSchedules | undefined {
try {
return foldScheduleEvents(
@@ -212,17 +212,17 @@ export class ScheduleOwner {
} catch (error: unknown) {
this.faulted = true
const detail = error instanceof ScheduleLogError ? error.message : renderThrown(error)
this.ctx.logger.warn(`tool-schedule: corrupt schedule log for agent "${this.agent.id}": ${detail}`)
this.ctx.logger.warn(`schedule: corrupt schedule log for agent "${this.agent.id}": ${detail}`)
return undefined
}
}
/** Contain an invalid wall-clock decision without permanently faulting this owner. */
/** Contain an invalid wall-clock decision without permanently faulting this runtime. */
private decide(folded: FoldedSchedules, now: number): DueDecision | undefined {
try {
return dueDecision(folded, now)
} catch (error: unknown) {
this.ctx.logger.warn(`tool-schedule: fixed-rate decision failed for agent "${this.agent.id}": ${renderThrown(error)}`)
this.ctx.logger.warn(`schedule: fixed-rate decision failed for agent "${this.agent.id}": ${renderThrown(error)}`)
return undefined
}
}
@@ -235,7 +235,7 @@ export class ScheduleOwner {
await flushSchedulePersistence(this.ctx, this.agent.session)
} catch (error: unknown) {
if (this.isLive()) {
this.ctx.logger.warn(`tool-schedule: preflight failed for agent "${this.agent.id}": ${renderThrown(error)}`)
this.ctx.logger.warn(`schedule: preflight failed for agent "${this.agent.id}": ${renderThrown(error)}`)
}
return
}
@@ -270,12 +270,12 @@ export class ScheduleOwner {
: renderEveryReminderBatchFraming(decision.reminders)
const message = createUserMessage({
content: [{ type: 'text', text }],
source: { kind: 'plugin', plugin: 'tool-schedule' },
source: { kind: 'plugin', plugin: 'schedule' },
})
this.agent.followup(message)
} catch (error: unknown) {
if (this.isLive()) {
this.ctx.logger.warn(`tool-schedule: framing or followup failed for agent "${this.agent.id}": ${renderThrown(error)}`)
this.ctx.logger.warn(`schedule: framing or followup failed for agent "${this.agent.id}": ${renderThrown(error)}`)
}
return Promise.resolve(false)
}
@@ -299,7 +299,7 @@ export class ScheduleOwner {
} catch (error: unknown) {
this.faulted = true
this.clearTimer()
this.ctx.logger.warn(`tool-schedule: dispatch append failed for agent "${this.agent.id}": ${renderThrown(error)}`)
this.ctx.logger.warn(`schedule: dispatch append failed for agent "${this.agent.id}": ${renderThrown(error)}`)
return Promise.resolve(false)
}
return Promise.resolve(true)
@@ -315,7 +315,7 @@ export class ScheduleOwner {
await flushSchedulePersistence(this.ctx, this.agent.session)
} catch (error: unknown) {
if (this.isLive()) {
this.ctx.logger.warn(`tool-schedule: dispatch barrier failed for agent "${this.agent.id}": ${renderThrown(error)}`)
this.ctx.logger.warn(`schedule: dispatch barrier failed for agent "${this.agent.id}": ${renderThrown(error)}`)
}
return
}

View File

@@ -1,6 +1,6 @@
/**
* Agent-scoped Schedule management tools over the durable session fold.
* @module @deepseek-ai/dsh-tool-schedule
* @module @deepseek-ai/dsh-schedule
*/
import type { Context } from '@deepseek-ai/cordis'
@@ -163,7 +163,7 @@ const DELETE_DESCRIPTION =
/** Deterministic model content for every canonical Schedule value. */
function renderValue(_args: unknown, value: unknown): ContentBlock[] {
// The ToolRegistry has already validated the value against the lossless-JSON output schema.
// The ToolRuntime has already validated the value against the lossless-JSON output schema.
const text = JSON.stringify(value)
return [{ type: 'text', text }]
}
@@ -309,7 +309,7 @@ export function registerScheduleTools(
try {
onDurableChange()
} catch (error: unknown) {
rootCtx.logger.warn(`tool-schedule: durable-change observer failed: ${error instanceof Error ? error.message : String(error)}`)
rootCtx.logger.warn(`schedule: durable-change observer failed: ${error instanceof Error ? error.message : String(error)}`)
}
}

View File

@@ -1,6 +1,6 @@
/**
* Durable and model-facing Schedule value types.
* @module @deepseek-ai/dsh-tool-schedule
* @module @deepseek-ai/dsh-schedule
*/
import type { Branded } from '@deepseek-ai/dsh-brand'

View File

@@ -1,6 +1,6 @@
import { describe, expect, it } from 'vitest'
import { Context } from '@deepseek-ai/cordis'
import InvariantService, { InvariantError } from '@deepseek-ai/dsh-invariants'
import InvariantRegistry, { InvariantError } from '@deepseek-ai/dsh-invariants'
import SessionStore, { SessionId } from '@deepseek-ai/dsh-session'
import type { SessionEvent } from '@deepseek-ai/dsh-session'
import * as scheduleInvariant from '../src/invariant.ts'
@@ -42,7 +42,7 @@ function createEvery(id: string): ScheduleChange {
async function harness() {
const ctx = new Context()
await ctx.plugin(SessionStore)
await ctx.plugin(InvariantService)
await ctx.plugin(InvariantRegistry)
const fiber = await ctx.plugin(scheduleInvariant)
return { ctx, fiber }
}
@@ -89,7 +89,7 @@ describe('Schedule package invariant', () => {
it('rejects a malformed existing owned stream during companion setup', async () => {
const ctx = new Context()
await ctx.plugin(SessionStore)
await ctx.plugin(InvariantService)
await ctx.plugin(InvariantRegistry)
ctx.sessions.create(SessionId('schedule-invalid-seed'), {
seed: [event({ version: 9, operation: 'delete', id: 'schedule-1' }, 0)],
})
@@ -110,7 +110,7 @@ describe('Schedule package invariant', () => {
it('ignores inherited Schedule events before a fork seed boundary', async () => {
const ctx = new Context()
await ctx.plugin(SessionStore)
await ctx.plugin(InvariantService)
await ctx.plugin(InvariantRegistry)
const child = ctx.sessions.create(SessionId('schedule-fork'), {
seed: [event({ version: 9, operation: 'delete', id: 'parent' }, 0)],
meta: { parentSession: SessionId('parent'), seedLength: 1 },

View File

@@ -9,7 +9,7 @@ import AgentLoop from '@deepseek-ai/dsh-agent-loop'
import { mountAgentLoopTestDependencies } from '@deepseek-ai/dsh-agent-loop-testkit'
import { LlmAdapter, type GenerateOptions, type StreamChunk } from '@deepseek-ai/dsh-llm'
import SessionStore, { SessionId } from '@deepseek-ai/dsh-session'
import SessionPersistenceJsonl from '@deepseek-ai/dsh-session-persistence-jsonl'
import JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
import * as toolSchedule from '../src/index.ts'
import {
ScheduleId,
@@ -43,7 +43,7 @@ async function mountPersistence(root: string): Promise<Context> {
const ctx = new Context()
contexts.push(ctx)
await ctx.plugin(SessionStore)
await ctx.plugin(SessionPersistenceJsonl, { root, compression: 'none' })
await ctx.plugin(JsonlSessionPersistence, { root, compression: 'none' })
return ctx
}
@@ -52,7 +52,7 @@ async function mountRuntime(root: string, adapter: RecordingAdapter): Promise<Co
contexts.push(ctx)
await mountAgentLoopTestDependencies(ctx)
await ctx.plugin(AgentLoop, { agents: [] })
await ctx.plugin(SessionPersistenceJsonl, { root, compression: 'none' })
await ctx.plugin(JsonlSessionPersistence, { root, compression: 'none' })
ctx.llm.registerAdapter(['mock'], adapter)
await ctx.plugin(toolSchedule)
return ctx

View File

@@ -30,7 +30,7 @@ async function settle(): Promise<void> {
describe('Schedule plugin composition', () => {
it('has the Loader-safe function-plugin export shape', () => {
expect('default' in toolSchedule).toBe(false)
expect(toolSchedule.name).toBe('tool-schedule')
expect(toolSchedule.name).toBe('schedule')
expect(toolSchedule.inject).toEqual(['agents', 'sessions', 'tools', 'sessionPersistence'])
const loader = Object.create(Loader.prototype) as Loader
expect(loader.unwrapExports(toolSchedule)).toBe(toolSchedule)

View File

@@ -10,10 +10,10 @@ import {
createEveryScheduleRecord,
foldScheduleEvents,
} from '../src/domain.ts'
import { MAX_TIMER_DELAY_MS, ScheduleOwner } from '../src/runtime.ts'
import { MAX_TIMER_DELAY_MS, ScheduleRuntime } from '../src/runtime.ts'
const contexts: Context[] = []
const owners: ScheduleOwner[] = []
const runtimes: ScheduleRuntime[] = []
interface RuntimeHarness {
readonly ctx: Context
@@ -138,10 +138,10 @@ async function settle(): Promise<void> {
for (let index = 0; index < 8; index += 1) await Promise.resolve()
}
function ownerFor(test: RuntimeHarness): ScheduleOwner {
const owner = new ScheduleOwner(test.ctx, test.agent)
owners.push(owner)
return owner
function runtimeFor(test: RuntimeHarness): ScheduleRuntime {
const runtime = new ScheduleRuntime(test.ctx, test.agent)
runtimes.push(runtime)
return runtime
}
beforeEach(() => {
@@ -150,7 +150,7 @@ beforeEach(() => {
})
afterEach(async () => {
await Promise.allSettled(owners.splice(0).map(owner => owner.dispose()))
await Promise.allSettled(runtimes.splice(0).map(runtime => runtime.dispose()))
await Promise.allSettled(contexts.splice(0).map(ctx => ctx.fiber.dispose()))
vi.useRealTimers()
})
@@ -161,8 +161,8 @@ describe('Schedule timer and admission runtime', () => {
const delaySeconds = Math.ceil((MAX_TIMER_DELAY_MS + 1_500) / 1_000)
const targetDelay = delaySeconds * 1_000
appendAfter(test, 'schedule-1', delaySeconds)
const owner = ownerFor(test)
owner.start()
const runtime = runtimeFor(test)
runtime.start()
await settle()
await vi.advanceTimersByTimeAsync(MAX_TIMER_DELAY_MS)
@@ -175,14 +175,14 @@ describe('Schedule timer and admission runtime', () => {
expect(test.controls.releaseCount).toBe(1)
expect(test.agent.session.events.find(event =>
event.type === 'schedule/change' && event.data.operation === 'dispatch')).toBeDefined()
await owner.dispose()
await runtime.dispose()
})
it('does not fire early after a wall-clock rollback', async () => {
const test = await harness()
appendAfter(test, 'schedule-1', 10)
const owner = ownerFor(test)
owner.start()
const runtime = runtimeFor(test)
runtime.start()
await settle()
vi.setSystemTime(new Date('2026-08-05T11:59:40.000Z'))
@@ -193,39 +193,39 @@ describe('Schedule timer and admission runtime', () => {
await vi.advanceTimersByTimeAsync(20_000)
await settle()
expect(test.followed).toHaveLength(1)
await owner.dispose()
await runtime.dispose()
})
it('treats a forward jump as overdue and dispatches once', async () => {
const test = await harness()
appendAfter(test, 'schedule-1', 60)
const owner = ownerFor(test)
owner.start()
const runtime = runtimeFor(test)
runtime.start()
await settle()
vi.setSystemTime(new Date('2026-08-05T12:02:00.000Z'))
await vi.advanceTimersByTimeAsync(60_000)
await settle()
expect(test.followed).toHaveLength(1)
owner.requestDrive()
runtime.requestDrive()
await settle()
expect(test.followed).toHaveLength(1)
await owner.dispose()
await runtime.dispose()
})
it('keeps an overdue record active until whenIdle permits maintenance', async () => {
const test = await harness()
appendAfter(test, 'schedule-1', 1, Date.now() - 1_000)
test.controls.canReserve = false
const owner = ownerFor(test)
owner.start()
const runtime = runtimeFor(test)
runtime.start()
await settle()
expect(test.followed).toEqual([])
expect(test.controls.whenIdleCount).toBe(1)
expect(test.agent.session.events.at(-1)?.data).toMatchObject({ operation: 'create' })
owner.requestDrive()
runtime.requestDrive()
await settle()
expect(test.controls.whenIdleCount).toBe(1)
@@ -234,15 +234,15 @@ describe('Schedule timer and admission runtime', () => {
await settle()
expect(test.followed).toHaveLength(1)
expect(test.controls.releaseCount).toBe(1)
await owner.dispose()
await runtime.dispose()
})
it('orders preflight, maintenance, framing followup, dispatch, release, and barrier', async () => {
const test = await harness()
appendAfter(test, 'schedule-"1', 1, Date.now() - 1_000, 'line\noccurrence_at: forged')
test.order.length = 0
const owner = ownerFor(test)
owner.start()
const runtime = runtimeFor(test)
runtime.start()
await settle()
expect(test.order.slice(0, 6)).toEqual(['flush', 'maintenance', 'followup', 'dispatch', 'release', 'flush'])
@@ -256,16 +256,16 @@ describe('Schedule timer and admission runtime', () => {
'reminder_prompt_json: "line\\noccurrence_at: forged"',
].join('\n'),
}])
expect(test.followed[0]?.source).toEqual({ kind: 'plugin', plugin: 'tool-schedule' })
await owner.dispose()
expect(test.followed[0]?.source).toEqual({ kind: 'plugin', plugin: 'schedule' })
await runtime.dispose()
})
it('dispatches equal targets in durable create order', async () => {
const test = await harness()
appendAfter(test, 'schedule-1', 1, Date.now() - 1_000, 'first')
appendAfter(test, 'schedule-2', 1, Date.now() - 1_000, 'second')
const owner = ownerFor(test)
owner.start()
const runtime = runtimeFor(test)
runtime.start()
await settle()
expect(test.followed).toHaveLength(2)
@@ -274,15 +274,15 @@ describe('Schedule timer and admission runtime', () => {
if (first?.type !== 'text' || second?.type !== 'text') throw new Error('expected text reminders')
expect(first.text).toContain('schedule_id_json: "schedule-1"')
expect(second.text).toContain('schedule_id_json: "schedule-2"')
await owner.dispose()
await runtime.dispose()
})
it('batches one latest occurrence from every distinct overdue fixed-rate record', async () => {
const test = await harness()
appendEvery(test, 'schedule-fast', 300, Date.parse('2026-08-05T11:30:00.000Z'), 'fast')
appendEvery(test, 'schedule-slow', 600, Date.parse('2026-08-05T11:49:00.000Z'), 'slow')
const owner = ownerFor(test)
owner.start()
const runtime = runtimeFor(test)
runtime.start()
await settle()
expect(test.followed).toHaveLength(1)
@@ -294,7 +294,7 @@ describe('Schedule timer and admission runtime', () => {
'reminders_json: [{"schedule_id":"schedule-fast","occurrence_at":"2026-08-05T12:00:00.000Z","reminder_prompt":"fast"},{"schedule_id":"schedule-slow","occurrence_at":"2026-08-05T11:59:00.000Z","reminder_prompt":"slow"}]',
].join('\n'),
}])
expect(test.followed[0]?.source).toEqual({ kind: 'plugin', plugin: 'tool-schedule' })
expect(test.followed[0]?.source).toEqual({ kind: 'plugin', plugin: 'schedule' })
const dispatches = test.agent.session.events.filter(event =>
event.type === 'schedule/change' && event.data.operation === 'dispatch')
expect(dispatches.map(event => event.data)).toEqual([
@@ -313,15 +313,15 @@ describe('Schedule timer and admission runtime', () => {
if (next?.type !== 'text') throw new Error('expected fixed-rate batch text')
expect(next.text).toContain('"occurrence_at":"2026-08-05T12:05:00.000Z"')
expect(next.text).not.toContain('schedule-slow')
await owner.dispose()
await runtime.dispose()
})
it('delivers due one-shots before one fixed-rate batch', async () => {
const test = await harness()
appendEvery(test, 'schedule-every', 300, Date.parse('2026-08-05T11:50:00.000Z'), 'repeat')
appendAfter(test, 'schedule-once', 1, Date.now() - 1_000, 'once')
const owner = ownerFor(test)
owner.start()
const runtime = runtimeFor(test)
runtime.start()
await settle()
expect(test.followed).toHaveLength(2)
@@ -331,7 +331,7 @@ describe('Schedule timer and admission runtime', () => {
expect(first.text).toContain('schedule_id_json: "schedule-once"')
expect(second.text).toContain('[SCHEDULE REMINDER BATCH]')
expect(second.text).toContain('"schedule_id":"schedule-every"')
await owner.dispose()
await runtime.dispose()
})
it('rechecks the wall clock after claiming maintenance before queuing', async () => {
@@ -341,8 +341,8 @@ describe('Schedule timer and admission runtime', () => {
vi.setSystemTime(new Date('2026-08-05T11:59:50.000Z'))
test.controls.onReserve = undefined
}
const owner = ownerFor(test)
owner.start()
const runtime = runtimeFor(test)
runtime.start()
await settle()
expect(test.followed).toEqual([])
expect(test.controls.releaseCount).toBe(1)
@@ -350,7 +350,7 @@ describe('Schedule timer and admission runtime', () => {
await vi.advanceTimersByTimeAsync(10_000)
await settle()
expect(test.followed).toHaveLength(1)
await owner.dispose()
await runtime.dispose()
})
it('rechecks the durable fold after claiming maintenance', async () => {
@@ -364,29 +364,29 @@ describe('Schedule timer and admission runtime', () => {
id: ScheduleId('schedule-1'),
})
}
const owner = ownerFor(test)
owner.start()
const runtime = runtimeFor(test)
runtime.start()
await settle()
expect(test.controls.releaseCount).toBe(1)
expect(test.followed).toEqual([])
expect(test.agent.session.events.at(-1)?.data).toMatchObject({ operation: 'delete' })
owner.requestDrive()
runtime.requestDrive()
await settle()
expect(test.followed).toEqual([])
await owner.dispose()
await runtime.dispose()
})
it('contains invalid fixed-rate clocks and a fold that becomes unreadable after claiming', async () => {
const wakeClock = await harness()
appendEvery(wakeClock, 'schedule-every', 300, Date.parse('2026-08-05T11:50:00.000Z'))
const wakeClockSpy = vi.spyOn(Date, 'now').mockReturnValue(Number.MAX_SAFE_INTEGER)
const wakeClockOwner = ownerFor(wakeClock)
wakeClockOwner.start()
const wakeClockRuntime = runtimeFor(wakeClock)
wakeClockRuntime.start()
await settle()
expect(wakeClock.followed).toEqual([])
wakeClockSpy.mockRestore()
await wakeClockOwner.dispose()
await wakeClockRuntime.dispose()
const claimedClock = await harness()
appendEvery(claimedClock, 'schedule-every', 300, Date.parse('2026-08-05T11:50:00.000Z'))
@@ -395,12 +395,12 @@ describe('Schedule timer and admission runtime', () => {
clockCalls += 1
return clockCalls === 1 ? Date.parse('2026-08-05T12:00:00.000Z') : Number.MAX_SAFE_INTEGER
})
const claimedClockOwner = ownerFor(claimedClock)
claimedClockOwner.start()
const claimedClockRuntime = runtimeFor(claimedClock)
claimedClockRuntime.start()
await settle()
expect(claimedClock.followed).toEqual([])
claimedClockSpy.mockRestore()
await claimedClockOwner.dispose()
await claimedClockRuntime.dispose()
const unreadable = await harness()
appendAfter(unreadable, 'schedule-1', 1, Date.now() - 1_000)
@@ -411,11 +411,11 @@ describe('Schedule timer and admission runtime', () => {
get() { throw new Error('became unreadable') },
})
}
const unreadableOwner = ownerFor(unreadable)
unreadableOwner.start()
const unreadableRuntime = runtimeFor(unreadable)
unreadableRuntime.start()
await settle()
expect(unreadable.followed).toEqual([])
await unreadableOwner.dispose()
await unreadableRuntime.dispose()
})
})
@@ -424,24 +424,24 @@ describe('Schedule runtime failure and teardown boundaries', () => {
const test = await harness()
appendAfter(test, 'schedule-1', 1, Date.now() - 1_000)
test.controls.throwFollowup = true
const owner = ownerFor(test)
owner.start()
const runtime = runtimeFor(test)
runtime.start()
await settle()
expect(test.controls.releaseCount).toBe(1)
expect(test.agent.session.events.filter(event =>
event.type === 'schedule/change' && event.data.operation === 'dispatch')).toEqual([])
await owner.dispose()
await runtime.dispose()
const departed = await harness()
appendAfter(departed, 'schedule-1', 1, Date.now() - 1_000)
departed.controls.throwFollowup = true
departed.controls.onFollowup = departed.disposeAgent
const departedOwner = ownerFor(departed)
departedOwner.start()
const departedRuntime = runtimeFor(departed)
departedRuntime.start()
await settle()
expect(departed.followed).toEqual([])
await departedOwner.dispose()
await departedRuntime.dispose()
})
it('faults after append throws so an already-queued reminder is not repeated', async () => {
@@ -454,19 +454,19 @@ describe('Schedule runtime failure and teardown boundaries', () => {
throw new Error('append failed')
}
}, { global: true })
const owner = ownerFor(test)
owner.start()
const runtime = runtimeFor(test)
runtime.start()
await settle()
expect(test.followed).toHaveLength(1)
expect(test.controls.releaseCount).toBe(1)
expect(test.agent.session.events.filter(event =>
event.type === 'schedule/change' && event.data.operation === 'dispatch')).toEqual([])
owner.requestDrive()
runtime.requestDrive()
await settle()
expect(test.followed).toHaveLength(1)
stop()
await owner.dispose()
await runtime.dispose()
})
it('faults after a partial fixed-rate batch append without repeating its queued message', async () => {
@@ -481,8 +481,8 @@ describe('Schedule runtime failure and teardown boundaries', () => {
dispatchAttempts += 1
if (dispatchAttempts === 2) throw new Error('second append failed')
}, { global: true })
const owner = ownerFor(test)
owner.start()
const runtime = runtimeFor(test)
runtime.start()
await settle()
expect(test.followed).toHaveLength(1)
@@ -499,28 +499,28 @@ describe('Schedule runtime failure and teardown boundaries', () => {
expect.objectContaining({ id: 'schedule-first', scheduledAt: '2026-08-05T12:05:00.000Z' }),
expect.objectContaining({ id: 'schedule-second', scheduledAt: '2026-08-05T11:55:00.000Z' }),
])
owner.requestDrive()
runtime.requestDrive()
await settle()
expect(test.followed).toHaveLength(1)
stop()
await owner.dispose()
await runtime.dispose()
})
it('does not retry a rejected dispatch barrier until another trigger preflights it', async () => {
const test = await harness()
appendAfter(test, 'schedule-1', 1, Date.now() - 1_000)
test.controls.flushOutcomes.push('resolve', 'reject', 'resolve')
const owner = ownerFor(test)
owner.start()
const runtime = runtimeFor(test)
runtime.start()
await settle()
expect(test.followed).toHaveLength(1)
expect(test.controls.flushCount).toBe(2)
owner.requestDrive()
runtime.requestDrive()
await settle()
expect(test.controls.flushCount).toBe(3)
expect(test.followed).toHaveLength(1)
await owner.dispose()
await runtime.dispose()
const departed = await harness()
appendAfter(departed, 'schedule-1', 1, Date.now() - 1_000)
@@ -529,75 +529,75 @@ describe('Schedule runtime failure and teardown boundaries', () => {
departed.disposeAgent()
return Promise.reject(new Error('detached barrier'))
}
const departedOwner = ownerFor(departed)
departedOwner.start()
const departedRuntime = runtimeFor(departed)
departedRuntime.start()
await settle()
expect(departed.followed).toHaveLength(1)
await departedOwner.dispose()
await departedRuntime.dispose()
})
it('keeps an overdue record pending after a rejected preflight', async () => {
const test = await harness()
appendAfter(test, 'schedule-1', 1, Date.now() - 1_000)
test.controls.flushOutcomes.push('reject')
const owner = ownerFor(test)
owner.start()
const runtime = runtimeFor(test)
runtime.start()
await settle()
expect(test.controls.flushCount).toBe(1)
expect(test.followed).toEqual([])
expect(test.agent.session.events.at(-1)?.data).toMatchObject({ operation: 'create' })
await owner.dispose()
await runtime.dispose()
const departed = await harness()
appendAfter(departed, 'schedule-1', 1, Date.now() - 1_000)
const rejected = Promise.withResolvers<undefined>()
departed.controls.flushHandler = () => rejected.promise
const departedOwner = ownerFor(departed)
departedOwner.start()
const departedRuntime = runtimeFor(departed)
departedRuntime.start()
await Promise.resolve()
departed.disposeAgent()
rejected.reject(new Error('detached preflight'))
await settle()
expect(departed.followed).toEqual([])
await departedOwner.dispose()
await departedRuntime.dispose()
})
it('contains idle-wait rejection without dispatching', async () => {
const test = await harness()
appendAfter(test, 'schedule-1', 1, Date.now() - 1_000)
test.controls.canReserve = false
const owner = ownerFor(test)
owner.start()
const runtime = runtimeFor(test)
runtime.start()
await settle()
test.controls.idle.reject('idle failed')
await settle()
expect(test.followed).toEqual([])
await owner.dispose()
await runtime.dispose()
const departed = await harness()
appendAfter(departed, 'schedule-1', 1, Date.now() - 1_000)
departed.controls.canReserve = false
const departedOwner = ownerFor(departed)
departedOwner.start()
const departedRuntime = runtimeFor(departed)
departedRuntime.start()
await settle()
departed.disposeAgent()
departed.controls.idle.reject(new Error('owner departed'))
departed.controls.idle.reject(new Error('runtime departed'))
await settle()
expect(departed.followed).toEqual([])
await departedOwner.dispose()
await departedRuntime.dispose()
})
it('stops an idle wait during dispose even if the agent never becomes idle', async () => {
const test = await harness()
appendAfter(test, 'schedule-1', 1, Date.now() - 1_000)
test.controls.canReserve = false
const owner = ownerFor(test)
owner.start()
const runtime = runtimeFor(test)
runtime.start()
await settle()
expect(test.controls.whenIdleCount).toBe(1)
let disposed = false
const disposal = owner.dispose().then(() => { disposed = true })
const disposal = runtime.dispose().then(() => { disposed = true })
await settle()
try {
expect(disposed).toBe(true)
@@ -620,8 +620,8 @@ describe('Schedule runtime failure and teardown boundaries', () => {
data: { version: 9, operation: 'delete', id: 'schedule-1' },
}],
})
const corruptOwner = ownerFor(corrupt)
corruptOwner.start()
const corruptRuntime = runtimeFor(corrupt)
corruptRuntime.start()
await settle()
expect(corrupt.followed).toEqual([])
@@ -630,18 +630,18 @@ describe('Schedule runtime failure and teardown boundaries', () => {
configurable: true,
get() { throw 'unreadable log' },
})
const unreadableOwner = ownerFor(unreadable)
unreadableOwner.start()
const unreadableRuntime = runtimeFor(unreadable)
unreadableRuntime.start()
await settle()
expect(unreadable.followed).toEqual([])
})
it('contains owner startup, maintenance, and framing failures', async () => {
it('contains runtime startup, maintenance, and framing failures', async () => {
const startup = await harness()
const startSpy = vi.spyOn(startup.ctx.agents, 'withoutInitiator')
.mockImplementation(() => { throw new Error('initiator closing') })
const startupOwner = ownerFor(startup)
startupOwner.start()
const startupRuntime = runtimeFor(startup)
startupRuntime.start()
expect(startup.controls.flushCount).toBe(0)
startSpy.mockRestore()
@@ -649,8 +649,8 @@ describe('Schedule runtime failure and teardown boundaries', () => {
departedStartup.disposeAgent()
const departedStartSpy = vi.spyOn(departedStartup.ctx.agents, 'withoutInitiator')
.mockImplementation(() => { throw new Error('initiator disposed') })
const departedStartupOwner = ownerFor(departedStartup)
departedStartupOwner.start()
const departedStartupRuntime = runtimeFor(departedStartup)
departedStartupRuntime.start()
expect(departedStartup.controls.flushCount).toBe(0)
departedStartSpy.mockRestore()
@@ -658,11 +658,11 @@ describe('Schedule runtime failure and teardown boundaries', () => {
appendAfter(maintenanceFailure, 'schedule-1', 1, Date.now() - 1_000)
const maintenanceSpy = vi.spyOn(maintenanceFailure.agent, 'runMaintenance')
.mockImplementation(() => Promise.reject(new Error('maintenance failed')))
const maintenanceOwner = ownerFor(maintenanceFailure)
maintenanceOwner.start()
const maintenanceRuntime = runtimeFor(maintenanceFailure)
maintenanceRuntime.start()
await settle()
expect(maintenanceFailure.followed).toEqual([])
maintenanceOwner.requestDrive()
maintenanceRuntime.requestDrive()
await settle()
expect(maintenanceSpy).toHaveBeenCalledOnce()
@@ -672,19 +672,19 @@ describe('Schedule runtime failure and teardown boundaries', () => {
departedMaintenance.disposeAgent()
return Promise.reject(new Error('maintenance failed after detach'))
})
const departedMaintenanceOwner = ownerFor(departedMaintenance)
departedMaintenanceOwner.start()
const departedMaintenanceRuntime = runtimeFor(departedMaintenance)
departedMaintenanceRuntime.start()
await settle()
expect(departedMaintenance.followed).toEqual([])
const runFailure = await harness()
appendAfter(runFailure, 'schedule-1', 1, Date.now() - 1_000)
const uuidSpy = vi.spyOn(globalThis.crypto, 'randomUUID').mockImplementation(() => { throw 'message failed' })
const failingOwner = ownerFor(runFailure)
failingOwner.start()
const failingRuntime = runtimeFor(runFailure)
failingRuntime.start()
for (let index = 0; index < 12; index += 1) await Promise.resolve()
uuidSpy.mockRestore()
failingOwner.requestDrive()
failingRuntime.requestDrive()
await settle()
expect(runFailure.followed).toHaveLength(1)
@@ -694,8 +694,8 @@ describe('Schedule runtime failure and teardown boundaries', () => {
departedRun.disposeAgent()
throw 'message failed after detach'
})
const departedRunOwner = ownerFor(departedRun)
departedRunOwner.start()
const departedRunRuntime = runtimeFor(departedRun)
departedRunRuntime.start()
for (let index = 0; index < 12; index += 1) await Promise.resolve()
departedUuidSpy.mockRestore()
expect(departedRun.followed).toEqual([])
@@ -705,23 +705,23 @@ describe('Schedule runtime failure and teardown boundaries', () => {
const test = await harness()
appendAfter(test, 'schedule-1', 1, Date.now() - 1_000)
test.controls.onReserve = test.disposeAgent
const owner = ownerFor(test)
owner.start()
const runtime = runtimeFor(test)
runtime.start()
await settle()
expect(test.controls.releaseCount).toBe(1)
expect(test.followed).toEqual([])
await owner.dispose()
await runtime.dispose()
const busy = await harness()
appendAfter(busy, 'schedule-1', 1, Date.now() - 1_000)
busy.controls.canReserve = false
busy.controls.onBusy = busy.disposeAgent
const busyOwner = ownerFor(busy)
busyOwner.start()
const busyRuntime = runtimeFor(busy)
busyRuntime.start()
await settle()
expect(busy.controls.whenIdleCount).toBe(0)
expect(busy.followed).toEqual([])
await busyOwner.dispose()
await busyRuntime.dispose()
})
it('waits for in-flight preflight during dispose and does no post-dispose work', async () => {
@@ -729,12 +729,12 @@ describe('Schedule runtime failure and teardown boundaries', () => {
appendAfter(test, 'schedule-1', 1, Date.now() - 1_000)
const pending = Promise.withResolvers<undefined>()
test.controls.flushHandler = () => pending.promise
const owner = ownerFor(test)
owner.start()
const runtime = runtimeFor(test)
runtime.start()
await Promise.resolve()
let disposed = false
const disposal = owner.dispose().then(() => { disposed = true })
const disposal = runtime.dispose().then(() => { disposed = true })
await Promise.resolve()
expect(disposed).toBe(false)
pending.resolve(undefined)
@@ -747,12 +747,12 @@ describe('Schedule runtime failure and teardown boundaries', () => {
appendAfter(test, 'schedule-1', 1, Date.now() - 1_000)
const barrier = Promise.withResolvers<undefined>()
test.controls.flushHandler = () => test.controls.flushCount === 2 ? barrier.promise : undefined
const owner = ownerFor(test)
owner.start()
const runtime = runtimeFor(test)
runtime.start()
for (let index = 0; index < 12; index += 1) await Promise.resolve()
expect(test.followed).toHaveLength(1)
const disposal = owner.dispose()
const disposal = runtime.dispose()
barrier.resolve(undefined)
await disposal
expect(test.controls.flushCount).toBe(2)
@@ -763,34 +763,34 @@ describe('Schedule runtime failure and teardown boundaries', () => {
appendAfter(test, 'schedule-1', 1, Date.now() - 1_000)
const pending = Promise.withResolvers<undefined>()
test.controls.flushHandler = () => pending.promise
const owner = ownerFor(test)
owner.start()
const runtime = runtimeFor(test)
runtime.start()
await Promise.resolve()
test.disposeAgent()
pending.resolve(undefined)
await settle()
expect(test.followed).toEqual([])
await owner.dispose()
await runtime.dispose()
})
it('does not start a preflight for an already non-live owner', async () => {
it('does not start a preflight for an already non-live runtime', async () => {
const test = await harness()
test.disposeAgent()
const owner = ownerFor(test)
owner.start()
const runtime = runtimeFor(test)
runtime.start()
await settle()
expect(test.controls.flushCount).toBe(0)
await owner.dispose()
await runtime.dispose()
})
it('clears a future timer during dispose', async () => {
const test = await harness()
appendAfter(test, 'schedule-1', 60)
const owner = ownerFor(test)
owner.start()
const runtime = runtimeFor(test)
runtime.start()
await settle()
await owner.dispose()
await runtime.dispose()
await vi.advanceTimersByTimeAsync(60_000)
await settle()
expect(test.followed).toEqual([])

View File

@@ -6,7 +6,7 @@ import { CallId } from '@deepseek-ai/dsh-llm'
import type { UserMessage } from '@deepseek-ai/dsh-llm'
import SessionStore, { SessionId } from '@deepseek-ai/dsh-session'
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
import ToolRegistry from '@deepseek-ai/dsh-tools'
import ToolRuntime from '@deepseek-ai/dsh-tools'
import type { ToolExecutionResult } from '@deepseek-ai/dsh-tools'
import { registerScheduleTools } from '../src/tools.ts'
import { runScheduleTransaction } from '../src/transaction.ts'
@@ -48,7 +48,7 @@ async function harness(withPersistence = true): Promise<ToolHarness> {
await ctx.plugin(SessionStore)
await ctx.plugin(AgentRegistry)
await ctx.plugin(SystemPrompt, {})
await ctx.plugin(ToolRegistry)
await ctx.plugin(ToolRuntime)
const agent = stubAgent(ctx, `schedule-tools-${Math.random()}`)
ctx.agents.register(agent)
const flushes = { count: 0, outcomes: [] as Array<'resolve' | 'reject' | Promise<'resolve' | 'reject'>> }

View File

@@ -36,7 +36,7 @@
"path": "../../session/session-persistence-jsonl"
},
{
"path": "../../support/invariants"
"path": "../../runtime-diagnostics/invariants"
}
]
}