@cursor/july 0.1.105 → 0.1.107

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.
Files changed (217) hide show
  1. package/dist/bin/agent-serve.js +1 -8
  2. package/dist/channels/bitbucket/types.d.ts +2 -4
  3. package/dist/channels/bitbucket/types.d.ts.map +1 -1
  4. package/dist/channels/deployments/types.d.ts +5 -8
  5. package/dist/channels/deployments/types.d.ts.map +1 -1
  6. package/dist/channels/github/types.d.ts +2 -4
  7. package/dist/channels/github/types.d.ts.map +1 -1
  8. package/dist/channels/gitlab/types.d.ts +2 -5
  9. package/dist/channels/gitlab/types.d.ts.map +1 -1
  10. package/dist/channels/origin/types.d.ts +2 -3
  11. package/dist/channels/origin/types.d.ts.map +1 -1
  12. package/dist/channels/slack/defaults.d.ts.map +1 -1
  13. package/dist/channels/slack/defaults.js +3 -0
  14. package/dist/channels/slack/dispatch.d.ts.map +1 -1
  15. package/dist/channels/slack/dispatch.js +1 -1
  16. package/dist/channels/slack/live-delivery.d.ts.map +1 -1
  17. package/dist/channels/slack/live-delivery.js +48 -11
  18. package/dist/channels/slack/types.d.ts +4 -3
  19. package/dist/channels/slack/types.d.ts.map +1 -1
  20. package/dist/docs/404.html +3 -3
  21. package/dist/docs/ab.html +6 -6
  22. package/dist/docs/assets/{app.BVwaffdK.js → app.CtuckIiL.js} +1 -1
  23. package/dist/docs/assets/chunks/@localSearchIndexroot.DXXZxiMv.js +1 -0
  24. package/dist/docs/assets/chunks/{VPLocalSearchBox.WuWTZ1x8.js → VPLocalSearchBox.Bkkhnu8K.js} +2 -2
  25. package/dist/docs/assets/chunks/{theme.BYdI_6sF.js → theme.DhpIe0Pa.js} +2 -2
  26. package/dist/docs/assets/{guides_opentelemetry.md.COh9jFVg.js → guides_opentelemetry.md.SCRMskno.js} +3 -3
  27. package/dist/docs/assets/{guides_opentelemetry.md.COh9jFVg.lean.js → guides_opentelemetry.md.SCRMskno.lean.js} +1 -1
  28. package/dist/docs/assets/{reference_channels.md.DBGcaB7m.js → reference_channels.md.D-qTqwcq.js} +2 -2
  29. package/dist/docs/assets/{reference_channels.md.DBGcaB7m.lean.js → reference_channels.md.D-qTqwcq.lean.js} +1 -1
  30. package/dist/docs/assets/{reference_cli.md.C6Lo8SEd.js → reference_cli.md.Dm67hd2D.js} +1 -1
  31. package/dist/docs/assets/{skills_otel.md.CqhobH3B.js → skills_otel.md.B5Ddv1xH.js} +1 -1
  32. package/dist/docs/assets/{style.BRuM8477.css → style.DCD_zyg3.css} +1 -1
  33. package/dist/docs/building-with-agents.html +6 -6
  34. package/dist/docs/concepts.html +5 -5
  35. package/dist/docs/deployment.html +6 -6
  36. package/dist/docs/evals.html +6 -6
  37. package/dist/docs/guides/agent-to-agent.html +6 -6
  38. package/dist/docs/guides/cloud-agents.html +6 -6
  39. package/dist/docs/guides/convert-automation.html +6 -6
  40. package/dist/docs/guides/github.html +6 -6
  41. package/dist/docs/guides/grokbot-agents.html +6 -6
  42. package/dist/docs/guides/human-in-the-loop.html +6 -6
  43. package/dist/docs/guides/mcp-oauth.html +6 -6
  44. package/dist/docs/guides/opentelemetry.html +8 -8
  45. package/dist/docs/guides/opentelemetry.md +3 -4
  46. package/dist/docs/guides/slack.html +6 -6
  47. package/dist/docs/guides/webhooks.html +6 -6
  48. package/dist/docs/hashmap.json +1 -1
  49. package/dist/docs/hillclimbing.html +6 -6
  50. package/dist/docs/index.html +6 -6
  51. package/dist/docs/llms-full.txt +22 -28
  52. package/dist/docs/quickstart.html +6 -6
  53. package/dist/docs/reference/agent-config.html +6 -6
  54. package/dist/docs/reference/artifacts.html +6 -6
  55. package/dist/docs/reference/channels.html +7 -7
  56. package/dist/docs/reference/channels.md +11 -19
  57. package/dist/docs/reference/cli.html +8 -8
  58. package/dist/docs/reference/cli.md +5 -2
  59. package/dist/docs/reference/connections.html +6 -6
  60. package/dist/docs/reference/extensions.html +6 -6
  61. package/dist/docs/reference/hooks.html +6 -6
  62. package/dist/docs/reference/http-api.html +6 -6
  63. package/dist/docs/reference/instructions.html +6 -6
  64. package/dist/docs/reference/playground.html +5 -5
  65. package/dist/docs/reference/project-layout.html +6 -6
  66. package/dist/docs/reference/prompt.html +6 -6
  67. package/dist/docs/reference/result.html +6 -6
  68. package/dist/docs/reference/schedules.html +6 -6
  69. package/dist/docs/reference/sessions.html +5 -5
  70. package/dist/docs/reference/skills.html +6 -6
  71. package/dist/docs/reference/subagents.html +6 -6
  72. package/dist/docs/reference/tools.html +6 -6
  73. package/dist/docs/scaffolding-agents.html +5 -5
  74. package/dist/docs/skills/ab.html +6 -6
  75. package/dist/docs/skills/create-agent.html +6 -6
  76. package/dist/docs/skills/debug.html +5 -5
  77. package/dist/docs/skills/evals.html +6 -6
  78. package/dist/docs/skills/framework-map.html +5 -5
  79. package/dist/docs/skills/github.html +6 -6
  80. package/dist/docs/skills/hillclimb.html +6 -6
  81. package/dist/docs/skills/index.html +5 -5
  82. package/dist/docs/skills/mcp-auth.html +6 -6
  83. package/dist/docs/skills/otel.html +8 -8
  84. package/dist/docs/skills/otel.md +3 -3
  85. package/dist/docs/skills/setup-slack.html +6 -6
  86. package/dist/docs/storage.html +6 -6
  87. package/dist/docs/templates/agentic-owners.html +6 -6
  88. package/dist/docs/templates/agents-md.html +6 -6
  89. package/dist/docs/templates/code-wiki.html +6 -6
  90. package/dist/docs/templates/demo.html +6 -6
  91. package/dist/docs/templates/grokbot-agents.html +6 -6
  92. package/dist/docs/templates/pr-autofixer.html +6 -6
  93. package/dist/docs/templates/security-help.html +6 -6
  94. package/dist/docs/templates/security-reviewer.html +6 -6
  95. package/dist/docs/templates/triage.html +6 -6
  96. package/dist/docs/troubleshooting.html +5 -5
  97. package/dist/index.d.ts +4 -5
  98. package/dist/index.d.ts.map +1 -1
  99. package/dist/index.js +3 -4
  100. package/dist/internal/cli-deploy.d.ts.map +1 -1
  101. package/dist/internal/cli-deploy.js +23 -11
  102. package/dist/internal/cursor/backend-client.d.ts +25 -0
  103. package/dist/internal/cursor/backend-client.d.ts.map +1 -1
  104. package/dist/internal/cursor/backend-client.js +57 -2
  105. package/dist/internal/deploy-client.d.ts +8 -0
  106. package/dist/internal/deploy-client.d.ts.map +1 -1
  107. package/dist/internal/deploy-client.js +14 -0
  108. package/dist/internal/deploy-manifest.d.ts +3 -0
  109. package/dist/internal/deploy-manifest.d.ts.map +1 -1
  110. package/dist/internal/deploy-manifest.js +15 -12
  111. package/dist/internal/discovery/agent.d.ts.map +1 -1
  112. package/dist/internal/discovery/agent.js +1 -0
  113. package/dist/internal/discovery/info.d.ts.map +1 -1
  114. package/dist/internal/discovery/info.js +7 -3
  115. package/dist/internal/discovery/modules.d.ts +1 -1
  116. package/dist/internal/discovery/modules.d.ts.map +1 -1
  117. package/dist/internal/discovery/modules.js +1 -0
  118. package/dist/internal/discovery/project.d.ts.map +1 -1
  119. package/dist/internal/discovery/project.js +3 -0
  120. package/dist/internal/discovery/reminders.d.ts +7 -0
  121. package/dist/internal/discovery/reminders.d.ts.map +1 -0
  122. package/dist/internal/discovery/reminders.js +65 -0
  123. package/dist/internal/discovery/subagent.js +1 -0
  124. package/dist/internal/distribution.d.ts +5 -0
  125. package/dist/internal/distribution.d.ts.map +1 -1
  126. package/dist/internal/distribution.js +17 -0
  127. package/dist/internal/framework-storage-selection.d.ts +14 -3
  128. package/dist/internal/framework-storage-selection.d.ts.map +1 -1
  129. package/dist/internal/framework-storage-selection.js +36 -6
  130. package/dist/internal/guest-network.d.ts.map +1 -1
  131. package/dist/internal/guest-network.js +2 -0
  132. package/dist/internal/hosted-delivery-protocol.d.ts +48 -0
  133. package/dist/internal/hosted-delivery-protocol.d.ts.map +1 -1
  134. package/dist/internal/hosted-delivery-protocol.js +60 -0
  135. package/dist/internal/hosted-delivery.d.ts +10 -0
  136. package/dist/internal/hosted-delivery.d.ts.map +1 -1
  137. package/dist/internal/hosted-delivery.js +83 -18
  138. package/dist/internal/local-control-plane.d.ts +18 -2
  139. package/dist/internal/local-control-plane.d.ts.map +1 -1
  140. package/dist/internal/local-control-plane.js +199 -17
  141. package/dist/internal/reminder-control-plane-protocol.d.ts +187 -0
  142. package/dist/internal/reminder-control-plane-protocol.d.ts.map +1 -0
  143. package/dist/internal/reminder-control-plane-protocol.js +244 -0
  144. package/dist/internal/reminder-runner.d.ts +48 -6
  145. package/dist/internal/reminder-runner.d.ts.map +1 -1
  146. package/dist/internal/reminder-runner.js +336 -48
  147. package/dist/internal/reminder-schedule.d.ts +5 -2
  148. package/dist/internal/reminder-schedule.d.ts.map +1 -1
  149. package/dist/internal/reminder-schedule.js +8 -6
  150. package/dist/internal/reminder-store.d.ts +24 -7
  151. package/dist/internal/reminder-store.d.ts.map +1 -1
  152. package/dist/internal/reminder-store.js +6 -5
  153. package/dist/internal/request-error-status.d.ts +2 -0
  154. package/dist/internal/request-error-status.d.ts.map +1 -1
  155. package/dist/internal/request-error-status.js +22 -3
  156. package/dist/internal/scm/pr-url.d.ts +40 -0
  157. package/dist/internal/scm/pr-url.d.ts.map +1 -1
  158. package/dist/internal/scm/pr-url.js +93 -14
  159. package/dist/internal/server.d.ts.map +1 -1
  160. package/dist/internal/server.js +67 -27
  161. package/dist/internal/session-engine.d.ts +4 -0
  162. package/dist/internal/session-engine.d.ts.map +1 -1
  163. package/dist/internal/session-engine.js +5 -0
  164. package/dist/playground/assets/{index-BaQzjaNK.js → index-Bq2HpEQB.js} +23 -23
  165. package/dist/playground/assets/index-CZKKNlmb.css +1 -0
  166. package/dist/playground/index.html +2 -2
  167. package/dist/reminders.d.ts +24 -5
  168. package/dist/reminders.d.ts.map +1 -1
  169. package/dist/reminders.js +28 -5
  170. package/dist/types.d.ts +89 -20
  171. package/dist/types.d.ts.map +1 -1
  172. package/docs/guides/opentelemetry.md +3 -4
  173. package/docs/reference/channels.md +11 -19
  174. package/docs/reference/cli.md +5 -2
  175. package/package.json +3 -3
  176. package/skills/otel/SKILL.md +3 -3
  177. package/src/bin/agent-serve.ts +1 -9
  178. package/src/channels/bitbucket/types.ts +2 -4
  179. package/src/channels/deployments/types.ts +5 -8
  180. package/src/channels/github/types.ts +2 -4
  181. package/src/channels/gitlab/types.ts +2 -5
  182. package/src/channels/origin/types.ts +2 -3
  183. package/src/channels/slack/defaults.ts +3 -0
  184. package/src/channels/slack/dispatch.ts +1 -0
  185. package/src/channels/slack/live-delivery.ts +53 -12
  186. package/src/channels/slack/types.ts +4 -3
  187. package/src/index.ts +6 -3
  188. package/src/internal/cli-deploy.ts +30 -12
  189. package/src/internal/cursor/backend-client.ts +90 -2
  190. package/src/internal/deploy-client.ts +16 -0
  191. package/src/internal/deploy-manifest.ts +16 -11
  192. package/src/internal/discovery/agent.ts +1 -0
  193. package/src/internal/discovery/info.ts +6 -0
  194. package/src/internal/discovery/modules.ts +2 -4
  195. package/src/internal/discovery/project.ts +3 -0
  196. package/src/internal/discovery/reminders.ts +65 -0
  197. package/src/internal/discovery/subagent.ts +1 -0
  198. package/src/internal/distribution.ts +20 -0
  199. package/src/internal/framework-storage-selection.ts +43 -6
  200. package/src/internal/guest-network.ts +3 -0
  201. package/src/internal/hosted-delivery-protocol.ts +96 -0
  202. package/src/internal/hosted-delivery.ts +154 -27
  203. package/src/internal/local-control-plane.ts +281 -22
  204. package/src/internal/reminder-control-plane-protocol.ts +499 -0
  205. package/src/internal/reminder-runner.ts +421 -48
  206. package/src/internal/reminder-schedule.ts +11 -6
  207. package/src/internal/reminder-store.ts +27 -7
  208. package/src/internal/request-error-status.ts +27 -3
  209. package/src/internal/scm/pr-url.ts +167 -20
  210. package/src/internal/server.ts +69 -16
  211. package/src/internal/session-engine.ts +5 -0
  212. package/src/reminders.ts +35 -4
  213. package/src/types.ts +95 -20
  214. package/dist/docs/assets/chunks/@localSearchIndexroot.CJxMklAy.js +0 -1
  215. package/dist/playground/assets/index-CceqnLTX.css +0 -1
  216. /package/dist/docs/assets/{reference_cli.md.C6Lo8SEd.lean.js → reference_cli.md.Dm67hd2D.lean.js} +0 -0
  217. /package/dist/docs/assets/{skills_otel.md.CqhobH3B.lean.js → skills_otel.md.B5Ddv1xH.lean.js} +0 -0
