@cursor/july 0.1.114 → 0.2.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.
Files changed (206) hide show
  1. package/dist/channels/slack/channel-watch.d.ts +19 -3
  2. package/dist/channels/slack/channel-watch.d.ts.map +1 -1
  3. package/dist/channels/slack/channel-watch.js +48 -9
  4. package/dist/channels/slack/inbound.d.ts +7 -0
  5. package/dist/channels/slack/inbound.d.ts.map +1 -1
  6. package/dist/channels/slack/inbound.js +23 -0
  7. package/dist/channels/slack/slack-channel.d.ts.map +1 -1
  8. package/dist/channels/slack/slack-channel.js +87 -8
  9. package/dist/channels/slack/types.d.ts +11 -12
  10. package/dist/channels/slack/types.d.ts.map +1 -1
  11. package/dist/docs/404.html +2 -2
  12. package/dist/docs/assets/{app.BqkJwOZ-.js → app.D23Y-7Tp.js} +4 -4
  13. package/dist/docs/assets/chunks/@localSearchIndexroot.CzCCM7N8.js +1 -0
  14. package/dist/docs/assets/chunks/{VPLocalSearchBox.BJAi2KiV.js → VPLocalSearchBox.CWBeTFRZ.js} +1 -1
  15. package/dist/docs/assets/chunks/{arc.BZpXTgvV.js → arc.DSF2O3pm.js} +1 -1
  16. package/dist/docs/assets/chunks/{architectureDiagram-Q4EWVU46.WYI-7F-Y.js → architectureDiagram-Q4EWVU46.J52Wzbkg.js} +1 -1
  17. package/dist/docs/assets/chunks/{baseUniq.CZaUPpg0.js → baseUniq.CQS3LPCt.js} +1 -1
  18. package/dist/docs/assets/chunks/{blockDiagram-DXYQGD6D.D6UES2pD.js → blockDiagram-DXYQGD6D.Dw339Gr5.js} +1 -1
  19. package/dist/docs/assets/chunks/{c4Diagram-AHTNJAMY.cwebIe4i.js → c4Diagram-AHTNJAMY.BUdtOaRZ.js} +1 -1
  20. package/dist/docs/assets/chunks/channel.Bfu4df88.js +1 -0
  21. package/dist/docs/assets/chunks/{chunk-4BX2VUAB.fVyFnjxg.js → chunk-4BX2VUAB.CuOrkEqk.js} +1 -1
  22. package/dist/docs/assets/chunks/{chunk-4TB4RGXK.BanufG1c.js → chunk-4TB4RGXK.BJNBcY7U.js} +1 -1
  23. package/dist/docs/assets/chunks/{chunk-55IACEB6.VaSMz5-2.js → chunk-55IACEB6.VJK5LAm_.js} +1 -1
  24. package/dist/docs/assets/chunks/{chunk-EDXVE4YY.CN2diZOM.js → chunk-EDXVE4YY.BYYLihvj.js} +1 -1
  25. package/dist/docs/assets/chunks/{chunk-FMBD7UC4.g4ivypu3.js → chunk-FMBD7UC4.CmoW8BXP.js} +1 -1
  26. package/dist/docs/assets/chunks/{chunk-OYMX7WX6.GZXKn9JJ.js → chunk-OYMX7WX6.DTGY4C-M.js} +1 -1
  27. package/dist/docs/assets/chunks/{chunk-QZHKN3VN.itXxJZCd.js → chunk-QZHKN3VN.Cg5n67vl.js} +1 -1
  28. package/dist/docs/assets/chunks/{chunk-YZCP3GAM.-rw2GfvX.js → chunk-YZCP3GAM.C3GR_ia5.js} +1 -1
  29. package/dist/docs/assets/chunks/classDiagram-6PBFFD2Q.DdfgtaWs.js +1 -0
  30. package/dist/docs/assets/chunks/classDiagram-v2-HSJHXN6E.DdfgtaWs.js +1 -0
  31. package/dist/docs/assets/chunks/clone.rkmfti6d.js +1 -0
  32. package/dist/docs/assets/chunks/{cose-bilkent-S5V4N54A.CmaI5br0.js → cose-bilkent-S5V4N54A.BTRG8N3b.js} +1 -1
  33. package/dist/docs/assets/chunks/{dagre-KV5264BT.4wY9S4Kt.js → dagre-KV5264BT.Bob_bp_p.js} +1 -1
  34. package/dist/docs/assets/chunks/{diagram-5BDNPKRD.Pc3c0u9W.js → diagram-5BDNPKRD.ggPcs9uO.js} +1 -1
  35. package/dist/docs/assets/chunks/{diagram-G4DWMVQ6.CYrWz-nj.js → diagram-G4DWMVQ6.BP0qyJkp.js} +1 -1
  36. package/dist/docs/assets/chunks/{diagram-MMDJMWI5.Bgj5hukb.js → diagram-MMDJMWI5.B0X24UKr.js} +1 -1
  37. package/dist/docs/assets/chunks/{diagram-TYMM5635.DGMEXalS.js → diagram-TYMM5635.B4rXHFVt.js} +1 -1
  38. package/dist/docs/assets/chunks/{erDiagram-SMLLAGMA.GepTV9Im.js → erDiagram-SMLLAGMA._55Rt9oX.js} +1 -1
  39. package/dist/docs/assets/chunks/{flowDiagram-DWJPFMVM.DVKywg3j.js → flowDiagram-DWJPFMVM.DGP4XvR5.js} +1 -1
  40. package/dist/docs/assets/chunks/{ganttDiagram-T4ZO3ILL.C7qt9Mlo.js → ganttDiagram-T4ZO3ILL.BtXtkL4E.js} +1 -1
  41. package/dist/docs/assets/chunks/{gitGraphDiagram-UUTBAWPF.U30_r82P.js → gitGraphDiagram-UUTBAWPF.B9cPWblK.js} +1 -1
  42. package/dist/docs/assets/chunks/{graph.CyyMyAWv.js → graph.D8HzNexS.js} +1 -1
  43. package/dist/docs/assets/chunks/{infoDiagram-42DDH7IO.Dn9ACW3y.js → infoDiagram-42DDH7IO.Bw7CQUpi.js} +1 -1
  44. package/dist/docs/assets/chunks/{ishikawaDiagram-UXIWVN3A.DlIdIGOA.js → ishikawaDiagram-UXIWVN3A.MwkzF6nQ.js} +1 -1
  45. package/dist/docs/assets/chunks/{journeyDiagram-VCZTEJTY.DZj4vy4E.js → journeyDiagram-VCZTEJTY.DIGFF-3C.js} +1 -1
  46. package/dist/docs/assets/chunks/{kanban-definition-6JOO6SKY.Dl63eMUV.js → kanban-definition-6JOO6SKY.DhYef2BN.js} +1 -1
  47. package/dist/docs/assets/chunks/{layout.BLHZLWPH.js → layout.C0XUxuPi.js} +1 -1
  48. package/dist/docs/assets/chunks/{linear.aXKGKaNw.js → linear.BwNPpZex.js} +1 -1
  49. package/dist/docs/assets/chunks/{min.zWnFcpcc.js → min.CwAQdL7z.js} +1 -1
  50. package/dist/docs/assets/chunks/{mindmap-definition-QFDTVHPH.Qs4MQBea.js → mindmap-definition-QFDTVHPH.pWsSVLsP.js} +1 -1
  51. package/dist/docs/assets/chunks/{pieDiagram-DEJITSTG.BmPHgsk7.js → pieDiagram-DEJITSTG.BDJ3FbBy.js} +1 -1
  52. package/dist/docs/assets/chunks/{quadrantDiagram-34T5L4WZ.D5MQ3gwA.js → quadrantDiagram-34T5L4WZ.Co80izyB.js} +1 -1
  53. package/dist/docs/assets/chunks/{requirementDiagram-MS252O5E.CkdUFrO7.js → requirementDiagram-MS252O5E.JveKw4yx.js} +1 -1
  54. package/dist/docs/assets/chunks/{sankeyDiagram-XADWPNL6.KZrljrAV.js → sankeyDiagram-XADWPNL6.B0A7adPi.js} +1 -1
  55. package/dist/docs/assets/chunks/{sequenceDiagram-FGHM5R23.XMoEW-Lx.js → sequenceDiagram-FGHM5R23.d6JZ5Hre.js} +1 -1
  56. package/dist/docs/assets/chunks/{stateDiagram-FHFEXIEX.BmTzePLj.js → stateDiagram-FHFEXIEX.DWnL0NQl.js} +1 -1
  57. package/dist/docs/assets/chunks/stateDiagram-v2-QKLJ7IA2.ZEetPk0E.js +1 -0
  58. package/dist/docs/assets/chunks/{theme.BfQzpxsg.js → theme.MJTLx0hh.js} +2 -2
  59. package/dist/docs/assets/chunks/{timeline-definition-GMOUNBTQ.Dug0oamp.js → timeline-definition-GMOUNBTQ.CFS7Ai4c.js} +1 -1
  60. package/dist/docs/assets/chunks/{vennDiagram-DHZGUBPP.BOTHrEFu.js → vennDiagram-DHZGUBPP.CwSlnjCf.js} +1 -1
  61. package/dist/docs/assets/chunks/wardley-RL74JXVD.3gurI8YA.js +162 -0
  62. package/dist/docs/assets/chunks/{wardleyDiagram-NUSXRM2D.CoXKdfi6.js → wardleyDiagram-NUSXRM2D.B_8mvtjh.js} +1 -1
  63. package/dist/docs/assets/chunks/{xychartDiagram-5P7HB3ND.DXoSCjAW.js → xychartDiagram-5P7HB3ND.DtjU5H85.js} +1 -1
  64. package/dist/docs/assets/{guides_slack.md.Bo96y42E.js → guides_slack.md.Bjw2r2gL.js} +5 -5
  65. package/dist/docs/assets/{guides_slack.md.Bo96y42E.lean.js → guides_slack.md.Bjw2r2gL.lean.js} +1 -1
  66. package/dist/docs/assets/reference_cli.md.BvnQM8wd.js +97 -0
  67. package/dist/docs/assets/reference_cli.md.BvnQM8wd.lean.js +1 -0
  68. package/dist/docs/building-with-agents.html +35 -35
  69. package/dist/docs/deployment.html +35 -35
  70. package/dist/docs/evals.html +35 -35
  71. package/dist/docs/guides/agent-to-agent.html +35 -35
  72. package/dist/docs/guides/bitbucket.html +35 -35
  73. package/dist/docs/guides/cloud-agents.html +35 -35
  74. package/dist/docs/guides/convert-automation.html +35 -35
  75. package/dist/docs/guides/github.html +35 -35
  76. package/dist/docs/guides/gitlab.html +35 -35
  77. package/dist/docs/guides/grokbot-agents.html +35 -35
  78. package/dist/docs/guides/hooks.html +35 -35
  79. package/dist/docs/guides/improve.html +35 -35
  80. package/dist/docs/guides/jev.html +35 -35
  81. package/dist/docs/guides/mcp-oauth.html +35 -35
  82. package/dist/docs/guides/opentelemetry.html +35 -35
  83. package/dist/docs/guides/slack.html +39 -39
  84. package/dist/docs/guides/slack.md +16 -1
  85. package/dist/docs/guides/webhooks.html +35 -35
  86. package/dist/docs/hashmap.json +1 -1
  87. package/dist/docs/hillclimbing.html +35 -35
  88. package/dist/docs/index.html +35 -35
  89. package/dist/docs/llms-full.txt +699 -752
  90. package/dist/docs/llms.txt +1 -1
  91. package/dist/docs/quickstart.html +35 -35
  92. package/dist/docs/reference/agent-config.html +35 -35
  93. package/dist/docs/reference/artifacts.html +35 -35
  94. package/dist/docs/reference/channels.html +35 -35
  95. package/dist/docs/reference/cli.html +127 -125
  96. package/dist/docs/reference/cli.md +685 -753
  97. package/dist/docs/reference/connections.html +35 -35
  98. package/dist/docs/reference/evals.html +35 -35
  99. package/dist/docs/reference/extensions.html +35 -35
  100. package/dist/docs/reference/hooks.html +35 -35
  101. package/dist/docs/reference/http-api.html +35 -35
  102. package/dist/docs/reference/instructions.html +35 -35
  103. package/dist/docs/reference/playground.html +35 -35
  104. package/dist/docs/reference/project-layout.html +35 -35
  105. package/dist/docs/reference/prompt.html +35 -35
  106. package/dist/docs/reference/schedules.html +35 -35
  107. package/dist/docs/reference/sessions.html +35 -35
  108. package/dist/docs/reference/skills.html +35 -35
  109. package/dist/docs/reference/subagents.html +35 -35
  110. package/dist/docs/reference/tools.html +35 -35
  111. package/dist/docs/templates/agentic-owners.html +35 -35
  112. package/dist/docs/templates/pr-autofixer.html +35 -35
  113. package/dist/docs/templates/security-reviewer.html +35 -35
  114. package/dist/docs/templates/thermo-quality-review.html +35 -35
  115. package/dist/docs/templates/thermo-review.html +35 -35
  116. package/dist/docs/templates/triage.html +35 -35
  117. package/dist/docs/troubleshooting.html +35 -35
  118. package/dist/files-backends/cursor-hosted.d.ts +11 -17
  119. package/dist/files-backends/cursor-hosted.d.ts.map +1 -1
  120. package/dist/files-backends/cursor-hosted.js +13 -41
  121. package/dist/index.d.ts +1 -1
  122. package/dist/index.d.ts.map +1 -1
  123. package/dist/internal/artifacts-store.d.ts +11 -0
  124. package/dist/internal/artifacts-store.d.ts.map +1 -1
  125. package/dist/internal/artifacts-store.js +113 -18
  126. package/dist/internal/cli-deploy.d.ts.map +1 -1
  127. package/dist/internal/cli-deploy.js +9 -1
  128. package/dist/internal/cursor/cursor-api-transport.d.ts +37 -0
  129. package/dist/internal/cursor/cursor-api-transport.d.ts.map +1 -0
  130. package/dist/internal/cursor/cursor-api-transport.js +44 -0
  131. package/dist/internal/cursor/hosted-store-secrets.d.ts +21 -0
  132. package/dist/internal/cursor/hosted-store-secrets.d.ts.map +1 -1
  133. package/dist/internal/cursor/hosted-store-secrets.js +26 -0
  134. package/dist/internal/cursor/store-api-client.d.ts +82 -0
  135. package/dist/internal/cursor/store-api-client.d.ts.map +1 -0
  136. package/dist/internal/cursor/store-api-client.js +227 -0
  137. package/dist/internal/deploy-client.d.ts +6 -0
  138. package/dist/internal/deploy-client.d.ts.map +1 -1
  139. package/dist/internal/deploy-client.js +3 -0
  140. package/dist/internal/deploy-manifest.d.ts +8 -0
  141. package/dist/internal/deploy-manifest.d.ts.map +1 -1
  142. package/dist/internal/deploy-manifest.js +7 -1
  143. package/dist/internal/discovery/agent-config.d.ts +3 -1
  144. package/dist/internal/discovery/agent-config.d.ts.map +1 -1
  145. package/dist/internal/discovery/agent-config.js +7 -4
  146. package/dist/internal/framework-storage-selection.d.ts +1 -1
  147. package/dist/internal/framework-storage-selection.d.ts.map +1 -1
  148. package/dist/internal/platform-timers.d.ts.map +1 -1
  149. package/dist/internal/platform-timers.js +20 -2
  150. package/dist/internal/reminder-runner.d.ts +79 -1
  151. package/dist/internal/reminder-runner.d.ts.map +1 -1
  152. package/dist/internal/reminder-runner.js +287 -46
  153. package/dist/internal/server.d.ts.map +1 -1
  154. package/dist/internal/server.js +7 -0
  155. package/dist/internal/session-engine.d.ts.map +1 -1
  156. package/dist/internal/session-engine.js +13 -15
  157. package/dist/internal/store-api-protocol.d.ts +134 -0
  158. package/dist/internal/store-api-protocol.d.ts.map +1 -0
  159. package/dist/internal/store-api-protocol.js +126 -0
  160. package/dist/memory.d.ts +49 -10
  161. package/dist/memory.d.ts.map +1 -1
  162. package/dist/memory.js +193 -50
  163. package/dist/playground/assets/{index-CrMWlgUU.js → index-Cs0MKsv4.js} +30 -30
  164. package/dist/playground/assets/index-DLwnR9ys.css +1 -0
  165. package/dist/playground/index.html +2 -2
  166. package/dist/reminders.d.ts +1 -1
  167. package/dist/reminders.d.ts.map +1 -1
  168. package/dist/types.d.ts +44 -10
  169. package/dist/types.d.ts.map +1 -1
  170. package/docs/guides/slack.md +16 -1
  171. package/docs/reference/cli.md +686 -754
  172. package/package.json +1 -1
  173. package/skills/setup-slack/SKILL.md +1 -1
  174. package/src/channels/slack/channel-watch.ts +57 -8
  175. package/src/channels/slack/inbound.ts +24 -0
  176. package/src/channels/slack/slack-channel.ts +127 -4
  177. package/src/channels/slack/types.ts +11 -12
  178. package/src/files-backends/cursor-hosted.ts +31 -68
  179. package/src/index.ts +1 -0
  180. package/src/internal/artifacts-store.ts +131 -25
  181. package/src/internal/cli-deploy.ts +12 -1
  182. package/src/internal/cursor/cursor-api-transport.ts +73 -0
  183. package/src/internal/cursor/hosted-store-secrets.ts +35 -0
  184. package/src/internal/cursor/store-api-client.ts +360 -0
  185. package/src/internal/deploy-client.ts +9 -0
  186. package/src/internal/deploy-manifest.ts +15 -0
  187. package/src/internal/discovery/agent-config.ts +8 -6
  188. package/src/internal/framework-storage-selection.ts +1 -1
  189. package/src/internal/platform-timers.ts +24 -2
  190. package/src/internal/reminder-runner.ts +454 -66
  191. package/src/internal/server.ts +8 -0
  192. package/src/internal/session-engine.ts +17 -22
  193. package/src/internal/store-api-protocol.ts +222 -0
  194. package/src/memory.ts +240 -59
  195. package/src/reminders.ts +1 -0
  196. package/src/types.ts +47 -10
  197. package/dist/docs/assets/chunks/@localSearchIndexroot.BnSgidYE.js +0 -1
  198. package/dist/docs/assets/chunks/channel.DdM5EfNW.js +0 -1
  199. package/dist/docs/assets/chunks/classDiagram-6PBFFD2Q.CjfGHeg2.js +0 -1
  200. package/dist/docs/assets/chunks/classDiagram-v2-HSJHXN6E.CjfGHeg2.js +0 -1
  201. package/dist/docs/assets/chunks/clone.wSOICb_f.js +0 -1
  202. package/dist/docs/assets/chunks/stateDiagram-v2-QKLJ7IA2.Cu5X28zZ.js +0 -1
  203. package/dist/docs/assets/chunks/wardley-RL74JXVD.DXy2i1LS.js +0 -162
  204. package/dist/docs/assets/reference_cli.md.DLWDz9ij.js +0 -95
  205. package/dist/docs/assets/reference_cli.md.DLWDz9ij.lean.js +0 -1
  206. package/dist/playground/assets/index-C61EWMBK.css +0 -1
