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:
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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 队列进入同一对话。
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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 都是该日志的可丢弃投影。
|
||||
|
||||
## 组合
|
||||
|
||||
@@ -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",
|
||||
@@ -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'
|
||||
@@ -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()')
|
||||
}
|
||||
@@ -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'
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)}`)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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'
|
||||
@@ -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 },
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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([])
|
||||
@@ -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'>> }
|
||||
@@ -36,7 +36,7 @@
|
||||
"path": "../../session/session-persistence-jsonl"
|
||||
},
|
||||
{
|
||||
"path": "../../support/invariants"
|
||||
"path": "../../runtime-diagnostics/invariants"
|
||||
}
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user