@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
@@ -3,6 +3,12 @@
3
3
  * and reminder documents + clocks, then fires into the kit the same way the
4
4
  * Cursor backend does over HTTP when `--no-control-plane` is set.
5
5
  *
6
+ * Reminder lifecycle (protocol v1): CP assigns id, arms timers, claims a due
7
+ * fire once, invokes a guest executor with an immutable descriptor, and
8
+ * applies the outcome with generation/fireCount CAS. Cancel/rekey racing an
9
+ * in-flight execution bumps generation so a stale outcome cannot resurrect
10
+ * the reminder. Legacy `fireReminder(id, fireCount)` remains for v1 runners.
11
+ *
6
12
  * v2 ingest lives here: `routeContinuation` stamps the mailbox (July twin
7
13
  * of hosted `routeVerifiedIngressContinuation`), and `attachScmEvents`
8
14
  * owns the `/v0/scm-events` long poll. v1 keeps engine-side scm-events.
@@ -17,10 +23,26 @@ import {
17
23
  routeLocalIngressContinuation,
18
24
  } from "./local-ingress.js";
19
25
  import type { PlatformTimers } from "./platform-timers.js";
26
+ import {
27
+ applyOutcomeToRecord,
28
+ claimFromDescriptor,
29
+ intentToNewRecord,
30
+ REMINDER_CP_PROTOCOL_VERSION,
31
+ type ReminderControlPlane,
32
+ type ReminderCreateIntent,
33
+ type ReminderExecutionOutcome,
34
+ type ReminderFireClaim,
35
+ type ReminderGuestExecutor,
36
+ type ReminderOutcomeApplyResult,
37
+ recordGeneration,
38
+ toFireDescriptor,
39
+ } from "./reminder-control-plane-protocol.js";
20
40
  import { MAX_TIMER_MS } from "./reminder-schedule.js";
21
41
  import { type ReminderRecord, ReminderStore } from "./reminder-store.js";
22
42
 
