@cursor/july 0.2.0 → 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 (172) 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/slack-channel.d.ts.map +1 -1
  5. package/dist/channels/slack/slack-channel.js +4 -4
  6. package/dist/channels/slack/types.d.ts +11 -12
  7. package/dist/channels/slack/types.d.ts.map +1 -1
  8. package/dist/docs/404.html +2 -2
  9. package/dist/docs/assets/{app.9eAtsjAM.js → app.D23Y-7Tp.js} +4 -4
  10. package/dist/docs/assets/chunks/@localSearchIndexroot.CzCCM7N8.js +1 -0
  11. package/dist/docs/assets/chunks/{VPLocalSearchBox.C9s69nxj.js → VPLocalSearchBox.CWBeTFRZ.js} +1 -1
  12. package/dist/docs/assets/chunks/{arc.DmWDRaF-.js → arc.DSF2O3pm.js} +1 -1
  13. package/dist/docs/assets/chunks/{architectureDiagram-Q4EWVU46.BE1Wj4f3.js → architectureDiagram-Q4EWVU46.J52Wzbkg.js} +1 -1
  14. package/dist/docs/assets/chunks/{baseUniq.pmEZGnWu.js → baseUniq.CQS3LPCt.js} +1 -1
  15. package/dist/docs/assets/chunks/{blockDiagram-DXYQGD6D.D6mM-5XN.js → blockDiagram-DXYQGD6D.Dw339Gr5.js} +1 -1
  16. package/dist/docs/assets/chunks/{c4Diagram-AHTNJAMY.ZQYTPC1b.js → c4Diagram-AHTNJAMY.BUdtOaRZ.js} +1 -1
  17. package/dist/docs/assets/chunks/channel.Bfu4df88.js +1 -0
  18. package/dist/docs/assets/chunks/{chunk-4BX2VUAB.CKJ7gaK8.js → chunk-4BX2VUAB.CuOrkEqk.js} +1 -1
  19. package/dist/docs/assets/chunks/{chunk-4TB4RGXK.CL61HnvB.js → chunk-4TB4RGXK.BJNBcY7U.js} +1 -1
  20. package/dist/docs/assets/chunks/{chunk-55IACEB6.DsA4XA1m.js → chunk-55IACEB6.VJK5LAm_.js} +1 -1
  21. package/dist/docs/assets/chunks/{chunk-EDXVE4YY.BODHCHFW.js → chunk-EDXVE4YY.BYYLihvj.js} +1 -1
  22. package/dist/docs/assets/chunks/{chunk-FMBD7UC4.D6NBaZTa.js → chunk-FMBD7UC4.CmoW8BXP.js} +1 -1
  23. package/dist/docs/assets/chunks/{chunk-OYMX7WX6.BLPuqFXg.js → chunk-OYMX7WX6.DTGY4C-M.js} +1 -1
  24. package/dist/docs/assets/chunks/{chunk-QZHKN3VN.BFs_ML8Z.js → chunk-QZHKN3VN.Cg5n67vl.js} +1 -1
  25. package/dist/docs/assets/chunks/{chunk-YZCP3GAM.BDjuFyZv.js → chunk-YZCP3GAM.C3GR_ia5.js} +1 -1
  26. package/dist/docs/assets/chunks/classDiagram-6PBFFD2Q.DdfgtaWs.js +1 -0
  27. package/dist/docs/assets/chunks/classDiagram-v2-HSJHXN6E.DdfgtaWs.js +1 -0
  28. package/dist/docs/assets/chunks/clone.rkmfti6d.js +1 -0
  29. package/dist/docs/assets/chunks/{cose-bilkent-S5V4N54A.D85HNEsi.js → cose-bilkent-S5V4N54A.BTRG8N3b.js} +1 -1
  30. package/dist/docs/assets/chunks/{dagre-KV5264BT.V463KlSF.js → dagre-KV5264BT.Bob_bp_p.js} +1 -1
  31. package/dist/docs/assets/chunks/{diagram-5BDNPKRD.DhBDu1ab.js → diagram-5BDNPKRD.ggPcs9uO.js} +1 -1
  32. package/dist/docs/assets/chunks/{diagram-G4DWMVQ6.v6sC68zl.js → diagram-G4DWMVQ6.BP0qyJkp.js} +1 -1
  33. package/dist/docs/assets/chunks/{diagram-MMDJMWI5.DhHsdXYv.js → diagram-MMDJMWI5.B0X24UKr.js} +1 -1
  34. package/dist/docs/assets/chunks/{diagram-TYMM5635.BPWNSFZc.js → diagram-TYMM5635.B4rXHFVt.js} +1 -1
  35. package/dist/docs/assets/chunks/{erDiagram-SMLLAGMA.Cq0wTMPL.js → erDiagram-SMLLAGMA._55Rt9oX.js} +1 -1
  36. package/dist/docs/assets/chunks/{flowDiagram-DWJPFMVM.MveMtucC.js → flowDiagram-DWJPFMVM.DGP4XvR5.js} +1 -1
  37. package/dist/docs/assets/chunks/{ganttDiagram-T4ZO3ILL.BzKfGUSf.js → ganttDiagram-T4ZO3ILL.BtXtkL4E.js} +1 -1
  38. package/dist/docs/assets/chunks/{gitGraphDiagram-UUTBAWPF.OToXTSW_.js → gitGraphDiagram-UUTBAWPF.B9cPWblK.js} +1 -1
  39. package/dist/docs/assets/chunks/{graph.D82tam-l.js → graph.D8HzNexS.js} +1 -1
  40. package/dist/docs/assets/chunks/{infoDiagram-42DDH7IO.DzFlRmcE.js → infoDiagram-42DDH7IO.Bw7CQUpi.js} +1 -1
  41. package/dist/docs/assets/chunks/{ishikawaDiagram-UXIWVN3A.CVPRJiKe.js → ishikawaDiagram-UXIWVN3A.MwkzF6nQ.js} +1 -1
  42. package/dist/docs/assets/chunks/{journeyDiagram-VCZTEJTY.CQvNTfQC.js → journeyDiagram-VCZTEJTY.DIGFF-3C.js} +1 -1
  43. package/dist/docs/assets/chunks/{kanban-definition-6JOO6SKY.BwywywUl.js → kanban-definition-6JOO6SKY.DhYef2BN.js} +1 -1
  44. package/dist/docs/assets/chunks/{layout.C4BkPPba.js → layout.C0XUxuPi.js} +1 -1
  45. package/dist/docs/assets/chunks/{linear.FoSfGKD4.js → linear.BwNPpZex.js} +1 -1
  46. package/dist/docs/assets/chunks/{min.yLh8jqfl.js → min.CwAQdL7z.js} +1 -1
  47. package/dist/docs/assets/chunks/{mindmap-definition-QFDTVHPH.C9Dq2_CN.js → mindmap-definition-QFDTVHPH.pWsSVLsP.js} +1 -1
  48. package/dist/docs/assets/chunks/{pieDiagram-DEJITSTG.CiGyBcES.js → pieDiagram-DEJITSTG.BDJ3FbBy.js} +1 -1
  49. package/dist/docs/assets/chunks/{quadrantDiagram-34T5L4WZ.1FWea9nj.js → quadrantDiagram-34T5L4WZ.Co80izyB.js} +1 -1
  50. package/dist/docs/assets/chunks/{requirementDiagram-MS252O5E.B1q7ntiV.js → requirementDiagram-MS252O5E.JveKw4yx.js} +1 -1
  51. package/dist/docs/assets/chunks/{sankeyDiagram-XADWPNL6.Cpr4oU_k.js → sankeyDiagram-XADWPNL6.B0A7adPi.js} +1 -1
  52. package/dist/docs/assets/chunks/{sequenceDiagram-FGHM5R23.CUK68STn.js → sequenceDiagram-FGHM5R23.d6JZ5Hre.js} +1 -1
  53. package/dist/docs/assets/chunks/{stateDiagram-FHFEXIEX.zU0yL2m4.js → stateDiagram-FHFEXIEX.DWnL0NQl.js} +1 -1
  54. package/dist/docs/assets/chunks/stateDiagram-v2-QKLJ7IA2.ZEetPk0E.js +1 -0
  55. package/dist/docs/assets/chunks/{theme.DhnKd0CD.js → theme.MJTLx0hh.js} +2 -2
  56. package/dist/docs/assets/chunks/{timeline-definition-GMOUNBTQ.CNYoXo3F.js → timeline-definition-GMOUNBTQ.CFS7Ai4c.js} +1 -1
  57. package/dist/docs/assets/chunks/{vennDiagram-DHZGUBPP.DFWH3G9s.js → vennDiagram-DHZGUBPP.CwSlnjCf.js} +1 -1
  58. package/dist/docs/assets/chunks/wardley-RL74JXVD.3gurI8YA.js +162 -0
  59. package/dist/docs/assets/chunks/{wardleyDiagram-NUSXRM2D.7WLkqM7s.js → wardleyDiagram-NUSXRM2D.B_8mvtjh.js} +1 -1
  60. package/dist/docs/assets/chunks/{xychartDiagram-5P7HB3ND.-SyUNgWw.js → xychartDiagram-5P7HB3ND.DtjU5H85.js} +1 -1
  61. package/dist/docs/assets/{guides_slack.md.DVjNyqq5.js → guides_slack.md.Bjw2r2gL.js} +4 -4
  62. package/dist/docs/assets/{guides_slack.md.DVjNyqq5.lean.js → guides_slack.md.Bjw2r2gL.lean.js} +1 -1
  63. package/dist/docs/building-with-agents.html +35 -35
  64. package/dist/docs/deployment.html +35 -35
  65. package/dist/docs/evals.html +35 -35
  66. package/dist/docs/guides/agent-to-agent.html +35 -35
  67. package/dist/docs/guides/bitbucket.html +35 -35
  68. package/dist/docs/guides/cloud-agents.html +35 -35
  69. package/dist/docs/guides/convert-automation.html +35 -35
  70. package/dist/docs/guides/github.html +35 -35
  71. package/dist/docs/guides/gitlab.html +35 -35
  72. package/dist/docs/guides/grokbot-agents.html +35 -35
  73. package/dist/docs/guides/hooks.html +35 -35
  74. package/dist/docs/guides/improve.html +35 -35
  75. package/dist/docs/guides/jev.html +35 -35
  76. package/dist/docs/guides/mcp-oauth.html +35 -35
  77. package/dist/docs/guides/opentelemetry.html +35 -35
  78. package/dist/docs/guides/slack.html +39 -39
  79. package/dist/docs/guides/slack.md +15 -7
  80. package/dist/docs/guides/webhooks.html +35 -35
  81. package/dist/docs/hashmap.json +1 -1
  82. package/dist/docs/hillclimbing.html +35 -35
  83. package/dist/docs/index.html +35 -35
  84. package/dist/docs/llms-full.txt +15 -7
  85. package/dist/docs/quickstart.html +35 -35
  86. package/dist/docs/reference/agent-config.html +35 -35
  87. package/dist/docs/reference/artifacts.html +35 -35
  88. package/dist/docs/reference/channels.html +35 -35
  89. package/dist/docs/reference/cli.html +35 -35
  90. package/dist/docs/reference/connections.html +35 -35
  91. package/dist/docs/reference/evals.html +35 -35
  92. package/dist/docs/reference/extensions.html +35 -35
  93. package/dist/docs/reference/hooks.html +35 -35
  94. package/dist/docs/reference/http-api.html +35 -35
  95. package/dist/docs/reference/instructions.html +35 -35
  96. package/dist/docs/reference/playground.html +35 -35
  97. package/dist/docs/reference/project-layout.html +35 -35
  98. package/dist/docs/reference/prompt.html +35 -35
  99. package/dist/docs/reference/schedules.html +35 -35
  100. package/dist/docs/reference/sessions.html +35 -35
  101. package/dist/docs/reference/skills.html +35 -35
  102. package/dist/docs/reference/subagents.html +35 -35
  103. package/dist/docs/reference/tools.html +35 -35
  104. package/dist/docs/templates/agentic-owners.html +35 -35
  105. package/dist/docs/templates/pr-autofixer.html +35 -35
  106. package/dist/docs/templates/security-reviewer.html +35 -35
  107. package/dist/docs/templates/thermo-quality-review.html +35 -35
  108. package/dist/docs/templates/thermo-review.html +35 -35
  109. package/dist/docs/templates/triage.html +35 -35
  110. package/dist/docs/troubleshooting.html +35 -35
  111. package/dist/files-backends/cursor-hosted.d.ts +11 -17
  112. package/dist/files-backends/cursor-hosted.d.ts.map +1 -1
  113. package/dist/files-backends/cursor-hosted.js +13 -41
  114. package/dist/index.d.ts +1 -1
  115. package/dist/index.d.ts.map +1 -1
  116. package/dist/internal/artifacts-store.d.ts +11 -0
  117. package/dist/internal/artifacts-store.d.ts.map +1 -1
  118. package/dist/internal/artifacts-store.js +113 -18
  119. package/dist/internal/cursor/cursor-api-transport.d.ts +37 -0
  120. package/dist/internal/cursor/cursor-api-transport.d.ts.map +1 -0
  121. package/dist/internal/cursor/cursor-api-transport.js +44 -0
  122. package/dist/internal/cursor/hosted-store-secrets.d.ts +21 -0
  123. package/dist/internal/cursor/hosted-store-secrets.d.ts.map +1 -1
  124. package/dist/internal/cursor/hosted-store-secrets.js +26 -0
  125. package/dist/internal/cursor/store-api-client.d.ts +82 -0
  126. package/dist/internal/cursor/store-api-client.d.ts.map +1 -0
  127. package/dist/internal/cursor/store-api-client.js +227 -0
  128. package/dist/internal/deploy-manifest.d.ts +8 -0
  129. package/dist/internal/deploy-manifest.d.ts.map +1 -1
  130. package/dist/internal/deploy-manifest.js +7 -1
  131. package/dist/internal/reminder-runner.d.ts.map +1 -1
  132. package/dist/internal/reminder-runner.js +13 -4
  133. package/dist/internal/session-engine.d.ts.map +1 -1
  134. package/dist/internal/session-engine.js +13 -15
  135. package/dist/internal/store-api-protocol.d.ts +134 -0
  136. package/dist/internal/store-api-protocol.d.ts.map +1 -0
  137. package/dist/internal/store-api-protocol.js +126 -0
  138. package/dist/memory.d.ts +49 -14
  139. package/dist/memory.d.ts.map +1 -1
  140. package/dist/memory.js +193 -74
  141. package/dist/playground/assets/{index-CX6oTKwz.js → index-Cs0MKsv4.js} +30 -30
  142. package/dist/playground/index.html +1 -1
  143. package/dist/reminders.d.ts +1 -1
  144. package/dist/reminders.d.ts.map +1 -1
  145. package/dist/types.d.ts +28 -1
  146. package/dist/types.d.ts.map +1 -1
  147. package/docs/guides/slack.md +15 -7
  148. package/package.json +1 -1
  149. package/skills/setup-slack/SKILL.md +1 -1
  150. package/src/channels/slack/channel-watch.ts +57 -8
  151. package/src/channels/slack/slack-channel.ts +4 -3
  152. package/src/channels/slack/types.ts +11 -12
  153. package/src/files-backends/cursor-hosted.ts +31 -68
  154. package/src/index.ts +1 -0
  155. package/src/internal/artifacts-store.ts +131 -25
  156. package/src/internal/cursor/cursor-api-transport.ts +73 -0
  157. package/src/internal/cursor/hosted-store-secrets.ts +35 -0
  158. package/src/internal/cursor/store-api-client.ts +360 -0
  159. package/src/internal/deploy-manifest.ts +15 -0
  160. package/src/internal/reminder-runner.ts +30 -5
  161. package/src/internal/session-engine.ts +17 -22
  162. package/src/internal/store-api-protocol.ts +222 -0
  163. package/src/memory.ts +240 -93
  164. package/src/reminders.ts +1 -0
  165. package/src/types.ts +31 -1
  166. package/dist/docs/assets/chunks/@localSearchIndexroot.DjNBgxTF.js +0 -1
  167. package/dist/docs/assets/chunks/channel.BNF8VK-B.js +0 -1
  168. package/dist/docs/assets/chunks/classDiagram-6PBFFD2Q.iViHzfrK.js +0 -1
  169. package/dist/docs/assets/chunks/classDiagram-v2-HSJHXN6E.iViHzfrK.js +0 -1
  170. package/dist/docs/assets/chunks/clone.CJoR2BBM.js +0 -1
  171. package/dist/docs/assets/chunks/stateDiagram-v2-QKLJ7IA2.nV8Nl-td.js +0 -1
  172. package/dist/docs/assets/chunks/wardley-RL74JXVD.Do4PwzeN.js +0 -162
