@deepseek-ai/dsh-schedule 0.1.1-rc.2 → 0.1.2-alpha.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Agent-scoped durable one-shot and fixed-rate reminders over the session event log.
3
+ * @module @deepseek-ai/dsh-schedule
4
+ */
5
+ import { scheduleProjectionDefinition } from "./projection.js";
6
+ import { ScheduleRuntime } from "./runtime.js";
7
+ import { registerScheduleTools } from "./tools.js";
8
+ export { SCHEDULE_CHANGE_VERSION, MIN_EVERY_INTERVAL_SECONDS, ScheduleId, ScheduleInputError, ScheduleLogError, allocateScheduleId, createAfterScheduleRecord, createAtScheduleRecord, createEveryScheduleRecord, decodeScheduleChange, foldScheduleEvents, renderReminderFraming, renderEveryReminderBatchFraming, resolveEveryOccurrence, scheduleView, } from "./domain.js";
9
+ export { registerScheduleTools } from "./tools.js";
10
+ /** Cordis function-plugin name. */
11
+ export const name = 'schedule';
12
+ /** Services required before future root agents can receive Schedule. */
13
+ export const inject = ['agents', 'sessions', 'tools', 'sessionPersistence'];
14
+ /** Install Schedule only for root agents published after this plugin loads. */
15
+ export function apply(ctx) {
16
+ ctx.inject(['sessionProjections'], (projectionCtx) => {
17
+ projectionCtx.sessionProjections.register(scheduleProjectionDefinition);
18
+ });
19
+ const runtimes = new Map();
20
+ let stopping = false;
21
+ ctx.effect(() => {
22
+ const stopCreated = ctx.on('agent/created', ({ agent }) => {
23
+ if (stopping || runtimes.has(agent) || !ctx.agents.roots().includes(agent))
24
+ return;
25
+ const runtime = new ScheduleRuntime(ctx, agent);
26
+ const cleanup = agent.ctx.effect(() => {
27
+ const disposeTools = registerScheduleTools(ctx, agent.ctx, agent, () => { runtime.requestDrive(); });
28
+ const stopStatus = agent.ctx.on('agent/status', ({ status }) => {
29
+ if (status === 'idle' && agent.session.events.some(event => event.type === 'schedule/change')) {
30
+ runtime.requestDrive();
31
+ }
32
+ });
33
+ runtime.start();
34
+ return async () => {
35
+ stopStatus();
36
+ disposeTools();
37
+ try {
38
+ await runtime.dispose();
39
+ }
40
+ finally {
41
+ if (runtimes.get(agent) === cleanup)
42
+ runtimes.delete(agent);
43
+ }
44
+ };
45
+ }, 'schedule.runtime()');
46
+ runtimes.set(agent, cleanup);
47
+ });
48
+ return async () => {
49
+ stopping = true;
50
+ stopCreated();
51
+ const cleanups = [...runtimes.values()];
52
+ runtimes.clear();
53
+ await Promise.allSettled(cleanups.map(cleanup => Promise.resolve(cleanup())));
54
+ };
55
+ }, 'schedule.lifecycle()');
56
+ }
57
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Package-owned strict Schedule stream invariant.
3
+ * @module @deepseek-ai/dsh-schedule/invariant
4
+ */
5
+ import { foldScheduleEvents, ScheduleLogError } from "./domain.js";
6
+ const PACKAGE_NAME = '@deepseek-ai/dsh-schedule';
7
+ /** Cordis invariant-companion plugin name. */
8
+ export const name = 'tool-schedule-invariant';
9
+ /** Service required before reserving this package's invariant ownership. */
10
+ export const inject = ['invariants'];
11
+ /** Validate a complete exact-session stream under its fork suffix policy. */
12
+ function validate(events, seedLength, fail) {
13
+ try {
14
+ foldScheduleEvents(events, seedLength);
15
+ }
16
+ catch (error) {
17
+ /* v8 ignore next -- foldScheduleEvents normalizes every rejected stream to ScheduleLogError. */
18
+ if (!(error instanceof ScheduleLogError))
19
+ throw error;
20
+ fail(error.message);
21
+ }
22
+ }
23
+ /* jscpd:ignore-start -- package companions share replay and dispatch plumbing */
24
+ /** Install replay and pre-append validation for the owned event stream. */
25
+ const install = Object.assign((ctx, fail) => {
26
+ for (const session of ctx.sessions.list()) {
27
+ validate(session.events, session.header.seedLength ?? 0, fail);
28
+ }
29
+ ctx.on('session/created', (session) => {
30
+ validate(session.events, session.header.seedLength ?? 0, fail);
31
+ }, { global: true });
32
+ ctx.on('internal/dispatch', (_mode, eventName, args) => {
33
+ if (eventName !== 'session/event')
34
+ return;
35
+ const [session, event] = args;
36
+ if (event.type !== 'schedule/change')
37
+ return;
38
+ validate([...session.events, event], session.header.seedLength ?? 0, fail);
39
+ }, { global: true });
40
+ }, { inject: ['sessions'] });
41
+ /* jscpd:ignore-end */
42
+ /**
43
+ * Register the package-owned invariant companion.
44
+ * @param ctx - Cordis context carrying the invariant registry.
45
+ * @returns Exact registration disposer after child setup succeeds.
46
+ */
47
+ export const apply = (ctx) => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install));
48
+ //# sourceMappingURL=invariant.js.map
@@ -0,0 +1,30 @@
1
+ /** Schedule-owned use of the shared session durability barrier. */
2
+ /** Failure to prove that the current live prefix reached a persistence listener. */
3
+ export class SchedulePersistenceError extends Error {
4
+ /**
5
+ * Construct a contained persistence failure.
6
+ * @param cause - Rejection returned by the shared barrier, when present.
7
+ */
8
+ constructor(cause) {
9
+ super('Schedule persistence did not complete.', cause === undefined ? undefined : { cause });
10
+ this.name = 'SchedulePersistenceError';
11
+ }
12
+ }
13
+ /**
14
+ * Require one successful shared persistence checkpoint.
15
+ * @param ctx - Context carrying the live session store.
16
+ * @param session - Exact live session to checkpoint.
17
+ * @returns After at least one listener explicitly acknowledges completed durability work.
18
+ */
19
+ export async function flushSchedulePersistence(ctx, session) {
20
+ try {
21
+ if (!await ctx.sessions.flush(session))
22
+ throw new SchedulePersistenceError();
23
+ }
24
+ catch (error) {
25
+ if (error instanceof SchedulePersistenceError)
26
+ throw error;
27
+ throw new SchedulePersistenceError(error);
28
+ }
29
+ }
30
+ //# sourceMappingURL=persistence.js.map
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Strict Session projection of the Schedule domain's active reminder set.
3
+ * @module @deepseek-ai/dsh-schedule/projection
4
+ */
5
+ import { z } from 'zod';
6
+ import type { FoldedSchedules } from './domain.ts';
7
+ import type { ScheduleRecord } from './types.ts';
8
+ /** Persisted projection state: the immutable fork boundary plus the complete Schedule fold. */
9
+ export interface ScheduleProjectionState extends FoldedSchedules {
10
+ readonly seedLength: number;
11
+ }
12
+ /** Projection definition sharing the Schedule domain's strict transition authority. */
13
+ export declare const scheduleProjectionDefinition: {
14
+ key: "schedule";
15
+ stateSchema: z.ZodType<ScheduleProjectionState, unknown, z.core.$ZodTypeInternals<ScheduleProjectionState, unknown>>;
16
+ init: (header: import("@deepseek-ai/dsh-session").SessionHeader) => {
17
+ seedLength: number;
18
+ active: never[];
19
+ seenIds: never[];
20
+ };
21
+ apply: (state: NoInfer<ScheduleProjectionState>, event: import("@deepseek-ai/dsh-session").SessionEvent) => ScheduleProjectionState;
22
+ wire: {
23
+ viewSchema: z.ZodType<readonly ScheduleRecord[], unknown, z.core.$ZodTypeInternals<readonly ScheduleRecord[], unknown>>;
24
+ view: (state: NoInfer<ScheduleProjectionState>) => readonly ScheduleRecord[];
25
+ };
26
+ stateVersion: number;
27
+ };
28
+ declare module '@deepseek-ai/dsh-session-projection/types' {
29
+ interface SessionProjectionStateMap {
30
+ schedule: ScheduleProjectionState;
31
+ }
32
+ }
33
+ //# sourceMappingURL=projection.d.ts.map
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Strict Session projection of the Schedule domain's active reminder set.
3
+ * @module @deepseek-ai/dsh-schedule/projection
4
+ */
5
+ import { z } from 'zod';
6
+ import { applyScheduleChanges, decodeScheduleChange } from "./domain.js";
7
+ const scheduleId = z.unknown().transform((value, context) => {
8
+ try {
9
+ const change = decodeScheduleChange({ version: 1, operation: 'delete', id: value });
10
+ return change.id;
11
+ }
12
+ catch {
13
+ context.addIssue({ code: 'custom', message: 'invalid Schedule id' });
14
+ return z.NEVER;
15
+ }
16
+ });
17
+ const scheduleRecord = z.unknown().transform((value, context) => {
18
+ try {
19
+ const change = decodeScheduleChange({ version: 1, operation: 'create', schedule: value });
20
+ return change.schedule;
21
+ }
22
+ catch {
23
+ context.addIssue({ code: 'custom', message: 'invalid Schedule record' });
24
+ return z.NEVER;
25
+ }
26
+ });
27
+ const scheduleRecords = z.array(scheduleRecord);
28
+ const scheduleProjectionStateSchema = z.object({
29
+ seedLength: z.number().int().nonnegative().max(Number.MAX_SAFE_INTEGER),
30
+ active: scheduleRecords,
31
+ seenIds: z.array(scheduleId),
32
+ }).strict().superRefine((state, context) => {
33
+ const seen = new Set(state.seenIds);
34
+ if (seen.size !== state.seenIds.length) {
35
+ context.addIssue({ code: 'custom', message: 'seen Schedule ids must be unique' });
36
+ }
37
+ const active = new Set();
38
+ for (const record of state.active) {
39
+ if (!seen.has(record.id)) {
40
+ context.addIssue({ code: 'custom', message: 'every active Schedule id must have been seen' });
41
+ }
42
+ if (active.has(record.id)) {
43
+ context.addIssue({ code: 'custom', message: 'active Schedule ids must be unique' });
44
+ }
45
+ active.add(record.id);
46
+ }
47
+ });
48
+ /** Projection definition sharing the Schedule domain's strict transition authority. */
49
+ export const scheduleProjectionDefinition = {
50
+ key: 'schedule',
51
+ stateSchema: scheduleProjectionStateSchema,
52
+ init: header => ({ seedLength: header.seedLength ?? 0, active: [], seenIds: [] }),
53
+ apply: (state, event) => {
54
+ if (event.seq < state.seedLength || event.type !== 'schedule/change')
55
+ return state;
56
+ return {
57
+ seedLength: state.seedLength,
58
+ ...applyScheduleChanges(state, [decodeScheduleChange(event.data)]),
59
+ };
60
+ },
61
+ wire: {
62
+ viewSchema: scheduleRecords,
63
+ view: state => state.active,
64
+ },
65
+ stateVersion: 1,
66
+ };
67
+ //# sourceMappingURL=projection.js.map
@@ -0,0 +1,296 @@
1
+ /**
2
+ * Disposable live timer projection for one exact root agent.
3
+ * @module @deepseek-ai/dsh-schedule
4
+ */
5
+ import { createUserMessage } from '@deepseek-ai/dsh-llm';
6
+ import { foldScheduleEvents, renderEveryReminderBatchFraming, renderReminderFraming, resolveEveryOccurrence, ScheduleLogError, } from "./domain.js";
7
+ import { flushSchedulePersistence } from "./persistence.js";
8
+ import { runScheduleTransaction } from "./transaction.js";
9
+ /** Largest delay that Node timers represent without clamping. */
10
+ export const MAX_TIMER_DELAY_MS = 2_147_483_647;
11
+ /** Select one due one-shot, one complete fixed-rate batch, or the next wake. */
12
+ function dueDecision(folded, now) {
13
+ const indexed = folded.active.map((record, index) => ({ record, index }));
14
+ const byTargetThenCreate = (left, right) => Date.parse(left.record.scheduledAt) - Date.parse(right.record.scheduledAt)
15
+ || left.index - right.index;
16
+ const oneShot = indexed
17
+ .filter((entry) => entry.record.kind !== 'every' && Date.parse(entry.record.scheduledAt) <= now)
18
+ .sort(byTargetThenCreate)[0]?.record;
19
+ if (oneShot !== undefined)
20
+ return { kind: 'one-shot', record: oneShot };
21
+ const every = indexed
22
+ .filter((entry) => entry.record.kind === 'every' && Date.parse(entry.record.scheduledAt) <= now)
23
+ .sort(byTargetThenCreate);
24
+ if (every.length > 0) {
25
+ return {
26
+ kind: 'every',
27
+ acceptedAt: new Date(now).toISOString(),
28
+ reminders: every.map(({ record }) => ({
29
+ record,
30
+ occurrenceAt: resolveEveryOccurrence(record, now).occurrenceAt,
31
+ })),
32
+ };
33
+ }
34
+ const target = folded.active.reduce((selected, record) => {
35
+ const candidate = Date.parse(record.scheduledAt);
36
+ return candidate > now && (selected === undefined || candidate < selected) ? candidate : selected;
37
+ }, undefined);
38
+ return { kind: 'wait', ...(target === undefined ? {} : { target }) };
39
+ }
40
+ /** Render an unknown value for process-local diagnostics only. */
41
+ function renderThrown(value) {
42
+ return value instanceof Error ? value.message : String(value);
43
+ }
44
+ /** One process-local, disposable projection of an exact agent's durable schedules. */
45
+ export class ScheduleRuntime {
46
+ ctx;
47
+ agent;
48
+ stop = Promise.withResolvers();
49
+ timer;
50
+ idleWait;
51
+ run;
52
+ requested = false;
53
+ stopping = false;
54
+ faulted = false;
55
+ disposal;
56
+ /**
57
+ * Construct an inactive runtime; {@link start} begins the first preflight.
58
+ * @param ctx - Global service context.
59
+ * @param agent - Exact live root agent.
60
+ */
61
+ constructor(ctx, agent) {
62
+ this.ctx = ctx;
63
+ this.agent = agent;
64
+ }
65
+ /** Begin the initial durability preflight and timer derivation. */
66
+ start() {
67
+ this.requestDrive();
68
+ }
69
+ /** Recompute the live projection after a committed mutation or idle transition. */
70
+ requestDrive() {
71
+ if (this.stopping || this.faulted)
72
+ return;
73
+ this.clearTimer();
74
+ this.requested = true;
75
+ if (this.run !== undefined)
76
+ return;
77
+ let run;
78
+ try {
79
+ run = this.ctx.agents.withoutInitiator(() => this.runRequested());
80
+ }
81
+ catch (error) {
82
+ if (this.isLive()) {
83
+ this.ctx.logger.warn(`schedule: could not start runtime for agent "${this.agent.id}": ${renderThrown(error)}`);
84
+ }
85
+ return;
86
+ }
87
+ this.run = run;
88
+ void run.then(() => { this.retire(run); }, (error) => {
89
+ if (this.isLive()) {
90
+ this.ctx.logger.warn(`schedule: runtime failed for agent "${this.agent.id}": ${renderThrown(error)}`);
91
+ }
92
+ this.faulted = true;
93
+ this.retire(run);
94
+ });
95
+ }
96
+ /** Stop future work, cancel timers, and await every outstanding runtime promise. */
97
+ dispose() {
98
+ return (this.disposal ??= (async () => {
99
+ this.stopping = true;
100
+ this.requested = false;
101
+ this.clearTimer();
102
+ this.stop.resolve();
103
+ const pending = [this.run, this.idleWait].filter((value) => value !== undefined);
104
+ await Promise.allSettled(pending);
105
+ })());
106
+ }
107
+ /** Drain coalesced triggers serially. */
108
+ async runRequested() {
109
+ while (this.requested && !this.stopping && !this.faulted) {
110
+ this.requested = false;
111
+ await runScheduleTransaction(this.agent, () => this.driveOnce());
112
+ }
113
+ }
114
+ /** Retire one exact run and honor a trigger that landed during its final microtask. */
115
+ retire(run) {
116
+ /* v8 ignore next -- only the exact stored run installs this callback. */
117
+ if (this.run !== run)
118
+ return;
119
+ this.run = undefined;
120
+ /* v8 ignore next -- covers a trigger in the promise-settlement microtask gap. */
121
+ if (this.requested && !this.stopping && !this.faulted)
122
+ this.requestDrive();
123
+ }
124
+ /** Whether this exact root lifecycle remains authoritative. */
125
+ isLive() {
126
+ return this.ctx.agents.get(this.agent.id) === this.agent
127
+ && this.ctx.agents.roots().includes(this.agent);
128
+ }
129
+ /** Whether this runtime may start or continue Schedule work. */
130
+ isRunnable() {
131
+ return !this.stopping && this.isLive();
132
+ }
133
+ /** Cancel the currently armed timer, if any. */
134
+ clearTimer() {
135
+ if (this.timer === undefined)
136
+ return;
137
+ clearTimeout(this.timer);
138
+ this.timer = undefined;
139
+ }
140
+ /** Arm one bounded timer segment; every wake rechecks the wall clock. */
141
+ arm(target, now) {
142
+ const delay = Math.min(target - now, MAX_TIMER_DELAY_MS);
143
+ this.timer = setTimeout(() => {
144
+ this.timer = undefined;
145
+ this.requestDrive();
146
+ }, delay);
147
+ }
148
+ /** Await one public idle boundary without holding admission or creating a retry timer. */
149
+ waitForIdle() {
150
+ if (this.idleWait !== undefined)
151
+ return;
152
+ const wait = Promise.race([this.agent.whenIdle(), this.stop.promise]);
153
+ this.idleWait = wait;
154
+ void wait.then(() => {
155
+ this.idleWait = undefined;
156
+ this.requestDrive();
157
+ }, (error) => {
158
+ this.idleWait = undefined;
159
+ if (this.isLive()) {
160
+ this.ctx.logger.warn(`schedule: idle wait failed for agent "${this.agent.id}": ${renderThrown(error)}`);
161
+ }
162
+ });
163
+ }
164
+ /** Fold the current exact runtime suffix and contain a corrupt durable stream. */
165
+ readFolded() {
166
+ try {
167
+ return foldScheduleEvents(this.agent.session.events, this.agent.session.header.seedLength ?? 0);
168
+ }
169
+ catch (error) {
170
+ this.faulted = true;
171
+ const detail = error instanceof ScheduleLogError ? error.message : renderThrown(error);
172
+ this.ctx.logger.warn(`schedule: corrupt schedule log for agent "${this.agent.id}": ${detail}`);
173
+ return undefined;
174
+ }
175
+ }
176
+ /** Contain an invalid wall-clock decision without permanently faulting this runtime. */
177
+ decide(folded, now) {
178
+ try {
179
+ return dueDecision(folded, now);
180
+ }
181
+ catch (error) {
182
+ this.ctx.logger.warn(`schedule: fixed-rate decision failed for agent "${this.agent.id}": ${renderThrown(error)}`);
183
+ return undefined;
184
+ }
185
+ }
186
+ /** Preflight, fold, arm, or dispatch the next one-shot or fixed-rate batch. */
187
+ async driveOnce() {
188
+ this.clearTimer();
189
+ if (!this.isRunnable())
190
+ return;
191
+ try {
192
+ await flushSchedulePersistence(this.ctx, this.agent.session);
193
+ }
194
+ catch (error) {
195
+ if (this.isLive()) {
196
+ this.ctx.logger.warn(`schedule: preflight failed for agent "${this.agent.id}": ${renderThrown(error)}`);
197
+ }
198
+ return;
199
+ }
200
+ if (!this.isRunnable())
201
+ return;
202
+ const folded = this.readFolded();
203
+ if (folded === undefined)
204
+ return;
205
+ const wakeNow = Date.now();
206
+ const wakeDecision = this.decide(folded, wakeNow);
207
+ if (wakeDecision === undefined)
208
+ return;
209
+ if (wakeDecision.kind === 'wait') {
210
+ if (wakeDecision.target !== undefined)
211
+ this.arm(wakeDecision.target, wakeNow);
212
+ return;
213
+ }
214
+ let maintenance;
215
+ try {
216
+ maintenance = this.agent.runMaintenance(() => {
217
+ if (!this.isRunnable())
218
+ return Promise.resolve(false);
219
+ const claimed = this.readFolded();
220
+ if (claimed === undefined)
221
+ return Promise.resolve(false);
222
+ const decisionNow = Date.now();
223
+ const decision = this.decide(claimed, decisionNow);
224
+ if (decision === undefined)
225
+ return Promise.resolve(false);
226
+ if (decision.kind === 'wait') {
227
+ if (decision.target !== undefined)
228
+ this.arm(decision.target, decisionNow);
229
+ return Promise.resolve(false);
230
+ }
231
+ try {
232
+ const text = decision.kind === 'one-shot'
233
+ ? renderReminderFraming(decision.record)
234
+ : renderEveryReminderBatchFraming(decision.reminders);
235
+ const message = createUserMessage({
236
+ content: [{ type: 'text', text }],
237
+ source: { kind: 'plugin', plugin: 'schedule' },
238
+ });
239
+ this.agent.followup(message);
240
+ }
241
+ catch (error) {
242
+ if (this.isLive()) {
243
+ this.ctx.logger.warn(`schedule: framing or followup failed for agent "${this.agent.id}": ${renderThrown(error)}`);
244
+ }
245
+ return Promise.resolve(false);
246
+ }
247
+ try {
248
+ if (decision.kind === 'one-shot') {
249
+ this.agent.session.append('schedule/change', {
250
+ version: 1,
251
+ operation: 'dispatch',
252
+ id: decision.record.id,
253
+ });
254
+ }
255
+ else {
256
+ for (const reminder of decision.reminders) {
257
+ this.agent.session.append('schedule/change', {
258
+ version: 1,
259
+ operation: 'dispatch',
260
+ id: reminder.record.id,
261
+ acceptedAt: decision.acceptedAt,
262
+ });
263
+ }
264
+ }
265
+ }
266
+ catch (error) {
267
+ this.faulted = true;
268
+ this.clearTimer();
269
+ this.ctx.logger.warn(`schedule: dispatch append failed for agent "${this.agent.id}": ${renderThrown(error)}`);
270
+ return Promise.resolve(false);
271
+ }
272
+ return Promise.resolve(true);
273
+ });
274
+ }
275
+ catch (_busy) {
276
+ // `runMaintenance` rejects synchronously only while another agent activity owns the idle phase.
277
+ if (this.isLive())
278
+ this.waitForIdle();
279
+ return;
280
+ }
281
+ if (!await maintenance)
282
+ return;
283
+ try {
284
+ await flushSchedulePersistence(this.ctx, this.agent.session);
285
+ }
286
+ catch (error) {
287
+ if (this.isLive()) {
288
+ this.ctx.logger.warn(`schedule: dispatch barrier failed for agent "${this.agent.id}": ${renderThrown(error)}`);
289
+ }
290
+ return;
291
+ }
292
+ if (this.isRunnable())
293
+ this.requestDrive();
294
+ }
295
+ }
296
+ //# sourceMappingURL=runtime.js.map