23
- export interface LocalControlPlane extends PlatformTimers {
43
+ export interface LocalControlPlane
44
+ extends PlatformTimers,
45
+ ReminderControlPlane {
24
46
  /**
25
47
  * Stamp a v2 mailbox from the channel selector + live payload. Same
26
48
  * contract as hosted verified ingress. `/v1` is a no-op.
@@ -40,7 +62,16 @@ export interface LocalControlPlane extends PlatformTimers {
40
62
 
41
63
  export async function createLocalControlPlane(opts: {
42
64
  fireSchedule: (name: string) => Promise<unknown>;
43
- fireReminder: (id: string, fireCount: number) => Promise<unknown>;
65
+ /**
66
+ * Legacy v1 path: CP calls this with (id, fireCount) and the runner mutates
67
+ * authoritative state. Prefer {@link executeReminder} for architecture v2.
68
+ */
69
+ fireReminder?: (id: string, fireCount: number) => Promise<unknown>;
70
+ /**
71
+ * Protocol guest executor. When set, due timers claim once, invoke this
72
+ * with an immutable descriptor, and apply the returned outcome via CAS.
73
+ */
74
+ executeReminder?: ReminderGuestExecutor;
44
75
  /**
45
76
  * Persist reminder documents under `<stateDir>/reminders`. Local serve
46
77
  * passes `<stateRoot>/control-plane`.
@@ -50,6 +81,12 @@ export async function createLocalControlPlane(opts: {
50
81
  armReminderClocks?: boolean;
51
82
  logger?: (line: string) => void;
52
83
  }): Promise<LocalControlPlane> {
84
+ if (opts.fireReminder === undefined && opts.executeReminder === undefined) {
85
+ throw new Error(
86
+ "createLocalControlPlane requires fireReminder and/or executeReminder"
87
+ );
88
+ }
89
+
53
90
  const logger = opts.logger ?? (() => {});
54
91
  const armReminderClocks = opts.armReminderClocks !== false;
55
92
  const store =
@@ -59,12 +96,22 @@ export async function createLocalControlPlane(opts: {
59
96
  const schedules = new Map<string, CronExpression>();
60
97
  const reminderTimers = new Map<string, NodeJS.Timeout>();
61
98
  const reminders = new Map<string, ReminderRecord>(
62
- ((await store?.list()) ?? []).map((record) => [record.id, record])
99
+ ((await store?.list()) ?? []).map((record) => [
100
+ record.id,
101
+ { ...record, generation: record.generation ?? 0 },
102
+ ])
63
103
  );
104
+ /** In-flight claims keyed by reminder id (single-flight per id). */
105
+ const inflight = new Map<string, ReminderFireClaim>();
64
106
  let scheduleTimer: NodeJS.Timeout | undefined;
65
107
  let lastFiredMinute = -1;
66
108
  let scmRelay: { start(): void; stop(): Promise<void> } | undefined;
67
109
 
110
+ async function persist(record: ReminderRecord): Promise<void> {
111
+ reminders.set(record.id, record);
112
+ await store?.save(record);
113
+ }
114
+
68
115
  function armScheduleTicker(): void {
69
116
  if (scheduleTimer !== undefined || schedules.size === 0) {
70
117
  return;
@@ -115,23 +162,18 @@ export async function createLocalControlPlane(opts: {
115
162
 
116
163
  function armReminder(record: ReminderRecord): void {
117
164
  clearReminderTimer(record.id);
165
+ if (record.status !== "armed") {
166
+ return;
167
+ }
118
168
  const delay = Math.min(
119
169
  MAX_TIMER_MS,
120
170
  Math.max(0, record.nextFireAtMs - Date.now())
121
171
  );
122
172
  const timer = setTimeout(() => {
123
173
  reminderTimers.delete(record.id);
124
- const current = reminders.get(record.id);
125
- if (current === undefined || current.status !== "armed") {
126
- return;
127
- }
128
- if (current.nextFireAtMs > Date.now() + 50) {
129
- armReminder(current);
130
- return;
131
- }
132
- void opts.fireReminder(current.id, current.fireCount).catch((error) => {
174
+ void onReminderDue(record.id).catch((error) => {
133
175
  logger(
134
- `[agent-sdk] reminder "${current.id}" dispatch failed: ${
176
+ `[agent-sdk] reminder "${record.id}" dispatch failed: ${
135
177
  error instanceof Error ? error.message : String(error)
136
178
  }`
137
179
  );
@@ -141,58 +183,275 @@ export async function createLocalControlPlane(opts: {
141
183
  reminderTimers.set(record.id, timer);
142
184
  }
143
185
 
186
+ async function claimDue(id: string): Promise<ReminderRecord | undefined> {
187
+ const current = reminders.get(id);
188
+ if (current === undefined || current.status !== "armed") {
189
+ return undefined;
190
+ }
191
+ if (current.nextFireAtMs > Date.now() + 50) {
192
+ armReminder(current);
193
+ return undefined;
194
+ }
195
+ if (inflight.has(id)) {
196
+ return undefined;
197
+ }
198
+ const claimed: ReminderRecord = {
199
+ ...current,
200
+ status: "firing",
201
+ generation: recordGeneration(current),
202
+ updatedAt: new Date().toISOString(),
203
+ };
204
+ await persist(claimed);
205
+ inflight.set(id, {
206
+ id: claimed.id,
207
+ fireCount: claimed.fireCount,
208
+ generation: recordGeneration(claimed),
209
+ });
210
+ return claimed;
211
+ }
212
+
213
+ async function applyReminderOutcome(
214
+ claim: ReminderFireClaim,
215
+ outcome: ReminderExecutionOutcome
216
+ ): Promise<ReminderOutcomeApplyResult> {
217
+ const current = reminders.get(claim.id);
218
+ if (current === undefined) {
219
+ inflight.delete(claim.id);
220
+ return { applied: false, reason: "missing" };
221
+ }
222
+ const result = applyOutcomeToRecord(current, claim, outcome);
223
+ inflight.delete(claim.id);
224
+ if (!result.applied) {
225
+ if (current.status === "armed" && armReminderClocks) {
226
+ armReminder(current);
227
+ }
228
+ return result;
229
+ }
230
+ await persist(result.record);
231
+ if (armReminderClocks && result.record.status === "armed") {
232
+ armReminder(result.record);
233
+ } else {
234
+ clearReminderTimer(result.record.id);
235
+ }
236
+ return result;
237
+ }
238
+
239
+ async function onReminderDue(id: string): Promise<void> {
240
+ // Protocol path: claim → guest execute → CAS apply.
241
+ if (opts.executeReminder !== undefined) {
242
+ const claimed = await claimDue(id);
243
+ if (claimed === undefined) {
244
+ return;
245
+ }
246
+ const descriptor = toFireDescriptor(claimed, Date.now());
247
+ let outcome: ReminderExecutionOutcome;
248
+ try {
249
+ outcome = await opts.executeReminder(descriptor);
250
+ } catch (error) {
251
+ logger(
252
+ `[agent-sdk] reminder "${id}" guest execution failed: ${
253
+ error instanceof Error ? error.message : String(error)
254
+ }`
255
+ );
256
+ outcome = {
257
+ kind: "retry",
258
+ delayMs: 1_000,
259
+ reason: "guest_error",
260
+ };
261
+ }
262
+ const result = await applyReminderOutcome(
263
+ claimFromDescriptor(descriptor),
264
+ outcome
265
+ );
266
+ if (!result.applied) {
267
+ logger(
268
+ `[agent-sdk] reminder "${id}" outcome not applied: ${result.reason}`
269
+ );
270
+ }
271
+ return;
272
+ }
273
+
274
+ // Legacy v1: do not claim/firing — runner mutates via putReminder after fire.
275
+ const current = reminders.get(id);
276
+ if (current === undefined || current.status !== "armed") {
277
+ return;
278
+ }
279
+ if (current.nextFireAtMs > Date.now() + 50) {
280
+ armReminder(current);
281
+ return;
282
+ }
283
+ try {
284
+ await opts.fireReminder!(current.id, current.fireCount);
285
+ } catch (error) {
286
+ logger(
287
+ `[agent-sdk] reminder "${id}" dispatch failed: ${
288
+ error instanceof Error ? error.message : String(error)
289
+ }`
290
+ );
291
+ }
292
+ }
293
+
144
294
  return {
295
+ protocolVersion: REMINDER_CP_PROTOCOL_VERSION,
296
+
145
297
  routeContinuation: routeLocalIngressContinuation,
298
+
146
299
  attachScmEvents(relay) {
147
300
  scmRelay = relay;
148
301
  relay.start();
149
302
  logger("[agent-sdk] cursor-events: control-plane ingest started");
150
303
  },
304
+
151
305
  async upsertSchedule(input) {
152
306
  schedules.set(input.name, parseCron(input.cron));
153
307
  armScheduleTicker();
154
308
  },
309
+
155
310
  async deleteSchedule(name) {
156
311
  schedules.delete(name);
157
312
  if (schedules.size === 0) {
158
313
  stopScheduleTicker();
159
314
  }
160
315
  },
316
+
161
317
  async putReminder(record) {
162
- reminders.set(record.id, record);
163
- await store?.save(record);
164
- if (armReminderClocks && record.status === "armed") {
165
- armReminder(record);
318
+ const normalized: ReminderRecord = {
319
+ ...record,
320
+ generation: record.generation ?? 0,
321
+ };
322
+ await persist(normalized);
323
+ if (armReminderClocks && normalized.status === "armed") {
324
+ armReminder(normalized);
166
325
  return;
167
326
  }
168
- clearReminderTimer(record.id);
327
+ clearReminderTimer(normalized.id);
328
+ if (normalized.status !== "firing") {
329
+ inflight.delete(normalized.id);
330
+ }
169
331
  },
332
+
170
333
  async getReminder(id) {
171
334
  return reminders.get(id);
172
335
  },
336
+
173
337
  async listReminders() {
174
338
  return [...reminders.values()];
175
339
  },
340
+
176
341
  async deleteReminder(id) {
177
342
  clearReminderTimer(id);
343
+ inflight.delete(id);
178
344
  reminders.delete(id);
179
345
  await store?.delete(id);
180
346
  },
181
- startClocks() {
182
- if (!armReminderClocks) {
183
- return;
347
+
348
+ async createReminder(intent: ReminderCreateIntent) {
349
+ if (intent.protocolVersion !== REMINDER_CP_PROTOCOL_VERSION) {
350
+ throw new Error(
351
+ `Unsupported reminder CP protocol version ${String(intent.protocolVersion)}`
352
+ );
353
+ }
354
+ const record = intentToNewRecord(intent);
355
+ await persist(record);
356
+ if (armReminderClocks && record.status === "armed") {
357
+ armReminder(record);
358
+ }
359
+ return record;
360
+ },
361
+
362
+ async cancelReminder(id, reason = "explicit_cancel") {
363
+ const current = reminders.get(id);
364
+ if (current === undefined) {
365
+ return undefined;
366
+ }
367
+ if (
368
+ current.status === "completed" ||
369
+ current.status === "cancelled" ||
370
+ current.status === "disarmed"
371
+ ) {
372
+ return current;
373
+ }
374
+ clearReminderTimer(id);
375
+ inflight.delete(id);
376
+ const updated: ReminderRecord = {
377
+ ...current,
378
+ status: "cancelled",
379
+ disarmReason: reason,
380
+ generation: recordGeneration(current) + 1,
381
+ updatedAt: new Date().toISOString(),
382
+ };
383
+ await persist(updated);
384
+ return updated;
385
+ },
386
+
387
+ async rekeyReminders(channelId, oldToken, newToken) {
388
+ let n = 0;
389
+ for (const record of [...reminders.values()]) {
390
+ if (
391
+ record.channelId !== channelId ||
392
+ record.continuationToken !== oldToken
393
+ ) {
394
+ continue;
395
+ }
396
+ if (record.status !== "armed" && record.status !== "firing") {
397
+ continue;
398
+ }
399
+ const wasInflight = inflight.has(record.id);
400
+ inflight.delete(record.id);
401
+ let updated: ReminderRecord = {
402
+ ...record,
403
+ continuationToken: newToken,
404
+ status: "armed",
405
+ generation: recordGeneration(record) + 1,
406
+ updatedAt: new Date().toISOString(),
407
+ };
408
+ // Interrupted in-flight fire: retry the same fireCount soon.
409
+ if (wasInflight) {
410
+ updated = {
411
+ ...updated,
412
+ nextFireAtMs: Math.min(updated.nextFireAtMs, Date.now()),
413
+ };
414
+ }
415
+ await persist(updated);
416
+ if (armReminderClocks) {
417
+ armReminder(updated);
418
+ }
419
+ n += 1;
184
420
  }
421
+ return n;
422
+ },
423
+
424
+ applyReminderOutcome,
425
+
426
+ startClocks() {
185
427
  for (const record of reminders.values()) {
186
- if (record.status === "armed") {
428
+ if (record.status === "firing") {
429
+ // Restart hydration: treat interrupted fires as armed for retry
430
+ // even when clocks are not armed (status must still recover).
431
+ const restored: ReminderRecord = {
432
+ ...record,
433
+ status: "armed",
434
+ updatedAt: new Date().toISOString(),
435
+ };
436
+ void persist(restored).then(() => {
437
+ if (armReminderClocks) {
438
+ armReminder(restored);
439
+ }
440
+ });
441
+ continue;
442
+ }
443
+ if (armReminderClocks && record.status === "armed") {
187
444
  armReminder(record);
188
445
  }
189
446
  }
190
447
  },
448
+
191
449
  stop() {
192
450
  stopScheduleTicker();
193
451
  for (const id of [...reminderTimers.keys()]) {
194
452
  clearReminderTimer(id);
195
453
  }
454
+ inflight.clear();
196
455
  if (scmRelay !== undefined) {
197
456
  void scmRelay.stop();
198
457
  scmRelay = undefined;