@@ -0,0 +1,360 @@
1
+ /**
2
+ * The store API client: July's one gateway to the factory domain-call lane,
3
+ * `POST /internal/agentsdk/store/api/<call>`. This module owns the whole
4
+ * transport — base URL and deployment credential (the same resolution the
5
+ * files sink uses, via `cursor-api-transport.ts`), envelope encoding,
6
+ * response validation, typed error mapping, and the bounded backoff policy.
7
+ * Feature modules (`memory.ts`, later `session-run-log.ts`, …) call typed
8
+ * methods like {@link StoreApiClient.memoryAppend} and never hand-roll
9
+ * fetch; future domain calls (`sessionRun.report`, `startLog.flush`, …) are
10
+ * added here as new methods on this one client.
11
+ *
12
+ * The lane is selected per pod by the task-bound store protocol pin:
13
+ * {@link isStoreApiLaneEnabled} is true only under the exact
14
+ * `factory-api-v1` pin AND with a provisioned deployment store. The
15
+ * availability check is not redundant: the pin file is written by the hosted
16
+ * task bind, but a local-dev env var could name the pin without any store to
17
+ * serve reads — appending into the void. Both resolve lazily per call, so a
18
+ * pin bound after serve start takes effect without a restart.
19
+ */
20
+
21
+ import {
22
+ MEMORY_APPEND_MAX_RECORD_BYTES,
23
+ memoryAppendResponseSchema,
24
+ memoryReadResponseSchema,
25
+ STORE_API_MEMORY_APPEND_CALL,
26
+ STORE_API_MEMORY_READ_CALL,
27
+ STORE_API_PATH,
28
+ STORE_API_PROTOCOL_VERSION,
29
+ serializedRecordBytes,
30
+ } from "../store-api-protocol.js";
31
+ import {
32
+ type CursorApiTransportOptions,
33
+ resolveCursorApiBaseUrl,
34
+ resolveCursorApiFetch,
35
+ resolveCursorApiKey,
36
+ } from "./cursor-api-transport.js";
37
+ import {
38
+ resolveDeploymentStoreSourceIdSync,
39
+ resolveStoreProtocolSync,
40
+ storeProtocolEnablesStoreApi,
41
+ } from "./hosted-store-secrets.js";
42
+
43
+ export interface StoreApiClientOptions extends CursorApiTransportOptions {
44
+ /** Deployment store source id override (lane availability check). */
45
+ storeSourceId?: string;
46
+ /** Lazy store source id, preferred over {@link storeSourceId}. */
47
+ resolveStoreSourceId?: () => string | undefined;
48
+ /** Lazy store protocol pin (default: env, then the task-bound file). */
49
+ resolveStoreProtocol?: () => string | undefined;
50
+ /** Backoff sleep override (tests). */
51
+ sleep?: (ms: number) => Promise<void>;
52
+ /** Jitter source override (tests). Returns [0, 1). */
53
+ random?: () => number;
54
+ }
55
+
56
+ /**
57
+ * How one domain call failed, after the client's bounded retries:
58
+ *
59
+ * - `rejected` — the server (or the client's own pre-check) refused the
60
+ * payload with a typed 4xx. Deterministic; never retried.
61
+ * - `shed` — 429 from the per-deployment limiter or kill switch. Retried
62
+ * only within the small Retry-After bound below; retrying harder against
63
+ * a shed is exactly the stampede the domain lane exists to prevent.
64
+ * - `unavailable` — 5xx or a network failure, retried up to the attempt
65
+ * bound.
66
+ * - `bad_response` — a 2xx whose body failed the response schema; a
67
+ * contract break, not a transient, so never retried.
68
+ */
69
+ export type StoreApiFailureKind =
70
+ | "rejected"
71
+ | "shed"
72
+ | "unavailable"
73
+ | "bad_response";
74
+
75
+ export class StoreApiCallError extends Error {
76
+ constructor(
77
+ readonly kind: StoreApiFailureKind,
78
+ message: string,
79
+ readonly status?: number
80
+ ) {
81
+ super(message);
82
+ this.name = "StoreApiCallError";
83
+ }
84
+ }
85
+
86
+ /**
87
+ * Bounded backoff policy. Domain calls run at turn boundaries (a memory
88
+ * append after `turn.completed`), so the budget is a few short retries, not
89
+ * persistence: 3 attempts total and never more than 2s asleep between them.
90
+ * A 429 whose Retry-After exceeds the cap is a deliberate shed (a kill
91
+ * switch, a hard budget) — give up immediately instead of camping on it.
92
+ */
93
+ const STORE_API_MAX_ATTEMPTS = 3;
94
+ const STORE_API_MAX_BACKOFF_MS = 2_000;
95
+
96
+ /**
97
+ * Hard deadline per fetch (the envelope POSTs and the presigned journal
98
+ * GET). Without one, a stalled — not failed — connection rides undici's
99
+ * ~300 s inactivity timers, and hydration runs at the head of the session
100
+ * dispatch chain where a stall delays every queued handler. 15 s matches
101
+ * the server's own presigned-fetch deadline and is generous for a ≤5 MiB
102
+ * journal.
103
+ */
104
+ const STORE_API_FETCH_TIMEOUT_MS = 15_000;
105
+
106
+ function backoffMs(attempt: number, random: () => number): number {
107
+ return Math.min(
108
+ 200 * 2 ** attempt * (0.5 + random()),
109
+ STORE_API_MAX_BACKOFF_MS
110
+ );
111
+ }
112
+
113
+ /**
114
+ * Is the domain-call lane on for this pod? Exact pin match plus a
115
+ * provisioned deployment store; anything else keeps the client-side
116
+ * patterns, fail-safe in both directions like the flush pin itself.
117
+ */
118
+ export function isStoreApiLaneEnabled(
119
+ options: StoreApiClientOptions = {}
120
+ ): boolean {
121
+ const protocol =
122
+ options.resolveStoreProtocol?.() ?? resolveStoreProtocolSync();
123
+ if (!storeProtocolEnablesStoreApi(protocol)) {
124
+ return false;
125
+ }
126
+ const sourceId = (
127
+ options.resolveStoreSourceId?.() ??
128
+ options.storeSourceId ??
129
+ resolveDeploymentStoreSourceIdSync()
130
+ )?.trim();
131
+ return sourceId !== undefined && sourceId !== "";
132
+ }
133
+
134
+ export type StoreApiClient = {
135
+ /**
136
+ * One `memory.append` domain call: the server appends the record to a
137
+ * server-owned journal shard. Resolves on success; throws
138
+ * {@link StoreApiCallError} after the bounded retries. The caller owns the
139
+ * failure policy (memory drops the record — appending is advisory).
140
+ */
141
+ memoryAppend(payload: {
142
+ agent: string;
143
+ record: Record<string, unknown>;
144
+ }): Promise<void>;
145
+ /**
146
+ * One `memory.read` domain call plus the presigned byte fetch: the server
147
+ * flushes the agent's pending append buffer (best-effort) and presigns
148
+ * the canonical journal; this method then downloads it. Undefined while
149
+ * the agent has no journal. Throws {@link StoreApiCallError} after the
150
+ * bounded retries — the caller owns the failure policy (hydration keeps
151
+ * the existing mirror).
152
+ */
153
+ memoryReadContent(payload: {
154
+ agent: string;
155
+ }): Promise<Uint8Array | undefined>;
156
+ };
157
+
158
+ export function createStoreApiClient(
159
+ options: StoreApiClientOptions = {}
160
+ ): StoreApiClient {
161
+ const sleep =
162
+ options.sleep ??
163
+ ((ms: number) => new Promise<void>(resolve => setTimeout(resolve, ms)));
164
+ const random = options.random ?? Math.random;
165
+
166
+ /** One POST of a versioned envelope; returns the parsed 2xx body. */
167
+ const postOnce = async (
168
+ call: string,
169
+ envelope: Record<string, unknown>
170
+ ): Promise<Response> => {
171
+ const fetchImpl = resolveCursorApiFetch(options);
172
+ return await fetchImpl(
173
+ `${resolveCursorApiBaseUrl(options)}${STORE_API_PATH}/${call}`,
174
+ {
175
+ method: "POST",
176
+ headers: {
177
+ Authorization: `Bearer ${resolveCursorApiKey(options)}`,
178
+ "Content-Type": "application/json",
179
+ },
180
+ body: JSON.stringify(envelope),
181
+ signal: AbortSignal.timeout(STORE_API_FETCH_TIMEOUT_MS),
182
+ }
183
+ );
184
+ };
185
+
186
+ /** The response's `Retry-After` in ms, when present and numeric. */
187
+ const retryAfterDelayMs = (response: Response): number | undefined => {
188
+ const header = response.headers.get("retry-after")?.trim();
189
+ const seconds =
190
+ header === undefined || header === "" ? Number.NaN : Number(header);
191
+ return Number.isFinite(seconds) ? seconds * 1_000 : undefined;
192
+ };
193
+
194
+ const describeErrorBody = async (response: Response): Promise<string> => {
195
+ try {
196
+ const parsed = (await response.json()) as { error?: unknown };
197
+ if (typeof parsed?.error === "string") {
198
+ return parsed.error;
199
+ }
200
+ } catch {
201
+ // Non-JSON error body; the status is the message.
202
+ }
203
+ return `HTTP ${response.status}`;
204
+ };
205
+
206
+ const callApi = async (
207
+ call: string,
208
+ envelope: Record<string, unknown>
209
+ ): Promise<unknown> => {
210
+ let lastFailure: StoreApiCallError | undefined;
211
+ // A shed's Retry-After (when within the cap) overrides the next jittered
212
+ // backoff; cleared before every attempt.
213
+ let nextDelayMs: number | undefined;
214
+ for (let attempt = 0; attempt < STORE_API_MAX_ATTEMPTS; attempt += 1) {
215
+ if (attempt > 0) {
216
+ await sleep(nextDelayMs ?? backoffMs(attempt, random));
217
+ }
218
+ nextDelayMs = undefined;
219
+ let response: Response;
220
+ try {
221
+ response = await postOnce(call, envelope);
222
+ } catch (error) {
223
+ lastFailure = new StoreApiCallError(
224
+ "unavailable",
225
+ `store api ${call} transport failure: ${
226
+ error instanceof Error ? error.message : String(error)
227
+ }`
228
+ );
229
+ continue;
230
+ }
231
+ if (response.ok) {
232
+ try {
233
+ return (await response.json()) as unknown;
234
+ } catch {
235
+ throw new StoreApiCallError(
236
+ "bad_response",
237
+ `store api ${call} answered non-JSON (${response.status})`,
238
+ response.status
239
+ );
240
+ }
241
+ }
242
+ if (response.status === 429) {
243
+ const retryDelayMs = retryAfterDelayMs(response);
244
+ const shed = new StoreApiCallError(
245
+ "shed",
246
+ `store api ${call} shed: ${await describeErrorBody(response)}`,
247
+ response.status
248
+ );
249
+ if (
250
+ retryDelayMs !== undefined &&
251
+ retryDelayMs > STORE_API_MAX_BACKOFF_MS
252
+ ) {
253
+ // A long Retry-After is a kill switch or hard budget: obey it by
254
+ // giving up now, not by camping on the route.
255
+ throw shed;
256
+ }
257
+ nextDelayMs = retryDelayMs;
258
+ lastFailure = shed;
259
+ continue;
260
+ }
261
+ if (response.status >= 500) {
262
+ // A degraded 503 carries `Retry-After` too (the server's buffer-down
263
+ // answer). Honor it clamped to the backoff cap — unlike a shed, a
264
+ // 5xx is transient by definition, so a long hint never means "give
265
+ // up", just "wait the cap".
266
+ const retryDelayMs = retryAfterDelayMs(response);
267
+ if (retryDelayMs !== undefined) {
268
+ nextDelayMs = Math.min(retryDelayMs, STORE_API_MAX_BACKOFF_MS);
269
+ }
270
+ lastFailure = new StoreApiCallError(
271
+ "unavailable",
272
+ `store api ${call} failed: ${await describeErrorBody(response)}`,
273
+ response.status
274
+ );
275
+ continue;
276
+ }
277
+ // Any other 4xx is deterministic; retrying re-sends the same refusal.
278
+ throw new StoreApiCallError(
279
+ "rejected",
280
+ `store api ${call} rejected: ${await describeErrorBody(response)}`,
281
+ response.status
282
+ );
283
+ }
284
+ throw lastFailure ?? new StoreApiCallError("unavailable", "no attempt ran");
285
+ };
286
+
287
+ return {
288
+ async memoryAppend(payload) {
289
+ const recordBytes = serializedRecordBytes(payload.record);
290
+ if (recordBytes > MEMORY_APPEND_MAX_RECORD_BYTES) {
291
+ // The server-side controller enforces the same cap; checking here
292
+ // spares a doomed request and fails with the same typed shape.
293
+ throw new StoreApiCallError(
294
+ "rejected",
295
+ `memory.append record is ${recordBytes} serialized bytes; the cap is ${MEMORY_APPEND_MAX_RECORD_BYTES}`
296
+ );
297
+ }
298
+ const body = await callApi(STORE_API_MEMORY_APPEND_CALL, {
299
+ v: STORE_API_PROTOCOL_VERSION,
300
+ call: STORE_API_MEMORY_APPEND_CALL,
301
+ payload,
302
+ });
303
+ if (!memoryAppendResponseSchema.safeParse(body).success) {
304
+ throw new StoreApiCallError(
305
+ "bad_response",
306
+ "unexpected memory.append response shape"
307
+ );
308
+ }
309
+ },
310
+
311
+ async memoryReadContent(payload) {
312
+ const body = await callApi(STORE_API_MEMORY_READ_CALL, {
313
+ v: STORE_API_PROTOCOL_VERSION,
314
+ call: STORE_API_MEMORY_READ_CALL,
315
+ payload,
316
+ });
317
+ const parsed = memoryReadResponseSchema.safeParse(body);
318
+ if (!parsed.success) {
319
+ throw new StoreApiCallError(
320
+ "bad_response",
321
+ "unexpected memory.read response shape"
322
+ );
323
+ }
324
+ if (parsed.data.journal === null) {
325
+ return undefined;
326
+ }
327
+ // Bulk bytes ride the presigned URL, never the control plane — same
328
+ // split as the verb lane. Unretried: the whole call reruns at the
329
+ // next session start, and hydration keeps the existing mirror.
330
+ const fetchImpl = resolveCursorApiFetch(options);
331
+ let response: Response;
332
+ try {
333
+ response = await fetchImpl(parsed.data.journal.url, {
334
+ method: "GET",
335
+ redirect: "manual",
336
+ signal: AbortSignal.timeout(STORE_API_FETCH_TIMEOUT_MS),
337
+ });
338
+ } catch (error) {
339
+ throw new StoreApiCallError(
340
+ "unavailable",
341
+ `memory.read journal fetch transport failure: ${
342
+ error instanceof Error ? error.message : String(error)
343
+ }`
344
+ );
345
+ }
346
+ if (response.status === 404) {
347
+ // The presign raced a rotation or delete; absent is absent.
348
+ return undefined;
349
+ }
350
+ if (!response.ok) {
351
+ throw new StoreApiCallError(
352
+ "unavailable",
353
+ `memory.read journal fetch failed (HTTP ${response.status})`,
354
+ response.status
355
+ );
356
+ }
357
+ return new Uint8Array(await response.arrayBuffer());
358
+ },
359
+ };
360
+ }
@@ -128,6 +128,10 @@ const agentDeployManifestV1Schema = z.object({
128
128
  continuation: continuationIdentitySchema,
129
129
  catch: catchDeclarationSchema.optional(),
130
130
  slackReply: slackReplyDeclarationSchema.optional(),
131
+ slackChannelPosts: z
132
+ .array(z.string().min(1).max(80))
133
+ .max(64)
134
+ .optional(),
131
135
  })
