@deepseek-ai/dsh-schedule 0.1.6-alpha.2 → 0.1.7-alpha.1
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.
- package/README.i18n.yaml +2 -2
- package/README.md +3 -1
- package/README.zh.md +4 -1
- package/lib/index.js +104 -37
- package/lib/types/index.d.ts +15 -1
- package/lib/types/index.js +61 -5
- package/lib/types/runtime.d.ts +15 -0
- package/lib/types/runtime.js +11 -1
- package/lib/types/types.d.ts +6 -0
- package/package.json +26 -24
package/README.i18n.yaml
CHANGED
|
@@ -2,5 +2,5 @@
|
|
|
2
2
|
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
|
3
3
|
# after editing either side, bring the other along and re-record with:
|
|
4
4
|
# pnpm run verify-translation-pairing --write packages/schedule/schedule/README.md
|
|
5
|
-
README.md:
|
|
6
|
-
README.zh.md:
|
|
5
|
+
README.md: 55a4c86b88ae5a28219fd927930ec68cc780185e
|
|
6
|
+
README.zh.md: 4670803e88b61501a03e9635f6b260948c98610f
|
package/README.md
CHANGED
|
@@ -53,7 +53,7 @@ Input that cannot become a reminder — an empty prompt, more than one selector,
|
|
|
53
53
|
|
|
54
54
|
### When reminders fire
|
|
55
55
|
|
|
56
|
-
Due reminders appear as ordinary follow-up messages after the conversation becomes idle; the agent never interrupts a running turn. An already-live idle agent can claim maintenance and deliver immediately without another resume. One-time reminders fire before any repeating batch, and several repeating reminders due at once arrive together in one message ordered by time. If the session is closed or cold when a reminder comes due, it stays overdue until a future live root agent resumes the session — nothing is sent outside the session. A repeating reminder that missed intervals while the session was away presents only its latest due occurrence, not a backlog. The optional Web catalog shows only active records and is not a delivery receipt; dispatch means the follow-up was queued and recorded, not that the model succeeded or the user read the answer.
|
|
56
|
+
Due reminders appear as ordinary follow-up messages after the conversation becomes idle; the agent never interrupts a running turn. An already-live idle agent can claim maintenance and deliver immediately without another resume. One-time reminders fire before any repeating batch, and several repeating reminders due at once arrive together in one message ordered by time. If the session is closed or cold when a reminder comes due, it stays overdue until a future live root agent resumes the session — nothing is sent outside the session. A repeating reminder that missed intervals while the session was away presents only its latest due occurrence, not a backlog. The optional Web catalog shows only active records and is not a delivery receipt; dispatch means the follow-up was queued and recorded, not that the model succeeded or the user read the answer. Archiving a live session with active reminders is refused until they stop, and choosing to stop them deletes every active reminder; unarchiving does not bring them back.
|
|
57
57
|
|
|
58
58
|
-----
|
|
59
59
|
|
|
@@ -122,6 +122,8 @@ The owner splits long waits into bounded timer segments and rereads the wall clo
|
|
|
122
122
|
|
|
123
123
|
An overdue reminder first checkpoints persistence, then claims the Agent's idle maintenance phase through `runMaintenance()`; if a turn or another maintenance task owns the Agent, the claim is rejected, the record stays active, and the owner retries after `whenIdle()`. A successful maintenance task refolds, samples one decision time, builds the fixed framing, synchronously queues `followup()`, and appends the dispatch before releasing the phase. Dispatch means the follow-up was queued and recorded, not that the model succeeded or the user read the answer. Framing or synchronous follow-up failure writes no dispatch; an append failure faults the owner because the message may already be queued; a barrier rejection leaves dispatch pending for a later ordinary preflight. Agent or plugin disposal cancels timers and stops new work without deleting durable records.
|
|
124
124
|
|
|
125
|
+
The plugin answers the Workspace registry's archive admission ([seam](../../workspace/workspace/README.md)) for every session it owns a runtime for, from that owner's own fold of the live log — the projection registry is a Client view and is not read here: `workspace/session-activity` reports the active records of the session's own suffix as the `schedule` family, one item per reminder with its prompt as label, and `workspace/session-stop` is a management delete in the [same serialized transaction and barriers](#management-pipeline) as the tool: it awaits `ctx.sessions.flush(session)` before reading the fold, appends the same `delete` change the `schedule_delete` tool records for each active reminder, asks the owner to redrive so its timers clear, then awaits a second barrier after the appends. A failed barrier rejects the stop; the registry logs it and keeps the archive, and the reminders stay for the archived-session `agent/pre-step` gate to block when they fire. The registry writes the archive before it dispatches the stop, so a crash between that write and the delete barrier leaves an archived session whose reminders are still recorded; the next unarchive shows them again, exactly as if the stop had never been requested. A session without a live agent, or a live agent without an owned runtime (published before this plugin loaded, or a runtime that stopped or faulted), reports nothing and has nothing to stop, because nothing of it is armed to fire.
|
|
126
|
+
|
|
125
127
|
</details>
|
|
126
128
|
|
|
127
129
|
-----
|
package/README.zh.md
CHANGED
|
@@ -53,7 +53,7 @@ dsh web --patch apps/cli/config/examples/schedule/cordis.yml
|
|
|
53
53
|
|
|
54
54
|
### 提醒何时触发
|
|
55
55
|
|
|
56
|
-
到期提醒会在会话空闲后作为普通 follow-up 消息出现;agent 绝不会中断正在运行的轮次。已经 live 且空闲的 agent 可以认领 maintenance 并立即交付,无需再次恢复。一次性提醒先于任何重复批次触发;同时到期的多条重复提醒会按时间顺序合并为一条消息。如果会话在提醒到期时已关闭或 cold,提醒会保持逾期,直到未来的 live 根 agent 恢复会话——会话之外不会发送任何内容。错过若干间隔的重复提醒只展示最新一个到期发生时点,不展示积压。可选 Web 目录只显示活动记录,并不充当交付回执;dispatch 表示 follow-up
|
|
56
|
+
到期提醒会在会话空闲后作为普通 follow-up 消息出现;agent 绝不会中断正在运行的轮次。已经 live 且空闲的 agent 可以认领 maintenance 并立即交付,无需再次恢复。一次性提醒先于任何重复批次触发;同时到期的多条重复提醒会按时间顺序合并为一条消息。如果会话在提醒到期时已关闭或 cold,提醒会保持逾期,直到未来的 live 根 agent 恢复会话——会话之外不会发送任何内容。错过若干间隔的重复提醒只展示最新一个到期发生时点,不展示积压。可选 Web 目录只显示活动记录,并不充当交付回执;dispatch 表示 follow-up 已入队并被记录,不表示模型成功或用户已读取回答。归档仍有活动提醒的活会话会被拒绝,直到这些提醒停止;选择停止它们会删除全部活动提醒,取消归档不会把它们带回来。
|
|
57
57
|
|
|
58
58
|
-----
|
|
59
59
|
|
|
@@ -110,6 +110,7 @@ projection 只携带持久记录。它不持久化或传输 scheduled/overdue
|
|
|
110
110
|
|
|
111
111
|
日历规范化是确定性的。夏令时缺口内的本地时间会被拒绝;重叠时选择第一次出现的较早时刻。Schedule 的时间校验不会读取浏览器、Session header 中的时区字段、模型 time-context、连接或进程时区,因此回放永不依赖环境时区状态。
|
|
112
112
|
|
|
113
|
+
<a id="management-pipeline"></a>
|
|
113
114
|
### 管理流水线
|
|
114
115
|
|
|
115
116
|
一条 agent 范围的队列把每项已接纳的管理事务与 live owner 的到期事务从 preflight 到任何 post-append barrier 全程串行化。`schedule_create` 建立检查点、分配永不复用的 id、追加 create 事件,再次建立检查点;被取消的调用方在追加前停止。每次成功的管理 preflight 还会要求 live owner 重新计算,这会在先前的 post-append barrier 返回 `persistence_uncertain` 后恢复所保留的 create 或 delete 批次。
|
|
@@ -122,6 +123,8 @@ owner 把长等待拆分为有界的 timer 段,并在每次唤醒后重新读
|
|
|
122
123
|
|
|
123
124
|
逾期提醒首先为持久化建立检查点,然后通过 `runMaintenance()` 认领 agent 的 idle maintenance phase;如果某个轮次或另一项 maintenance task 已占用 agent,认领会失败,记录保持活动,owner 在 `whenIdle()` 后重试。获准的 maintenance task 会重新折叠、采样一个决策时点、构造固定 framing、同步将 `followup()` 入队,并在释放 phase 前追加 dispatch。dispatch 表示 follow-up 已入队并被记录,不表示模型成功或用户已读取回答。framing 构造或同步 follow-up 失败不会写入 dispatch;追加失败会使 owner 进入故障状态,因为消息可能已经入队;barrier 拒绝则把 dispatch 留给后续普通 preflight。agent 或插件执行资源释放时取消 timer 并停止新工作,但不删除持久记录。
|
|
124
125
|
|
|
126
|
+
本插件为每个它拥有 runtime 的会话回答 Workspace 注册表的归档准入([接缝](../../workspace/workspace/README.zh.md)),答案来自该 owner 对活日志的自有 fold——投影注册表是 Client 视图,这里不读它:`workspace/session-activity` 把会话自身后缀中的活动记录作为 `schedule` 族报告,每条提醒一项、以其 prompt 作名称;`workspace/session-stop` 是与工具处于[同一串行事务与屏障](#management-pipeline)之下的管理删除:先 await `ctx.sessions.flush(session)` 再读取 fold,为每条活动提醒追加与 `schedule_delete` 工具所记录的相同的 `delete` 变更,请求 owner 重新驱动以清除其 timer,然后在追加之后 await 第二道屏障。屏障失败会让该 stop 拒绝;注册表记录日志并保留归档,这些提醒留给已归档会话的 `agent/pre-step` 门禁在触发时拦下。注册表先写入归档再派发 stop,因此在该写入与删除屏障之间崩溃会留下一个提醒仍被记录的已归档会话;下次取消归档时它们会再次出现,与从未请求过 stop 时完全一样。没有活 agent 的会话,或没有归属 runtime 的活 agent(在本插件加载前发布,或其 runtime 已停止、已故障),不报告任何内容也没有可停的东西,因为它没有任何已武装、可触发的提醒。
|
|
127
|
+
|
|
125
128
|
</details>
|
|
126
129
|
|
|
127
130
|
-----
|
package/lib/index.js
CHANGED
|
@@ -2,6 +2,34 @@ import { z } from "zod";
|
|
|
2
2
|
import { SessionLogOffset } from "@deepseek-ai/dsh-session";
|
|
3
3
|
import { createUserMessage } from "@deepseek-ai/dsh-llm";
|
|
4
4
|
import { defineTool } from "@deepseek-ai/dsh-tools";
|
|
5
|
+
//#region lib/types/persistence.js
|
|
6
|
+
/** Schedule-owned use of the shared session durability barrier. */
|
|
7
|
+
/** Failure to prove that the current live prefix reached a persistence listener. */
|
|
8
|
+
var SchedulePersistenceError = class extends Error {
|
|
9
|
+
/**
|
|
10
|
+
* Construct a contained persistence failure.
|
|
11
|
+
* @param cause - Rejection returned by the shared barrier, when present.
|
|
12
|
+
*/
|
|
13
|
+
constructor(cause) {
|
|
14
|
+
super("Schedule persistence did not complete.", cause === void 0 ? void 0 : { cause });
|
|
15
|
+
this.name = "SchedulePersistenceError";
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* Require one successful shared persistence checkpoint.
|
|
20
|
+
* @param ctx - Context carrying the live session store.
|
|
21
|
+
* @param session - Exact live session to checkpoint.
|
|
22
|
+
* @returns After at least one listener explicitly acknowledges completed durability work.
|
|
23
|
+
*/
|
|
24
|
+
async function flushSchedulePersistence(ctx, session) {
|
|
25
|
+
try {
|
|
26
|
+
if (!await ctx.sessions.flush(session)) throw new SchedulePersistenceError();
|
|
27
|
+
} catch (error) {
|
|
28
|
+
if (error instanceof SchedulePersistenceError) throw error;
|
|
29
|
+
throw new SchedulePersistenceError(error);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
//#endregion
|
|
5
33
|
//#region lib/types/domain.js
|
|
6
34
|
/**
|
|
7
35
|
* Strict Schedule decoding, replay, time validation, and framing.
|
|
@@ -670,34 +698,6 @@ const scheduleProjectionDefinition = {
|
|
|
670
698
|
stateVersion: 2
|
|
671
699
|
};
|
|
672
700
|
//#endregion
|
|
673
|
-
//#region lib/types/persistence.js
|
|
674
|
-
/** Schedule-owned use of the shared session durability barrier. */
|
|
675
|
-
/** Failure to prove that the current live prefix reached a persistence listener. */
|
|
676
|
-
var SchedulePersistenceError = class extends Error {
|
|
677
|
-
/**
|
|
678
|
-
* Construct a contained persistence failure.
|
|
679
|
-
* @param cause - Rejection returned by the shared barrier, when present.
|
|
680
|
-
*/
|
|
681
|
-
constructor(cause) {
|
|
682
|
-
super("Schedule persistence did not complete.", cause === void 0 ? void 0 : { cause });
|
|
683
|
-
this.name = "SchedulePersistenceError";
|
|
684
|
-
}
|
|
685
|
-
};
|
|
686
|
-
/**
|
|
687
|
-
* Require one successful shared persistence checkpoint.
|
|
688
|
-
* @param ctx - Context carrying the live session store.
|
|
689
|
-
* @param session - Exact live session to checkpoint.
|
|
690
|
-
* @returns After at least one listener explicitly acknowledges completed durability work.
|
|
691
|
-
*/
|
|
692
|
-
async function flushSchedulePersistence(ctx, session) {
|
|
693
|
-
try {
|
|
694
|
-
if (!await ctx.sessions.flush(session)) throw new SchedulePersistenceError();
|
|
695
|
-
} catch (error) {
|
|
696
|
-
if (error instanceof SchedulePersistenceError) throw error;
|
|
697
|
-
throw new SchedulePersistenceError(error);
|
|
698
|
-
}
|
|
699
|
-
}
|
|
700
|
-
//#endregion
|
|
701
701
|
//#region lib/types/transaction.js
|
|
702
702
|
/** Agent-scoped serialization for Schedule reads and durable mutations. */
|
|
703
703
|
const tails = /* @__PURE__ */ new WeakMap();
|
|
@@ -784,6 +784,15 @@ var ScheduleRuntime = class {
|
|
|
784
784
|
start() {
|
|
785
785
|
this.requestDrive();
|
|
786
786
|
}
|
|
787
|
+
/**
|
|
788
|
+
* Active records of the exact runtime suffix, folded from the live log the
|
|
789
|
+
* way every drive folds it: the owner answers for what its timers can fire.
|
|
790
|
+
* @returns the active records in create order, or nothing while this runtime is stopping, faulted, or reading a corrupt stream.
|
|
791
|
+
*/
|
|
792
|
+
activeRecords() {
|
|
793
|
+
if (this.stopping || this.faulted) return void 0;
|
|
794
|
+
return this.readFolded()?.active;
|
|
795
|
+
}
|
|
787
796
|
/** Recompute the live projection after a committed mutation or idle transition. */
|
|
788
797
|
requestDrive() {
|
|
789
798
|
if (this.stopping || this.faulted) return;
|
|
@@ -926,10 +935,7 @@ var ScheduleRuntime = class {
|
|
|
926
935
|
type: "text",
|
|
927
936
|
text: decision.kind === "one-shot" ? renderReminderFraming(decision.record) : renderEveryReminderBatchFraming(decision.reminders)
|
|
928
937
|
}],
|
|
929
|
-
source: {
|
|
930
|
-
kind: "plugin",
|
|
931
|
-
plugin: "schedule"
|
|
932
|
-
}
|
|
938
|
+
source: { kind: "schedule" }
|
|
933
939
|
});
|
|
934
940
|
this.agent.followup(message);
|
|
935
941
|
} catch (error) {
|
|
@@ -1447,13 +1453,71 @@ const inject = [
|
|
|
1447
1453
|
"tools",
|
|
1448
1454
|
"sessionPersistence"
|
|
1449
1455
|
];
|
|
1450
|
-
/**
|
|
1456
|
+
/**
|
|
1457
|
+
* Install Schedule only for root agents published after this plugin loads,
|
|
1458
|
+
* and answer the Workspace registry's archive admission for every session
|
|
1459
|
+
* this plugin owns a runtime for. The owner answers from its own fold of the
|
|
1460
|
+
* live log, so a session without a live agent or without an owned runtime
|
|
1461
|
+
* — nothing armed that could fire — reports nothing and has nothing to stop.
|
|
1462
|
+
* Active reminders count as activity. A stop request is a management delete
|
|
1463
|
+
* in the agent's transaction queue, serialized with the tools and the
|
|
1464
|
+
* owner's due transaction: it awaits the shared persistence barrier before
|
|
1465
|
+
* reading the fold, appends the same durable `schedule/change` delete the
|
|
1466
|
+
* `schedule_delete` tool records for every active reminder, and awaits a
|
|
1467
|
+
* second barrier after the appends. A failed barrier rejects the stop, which
|
|
1468
|
+
* the registry logs, leaving the reminders for the archived-session gate to
|
|
1469
|
+
* block when they fire.
|
|
1470
|
+
*/
|
|
1451
1471
|
function apply(ctx) {
|
|
1452
1472
|
ctx.inject(["sessionProjections"], (projectionCtx) => {
|
|
1453
1473
|
projectionCtx.sessionProjections.register(scheduleProjectionDefinition);
|
|
1454
1474
|
});
|
|
1455
1475
|
const runtimes = /* @__PURE__ */ new Map();
|
|
1456
1476
|
let stopping = false;
|
|
1477
|
+
/** The owned runtime's active records for a session id; none without a live agent or an owned runtime. */
|
|
1478
|
+
const activeReminders = (sessionId) => {
|
|
1479
|
+
const agent = ctx.agents.get(sessionId);
|
|
1480
|
+
const active = agent === void 0 ? void 0 : runtimes.get(agent)?.runtime.activeRecords();
|
|
1481
|
+
return agent === void 0 || active === void 0 ? void 0 : {
|
|
1482
|
+
agent,
|
|
1483
|
+
active
|
|
1484
|
+
};
|
|
1485
|
+
};
|
|
1486
|
+
ctx.effect(() => {
|
|
1487
|
+
const stopActivity = ctx.on("workspace/session-activity", async ({ sessionId }, next) => {
|
|
1488
|
+
const reminders = activeReminders(sessionId);
|
|
1489
|
+
const rest = await next();
|
|
1490
|
+
if (reminders === void 0 || reminders.active.length === 0) return rest;
|
|
1491
|
+
return [{
|
|
1492
|
+
kind: "schedule",
|
|
1493
|
+
items: reminders.active.map((record) => ({
|
|
1494
|
+
id: record.id,
|
|
1495
|
+
label: record.prompt
|
|
1496
|
+
}))
|
|
1497
|
+
}, ...rest];
|
|
1498
|
+
});
|
|
1499
|
+
const stopStop = ctx.on("workspace/session-stop", async ({ sessionId }) => {
|
|
1500
|
+
const owned = activeReminders(sessionId);
|
|
1501
|
+
if (owned === void 0) return;
|
|
1502
|
+
const { agent } = owned;
|
|
1503
|
+
await runScheduleTransaction(agent, async () => {
|
|
1504
|
+
await flushSchedulePersistence(ctx, agent.session);
|
|
1505
|
+
const active = activeReminders(sessionId)?.active ?? [];
|
|
1506
|
+
if (active.length === 0) return;
|
|
1507
|
+
for (const record of active) agent.session.append("schedule/change", {
|
|
1508
|
+
version: 1,
|
|
1509
|
+
operation: "delete",
|
|
1510
|
+
id: record.id
|
|
1511
|
+
});
|
|
1512
|
+
runtimes.get(agent)?.runtime.requestDrive();
|
|
1513
|
+
await flushSchedulePersistence(ctx, agent.session);
|
|
1514
|
+
});
|
|
1515
|
+
});
|
|
1516
|
+
return () => {
|
|
1517
|
+
stopStop();
|
|
1518
|
+
stopActivity();
|
|
1519
|
+
};
|
|
1520
|
+
}, "schedule.archiveAdmission()");
|
|
1457
1521
|
ctx.effect(() => {
|
|
1458
1522
|
const stopCreated = ctx.on("agent/created", ({ agent }) => {
|
|
1459
1523
|
if (stopping || runtimes.has(agent) || !ctx.agents.roots().includes(agent)) return;
|
|
@@ -1472,18 +1536,21 @@ function apply(ctx) {
|
|
|
1472
1536
|
try {
|
|
1473
1537
|
await runtime.dispose();
|
|
1474
1538
|
} finally {
|
|
1475
|
-
if (runtimes.get(agent) === cleanup) runtimes.delete(agent);
|
|
1539
|
+
if (runtimes.get(agent)?.cleanup === cleanup) runtimes.delete(agent);
|
|
1476
1540
|
}
|
|
1477
1541
|
};
|
|
1478
1542
|
}, "schedule.runtime()");
|
|
1479
|
-
runtimes.set(agent,
|
|
1543
|
+
runtimes.set(agent, {
|
|
1544
|
+
runtime,
|
|
1545
|
+
cleanup
|
|
1546
|
+
});
|
|
1480
1547
|
});
|
|
1481
1548
|
return async () => {
|
|
1482
1549
|
stopping = true;
|
|
1483
1550
|
stopCreated();
|
|
1484
|
-
const
|
|
1551
|
+
const owned = [...runtimes.values()];
|
|
1485
1552
|
runtimes.clear();
|
|
1486
|
-
await Promise.allSettled(
|
|
1553
|
+
await Promise.allSettled(owned.map(({ cleanup }) => Promise.resolve(cleanup())));
|
|
1487
1554
|
};
|
|
1488
1555
|
}, "schedule.lifecycle()");
|
|
1489
1556
|
}
|
package/lib/types/index.d.ts
CHANGED
|
@@ -10,6 +10,20 @@ export { registerScheduleTools } from './tools.ts';
|
|
|
10
10
|
export declare const name = "schedule";
|
|
11
11
|
/** Services required before future root agents can receive Schedule. */
|
|
12
12
|
export declare const inject: string[];
|
|
13
|
-
/**
|
|
13
|
+
/**
|
|
14
|
+
* Install Schedule only for root agents published after this plugin loads,
|
|
15
|
+
* and answer the Workspace registry's archive admission for every session
|
|
16
|
+
* this plugin owns a runtime for. The owner answers from its own fold of the
|
|
17
|
+
* live log, so a session without a live agent or without an owned runtime
|
|
18
|
+
* — nothing armed that could fire — reports nothing and has nothing to stop.
|
|
19
|
+
* Active reminders count as activity. A stop request is a management delete
|
|
20
|
+
* in the agent's transaction queue, serialized with the tools and the
|
|
21
|
+
* owner's due transaction: it awaits the shared persistence barrier before
|
|
22
|
+
* reading the fold, appends the same durable `schedule/change` delete the
|
|
23
|
+
* `schedule_delete` tool records for every active reminder, and awaits a
|
|
24
|
+
* second barrier after the appends. A failed barrier rejects the stop, which
|
|
25
|
+
* the registry logs, leaving the reminders for the archived-session gate to
|
|
26
|
+
* block when they fire.
|
|
27
|
+
*/
|
|
14
28
|
export declare function apply(ctx: Context): void;
|
|
15
29
|
//# sourceMappingURL=index.d.ts.map
|
package/lib/types/index.js
CHANGED
|
@@ -2,22 +2,78 @@
|
|
|
2
2
|
* Agent-scoped durable one-shot and fixed-rate reminders over the session event log.
|
|
3
3
|
* @module @deepseek-ai/dsh-schedule
|
|
4
4
|
*/
|
|
5
|
+
import { flushSchedulePersistence } from "./persistence.js";
|
|
5
6
|
import { scheduleProjectionDefinition } from "./projection.js";
|
|
6
7
|
import { ScheduleRuntime } from "./runtime.js";
|
|
7
8
|
import { registerScheduleTools } from "./tools.js";
|
|
9
|
+
import { runScheduleTransaction } from "./transaction.js";
|
|
8
10
|
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
11
|
export { registerScheduleTools } from "./tools.js";
|
|
10
12
|
/** Cordis function-plugin name. */
|
|
11
13
|
export const name = 'schedule';
|
|
12
14
|
/** Services required before future root agents can receive Schedule. */
|
|
13
15
|
export const inject = ['agents', 'sessions', 'tools', 'sessionPersistence'];
|
|
14
|
-
/**
|
|
16
|
+
/**
|
|
17
|
+
* Install Schedule only for root agents published after this plugin loads,
|
|
18
|
+
* and answer the Workspace registry's archive admission for every session
|
|
19
|
+
* this plugin owns a runtime for. The owner answers from its own fold of the
|
|
20
|
+
* live log, so a session without a live agent or without an owned runtime
|
|
21
|
+
* — nothing armed that could fire — reports nothing and has nothing to stop.
|
|
22
|
+
* Active reminders count as activity. A stop request is a management delete
|
|
23
|
+
* in the agent's transaction queue, serialized with the tools and the
|
|
24
|
+
* owner's due transaction: it awaits the shared persistence barrier before
|
|
25
|
+
* reading the fold, appends the same durable `schedule/change` delete the
|
|
26
|
+
* `schedule_delete` tool records for every active reminder, and awaits a
|
|
27
|
+
* second barrier after the appends. A failed barrier rejects the stop, which
|
|
28
|
+
* the registry logs, leaving the reminders for the archived-session gate to
|
|
29
|
+
* block when they fire.
|
|
30
|
+
*/
|
|
15
31
|
export function apply(ctx) {
|
|
16
32
|
ctx.inject(['sessionProjections'], (projectionCtx) => {
|
|
17
33
|
projectionCtx.sessionProjections.register(scheduleProjectionDefinition);
|
|
18
34
|
});
|
|
19
35
|
const runtimes = new Map();
|
|
20
36
|
let stopping = false;
|
|
37
|
+
/** The owned runtime's active records for a session id; none without a live agent or an owned runtime. */
|
|
38
|
+
const activeReminders = (sessionId) => {
|
|
39
|
+
const agent = ctx.agents.get(sessionId);
|
|
40
|
+
const active = agent === undefined ? undefined : runtimes.get(agent)?.runtime.activeRecords();
|
|
41
|
+
return agent === undefined || active === undefined ? undefined : { agent, active };
|
|
42
|
+
};
|
|
43
|
+
ctx.effect(() => {
|
|
44
|
+
const stopActivity = ctx.on('workspace/session-activity', async ({ sessionId }, next) => {
|
|
45
|
+
const reminders = activeReminders(sessionId);
|
|
46
|
+
const rest = await next();
|
|
47
|
+
if (reminders === undefined || reminders.active.length === 0)
|
|
48
|
+
return rest;
|
|
49
|
+
const own = {
|
|
50
|
+
kind: 'schedule',
|
|
51
|
+
items: reminders.active.map(record => ({ id: record.id, label: record.prompt })),
|
|
52
|
+
};
|
|
53
|
+
return [own, ...rest];
|
|
54
|
+
});
|
|
55
|
+
const stopStop = ctx.on('workspace/session-stop', async ({ sessionId }) => {
|
|
56
|
+
const owned = activeReminders(sessionId);
|
|
57
|
+
if (owned === undefined)
|
|
58
|
+
return;
|
|
59
|
+
const { agent } = owned;
|
|
60
|
+
await runScheduleTransaction(agent, async () => {
|
|
61
|
+
await flushSchedulePersistence(ctx, agent.session);
|
|
62
|
+
const active = activeReminders(sessionId)?.active ?? [];
|
|
63
|
+
if (active.length === 0)
|
|
64
|
+
return;
|
|
65
|
+
for (const record of active) {
|
|
66
|
+
agent.session.append('schedule/change', { version: 1, operation: 'delete', id: record.id });
|
|
67
|
+
}
|
|
68
|
+
runtimes.get(agent)?.runtime.requestDrive();
|
|
69
|
+
await flushSchedulePersistence(ctx, agent.session);
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
return () => {
|
|
73
|
+
stopStop();
|
|
74
|
+
stopActivity();
|
|
75
|
+
};
|
|
76
|
+
}, 'schedule.archiveAdmission()');
|
|
21
77
|
ctx.effect(() => {
|
|
22
78
|
const stopCreated = ctx.on('agent/created', ({ agent }) => {
|
|
23
79
|
if (stopping || runtimes.has(agent) || !ctx.agents.roots().includes(agent))
|
|
@@ -39,19 +95,19 @@ export function apply(ctx) {
|
|
|
39
95
|
await runtime.dispose();
|
|
40
96
|
}
|
|
41
97
|
finally {
|
|
42
|
-
if (runtimes.get(agent) === cleanup)
|
|
98
|
+
if (runtimes.get(agent)?.cleanup === cleanup)
|
|
43
99
|
runtimes.delete(agent);
|
|
44
100
|
}
|
|
45
101
|
};
|
|
46
102
|
}, 'schedule.runtime()');
|
|
47
|
-
runtimes.set(agent, cleanup);
|
|
103
|
+
runtimes.set(agent, { runtime, cleanup });
|
|
48
104
|
});
|
|
49
105
|
return async () => {
|
|
50
106
|
stopping = true;
|
|
51
107
|
stopCreated();
|
|
52
|
-
const
|
|
108
|
+
const owned = [...runtimes.values()];
|
|
53
109
|
runtimes.clear();
|
|
54
|
-
await Promise.allSettled(
|
|
110
|
+
await Promise.allSettled(owned.map(({ cleanup }) => Promise.resolve(cleanup())));
|
|
55
111
|
};
|
|
56
112
|
}, 'schedule.lifecycle()');
|
|
57
113
|
}
|
package/lib/types/runtime.d.ts
CHANGED
|
@@ -4,6 +4,15 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import type { Context } from '@deepseek-ai/cordis';
|
|
6
6
|
import type { Agent } from '@deepseek-ai/dsh-agent';
|
|
7
|
+
import type { ContextFormed } from '@deepseek-ai/dsh-llm';
|
|
8
|
+
declare module '@deepseek-ai/dsh-llm' {
|
|
9
|
+
interface MessageSourceMap {
|
|
10
|
+
'schedule': {
|
|
11
|
+
kind: 'schedule';
|
|
12
|
+
} & ContextFormed;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
import type { ScheduleRecord } from './types.ts';
|
|
7
16
|
/** Largest delay that Node timers represent without clamping. */
|
|
8
17
|
export declare const MAX_TIMER_DELAY_MS = 2147483647;
|
|
9
18
|
/** One process-local, disposable projection of an exact agent's durable schedules. */
|
|
@@ -26,6 +35,12 @@ export declare class ScheduleRuntime {
|
|
|
26
35
|
constructor(ctx: Context, agent: Agent);
|
|
27
36
|
/** Begin the initial durability preflight and timer derivation. */
|
|
28
37
|
start(): void;
|
|
38
|
+
/**
|
|
39
|
+
* Active records of the exact runtime suffix, folded from the live log the
|
|
40
|
+
* way every drive folds it: the owner answers for what its timers can fire.
|
|
41
|
+
* @returns the active records in create order, or nothing while this runtime is stopping, faulted, or reading a corrupt stream.
|
|
42
|
+
*/
|
|
43
|
+
activeRecords(): readonly ScheduleRecord[] | undefined;
|
|
29
44
|
/** Recompute the live projection after a committed mutation or idle transition. */
|
|
30
45
|
requestDrive(): void;
|
|
31
46
|
/** Stop future work, cancel timers, and await every outstanding runtime promise. */
|
package/lib/types/runtime.js
CHANGED
|
@@ -66,6 +66,16 @@ export class ScheduleRuntime {
|
|
|
66
66
|
start() {
|
|
67
67
|
this.requestDrive();
|
|
68
68
|
}
|
|
69
|
+
/**
|
|
70
|
+
* Active records of the exact runtime suffix, folded from the live log the
|
|
71
|
+
* way every drive folds it: the owner answers for what its timers can fire.
|
|
72
|
+
* @returns the active records in create order, or nothing while this runtime is stopping, faulted, or reading a corrupt stream.
|
|
73
|
+
*/
|
|
74
|
+
activeRecords() {
|
|
75
|
+
if (this.stopping || this.faulted)
|
|
76
|
+
return undefined;
|
|
77
|
+
return this.readFolded()?.active;
|
|
78
|
+
}
|
|
69
79
|
/** Recompute the live projection after a committed mutation or idle transition. */
|
|
70
80
|
requestDrive() {
|
|
71
81
|
if (this.stopping || this.faulted)
|
|
@@ -235,7 +245,7 @@ export class ScheduleRuntime {
|
|
|
235
245
|
: renderEveryReminderBatchFraming(decision.reminders);
|
|
236
246
|
const message = createUserMessage({
|
|
237
247
|
content: [{ type: 'text', text }],
|
|
238
|
-
source: { kind: '
|
|
248
|
+
source: { kind: 'schedule' },
|
|
239
249
|
});
|
|
240
250
|
this.agent.followup(message);
|
|
241
251
|
}
|
package/lib/types/types.d.ts
CHANGED
|
@@ -169,6 +169,12 @@ export type ScheduleDeleteResult = {
|
|
|
169
169
|
};
|
|
170
170
|
/** Canonical `schedule_delete` value. */
|
|
171
171
|
export type ScheduleDeleteValue = ScheduleDeleteResult | ScheduleToolError;
|
|
172
|
+
declare module '@deepseek-ai/dsh-workspace/types' {
|
|
173
|
+
interface SessionActivityKindMap {
|
|
174
|
+
/** A scheduled follow-up for this session is still active. */
|
|
175
|
+
schedule: true;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
172
178
|
declare module '@deepseek-ai/dsh-session/types' {
|
|
173
179
|
interface SessionEventMap {
|
|
174
180
|
/**
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@deepseek-ai/dsh-schedule",
|
|
3
3
|
"description": "Agent-scoped durable after, at, and fixed-rate reminders over the session event log",
|
|
4
|
-
"version": "0.1.
|
|
4
|
+
"version": "0.1.7-alpha.1",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
7
7
|
},
|
|
@@ -37,33 +37,35 @@
|
|
|
37
37
|
],
|
|
38
38
|
"license": "MIT",
|
|
39
39
|
"peerDependencies": {
|
|
40
|
-
"@deepseek-ai/dsh-
|
|
41
|
-
"@deepseek-ai/dsh-
|
|
42
|
-
"@deepseek-ai/dsh-
|
|
43
|
-
"@deepseek-ai/dsh-
|
|
44
|
-
"@deepseek-ai/dsh-session-
|
|
45
|
-
"@deepseek-ai/dsh-
|
|
46
|
-
"@deepseek-ai/dsh-
|
|
47
|
-
"@deepseek-ai/cordis": "^4.0.
|
|
48
|
-
"@deepseek-ai/dsh-
|
|
40
|
+
"@deepseek-ai/dsh-invariants": "^0.1.7-alpha.1",
|
|
41
|
+
"@deepseek-ai/dsh-session": "^0.1.7-alpha.1",
|
|
42
|
+
"@deepseek-ai/dsh-llm": "^0.1.7-alpha.1",
|
|
43
|
+
"@deepseek-ai/dsh-session-persistence": "^0.1.7-alpha.1",
|
|
44
|
+
"@deepseek-ai/dsh-session-projection": "^0.1.7-alpha.1",
|
|
45
|
+
"@deepseek-ai/dsh-tools": "^0.1.7-alpha.1",
|
|
46
|
+
"@deepseek-ai/dsh-workspace": "^0.1.7-alpha.1",
|
|
47
|
+
"@deepseek-ai/cordis": "^4.0.3",
|
|
48
|
+
"@deepseek-ai/dsh-brand": "^0.1.7-alpha.1",
|
|
49
|
+
"@deepseek-ai/dsh-agent": "^0.1.7-alpha.1"
|
|
49
50
|
},
|
|
50
51
|
"dependencies": {
|
|
51
52
|
"zod": "^4.4.3"
|
|
52
53
|
},
|
|
53
54
|
"devDependencies": {
|
|
54
|
-
"@deepseek-ai/cordis-plugin-loader": "^1.0.
|
|
55
|
-
"@deepseek-ai/dsh-agent": "^0.1.
|
|
56
|
-
"@deepseek-ai/dsh-
|
|
57
|
-
"@deepseek-ai/dsh-agent-loop-testkit": "^0.1.
|
|
58
|
-
"@deepseek-ai/dsh-
|
|
59
|
-
"@deepseek-ai/dsh-
|
|
60
|
-
"@deepseek-ai/dsh-
|
|
61
|
-
"@deepseek-ai/dsh-
|
|
62
|
-
"@deepseek-ai/dsh-session-persistence": "^0.1.
|
|
63
|
-
"@deepseek-ai/dsh-session-
|
|
64
|
-
"@deepseek-ai/dsh-
|
|
65
|
-
"@deepseek-ai/dsh-
|
|
66
|
-
"@deepseek-ai/cordis": "^4.0.
|
|
67
|
-
"@deepseek-ai/dsh-
|
|
55
|
+
"@deepseek-ai/cordis-plugin-loader": "^1.0.4",
|
|
56
|
+
"@deepseek-ai/dsh-agent-loop": "^0.1.7-alpha.1",
|
|
57
|
+
"@deepseek-ai/dsh-brand": "^0.1.7-alpha.1",
|
|
58
|
+
"@deepseek-ai/dsh-agent-loop-testkit": "^0.1.7-alpha.1",
|
|
59
|
+
"@deepseek-ai/dsh-invariants": "^0.1.7-alpha.1",
|
|
60
|
+
"@deepseek-ai/dsh-session": "^0.1.7-alpha.1",
|
|
61
|
+
"@deepseek-ai/dsh-llm": "^0.1.7-alpha.1",
|
|
62
|
+
"@deepseek-ai/dsh-session-persistence": "^0.1.7-alpha.1",
|
|
63
|
+
"@deepseek-ai/dsh-session-persistence-jsonl": "^0.1.7-alpha.1",
|
|
64
|
+
"@deepseek-ai/dsh-session-projection": "^0.1.7-alpha.1",
|
|
65
|
+
"@deepseek-ai/dsh-system-prompt": "^0.1.7-alpha.1",
|
|
66
|
+
"@deepseek-ai/dsh-tools": "^0.1.7-alpha.1",
|
|
67
|
+
"@deepseek-ai/cordis": "^4.0.3",
|
|
68
|
+
"@deepseek-ai/dsh-workspace": "^0.1.7-alpha.1",
|
|
69
|
+
"@deepseek-ai/dsh-agent": "^0.1.7-alpha.1"
|
|
68
70
|
}
|
|
69
71
|
}
|