@@ -1,20 +1,35 @@
1
1
  /**
2
2
  * Per-session reminder runner: durable wake subscriptions with
3
- * prompt XOR run, every/cron/delay/at schedules, and until as
4
- * function XOR prompt.
3
+ * prompt XOR named handler XOR run, every/cron/delay/at schedules, and
4
+ * until as function XOR prompt.
5
5
  */
6
6
 
7
7
  import { randomBytes } from "node:crypto";
8
8
  import type {
9
9
  AuthContext,
10
+ DiscoveredReminder,
11
+ JsonObject,
12
+ JsonValue,
10
13
  ReminderCreateInput,
14
+ ReminderDefinition,
11
15
  ReminderFireContext,
12
16
  ReminderFireResult,
13
17
  ReminderInfo,
18
+ ReminderStatus,
14
19
  ReminderUntilContext,
15
20
  } from "../types.js";
21
+ import { NAME_PATTERN } from "./discovery/names.js";
16
22
  import { parseDuration } from "./duration.js";
17
23
  import type { PlatformTimers } from "./platform-timers.js";
24
+ import {
25
+ REMINDER_CP_PROTOCOL_VERSION,
26
+ type ReminderControlPlane,
27
+ type ReminderCreateIntent,
28
+ type ReminderExecutionOutcome,
29
+ type ReminderFireDescriptor,
30
+ scheduleSpecFromCreateInput,
31
+ toWireReschedule,
32
+ } from "./reminder-control-plane-protocol.js";
18
33
  import {
19
34
  defaultBusyPolicy,
20
35
  MAX_TIMER_MS,
@@ -28,6 +43,9 @@ import { SessionBusyError, type SessionEngine } from "./session-engine.js";
28
43
 
29
44
  export { ReminderValidationError } from "./reminder-schedule.js";
30
45
 
46
+ /** Max UTF-8 byte length of `JSON.stringify(args)` on named-handler create. */
47
+ export const REMINDER_ARGS_MAX_BYTES: number = 32 * 1024;
48
+
31
49
  export class UnknownReminderError extends Error {
32
50
  constructor(
33
51
  reminderId: string,
@@ -43,10 +61,79 @@ interface LiveHandlers {
43
61
  untilFn?: (ctx: ReminderUntilContext) => Promise<boolean>;
44
62
  }
45
63
 
64
+ export interface ReminderRunnerOptions {
65
+ /** Discovered `agent/reminders/*` handlers, keyed by module stem. */
66
+ namedHandlers?: readonly DiscoveredReminder[];
67
+ /**
68
+ * Architecture v2: reject anonymous `run` and function `until` at create
69
+ * (named `handler` + prompt remain), whether local or hosted.
70
+ */
71
+ rejectAnonymousHandlers?: boolean;
72
+ /**
73
+ * When set (local self-hosted architecture v2), create/cancel/rekey go
74
+ * through the control-plane protocol and fires are guest executions of
75
+ * immutable descriptors.
76
+ */
77
+ controlPlane?: ReminderControlPlane;
78
+ /** Agent architecture; used with {@link controlPlane} for v2 protocol path. */
79
+ architecture?: "v1" | "v2";
80
+ }
81
+
82
+ /**
83
+ * Server wiring for {@link ReminderRunnerOptions.rejectAnonymousHandlers}:
84
+ * architecture v2 always rejects anonymous run/until (local or hosted).
85
+ */
86
+ export function rejectAnonymousReminderHandlers(options: {
87
+ noControlPlane?: boolean;
88
+ architecture?: string;
89
+ }): boolean {
90
+ return options.architecture === "v2";
91
+ }
92
+
46
93
  function newReminderId(): string {
47
94
  return `rem_${randomBytes(8).toString("hex")}`;
48
95
  }
49
96
 
97
+ function isJsonObject(value: unknown): value is JsonObject {
98
+ return (
99
+ typeof value === "object" &&
100
+ value !== null &&
101
+ !Array.isArray(value) &&
102
+ Object.values(value as Record<string, unknown>).every(isJsonValue)
103
+ );
104
+ }
105
+
106
+ function isJsonValue(value: unknown): value is JsonValue {
107
+ if (
108
+ value === null ||
109
+ typeof value === "string" ||
110
+ typeof value === "boolean"
111
+ ) {
112
+ return true;
113
+ }
114
+ if (typeof value === "number") {
115
+ return Number.isFinite(value);
116
+ }
117
+ if (Array.isArray(value)) {
118
+ return value.every(isJsonValue);
119
+ }
120
+ return isJsonObject(value);
121
+ }
122
+
123
+ /** Same stem rules as `agent/reminders/<name>.ts` discovery. */
124
+ function isValidReminderHandlerName(name: string): boolean {
125
+ return NAME_PATTERN.test(name) && !name.includes("__");
126
+ }
127
+
128
+ function assertReminderArgsSize(args: JsonObject): void {
129
+ const bytes = Buffer.byteLength(JSON.stringify(args), "utf8");
130
+ if (bytes > REMINDER_ARGS_MAX_BYTES) {
131
+ throw new ReminderValidationError(
132
+ `reminders.create args exceed ${REMINDER_ARGS_MAX_BYTES} bytes (got ${bytes})`
133
+ );
134
+ }
135
+ }
136
+
50
137
  function toInfo(record: ReminderRecord): ReminderInfo {
51
138
  return {
52
139
  id: record.id,
@@ -57,9 +144,11 @@ function toInfo(record: ReminderRecord): ReminderInfo {
57
144
  scheduleKind: record.scheduleKind,
58
145
  scheduleValue: record.scheduleValue,
59
146
  prompt: record.prompt,
147
+ handler: record.handler,
148
+ args: record.args,
60
149
  untilPrompt: record.untilPrompt,
61
150
  busyPolicy: record.busyPolicy,
62
- status: record.status,
151
+ status: record.status === "firing" ? "armed" : record.status,
63
152
  disarmReason: record.disarmReason,
64
153
  fireCount: record.fireCount,
65
154
  nextFireAt: new Date(record.nextFireAtMs).toISOString(),
@@ -96,17 +185,36 @@ function buildFirePrompt(record: ReminderRecord): string {
96
185
  export class ReminderRunner {
97
186
  private readonly records = new Map<string, ReminderRecord>();
98
187
  private readonly handlers = new Map<string, LiveHandlers>();
188
+ private readonly namedHandlers = new Map<string, ReminderDefinition>();
189
+ private readonly namedHandlerNames = new Map<ReminderDefinition, string>();
99
190
  private readonly timers = new Map<string, NodeJS.Timeout>();
100
191
  private readonly firing = new Set<string>();
101
192
  /** Single-flight idle gates keyed by `channelId:continuationToken`. */
102
193
  private readonly idleGates = new Map<string, Promise<void>>();
103
194
  private started = false;
195
+ private readonly rejectAnonymousHandlers: boolean;
196
+ private readonly controlPlane: ReminderControlPlane | undefined;
197
+ private readonly architecture: "v1" | "v2" | undefined;
104
198
 
105
199
  constructor(
106
200
  private readonly engine: SessionEngine,
107
201
  private readonly logger: (line: string) => void = () => {},
108
- private readonly platformTimers?: PlatformTimers
109
- ) {}
202
+ private readonly platformTimers?: PlatformTimers,
203
+ options: ReminderRunnerOptions = {}
204
+ ) {
205
+ this.rejectAnonymousHandlers = options.rejectAnonymousHandlers === true;
206
+ this.controlPlane = options.controlPlane;
207
+ this.architecture = options.architecture;
208
+ for (const reminder of options.namedHandlers ?? []) {
209
+ this.namedHandlers.set(reminder.name, reminder.definition);
210
+ this.namedHandlerNames.set(reminder.definition, reminder.name);
211
+ }
212
+ }
213
+
214
+ /** True when create/cancel/rekey/fire go through the CP protocol. */
215
+ private get usesControlPlaneProtocol(): boolean {
216
+ return this.controlPlane !== undefined && this.architecture === "v2";
217
+ }
110
218
 
111
219
  /**
112
220
  * Load reminder documents from the control plane into memory. Does not
@@ -136,6 +244,13 @@ export class ReminderRunner {
136
244
  if (record.status !== "armed") {
137
245
  continue;
138
246
  }
247
+ if (record.handler !== undefined) {
248
+ if (this.namedHandlers.get(record.handler) === undefined) {
249
+ await this.disarm(record, "handler_missing");
250
+ disarmedOrphan += 1;
251
+ }
252
+ continue;
253
+ }
139
254
  if (
140
255
  (record.hasRunHandler === true || record.hasUntilFn === true) &&
141
256
  this.handlers.get(record.id) === undefined
@@ -168,6 +283,16 @@ export class ReminderRunner {
168
283
  let restored = 0;
169
284
  let disarmedOrphan = 0;
170
285
  for (const record of armed) {
286
+ if (record.handler !== undefined) {
287
+ if (this.namedHandlers.get(record.handler) === undefined) {
288
+ await this.disarm(record, "handler_missing");
289
+ disarmedOrphan += 1;
290
+ continue;
291
+ }
292
+ this.armTimer(record);
293
+ restored += 1;
294
+ continue;
295
+ }
171
296
  if (
172
297
  (record.hasRunHandler === true || record.hasUntilFn === true) &&
173
298
  this.handlers.get(record.id) === undefined
@@ -249,9 +374,11 @@ export class ReminderRunner {
249
374
  async create(input: ReminderCreateInput): Promise<ReminderInfo> {
250
375
  const hasPrompt = input.prompt !== undefined;
251
376
  const hasRun = input.run !== undefined;
252
- if (hasPrompt === hasRun) {
377
+ const hasHandler = input.handler !== undefined;
378
+ const formCount = [hasPrompt, hasRun, hasHandler].filter(Boolean).length;
379
+ if (formCount !== 1) {
253
380
  throw new ReminderValidationError(
254
- "reminders.create requires exactly one of prompt or run"
381
+ "reminders.create requires exactly one of prompt, handler, or run"
255
382
  );
256
383
  }
257
384
  if (input.continuationToken.trim() === "") {
@@ -264,46 +391,136 @@ export class ReminderRunner {
264
391
  throw new ReminderValidationError("purpose is required");
265
392
  }
266
393
 
267
- const schedule = resolveSchedule(input);
268
- const busyPolicy: ReminderBusyPolicy =
269
- input.busyPolicy ?? defaultBusyPolicy(schedule.kind);
270
- const now = new Date().toISOString();
271
- const id = newReminderId();
272
-
273
394
  const untilPrompt =
274
395
  typeof input.until === "string" ? input.until : undefined;
275
396
  const untilFn = typeof input.until === "function" ? input.until : undefined;
397
+
398
+ if (hasHandler && input.until !== undefined) {
399
+ throw new ReminderValidationError(
400
+ "reminders.create: until is not valid with handler; encode the stop condition in args and the handler body"
401
+ );
402
+ }
403
+
404
+ if (this.rejectAnonymousHandlers) {
405
+ if (hasRun) {
406
+ throw new ReminderValidationError(
407
+ "reminders.create: anonymous run handlers are not durable on architecture v2; author agent/reminders/<name>.ts and pass handler + args"
408
+ );
409
+ }
410
+ if (untilFn !== undefined) {
411
+ throw new ReminderValidationError(
412
+ "reminders.create: function until is not durable on architecture v2; use a string until prompt"
413
+ );
414
+ }
415
+ }
416
+
417
+ let handlerName: string | undefined;
418
+ let handlerArgs: JsonObject | undefined;
419
+ if (hasHandler) {
420
+ const handler = input.handler!;
421
+ const name =
422
+ typeof handler === "string"
423
+ ? handler.trim()
424
+ : this.namedHandlerNames.get(handler);
425
+ if (name === undefined) {
426
+ throw new ReminderValidationError(
427
+ "Unknown reminder handler definition (expected a default export discovered under agent/reminders/)"
428
+ );
429
+ }
430
+ if (name === "") {
431
+ throw new ReminderValidationError("handler name is required");
432
+ }
433
+ if (!isValidReminderHandlerName(name)) {
434
+ throw new ReminderValidationError(
435
+ name.includes("__")
436
+ ? `Invalid reminder handler name "${name}": may not contain "__"`
437
+ : `Invalid reminder handler name "${name}": must match ${NAME_PATTERN}`
438
+ );
439
+ }
440
+ if (this.namedHandlers.get(name) === undefined) {
441
+ throw new ReminderValidationError(
442
+ `Unknown reminder handler "${name}" (expected agent/reminders/${name}.ts)`
443
+ );
444
+ }
445
+ if (input.args !== undefined && !isJsonObject(input.args)) {
446
+ throw new ReminderValidationError(
447
+ "reminders.create args must be a JSON object"
448
+ );
449
+ }
450
+ handlerName = name;
451
+ handlerArgs = input.args ?? {};
452
+ assertReminderArgsSize(handlerArgs);
453
+ } else if (input.args !== undefined) {
454
+ throw new ReminderValidationError(
455
+ "reminders.create args is only valid with handler"
456
+ );
457
+ }
458
+
459
+ const schedule = resolveSchedule(input);
460
+ const busyPolicy: ReminderBusyPolicy =
461
+ input.busyPolicy ?? defaultBusyPolicy(schedule.kind);
276
462
  const hasRunHandler = input.run !== undefined;
277
463
  const hasUntilFn = untilFn !== undefined;
278
464
 
279
- const record: ReminderRecord = {
280
- version: 1,
281
- id,
282
- purpose: input.purpose,
283
- channelId: input.channelId,
284
- continuationToken: input.continuationToken,
285
- target: input.target,
286
- scheduleKind: schedule.kind,
287
- scheduleValue: schedule.value,
288
- intervalMs: schedule.intervalMs,
289
- prompt: input.prompt,
290
- hasRunHandler,
291
- hasUntilFn,
292
- untilPrompt,
293
- busyPolicy,
294
- status: "armed",
295
- fireCount: 0,
296
- nextFireAtMs: schedule.nextFireAtMs,
297
- createdAt: now,
298
- updatedAt: now,
299
- };
465
+ let record: ReminderRecord;
466
+ if (this.usesControlPlaneProtocol) {
467
+ const intent: ReminderCreateIntent = {
468
+ protocolVersion: REMINDER_CP_PROTOCOL_VERSION,
469
+ purpose: input.purpose,
470
+ channelId: input.channelId,
471
+ continuationToken: input.continuationToken,
472
+ target: input.target,
473
+ // CP recalculates nextFireAtMs / intervalMs from the raw schedule.
474
+ schedule: scheduleSpecFromCreateInput(input),
475
+ prompt: input.prompt,
476
+ handler: handlerName,
477
+ args: handlerArgs,
478
+ hasRunHandler,
479
+ hasUntilFn,
480
+ untilPrompt,
481
+ busyPolicy,
482
+ };
483
+ record = await this.controlPlane!.createReminder(intent);
484
+ this.records.set(record.id, record);
485
+ } else {
486
+ const now = new Date().toISOString();
487
+ const id = newReminderId();
488
+ record = {
489
+ version: 1,
490
+ id,
491
+ purpose: input.purpose,
492
+ channelId: input.channelId,
493
+ continuationToken: input.continuationToken,
494
+ target: input.target,
495
+ scheduleKind: schedule.kind,
496
+ scheduleValue: schedule.value,
497
+ intervalMs: schedule.intervalMs,
498
+ prompt: input.prompt,
499
+ handler: handlerName,
500
+ args: handlerArgs,
501
+ hasRunHandler,
502
+ hasUntilFn,
503
+ untilPrompt,
504
+ busyPolicy,
505
+ status: "armed",
506
+ fireCount: 0,
507
+ nextFireAtMs: schedule.nextFireAtMs,
508
+ generation: 0,
509
+ createdAt: now,
510
+ updatedAt: now,
511
+ };
512
+ await this.persist(record);
513
+ }
300
514
 
301
- await this.persist(record);
302
- this.handlers.set(id, {
515
+ this.handlers.set(record.id, {
303
516
  run: input.run,
304
517
  untilFn,
305
518
  });
306
- if (this.started && this.platformTimers === undefined) {
519
+ if (
520
+ this.started &&
521
+ this.platformTimers === undefined &&
522
+ !this.usesControlPlaneProtocol
523
+ ) {
307
524
  this.armTimer(record);
308
525
  }
309
526
 
@@ -311,15 +528,18 @@ export class ReminderRunner {
311
528
  }
312
529
 
313
530
  async list(filter?: {
314
- status?: ReminderRecord["status"];
531
+ status?: ReminderStatus;
315
532
  purpose?: string;
316
533
  continuationToken?: string;
317
534
  }): Promise<ReminderInfo[]> {
318
535
  const all = await this.allRecords();
319
536
  return all
320
537
  .filter((r) => {
321
- if (filter?.status !== undefined && r.status !== filter.status) {
322
- return false;
538
+ if (filter?.status !== undefined) {
539
+ const publicStatus = r.status === "firing" ? "armed" : r.status;
540
+ if (publicStatus !== filter.status) {
541
+ return false;
542
+ }
323
543
  }
324
544
  if (filter?.purpose !== undefined && r.purpose !== filter.purpose) {
325
545
  return false;
@@ -348,11 +568,22 @@ export class ReminderRunner {
348
568
  id: string,
349
569
  reason = "explicit_cancel"
350
570
  ): Promise<ReminderInfo | undefined> {
571
+ if (this.usesControlPlaneProtocol) {
572
+ const updated = await this.controlPlane!.cancelReminder(id, reason);
573
+ if (updated === undefined) {
574
+ this.records.delete(id);
575
+ this.handlers.delete(id);
576
+ return undefined;
577
+ }
578
+ this.records.set(id, updated);
579
+ this.handlers.delete(id);
580
+ return toInfo(updated);
581
+ }
351
582
  const record = await this.getRecord(id);
352
583
  if (record === undefined) {
353
584
  return undefined;
354
585
  }
355
- if (record.status !== "armed") {
586
+ if (record.status !== "armed" && record.status !== "firing") {
356
587
  return toInfo(record);
357
588
  }
358
589
  this.clearTimer(id);
@@ -360,6 +591,7 @@ export class ReminderRunner {
360
591
  ...record,
361
592
  status: "cancelled",
362
593
  disarmReason: reason,
594
+ generation: (record.generation ?? 0) + 1,
363
595
  updatedAt: new Date().toISOString(),
364
596
  };
365
597
  await this.persist(updated);
@@ -394,7 +626,12 @@ export class ReminderRunner {
394
626
  oldToken: string,
395
627
  newToken: string
396
628
  ): Promise<number> {
397
- const armed = (await this.allRecords()).filter((r) => r.status === "armed");
629
+ if (this.usesControlPlaneProtocol) {
630
+ return this.controlPlane!.rekeyReminders(channelId, oldToken, newToken);
631
+ }
632
+ const armed = (await this.allRecords()).filter(
633
+ (r) => r.status === "armed" || r.status === "firing"
634
+ );
398
635
  let n = 0;
399
636
  for (const record of armed) {
400
637
  if (
@@ -406,6 +643,7 @@ export class ReminderRunner {
406
643
  await this.persist({
407
644
  ...record,
408
645
  continuationToken: newToken,
646
+ generation: (record.generation ?? 0) + 1,
409
647
  updatedAt: new Date().toISOString(),
410
648
  });
411
649
  n += 1;
@@ -441,6 +679,121 @@ export class ReminderRunner {
441
679
  return { reminderId, ...result };
442
680
  }
443
681
 
682
+ /**
683
+ * Architecture v2 / CP protocol guest entrypoint: execute an immutable fire
684
+ * descriptor and return an outcome. Does not mint ids or mutate
685
+ * authoritative status/fireCount — the control plane applies the outcome.
686
+ */
687
+ async executeFire(
688
+ descriptor: ReminderFireDescriptor
689
+ ): Promise<ReminderExecutionOutcome> {
690
+ const record = this.recordFromDescriptor(descriptor);
691
+ this.records.set(record.id, record);
692
+
693
+ const live = this.handlers.get(record.id) ?? {};
694
+ if (record.handler !== undefined) {
695
+ if (this.resolveNamedHandler(record.handler) === undefined) {
696
+ return { kind: "stop", reason: "handler_missing" };
697
+ }
698
+ } else if (
699
+ (record.hasRunHandler === true || record.hasUntilFn === true) &&
700
+ live.run === undefined &&
701
+ live.untilFn === undefined
702
+ ) {
703
+ return { kind: "stop", reason: "handler_lost_on_restart" };
704
+ }
705
+
706
+ if (live.untilFn !== undefined) {
707
+ try {
708
+ const done = await live.untilFn(this.buildUntilContext(record));
709
+ if (done) {
710
+ return { kind: "stop", reason: "until_fn" };
711
+ }
712
+ } catch (error) {
713
+ this.logger(
714
+ `[agent-sdk] reminder "${record.id}" until() failed: ${
715
+ error instanceof Error ? error.message : String(error)
716
+ }`
717
+ );
718
+ }
719
+ }
720
+
721
+ if (await this.isSessionBusy(record.channelId, record.continuationToken)) {
722
+ if (record.busyPolicy === "skipIfBusy") {
723
+ this.logger(
724
+ `[agent-sdk] reminder "${record.id}" (${record.purpose}) skipped: session busy`
725
+ );
726
+ return { kind: "skipped" };
727
+ }
728
+ await this.whenSessionIdle(record.channelId, record.continuationToken);
729
+ }
730
+
731
+ let result: ReminderFireResult;
732
+ if (record.handler !== undefined) {
733
+ const named = this.resolveNamedHandler(record.handler);
734
+ if (named === undefined) {
735
+ return { kind: "stop", reason: "handler_missing" };
736
+ }
737
+ result = await named.run(this.buildFireContext(record));
738
+ } else if (live.run !== undefined) {
739
+ result = await live.run(this.buildFireContext(record));
740
+ } else if (record.prompt !== undefined) {
741
+ result = await this.deliverPrompt(record);
742
+ } else {
743
+ return { kind: "stop", reason: "missing_handler" };
744
+ }
745
+
746
+ if (result.action === "stop") {
747
+ return { kind: "stop", reason: "run_stop" };
748
+ }
749
+ if (result.action === "skip") {
750
+ return {
751
+ kind: "skipped",
752
+ reschedule:
753
+ result.reschedule !== undefined
754
+ ? toWireReschedule(result.reschedule)
755
+ : undefined,
756
+ };
757
+ }
758
+ return {
759
+ kind: "delivered",
760
+ reschedule:
761
+ result.reschedule !== undefined
762
+ ? toWireReschedule(result.reschedule)
763
+ : undefined,
764
+ };
765
+ }
766
+
767
+ private recordFromDescriptor(
768
+ descriptor: ReminderFireDescriptor
769
+ ): ReminderRecord {
770
+ const now = new Date().toISOString();
771
+ return {
772
+ version: 1,
773
+ id: descriptor.id,
774
+ purpose: descriptor.purpose,
775
+ channelId: descriptor.channelId,
776
+ continuationToken: descriptor.continuationToken,
777
+ target: descriptor.target,
778
+ scheduleKind: descriptor.scheduleKind,
779
+ scheduleValue: descriptor.scheduleValue,
780
+ intervalMs: descriptor.intervalMs,
781
+ prompt: descriptor.prompt,
782
+ handler: descriptor.handler,
783
+ args: descriptor.args,
784
+ hasRunHandler: descriptor.hasRunHandler,
785
+ hasUntilFn: descriptor.hasUntilFn,
786
+ untilPrompt: descriptor.untilPrompt,
787
+ busyPolicy: descriptor.busyPolicy,
788
+ status: "firing",
789
+ fireCount: descriptor.fireCount,
790
+ nextFireAtMs: descriptor.nextFireAtMs,
791
+ generation: descriptor.generation,
792
+ createdAt: now,
793
+ updatedAt: now,
794
+ };
795
+ }
796
+
444
797
  private armTimer(record: ReminderRecord): void {
445
798
  this.clearTimer(record.id);
446
799
  if (!this.started || record.status !== "armed") return;
@@ -493,6 +846,10 @@ export class ReminderRunner {
493
846
  }
494
847
  }
495
848
 
849
+ private resolveNamedHandler(name: string): ReminderDefinition | undefined {
850
+ return this.namedHandlers.get(name);
851
+ }
852
+
496
853
  private async fire(
497
854
  id: string,
498
855
  opts: { force?: boolean } = {}
@@ -515,7 +872,12 @@ export class ReminderRunner {
515
872
  }
516
873
 
517
874
  const live = this.handlers.get(id) ?? {};
518
- if (
875
+ if (record.handler !== undefined) {
876
+ if (this.resolveNamedHandler(record.handler) === undefined) {
877
+ await this.disarm(record, "handler_missing");
878
+ return { outcome: "noop", reason: "handler_missing" };
879
+ }
880
+ } else if (
519
881
  (record.hasRunHandler === true || record.hasUntilFn === true) &&
520
882
  live.run === undefined &&
521
883
  live.untilFn === undefined
@@ -560,9 +922,15 @@ export class ReminderRunner {
560
922
  }
561
923
 
562
924
  let result: ReminderFireResult;
563
- if (live.run !== undefined) {
564
- const fireCtx = this.buildFireContext(record);
565
- result = await live.run(fireCtx);
925
+ if (record.handler !== undefined) {
926
+ const named = this.resolveNamedHandler(record.handler);
927
+ if (named === undefined) {
928
+ await this.disarm(record, "handler_missing");
929
+ return { outcome: "noop", reason: "handler_missing" };
930
+ }
931
+ result = await named.run(this.buildFireContext(record));
932
+ } else if (live.run !== undefined) {
933
+ result = await live.run(this.buildFireContext(record));
566
934
  } else if (record.prompt !== undefined) {
567
935
  result = await this.deliverPrompt(record);
568
936
  } else {
@@ -595,7 +963,7 @@ export class ReminderRunner {
595
963
  return { outcome: "fired", reason: "skipped" };
596
964
  }
597
965
  await this.afterFire(latest, "delivered", result.reschedule);
598
- this.logger(`[agent-sdk] reminder "${id}" (${record.purpose}) delivered`);
966
+ this.logger(`[agent-sdk] reminder "${id}" (${record.purpose}) fired`);
599
967
  return { outcome: "fired", reason: "delivered" };
600
968
  } finally {
601
969
  this.firing.delete(id);
@@ -662,7 +1030,11 @@ export class ReminderRunner {
662
1030
  record.continuationToken
663
1031
  );
664
1032
  if (auth === undefined) {
665
- await this.disarm(record, "session_gone");
1033
+ // Under CP protocol, return stop and let the CP apply/CAS; do not
1034
+ // mutate authoritative status from the guest mid-fire.
1035
+ if (!this.usesControlPlaneProtocol) {
1036
+ await this.disarm(record, "session_gone");
1037
+ }
666
1038
  return { action: "stop" };
667
1039
  }
668
1040
  return this.sendFollowup(record, buildFirePrompt(record), auth);
@@ -721,6 +1093,7 @@ export class ReminderRunner {
721
1093
  continuationToken: record.continuationToken,
722
1094
  ...(record.target ?? {}),
723
1095
  },
1096
+ args: record.args ?? {},
724
1097
  host: this.engine.host,
725
1098
  mcp: this.engine.host.mcp,
726
1099
  appAuth: APP_AUTH,