132
136
  )
133
137
  .max(64),
@@ -166,6 +170,14 @@ export interface AgentDeployManifestDeclarations {
166
170
  * hosted runtime starts. Present only for `slack` channels in agentic mode.
167
171
  */
168
172
  slackReply?: AgentDeployManifestSlackReplyDeclaration;
173
+ /**
174
+ * The channel-watch allowlist (`engagement.channelPosts.allow`).
175
+ * Present only for `slack` channels that watch channel posts. The
176
+ * control plane materializes channel-scoped subscription edges from
177
+ * the channel-id entries so posts outside the allowlist drop at
178
+ * ingress.
179
+ */
180
+ slackChannelPosts?: string[];
169
181
  /**
170
182
  * Control-plane catch routes: present only for an authored channel
171
183
  * whose auth chain carries a `sharedSecretAuth` / `hmacSignatureAuth`
@@ -242,6 +254,9 @@ export function buildAgentDeployManifest(
242
254
  ...(kind === "slack" && meta?.slackReply !== undefined
243
255
  ? { slackReply: meta.slackReply }
244
256
  : {}),
257
+ ...(kind === "slack" && meta?.slackChannelPosts !== undefined
258
+ ? { slackChannelPosts: [...meta.slackChannelPosts] }
259
+ : {}),
245
260
  };
246
261
  });
247
262
 
@@ -16,6 +16,7 @@ import type {
16
16
  ReminderDefinition,
17
17
  ReminderFireContext,
18
18
  ReminderFireResult,
19
+ ReminderFollowupOptions,
19
20
  ReminderInfo,
20
21
  ReminderStatus,
21
22
  ReminderUntilContext,
@@ -1447,9 +1448,16 @@ export class ReminderRunner {
1447
1448
  record: ReminderRecord,
1448
1449
  message: string,
1449
1450
  auth: AuthContext | null,
1450
- settle?: FollowupSettle
1451
+ settle?: FollowupSettle,
1452
+ extra?: ReminderFollowupOptions
1451
1453
  ): Promise<ReminderFireResult> {
1452
- 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
+ };
1453
1461
  try {
1454
1462
  const session = await this.engine.send(
1455
1463
  record.channelId,
@@ -1509,8 +1517,19 @@ export class ReminderRunner {
1509
1517
  mcp: this.engine.host.mcp,
1510
1518
  appAuth: APP_AUTH,
1511
1519
  artifacts: this.engine.artifacts,
1512
- async followup(args: { message: string }) {
1513
- const { auth } = await self.resolveFollowupTarget(record);
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
+ }
1514
1533
  if (auth === undefined) {
1515
1534
  // Under the CP protocol the guest never writes reminder status;
1516
1535
  // the fire's `stop` outcome carries the reason instead.
@@ -1521,7 +1540,13 @@ export class ReminderRunner {
1521
1540
  `Reminder "${record.id}" followup: session gone for continuation`
1522
1541
  );
1523
1542
  }
1524
- 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
+ );
1525
1550
  if (result.action === "skip") {
1526
1551
  throw new Error(
1527
1552
  `Reminder "${record.id}" followup skipped: session busy`
@@ -1105,17 +1105,23 @@ export class SessionEngine {
1105
1105
  sendOptions.continuationToken
1106
1106
  );
1107
1107
  }
1108
- if (
1109
- existing === undefined &&
1110
- sendOptions.hostedSessionId !== undefined &&
1111
- sendOptions.mailboxContinuation?.key ===
1112
- hostedSessionResumeKey(sendOptions.hostedSessionId)
1113
- ) {
1114
- // Explicit control-plane resume of an existing session: the CP named
1115
- // the session by its own id and stamped the matching resume key. The
1116
- // session keeps its original continuation key (a PR lane must stay
1117
- // addressable by its webhooks); this is a follow-up turn by id.
1118
- existing = await this.getOrRestoreSession(sendOptions.hostedSessionId);
1108
+ if (existing === undefined && sendOptions.hostedSessionId !== undefined) {
1109
+ const byId = await this.getOrRestoreSession(sendOptions.hostedSessionId);
1110
+ if (byId !== undefined) {
1111
+ // evt: skips the mailbox index; same hostedSessionId is a Temporal
1112
+ // retry. cont:session:<id> is the explicit CP resume (keep the
1113
+ // original key so a PR lane stays addressable by its webhooks).
1114
+ if (
1115
+ byId.continuationKey !== sendOptions.continuationToken &&
1116
+ sendOptions.mailboxContinuation?.key !==
1117
+ hostedSessionResumeKey(sendOptions.hostedSessionId)
1118
+ ) {
1119
+ throw new V2EngineError(
1120
+ `v2: hosted session id ${sendOptions.hostedSessionId} already belongs to another mailbox`
1121
+ );
1122
+ }
1123
+ existing = byId;
1124
+ }
1119
1125
  }
1120
1126
 
1121
1127
  if (existing !== undefined) {
@@ -1223,17 +1229,6 @@ export class SessionEngine {
1223
1229
  throw new HostedEnqueueNotActiveError(sendOptions.hostedSessionId);
1224
1230
  }
1225
1231
 
1226
- if (sendOptions.hostedSessionId !== undefined) {
1227
- const collision = await this.getOrRestoreSession(
1228
- sendOptions.hostedSessionId
1229
- );
1230
- if (collision !== undefined) {
1231
- throw new V2EngineError(
1232
- `v2: hosted session id ${sendOptions.hostedSessionId} already belongs to another mailbox`
1233
- );
1234
- }
1235
- }
1236
-
1237
1232
  const now = new Date().toISOString();
1238
1233
  const channelState = this.initialChannelState(channelId, sendOptions.state);
1239
1234
  const purpose = sendOptions.purpose === "eval" ? "eval" : "live";