@@ -7,16 +7,20 @@
7
7
  import { randomBytes } from "node:crypto";
8
8
  import type {
9
9
  AuthContext,
10
+ ChannelSession,
10
11
  DiscoveredReminder,
12
+ HostOtelApi,
11
13
  JsonObject,
12
14
  JsonValue,
13
15
  ReminderCreateInput,
14
16
  ReminderDefinition,
15
17
  ReminderFireContext,
16
18
  ReminderFireResult,
19
+ ReminderFollowupOptions,
17
20
  ReminderInfo,
18
21
  ReminderStatus,
19
22
  ReminderUntilContext,
23
+ TurnOutcome,
20
24
  } from "../types.js";
21
25
  import {
22
26
  channelContinuationIdentity,
@@ -47,6 +51,7 @@ import { APP_AUTH } from "./schedule-runner.js";
47
51
  import {
48
52
  type EngineSendOptions,
49
53
  HTTP_CHANNEL_ID,
54
+ SessionAuthError,
50
55
  SessionBusyError,
51
56
  type SessionEngine,
52
57
  } from "./session-engine.js";
@@ -87,6 +92,103 @@ export interface ReminderRunnerOptions {
87
92
  controlPlane?: ReminderControlPlane;
88
93
  /** Agent architecture; used with {@link controlPlane} for v2 protocol path. */
89
94
  architecture?: "v1" | "v2";
95
+ /**
96
+ * Hosted v2 guest (`--no-control-plane`, architecture v2). A pod starts
97
+ * for one delivery and stops when it settles, so a fire usually lands on
98
+ * a pod that holds no session for the reminder's continuation, and the
99
+ * pod may stop as soon as the fire's outcome is applied. The runner then
100
+ * opens a session on the stored continuation key and waits for the turn.
101
+ */
102
+ hostedGuest?: ReminderHostedGuestOptions;
103
+ }
104
+
105
+ export interface ReminderHostedGuestOptions {
106
+ /**
107
+ * Principal for the session a prompt fire opens when the pod holds none.
108
+ * Hosted admissions open sessions with the same value, so a session the
109
+ * engine restores from durable storage keeps its principal. Return
110
+ * `undefined` when no principal applies; the fire then skips with a
111
+ * reschedule so the control plane keeps the reminder armed.
112
+ */
113
+ fireAuth: () => AuthContext | null | undefined;
114
+ }
115
+
116
+ /**
117
+ * Reschedule applied when a hosted fire cannot open a session (no
118
+ * principal). A one-shot `skipped` outcome without a reschedule completes
119
+ * the reminder, which would lose the check-in for good.
120
+ */
121
+ export const REMINDER_SESSION_OPEN_RETRY_DELAY = "5m";
122
+
123
+ /**
124
+ * `reason` tag on `agent_sdk.reminder.fire_outcome`. Prompt fires report
125
+ * how the session was reached: `session_joined` (held by this pod),
126
+ * `session_restored` (read back from durable storage), `session_opened`
127
+ * (fresh session on the stored key), `session_gone` (no session and none
128
+ * could be opened). `fire_threw` pairs with kind `error`: the fire raised
129
+ * and the hosted admission retries it.
130
+ */
131
+ export type ReminderFireOutcomeReason =
132
+ | "session_gone"
133
+ | "session_opened"
134
+ | "session_restored"
135
+ | "session_joined"
136
+ | "handler_missing"
137
+ | "handler_lost_on_restart"
138
+ | "missing_handler"
139
+ | "run_stop"
140
+ | "run_skip"
141
+ | "run_delivered"
142
+ | "until_fn"
143
+ | "busy"
144
+ | "followup_failed"
145
+ | "fire_threw";
146
+
147
+ export const REMINDER_FIRE_OUTCOME_METRIC = "agent_sdk.reminder.fire_outcome";
148
+
149
+ /** What one `executeFire` reports: the CP outcome plus the telemetry tags. */
150
+ interface FireExecution {
151
+ outcome: ReminderExecutionOutcome;
152
+ reason: ReminderFireOutcomeReason;
153
+ /** Set when the hosted guest awaited the turn before reporting. */
154
+ turnStatus?: TurnOutcome["status"];
155
+ }
156
+
157
+ interface PromptDelivery {
158
+ result: ReminderFireResult;
159
+ reason: ReminderFireOutcomeReason;
160
+ turnStatus?: TurnOutcome["status"];
161
+ }
162
+
163
+ /** Out-params `sendFollowup` fills in for the fire's telemetry. */
164
+ interface FollowupSettle {
165
+ turnStatus?: TurnOutcome["status"];
166
+ skipReason?: Extract<ReminderFireOutcomeReason, "busy" | "followup_failed">;
167
+ }
168
+
169
+ /** How a prompt fire reached (or failed to reach) its session. */
170
+ type FollowupTarget =
171
+ | {
172
+ auth: AuthContext | null;
173
+ reached: Extract<
174
+ ReminderFireOutcomeReason,
175
+ "session_joined" | "session_restored"
176
+ >;
177
+ }
178
+ | {
179
+ auth: undefined;
180
+ /**
181
+ * `session_gone`: no session anywhere. `session_unreadable`: the durable
182
+ * index has one but the local re-read missed; never open a fresh
183
+ * session over it.
184
+ */
185
+ reached: "session_gone" | "session_unreadable";
186
+ };
187
+
188
+ /** Guest-side read surface shared by v1 timed-work and the v2 CP client. */
189
+ interface ReminderReader {
190
+ getReminder(id: string): Promise<ReminderRecord | undefined>;
191
+ listReminders(): Promise<ReminderRecord[]>;
90
192
  }
91
193
 
92
194
  /**
@@ -169,6 +271,29 @@ function toInfo(record: ReminderRecord): ReminderInfo {
169
271
  };
170
272
  }
171
273
 
274
+ function stopFire(
275
+ reason: Extract<
276
+ ReminderFireOutcomeReason,
277
+ | "handler_missing"
278
+ | "handler_lost_on_restart"
279
+ | "missing_handler"
280
+ | "until_fn"
281
+ >
282
+ ): FireExecution {
283
+ return { outcome: { kind: "stop", reason }, reason };
284
+ }
285
+
286
+ function runReason(result: ReminderFireResult): ReminderFireOutcomeReason {
287
+ switch (result.action) {
288
+ case "stop":
289
+ return "run_stop";
290
+ case "skip":
291
+ return "run_skip";
292
+ case "delivered":
293
+ return "run_delivered";
294
+ }
295
+ }
296
+
172
297
  function buildFirePrompt(record: ReminderRecord): string {
173
298
  const recurring =
174
299
  record.scheduleKind === "every" || record.scheduleKind === "cron";
@@ -205,6 +330,12 @@ export class ReminderRunner {
205
330
  private readonly rejectAnonymousHandlers: boolean;
206
331
  private readonly controlPlane: ReminderControlPlane | undefined;
207
332
  private readonly architecture: "v1" | "v2" | undefined;
333
+ private readonly hostedGuest: ReminderHostedGuestOptions | undefined;
334
+ /**
335
+ * A control-plane read failure is logged once per outage, not per call.
336
+ * The next successful read re-arms the log.
337
+ */
338
+ private controlPlaneReadFailureLogged = false;
208
339
 
209
340
  constructor(
210
341
  private readonly engine: SessionEngine,
@@ -215,6 +346,7 @@ export class ReminderRunner {
215
346
  this.rejectAnonymousHandlers = options.rejectAnonymousHandlers === true;
216
347
  this.controlPlane = options.controlPlane;
217
348
  this.architecture = options.architecture;
349
+ this.hostedGuest = options.hostedGuest;
218
350
  for (const reminder of options.namedHandlers ?? []) {
219
351
  this.namedHandlers.set(reminder.name, reminder.definition);
220
352
  this.namedHandlerNames.set(reminder.definition, reminder.name);
@@ -226,29 +358,62 @@ export class ReminderRunner {
226
358
  return this.controlPlane !== undefined && this.architecture === "v2";
227
359
  }
228
360
 
361
+ /**
362
+ * Where reminder documents are read from. Under the CP protocol that is
363
+ * the v2 control-plane client; the v1 timed-work route answers 405 for a
364
+ * v2 deployment, which used to leave `list()` with only this process's
365
+ * in-memory map (breaking arm idempotence and `cancelForContinuation`).
366
+ */
367
+ private get reminderReader(): ReminderReader | undefined {
368
+ if (this.usesControlPlaneProtocol) {
369
+ return this.controlPlane;
370
+ }
371
+ return this.platformTimers;
372
+ }
373
+
374
+ private logControlPlaneReadFailure(what: string, error: unknown): void {
375
+ if (this.usesControlPlaneProtocol && this.controlPlaneReadFailureLogged) {
376
+ return;
377
+ }
378
+ this.controlPlaneReadFailureLogged = true;
379
+ this.logger(
380
+ `[agent-sdk] reminders: control-plane ${what} failed: ${
381
+ error instanceof Error ? error.message : String(error)
382
+ }`
383
+ );
384
+ }
385
+
229
386
  /**
230
387
  * Load reminder documents from the control plane into memory. Does not
231
388
  * arm in-process timers — the CP owns clocks.
232
389
  */
233
390
  async hydrate(): Promise<void> {
234
- if (this.platformTimers === undefined) {
391
+ const reader = this.reminderReader;
392
+ if (reader === undefined) {
235
393
  return;
236
394
  }
237
395
  let remote: ReminderRecord[] = [];
238
396
  try {
239
- remote = await this.platformTimers.listReminders();
397
+ remote = await reader.listReminders();
240
398
  } catch (error) {
241
- this.logger(
242
- `[agent-sdk] reminders: control-plane list failed: ${
243
- error instanceof Error ? error.message : String(error)
244
- }`
245
- );
399
+ this.logControlPlaneReadFailure("list", error);
246
400
  return;
247
401
  }
402
+ this.controlPlaneReadFailureLogged = false;
248
403
  this.records.clear();
249
404
  for (const record of remote) {
250
405
  this.records.set(record.id, record);
251
406
  }
407
+ if (this.usesControlPlaneProtocol) {
408
+ // The CP owns status; a fire whose handler is missing reports `stop`
409
+ // from executeFire instead of the guest writing a disarm.
410
+ if (remote.length > 0) {
411
+ this.logger(
412
+ `[agent-sdk] reminders: hydrated ${remote.length} reminder(s) from control plane`
413
+ );
414
+ }
415
+ return;
416
+ }
252
417
  let disarmedOrphan = 0;
253
418
  for (const record of [...this.records.values()]) {
254
419
  if (record.status !== "armed") {
@@ -330,9 +495,11 @@ export class ReminderRunner {
330
495
  }
331
496
 
332
497
  private async getRecord(id: string): Promise<ReminderRecord | undefined> {
333
- if (this.platformTimers !== undefined) {
498
+ const reader = this.reminderReader;
499
+ if (reader !== undefined) {
334
500
  try {
335
- const remote = await this.platformTimers.getReminder(id);
501
+ const remote = await reader.getReminder(id);
502
+ this.controlPlaneReadFailureLogged = false;
336
503
  if (remote === undefined) {
337
504
  this.records.delete(id);
338
505
  return undefined;
@@ -340,31 +507,33 @@ export class ReminderRunner {
340
507
  this.records.set(id, remote);
341
508
  return remote;
342
509
  } catch (error) {
343
- this.logger(
344
- `[agent-sdk] reminders: control-plane get failed for ${id}: ${
345
- error instanceof Error ? error.message : String(error)
346
- }`
347
- );
510
+ this.logControlPlaneReadFailure(`get for ${id}`, error);
348
511
  return this.records.get(id);
349
512
  }
350
513
  }
351
514
  return this.records.get(id);
352
515
  }
353
516
 
517
+ /**
518
+ * Under the CP protocol a list failure propagates: the in-memory map is
519
+ * only what this process created, and returning it would let a caller
520
+ * arm duplicates or miss rows to cancel. v1 keeps the best-effort form.
521
+ */
354
522
  private async allRecords(): Promise<ReminderRecord[]> {
355
- if (this.platformTimers !== undefined) {
523
+ const reader = this.reminderReader;
524
+ if (reader !== undefined) {
356
525
  try {
357
- const remote = await this.platformTimers.listReminders();
526
+ const remote = await reader.listReminders();
527
+ this.controlPlaneReadFailureLogged = false;
358
528
  this.records.clear();
359
529
  for (const record of remote) {
360
530
  this.records.set(record.id, record);
361
531
  }
362
532
  } catch (error) {
363
- this.logger(
364
- `[agent-sdk] reminders: control-plane list failed: ${
365
- error instanceof Error ? error.message : String(error)
366
- }`
367
- );
533
+ this.logControlPlaneReadFailure("list", error);
534
+ if (this.usesControlPlaneProtocol) {
535
+ throw error;
536
+ }
368
537
  }
369
538
  }
370
539
  return [...this.records.values()];
@@ -697,25 +866,46 @@ export class ReminderRunner {
697
866
  ): Promise<ReminderExecutionOutcome> {
698
867
  const record = this.recordFromDescriptor(descriptor);
699
868
  this.records.set(record.id, record);
869
+ let fire: FireExecution;
870
+ try {
871
+ fire = await this.runFire(record);
872
+ } catch (error) {
873
+ // The hosted admission maps the throw to a retryable failure; count
874
+ // it so the retry loop is visible beside the settled outcomes.
875
+ this.recordFireOutcome(record, {
876
+ kind: "error",
877
+ reason: "fire_threw",
878
+ detail: error instanceof Error ? error.message : String(error),
879
+ });
880
+ throw error;
881
+ }
882
+ this.recordFireOutcome(record, {
883
+ kind: fire.outcome.kind,
884
+ reason: fire.reason,
885
+ turnStatus: fire.turnStatus,
886
+ });
887
+ return fire.outcome;
888
+ }
700
889
 
890
+ private async runFire(record: ReminderRecord): Promise<FireExecution> {
701
891
  const live = this.handlers.get(record.id) ?? {};
702
892
  if (record.handler !== undefined) {
703
893
  if (this.resolveNamedHandler(record.handler) === undefined) {
704
- return { kind: "stop", reason: "handler_missing" };
894
+ return stopFire("handler_missing");
705
895
  }
706
896
  } else if (
707
897
  (record.hasRunHandler === true || record.hasUntilFn === true) &&
708
898
  live.run === undefined &&
709
899
  live.untilFn === undefined
710
900
  ) {
711
- return { kind: "stop", reason: "handler_lost_on_restart" };
901
+ return stopFire("handler_lost_on_restart");
712
902
  }
713
903
 
714
904
  if (live.untilFn !== undefined) {
715
905
  try {
716
906
  const done = await live.untilFn(this.buildUntilContext(record));
717
907
  if (done) {
718
- return { kind: "stop", reason: "until_fn" };
908
+ return stopFire("until_fn");
719
909
  }
720
910
  } catch (error) {
721
911
  this.logger(
@@ -728,50 +918,94 @@ export class ReminderRunner {
728
918
 
729
919
  if (await this.isSessionBusy(record.channelId, record.continuationToken)) {
730
920
  if (record.busyPolicy === "skipIfBusy") {
731
- this.logger(
732
- `[agent-sdk] reminder "${record.id}" (${record.purpose}) skipped: session busy`
733
- );
734
- return { kind: "skipped" };
921
+ return { outcome: { kind: "skipped" }, reason: "busy" };
735
922
  }
736
923
  await this.whenSessionIdle(record.channelId, record.continuationToken);
737
924
  }
738
925
 
739
926
  let result: ReminderFireResult;
927
+ let reason: ReminderFireOutcomeReason;
928
+ let turnStatus: TurnOutcome["status"] | undefined;
740
929
  if (record.handler !== undefined) {
741
930
  const named = this.resolveNamedHandler(record.handler);
742
931
  if (named === undefined) {
743
- return { kind: "stop", reason: "handler_missing" };
932
+ return stopFire("handler_missing");
744
933
  }
745
934
  result = await named.run(this.buildFireContext(record));
935
+ reason = runReason(result);
746
936
  } else if (live.run !== undefined) {
747
937
  result = await live.run(this.buildFireContext(record));
938
+ reason = runReason(result);
748
939
  } else if (record.prompt !== undefined) {
749
- result = await this.deliverPrompt(record);
940
+ const delivery = await this.deliverPromptFire(record);
941
+ result = delivery.result;
942
+ reason = delivery.reason;
943
+ turnStatus = delivery.turnStatus;
750
944
  } else {
751
- return { kind: "stop", reason: "missing_handler" };
945
+ return stopFire("missing_handler");
752
946
  }
753
947
 
754
948
  if (result.action === "stop") {
755
- return { kind: "stop", reason: "run_stop" };
756
- }
757
- if (result.action === "skip") {
758
- return {
759
- kind: "skipped",
760
- reschedule:
761
- result.reschedule !== undefined
762
- ? toWireReschedule(result.reschedule)
763
- : undefined,
764
- };
949
+ return { outcome: { kind: "stop", reason }, reason };
765
950
  }
951
+ const reschedule =
952
+ result.reschedule !== undefined
953
+ ? toWireReschedule(result.reschedule)
954
+ : undefined;
766
955
  return {
767
- kind: "delivered",
768
- reschedule:
769
- result.reschedule !== undefined
770
- ? toWireReschedule(result.reschedule)
771
- : undefined,
956
+ outcome: {
957
+ kind: result.action === "skip" ? "skipped" : "delivered",
958
+ reschedule,
959
+ },
960
+ reason,
961
+ turnStatus,
772
962
  };
773
963
  }
774
964
 
965
+ /**
966
+ * One structured line and one `agent_sdk.reminder.fire_outcome` count per
967
+ * fire. Emitted through the host OTel meter, the same path hosted v2
968
+ * exports turn/tool metrics on, so a fire that ends in `stop` is visible
969
+ * beside the control plane's `timed_work.fire` count.
970
+ */
971
+ private recordFireOutcome(
972
+ record: ReminderRecord,
973
+ fire: {
974
+ kind: ReminderExecutionOutcome["kind"] | "error";
975
+ reason: ReminderFireOutcomeReason;
976
+ turnStatus?: TurnOutcome["status"];
977
+ detail?: string;
978
+ }
979
+ ): void {
980
+ this.logger(
981
+ `[agent-sdk] reminder fire_outcome kind=${fire.kind} reason=${fire.reason}` +
982
+ ` reminderId=${record.id} purpose=${record.purpose}` +
983
+ ` channelId=${record.channelId} fireCount=${record.fireCount}` +
984
+ (fire.turnStatus === undefined ? "" : ` turn=${fire.turnStatus}`) +
985
+ (fire.detail === undefined
986
+ ? ""
987
+ : ` detail=${JSON.stringify(fire.detail)}`)
988
+ );
989
+ // Test doubles may omit `host.otel`; the metric is best-effort.
990
+ const otel: HostOtelApi | undefined = this.engine.host.otel;
991
+ if (otel === undefined) {
992
+ return;
993
+ }
994
+ try {
995
+ otel.increment(REMINDER_FIRE_OUTCOME_METRIC, {
996
+ kind: fire.kind,
997
+ reason: fire.reason,
998
+ purpose: record.purpose,
999
+ });
1000
+ } catch (error) {
1001
+ this.logger(
1002
+ `[agent-sdk] reminder fire_outcome metric failed: ${
1003
+ error instanceof Error ? error.message : String(error)
1004
+ }`
1005
+ );
1006
+ }
1007
+ }
1008
+
775
1009
  private recordFromDescriptor(
776
1010
  descriptor: ReminderFireDescriptor
777
1011
  ): ReminderRecord {
@@ -1033,19 +1267,121 @@ export class ReminderRunner {
1033
1267
  private async deliverPrompt(
1034
1268
  record: ReminderRecord
1035
1269
  ): Promise<ReminderFireResult> {
1270
+ return (await this.deliverPromptFire(record)).result;
1271
+ }
1272
+
1273
+ /**
1274
+ * Where a prompt fire joins. The pod's own store first; on a hosted guest
1275
+ * the durable mirror next, because the pod that armed the reminder is
1276
+ * usually gone and the session lives only in storage. A restored session
1277
+ * keeps its own principal (channel admissions may carry a sender), so the
1278
+ * fire joins under that, never under {@link ReminderHostedGuestOptions.fireAuth}.
1279
+ */
1280
+ private async resolveFollowupTarget(
1281
+ record: ReminderRecord
1282
+ ): Promise<FollowupTarget> {
1283
+ const local = await this.resolveFollowupAuth(
1284
+ record.channelId,
1285
+ record.continuationToken
1286
+ );
1287
+ if (local !== undefined) {
1288
+ return { auth: local, reached: "session_joined" };
1289
+ }
1290
+ if (this.hostedGuest === undefined) {
1291
+ return { auth: undefined, reached: "session_gone" };
1292
+ }
1293
+ // Materializes the durable record locally; a storage error propagates
1294
+ // and the hosted admission retries the fire.
1295
+ const restored = await this.engine.hasContinuationSession(
1296
+ record.channelId,
1297
+ record.continuationToken
1298
+ );
1299
+ if (!restored) {
1300
+ return { auth: undefined, reached: "session_gone" };
1301
+ }
1036
1302
  const auth = await this.resolveFollowupAuth(
1037
1303
  record.channelId,
1038
1304
  record.continuationToken
1039
1305
  );
1040
- if (auth === undefined) {
1041
- // Under CP protocol, return stop and let the CP apply/CAS; do not
1042
- // mutate authoritative status from the guest mid-fire.
1043
- if (!this.usesControlPlaneProtocol) {
1044
- await this.disarm(record, "session_gone");
1306
+ return auth === undefined
1307
+ ? { auth: undefined, reached: "session_unreadable" }
1308
+ : { auth, reached: "session_restored" };
1309
+ }
1310
+
1311
+ private async deliverPromptFire(
1312
+ record: ReminderRecord
1313
+ ): Promise<PromptDelivery> {
1314
+ const target = await this.resolveFollowupTarget(record);
1315
+ if (target.auth !== undefined) {
1316
+ return this.promptDelivery(record, target.auth, target.reached);
1317
+ }
1318
+ if (target.reached === "session_unreadable") {
1319
+ this.logger(
1320
+ `[agent-sdk] reminder "${record.id}" (${record.purpose}) durable session did not materialize locally; not opening a new one`
1321
+ );
1322
+ return { result: { action: "stop" }, reason: "session_gone" };
1323
+ }
1324
+ if (
1325
+ this.hostedGuest !== undefined &&
1326
+ this.channelUsesControlPlaneContinuation(record.channelId)
1327
+ ) {
1328
+ // No session on this pod and none in durable storage. Open one on the
1329
+ // stored key the way an inbound event on a fresh key does, instead of
1330
+ // consuming the fire with no turn.
1331
+ const fireAuth = this.hostedGuest.fireAuth();
1332
+ if (fireAuth === undefined) {
1333
+ return {
1334
+ result: {
1335
+ action: "skip",
1336
+ reschedule: { delay: REMINDER_SESSION_OPEN_RETRY_DELAY },
1337
+ },
1338
+ reason: "session_gone",
1339
+ };
1340
+ }
1341
+ try {
1342
+ return await this.promptDelivery(record, fireAuth, "session_opened");
1343
+ } catch (error) {
1344
+ if (!(error instanceof SessionAuthError)) {
1345
+ throw error;
1346
+ }
1347
+ // A session landed on this key under another principal between the
1348
+ // lookup and the send. That does not change on retry, so stop
1349
+ // rather than let the control plane re-admit until its attempt cap.
1350
+ this.logger(
1351
+ `[agent-sdk] reminder "${record.id}" (${record.purpose}) cannot join session on its continuation: ${error.message}`
1352
+ );
1353
+ return { result: { action: "stop" }, reason: "session_gone" };
1045
1354
  }
1046
- return { action: "stop" };
1047
1355
  }
1048
- return this.sendFollowup(record, buildFirePrompt(record), auth);
1356
+ // Under CP protocol, return stop and let the CP apply/CAS; do not
1357
+ // mutate authoritative status from the guest mid-fire.
1358
+ if (!this.usesControlPlaneProtocol) {
1359
+ await this.disarm(record, "session_gone");
1360
+ }
1361
+ return { result: { action: "stop" }, reason: "session_gone" };
1362
+ }
1363
+
1364
+ private async promptDelivery(
1365
+ record: ReminderRecord,
1366
+ auth: AuthContext | null,
1367
+ delivered: Extract<
1368
+ ReminderFireOutcomeReason,
1369
+ "session_joined" | "session_restored" | "session_opened"
1370
+ >
1371
+ ): Promise<PromptDelivery> {
1372
+ const settle: FollowupSettle = {};
1373
+ const result = await this.sendFollowup(
1374
+ record,
1375
+ buildFirePrompt(record),
1376
+ auth,
1377
+ settle
1378
+ );
1379
+ return {
1380
+ result,
1381
+ reason:
1382
+ result.action === "skip" ? (settle.skipReason ?? "busy") : delivered,
1383
+ turnStatus: settle.turnStatus,
1384
+ };
1049
1385
  }
1050
1386
 
1051
1387
  /**
@@ -1086,23 +1422,56 @@ export class ReminderRunner {
1086
1422
  return continuationSource(identity) === "control-plane";
1087
1423
  }
1088
1424
 
1425
+ /**
1426
+ * Local and v1 track the turn in the background: the process outlives the
1427
+ * fire. A hosted v2 guest waits for the turn and its durable writes first,
1428
+ * because the control plane may stop the pod as soon as the fire's
1429
+ * outcome is applied, and `delivered` must mean the turn ran.
1430
+ */
1431
+ private async settleFollowup(
1432
+ session: ChannelSession,
1433
+ settle: FollowupSettle | undefined
1434
+ ): Promise<void> {
1435
+ const completion = this.engine.waitForCompletion(session.id);
1436
+ if (this.hostedGuest === undefined) {
1437
+ this.engine.trackBackground(completion);
1438
+ return;
1439
+ }
1440
+ const outcome = await completion;
1441
+ await this.engine.drainSessionDurability(session.id);
1442
+ if (settle !== undefined) {
1443
+ settle.turnStatus = outcome.status;
1444
+ }
1445
+ }
1446
+
1089
1447
  private async sendFollowup(
1090
1448
  record: ReminderRecord,
1091
1449
  message: string,
1092
- auth: AuthContext | null
1450
+ auth: AuthContext | null,
1451
+ settle?: FollowupSettle,
1452
+ extra?: ReminderFollowupOptions
1093
1453
  ): Promise<ReminderFireResult> {
1094
- const sendOptions = this.followupSendOptions(record, auth);
1454
+ const sendOptions: EngineSendOptions = {
1455
+ ...this.followupSendOptions(record, auth),
1456
+ state: extra?.state,
1457
+ refreshState: extra?.refreshState,
1458
+ title: extra?.title,
1459
+ clearCloudOverride: extra?.clearCloudOverride,
1460
+ };
1095
1461
  try {
1096
1462
  const session = await this.engine.send(
1097
1463
  record.channelId,
1098
1464
  message,
1099
1465
  sendOptions
1100
1466
  );
1101
- this.engine.trackBackground(this.engine.waitForCompletion(session.id));
1467
+ await this.settleFollowup(session, settle);
1102
1468
  return { action: "delivered" };
1103
1469
  } catch (error) {
1104
1470
  if (error instanceof SessionBusyError) {
1105
1471
  if (record.busyPolicy === "skipIfBusy") {
1472
+ if (settle !== undefined) {
1473
+ settle.skipReason = "busy";
1474
+ }
1106
1475
  return { action: "skip" };
1107
1476
  }
1108
1477
  await this.whenSessionIdle(record.channelId, record.continuationToken);
@@ -1112,9 +1481,7 @@ export class ReminderRunner {
1112
1481
  message,
1113
1482
  sendOptions
1114
1483
  );
1115
- this.engine.trackBackground(
1116
- this.engine.waitForCompletion(session.id)
1117
- );
1484
+ await this.settleFollowup(session, settle);
1118
1485
  return { action: "delivered" };
1119
1486
  } catch (retryError) {
1120
1487
  this.logger(
@@ -1124,6 +1491,9 @@ export class ReminderRunner {
1124
1491
  : String(retryError)
1125
1492
  }`
1126
1493
  );
1494
+ if (settle !== undefined) {
1495
+ settle.skipReason = "followup_failed";
1496
+ }
1127
1497
  return { action: "skip" };
1128
1498
  }
1129
1499
  }
@@ -1147,18 +1517,36 @@ export class ReminderRunner {
1147
1517
  mcp: this.engine.host.mcp,
1148
1518
  appAuth: APP_AUTH,
1149
1519
  artifacts: this.engine.artifacts,
1150
- async followup(args: { message: string }) {
1151
- const auth = await self.resolveFollowupAuth(
1152
- record.channelId,
1153
- record.continuationToken
1154
- );
1520
+ async followup(args: { message: string } & ReminderFollowupOptions) {
1521
+ const target = await self.resolveFollowupTarget(record);
1522
+ let auth = target.auth;
1523
+ // `openAuth` opens only when nothing exists anywhere: an unreadable
1524
+ // durable session must not be shadowed by a second one on the same
1525
+ // key (prompt-fire parity).
1526
+ if (
1527
+ auth === undefined &&
1528
+ target.reached === "session_gone" &&
1529
+ args.openAuth !== undefined
1530
+ ) {
1531
+ auth = args.openAuth;
1532
+ }
1155
1533
  if (auth === undefined) {
1156
- await self.disarm(record, "session_gone");
1534
+ // Under the CP protocol the guest never writes reminder status;
1535
+ // the fire's `stop` outcome carries the reason instead.
1536
+ if (!self.usesControlPlaneProtocol) {
1537
+ await self.disarm(record, "session_gone");
1538
+ }
1157
1539
  throw new Error(
1158
1540
  `Reminder "${record.id}" followup: session gone for continuation`
1159
1541
  );
1160
1542
  }
1161
- const result = await self.sendFollowup(record, args.message, auth);
1543
+ const result = await self.sendFollowup(
1544
+ record,
1545
+ args.message,
1546
+ auth,
1547
+ undefined,
1548
+ args
1549
+ );
1162
1550
  if (result.action === "skip") {
1163
1551
  throw new Error(
1164
1552
  `Reminder "${record.id}" followup skipped: session busy`