@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,222 @@
1
+ /**
2
+ * Domain-call wire contract for the factory store API:
3
+ * `POST /internal/agentsdk/store/api/<call>` carrying a versioned JSON
4
+ * envelope `{ v, call, payload }`. This is the lane where the **server** owns
5
+ * the storage pattern (journal sharding, rotation, compaction), so a
6
+ * pathological pattern is fixable in a backend deploy instead of a July
7
+ * release plus a rebake of every deployment. The verb lane (`/flush`,
8
+ * `/read`, `/list`) keeps carrying bulk presigned file IO.
9
+ *
10
+ * Deliberately duplicated on the backend
11
+ * (`backend/server/src/factory/agent-sdk/agentsdkStoreApiProtocol.ts`): this
12
+ * package is published to npm, so the backend cannot depend on it and it
13
+ * cannot depend on backend packages. A backend drift test compares the two
14
+ * sources — same shape as the hosted-delivery protocol twins — which is why
15
+ * every exported constant here stays a single `export const NAME = <literal>;`
16
+ * line and every schema field stays on its own line.
17
+ *
18
+ * Evolution rules (the compat contract): additive optional fields only; no
19
+ * renames, no retypes, no semantic reuse of an old field; unknown fields are
20
+ * ignored, never rejected; removing a field means deprecating it in place
21
+ * forever. A breaking change means a new `v`, and the server keeps serving
22
+ * every previous `v`.
23
+ */
24
+
25
+ import { z } from "zod";
26
+
27
+ export const STORE_API_PROTOCOL_VERSION = 1;
28
+
29
+ /** Path family on the factory surface; one segment per call. */
30
+ export const STORE_API_PATH = "/internal/agentsdk/store/api";
31
+
32
+ export const STORE_API_MEMORY_APPEND_CALL = "memory.append";
33
+
34
+ /**
35
+ * Semantic cap on one serialized memory record. Text fields are truncated
36
+ * client-side (`memoryHook` default 2000 chars per field), so a well-formed
37
+ * record is a few KiB; 16 KiB is generous headroom, not a target. The cap is
38
+ * part of the wire contract but rides the layers, not the schema: the
39
+ * backend's store controller enforces it as a typed 4xx, and the store API
40
+ * client (`cursor/store-api-client.ts`) pre-checks it before spending a
41
+ * doomed request.
42
+ */
43
+ export const MEMORY_APPEND_MAX_RECORD_BYTES: number = 16 * 1024;
44
+
45
+ /** Serialized size of a record, as counted against the byte cap. */
46
+ export function serializedRecordBytes(record: unknown): number {
47
+ return new TextEncoder().encode(JSON.stringify(record)).byteLength;
48
+ }
49
+
50
+ /**
51
+ * One turn's memory record as it crosses the wire — July's
52
+ * `TurnMemoryRecord` (memory.ts). The extra-key record mirrors the schema's
53
+ * `.passthrough()`: a newer client's additive optional fields are journaled
54
+ * verbatim rather than stripped by an older server.
55
+ */
56
+ export type TurnMemoryRecordWire = {
57
+ at: string;
58
+ sessionId: string;
59
+ channelId: string;
60
+ status: "completed" | "failed";
61
+ title?: string;
62
+ sdkAgentId?: string;
63
+ userMessage?: string;
64
+ result?: string;
65
+ usage?: Record<string, unknown>;
66
+ } & Record<string, unknown>;
67
+
68
+ /**
69
+ * Wire schema for {@link TurnMemoryRecordWire}. Per-field caps are
70
+ * deliberately absent: the client truncates text at authoring time and
71
+ * {@link MEMORY_APPEND_MAX_RECORD_BYTES} bounds the whole record.
72
+ */
73
+ export const turnMemoryRecordWireSchema: z.ZodType<TurnMemoryRecordWire> = z
74
+ .object({
75
+ at: z.string().min(1),
76
+ sessionId: z.string().min(1),
77
+ channelId: z.string().min(1),
78
+ status: z.enum(["completed", "failed"]),
79
+ title: z.string().optional(),
80
+ sdkAgentId: z.string().optional(),
81
+ userMessage: z.string().optional(),
82
+ result: z.string().optional(),
83
+ usage: z.object({}).passthrough().optional(),
84
+ })
85
+ .passthrough();
86
+
87
+ /** `memory.append` validated payload. */
88
+ export type MemoryAppendPayload = {
89
+ agent: string;
90
+ record: TurnMemoryRecordWire;
91
+ };
92
+
93
+ /**
94
+ * `memory.append` payload shape. The agent name becomes a store key segment
95
+ * on the server, so it must be exactly one segment. Unknown payload fields
96
+ * are stripped (= ignored), per the evolution rules. The byte cap is
97
+ * semantics, not shape — the backend's store controller owns it.
98
+ */
99
+ export const memoryAppendPayloadSchema: z.ZodType<MemoryAppendPayload> =
100
+ z.object({
101
+ agent: z
102
+ .string()
103
+ .min(1)
104
+ .max(256)
105
+ .refine(
106
+ name =>
107
+ !name.includes("/") &&
108
+ !name.includes("\\") &&
109
+ name !== "." &&
110
+ name !== "..",
111
+ { message: "agent must be a single store key segment" }
112
+ ),
113
+ record: turnMemoryRecordWireSchema,
114
+ });
115
+
116
+ /** The versioned request body for the `memory.append` call. */
117
+ export type MemoryAppendEnvelope = {
118
+ v: typeof STORE_API_PROTOCOL_VERSION;
119
+ call: typeof STORE_API_MEMORY_APPEND_CALL;
120
+ payload: MemoryAppendPayload;
121
+ };
122
+
123
+ /** The whole request body. Unknown envelope fields are stripped (= ignored). */
124
+ export const memoryAppendEnvelopeSchema: z.ZodType<MemoryAppendEnvelope> =
125
+ z.object({
126
+ v: z.literal(STORE_API_PROTOCOL_VERSION),
127
+ call: z.literal(STORE_API_MEMORY_APPEND_CALL),
128
+ payload: memoryAppendPayloadSchema,
129
+ });
130
+
131
+ /** `memory.append` success body. */
132
+ export type MemoryAppendResponse = {
133
+ v: typeof STORE_API_PROTOCOL_VERSION;
134
+ ok: true;
135
+ };
136
+
137
+ /**
138
+ * Success body. Failures answer the store surface's standard error JSON
139
+ * (`{ code, error }`) with a matching HTTP status.
140
+ */
141
+ export const memoryAppendResponseSchema: z.ZodType<MemoryAppendResponse> =
142
+ z.object({
143
+ v: z.literal(STORE_API_PROTOCOL_VERSION),
144
+ ok: z.literal(true),
145
+ });
146
+
147
+ export const STORE_API_MEMORY_READ_CALL = "memory.read";
148
+
149
+ /** `memory.read` validated payload. */
150
+ export type MemoryReadPayload = {
151
+ agent: string;
152
+ };
153
+
154
+ /**
155
+ * `memory.read` payload shape — the same single-segment agent name rule as
156
+ * `memory.append`, since the name addresses the same journal key family.
157
+ */
158
+ export const memoryReadPayloadSchema: z.ZodType<MemoryReadPayload> = z.object({
159
+ agent: z
160
+ .string()
161
+ .min(1)
162
+ .max(256)
163
+ .refine(
164
+ name =>
165
+ !name.includes("/") &&
166
+ !name.includes("\\") &&
167
+ name !== "." &&
168
+ name !== "..",
169
+ { message: "agent must be a single store key segment" }
170
+ ),
171
+ });
172
+
173
+ /** The versioned request body for the `memory.read` call. */
174
+ export type MemoryReadEnvelope = {
175
+ v: typeof STORE_API_PROTOCOL_VERSION;
176
+ call: typeof STORE_API_MEMORY_READ_CALL;
177
+ payload: MemoryReadPayload;
178
+ };
179
+
180
+ /** The whole request body. Unknown envelope fields are stripped (= ignored). */
181
+ export const memoryReadEnvelopeSchema: z.ZodType<MemoryReadEnvelope> = z.object(
182
+ {
183
+ v: z.literal(STORE_API_PROTOCOL_VERSION),
184
+ call: z.literal(STORE_API_MEMORY_READ_CALL),
185
+ payload: memoryReadPayloadSchema,
186
+ }
187
+ );
188
+
189
+ /**
190
+ * `memory.read` success body. The call is flush-on-read: the server drains
191
+ * the agent's pending append buffer before presigning. Best-effort: when
192
+ * the drain succeeds the journal includes every append that preceded the
193
+ * read; a busy flush lock, a buffer outage, or an oversized backlog
194
+ * degrades to the already-flushed state (callers must not regress their
195
+ * local view on a shorter download). `journal` is null while the agent has
196
+ * no journal at all.
197
+ */
198
+ export type MemoryReadResponse = {
199
+ v: typeof STORE_API_PROTOCOL_VERSION;
200
+ ok: true;
201
+ journal: {
202
+ url: string;
203
+ expiresAt: string;
204
+ } | null;
205
+ };
206
+
207
+ /**
208
+ * Success body. Failures answer the store surface's standard error JSON
209
+ * (`{ code, error }`) with a matching HTTP status.
210
+ */
211
+ export const memoryReadResponseSchema: z.ZodType<MemoryReadResponse> = z.object(
212
+ {
213
+ v: z.literal(STORE_API_PROTOCOL_VERSION),
214
+ ok: z.literal(true),
215
+ journal: z
216
+ .object({
217
+ url: z.string().min(1),
218
+ expiresAt: z.string().min(1),
219
+ })
220
+ .nullable(),
221
+ }
222
+ );
package/src/memory.ts CHANGED
@@ -18,20 +18,29 @@
18
18
  *
19
19
  * On Cursor-managed hosting the default is {@link agentStoreMemoryBackend}:
20
20
  * the journal lives on the deployment's Agent Store (survives deploys,
21
- * visible on cloud VMs under the store mount) and is mirrored to
22
- * `<stateRoot>/memory/journal.jsonl` so the workspace symlink read path
23
- * keeps working for local-runtime turns.
21
+ * visible on cloud VMs under the store mount), written exclusively through
22
+ * the `memory.append` domain call — the server owns the journal pattern —
23
+ * and mirrored to `<stateRoot>/memory/journal.jsonl` (hydrated per session
24
+ * via `memory.read`, appended locally per turn) so the workspace symlink
25
+ * read path keeps working for local-runtime turns. The
26
+ * `factory-api-v1` pin file exists only on hosted pods — plain local dev
27
+ * runs {@link fileMemoryBackend} and never sees the hosted lane.
24
28
  */
25
29
 
26
30
  import { randomUUID } from "node:crypto";
27
31
  import { appendFile, mkdir, rename, stat, writeFile } from "node:fs/promises";
28
32
  import { join } from "node:path";
29
- import { agentStoreKeys, FileConflictError, type FileSink } from "./files.js";
33
+ import { agentStoreKeys } from "./files.js";
30
34
  import {
31
- cursorHostedFiles,
35
+ type CursorHostedFilesOptions,
32
36
  isCursorHostedFilesAvailable,
33
37
  } from "./files-backends/cursor-hosted.js";
34
38
  import { defineHook } from "./hooks.js";
39
+ import {
40
+ createStoreApiClient,
41
+ isStoreApiLaneEnabled,
42
+ type StoreApiClient,
43
+ } from "./internal/cursor/store-api-client.js";
35
44
  import type { HookContext, HookDefinition, TurnUsage } from "./types.js";
36
45
 
37
46
  /** Name of the shared memory directory under the agent state root. */
@@ -61,6 +70,13 @@ export interface MemoryBackend {
61
70
  record: TurnMemoryRecord,
62
71
  ctx: { stateRoot: string; agentName: string }
63
72
  ): Promise<void>;
73
+ /**
74
+ * Optional session-start hook: bring the backend's local read path up to
75
+ * date before the session's first turn reads it. A freshness upgrade, not
76
+ * a correctness gate — implementations soft-fail and keep whatever read
77
+ * state already exists.
78
+ */
79
+ prepareSession?(ctx: { stateRoot: string; agentName: string }): Promise<void>;
64
80
  }
65
81
 
66
82
  export interface FileMemoryBackendOptions {
@@ -113,120 +129,233 @@ export function fileMemoryBackend(
113
129
  }
114
130
 
115
131
  export interface AgentStoreMemoryBackendOptions {
116
- /** Sink override (tests). Defaults to the deployment's Agent Store. */
117
- sink?: FileSink;
118
- /** Rotation threshold, same meaning as {@link FileMemoryBackendOptions}. */
132
+ /**
133
+ * Local mirror rotation threshold, same meaning as
134
+ * {@link FileMemoryBackendOptions}. The durable journal's rotation is the
135
+ * server's (`memory.append` controller), not this.
136
+ */
119
137
  maxJournalBytes?: number;
120
- /** Backoff sleep override (tests). */
121
- sleep?: (ms: number) => Promise<void>;
122
- /** Jitter source override (tests). Returns [0, 1). */
123
- random?: () => number;
124
- }
125
-
126
- /**
127
- * CAS attempts per append. The journal is one deployment-wide key written by
128
- * every engine pod, so under concurrent turn traffic the etag precondition
129
- * loses races routinely; each retry needs headroom to land.
130
- */
131
- const MEMORY_CAS_MAX_ATTEMPTS = 5;
132
-
133
- /**
134
- * Jittered exponential backoff between CAS attempts. Retrying immediately is
135
- * a fleet-wide stampede: every losing pod re-GETs the full journal and
136
- * re-PUTs in lockstep, which multiplied reads ~65x on one deployment during
137
- * the 2026-09-18 Agent Store read storm and tripped the store's
138
- * per-service-account read limit (every pod shares the deployment
139
- * credential). Jitter desynchronizes the writers so most retries land on the
140
- * first or second attempt.
141
- */
142
- function memoryCasBackoffMs(attempt: number, random: () => number): number {
143
- return 100 * 2 ** attempt * (0.5 + random());
138
+ /**
139
+ * Cursor-hosted transport overrides (tests): base URL, credential, store
140
+ * source id, protocol pin, fetch. Feeds the default
141
+ * {@link storeApiClient} and the lane-availability check.
142
+ */
143
+ hosted?: CursorHostedFilesOptions;
144
+ /**
145
+ * Domain-lane client override (tests). Defaults to
146
+ * {@link createStoreApiClient} over {@link hosted} the module that owns
147
+ * the `memory.append` / `memory.read` transport, typed errors, and
148
+ * bounded backoff.
149
+ */
150
+ storeApiClient?: StoreApiClient;
144
151
  }
145
152
 
146
153
  /**
147
154
  * Journal on the deployment's Agent Store — durable across deploys, visible
148
- * on cloud VMs under the store mount. Appends are read-modify-write with
149
- * etag preconditions (retried on a lost race), then mirrored to
150
- * `<stateRoot>/memory/journal.jsonl` for the workspace symlink read path.
155
+ * on cloud VMs under the store mount, written exclusively through the
156
+ * `memory.append` domain call: the server owns the journal's storage
157
+ * pattern (Redis-buffered flush, rotation), so a pathological pattern is
158
+ * fixable in a backend deploy instead of a July release plus a rebake.
159
+ *
160
+ * There is no client-composed fallback. The read-modify-write CAS lane that
161
+ * predated the domain call (and caused the 2026-09-18 Agent Store read
162
+ * storm) was removed in 0.2.1 together with v2 hosting binding the
163
+ * `factory-api-v1` pin unconditionally and refusing artifacts frozen on an
164
+ * older July. A hosted pod without the pin is therefore a misconfiguration
165
+ * (a server predating the pin change, or a local env naming a hosted store
166
+ * without one): records are dropped loudly, never written client-side.
151
167
  */
152
168
  export function agentStoreMemoryBackend(
153
169
  options: AgentStoreMemoryBackendOptions = {}
154
170
  ): MemoryBackend {
155
- const sink = options.sink ?? cursorHostedFiles();
171
+ const hosted = options.hosted ?? {};
156
172
  const maxJournalBytes = options.maxJournalBytes ?? 5 * 1024 * 1024;
157
- const sleep =
158
- options.sleep ??
159
- ((ms: number) => new Promise<void>(resolve => setTimeout(resolve, ms)));
160
- const random = options.random ?? Math.random;
161
- // Appends are chained per journal key (same shape as fileMemoryBackend);
162
- // a failed append must not poison the chain for later turns. The chain
163
- // serializes appends within this pod only — pods race each other on the
164
- // shared key, which is what the CAS retry (with backoff) absorbs.
173
+ // Mirror work is chained per journal key (same shape as
174
+ // fileMemoryBackend): hydration and appends on one agent's mirror never
175
+ // interleave, and a failed step must not poison the chain for later turns.
165
176
  const appendChains = new Map<string, Promise<void>>();
166
177
 
167
- const append = async (
178
+ // Transport, envelope, typed errors, and bounded backoff all live in the
179
+ // client; this module only decides which lane an append takes.
180
+ const storeApiClient = options.storeApiClient ?? createStoreApiClient(hosted);
181
+
182
+ /**
183
+ * The domain lane still owes local-runtime sessions their read path:
184
+ * cloud-runtime turns read the journal through the store mount, but
185
+ * local-runtime turns on hosted pods (the default `runtime: "local"`)
186
+ * read `<stateRoot>/memory/journal.jsonl` via the workspace symlink, and
187
+ * on hosting the only writer of that file is this backend. So the lane
188
+ * keeps the mirror live without reintroducing the CAS read pattern:
189
+ * hydrate it once from the store when the file is absent (a fresh pod
190
+ * after a deploy), then append each successfully posted record locally,
191
+ * rotating at {@link maxJournalBytes} like {@link fileMemoryBackend}.
192
+ * Server-buffered records not yet flushed when a pod hydrates are the
193
+ * lane's documented staleness window (~30 s / 64 KiB), and other pods'
194
+ * appends surface on the next hydration — advisory memory, same envelope
195
+ * as the server side.
196
+ */
197
+ const hydratedMirrors = new Set<string>();
198
+ let mirrorRotationSeq = 0;
199
+ const maintainLocalMirror = async (
168
200
  record: TurnMemoryRecord,
169
201
  ctx: { stateRoot: string; agentName: string }
170
202
  ): Promise<void> => {
171
- const key = agentStoreKeys.memoryJournal(ctx.agentName);
172
- const line = Buffer.from(`${JSON.stringify(record)}\n`, "utf8");
173
- for (let attempt = 0; attempt < MEMORY_CAS_MAX_ATTEMPTS; attempt += 1) {
174
- const current = await sink.get(key);
175
- const currentBody =
176
- current === undefined
177
- ? new Uint8Array(0)
178
- : current instanceof Uint8Array
179
- ? current
180
- : current.body;
181
- const etag =
182
- current === undefined || current instanceof Uint8Array
183
- ? undefined
184
- : current.etag;
185
- let next: Uint8Array;
186
- let rotatedStamp: string | undefined;
187
- if (currentBody.byteLength >= maxJournalBytes) {
188
- rotatedStamp = `${Date.now()}-${randomUUID().slice(0, 8)}`;
189
- await sink.put(
190
- agentStoreKeys.memoryJournalRotated(ctx.agentName, rotatedStamp),
191
- currentBody,
192
- { ifMatch: null }
193
- );
194
- next = line;
195
- } else {
196
- next = Buffer.concat([currentBody, line]);
197
- }
198
- try {
199
- await sink.put(key, next, {
200
- ifMatch: current === undefined ? null : etag,
203
+ const dir = join(ctx.stateRoot, MEMORY_DIR_NAME);
204
+ const path = join(dir, "journal.jsonl");
205
+ if (!hydratedMirrors.has(path)) {
206
+ // Appends are chained per journal key, so this guard never races
207
+ // itself for one agent. Backstop for a session that appended before
208
+ // any prepareSession hydration ran (e.g. a custom hook ordering).
209
+ hydratedMirrors.add(path);
210
+ const existing = await stat(path).catch(() => undefined);
211
+ if (existing === undefined) {
212
+ const content = await storeApiClient.memoryReadContent({
213
+ agent: ctx.agentName,
201
214
  });
202
- } catch (error) {
203
- if (
204
- error instanceof FileConflictError &&
205
- attempt < MEMORY_CAS_MAX_ATTEMPTS - 1
206
- ) {
207
- await sleep(memoryCasBackoffMs(attempt, random));
208
- continue;
215
+ if (content !== undefined) {
216
+ await mirrorJournalLocally(ctx.stateRoot, content);
209
217
  }
210
- throw error;
211
- }
212
- await mirrorJournalLocally(ctx.stateRoot, next);
213
- if (rotatedStamp !== undefined) {
214
- // Rotated segments sit beside the live journal locally too, so the
215
- // workspace symlink read path keeps pre-rotation history.
216
- await writeFile(
217
- join(ctx.stateRoot, MEMORY_DIR_NAME, `journal-${rotatedStamp}.jsonl`),
218
- currentBody
219
- );
220
218
  }
219
+ }
220
+ await mkdir(dir, { recursive: true });
221
+ const size = (await stat(path).catch(() => undefined))?.size ?? 0;
222
+ if (size >= maxJournalBytes) {
223
+ mirrorRotationSeq += 1;
224
+ await rename(
225
+ path,
226
+ join(dir, `journal-${Date.now()}-${mirrorRotationSeq}.jsonl`)
227
+ );
228
+ }
229
+ await appendFile(path, `${JSON.stringify(record)}\n`, "utf8");
230
+ };
231
+
232
+ /**
233
+ * One `memory.append` domain call; the server appends to the server-owned
234
+ * journal. On success the record is also appended to the local mirror so
235
+ * the workspace symlink read path stays live (see
236
+ * {@link maintainLocalMirror}).
237
+ *
238
+ * Failure policy: log and give up for this record. Memory is advisory and
239
+ * the hook runner treats an append failure as non-fatal to the turn;
240
+ * falling back to the CAS path per-record would reintroduce the write
241
+ * storm under exactly the server brownout that makes this call fail. A
242
+ * failed POST also skips the mirror — the mirror must never show a record
243
+ * the durable journal will not have.
244
+ */
245
+ const appendViaStoreApi = async (
246
+ record: TurnMemoryRecord,
247
+ ctx: { stateRoot: string; agentName: string }
248
+ ): Promise<void> => {
249
+ try {
250
+ await storeApiClient.memoryAppend({ agent: ctx.agentName, record });
251
+ } catch (error) {
252
+ console.warn(
253
+ `[agent-serve] memory.append dropped one record for "${ctx.agentName}": ${
254
+ error instanceof Error ? error.message : String(error)
255
+ }`
256
+ );
221
257
  return;
222
258
  }
259
+ try {
260
+ await maintainLocalMirror(record, ctx);
261
+ } catch (error) {
262
+ console.warn(
263
+ `[agent-serve] memory mirror update failed for "${ctx.agentName}" (journal record is durable): ${
264
+ error instanceof Error ? error.message : String(error)
265
+ }`
266
+ );
267
+ }
268
+ };
269
+
270
+ /**
271
+ * Session-start hydration: one `memory.read` (flush-on-read server-side,
272
+ * so the pending buffer — including a burst another pod appended — lands
273
+ * first) and the mirror is rewritten to global state; the session then
274
+ * reads everything appended before it started. Soft-fail: a failed
275
+ * hydration keeps the existing mirror.
276
+ */
277
+ const hydrateMirror = async (ctx: {
278
+ stateRoot: string;
279
+ agentName: string;
280
+ }): Promise<void> => {
281
+ const path = join(ctx.stateRoot, MEMORY_DIR_NAME, "journal.jsonl");
282
+ try {
283
+ const content = await storeApiClient.memoryReadContent({
284
+ agent: ctx.agentName,
285
+ });
286
+ if (content !== undefined) {
287
+ // Never regress the read path: every mirrored record was
288
+ // server-acked before it was written locally, so a download SHORTER
289
+ // than the mirror means the server's flush-on-read was degraded (a
290
+ // busy lock, a Redis outage — the acked tail is still buffered) or
291
+ // the journal rotated. Keeping the richer mirror loses nothing;
292
+ // rewriting would hide records the agent has already seen.
293
+ const mirroredBytes =
294
+ (await stat(path).catch(() => undefined))?.size ?? 0;
295
+ if (content.byteLength >= mirroredBytes) {
296
+ await mirrorJournalLocally(ctx.stateRoot, content);
297
+ }
298
+ }
299
+ // Either way the read answered: skip the append path's absent-file
300
+ // hydration for the rest of this process.
301
+ hydratedMirrors.add(path);
302
+ } catch (error) {
303
+ console.warn(
304
+ `[agent-serve] memory hydration failed for "${ctx.agentName}" (keeping the existing mirror): ${
305
+ error instanceof Error ? error.message : String(error)
306
+ }`
307
+ );
308
+ }
309
+ };
310
+
311
+ /**
312
+ * Hosted pods carry the pin and a provisioned store by construction (v2
313
+ * hosting binds `factory-api-v1` unconditionally and refuses pre-0.2.1
314
+ * artifacts). Reaching this without them means a misconfigured
315
+ * environment; there is no client-side write to fall back to.
316
+ */
317
+ const refuseMisconfiguredLane = (agentName: string, verb: string): void => {
318
+ console.error(
319
+ `[agent-serve] memory ${verb} for "${agentName}" dropped: the store API lane is unavailable (missing/foreign AGENT_SERVE_STORE_PROTOCOL pin or no provisioned store). This July has no client-side journal fallback; fix the hosting pin.`
320
+ );
223
321
  };
224
322
 
225
323
  return {
324
+ async prepareSession(ctx) {
325
+ // Chained on the same per-journal key as appends, so hydration never
326
+ // interleaves with an append's mirror write.
327
+ const key = agentStoreKeys.memoryJournal(ctx.agentName);
328
+ const prior = appendChains.get(key) ?? Promise.resolve();
329
+ const next = prior
330
+ .catch(() => {})
331
+ .then(async () => {
332
+ if (!isStoreApiLaneEnabled(hosted)) {
333
+ refuseMisconfiguredLane(ctx.agentName, "hydration");
334
+ return;
335
+ }
336
+ await hydrateMirror(ctx);
337
+ });
338
+ appendChains.set(key, next);
339
+ await next;
340
+ },
341
+
226
342
  async appendTurn(record, ctx) {
343
+ // Soft-fail by construction (appendViaStoreApi never throws), but
344
+ // still chained per journal key: the local mirror's hydrate-then-
345
+ // append must not interleave with itself. Lane availability is
346
+ // re-resolved lazily per append, so a pin bound after serve start
347
+ // takes effect without a restart.
227
348
  const key = agentStoreKeys.memoryJournal(ctx.agentName);
228
349
  const prior = appendChains.get(key) ?? Promise.resolve();
229
- const next = prior.catch(() => {}).then(() => append(record, ctx));
350
+ const next = prior
351
+ .catch(() => {})
352
+ .then(async () => {
353
+ if (!isStoreApiLaneEnabled(hosted)) {
354
+ refuseMisconfiguredLane(ctx.agentName, "append");
355
+ return;
356
+ }
357
+ await appendViaStoreApi(record, ctx);
358
+ });
230
359
  appendChains.set(key, next);
231
360
  await next;
232
361
  },
@@ -334,6 +463,24 @@ export function memoryHook(options: MemoryHookOptions = {}): HookDefinition {
334
463
 
335
464
  return defineHook({
336
465
  events: {
466
+ async "session.started"(_event, ctx) {
467
+ // Bring the mirror (the workspace symlink read path) up to date
468
+ // before the session's first turn reads it. Backends without a
469
+ // prepareSession (plain files) have nothing to freshen. Soft-fail:
470
+ // hydration is a freshness upgrade, never a turn blocker.
471
+ try {
472
+ await backend.prepareSession?.({
473
+ stateRoot: ctx.stateRoot,
474
+ agentName: ctx.agent.name,
475
+ });
476
+ } catch (error) {
477
+ console.warn(
478
+ `[agent-serve] memory hydration at session start failed: ${
479
+ error instanceof Error ? error.message : String(error)
480
+ }`
481
+ );
482
+ }
483
+ },
337
484
  async "message.received"(event, ctx) {
338
485
  pendingMessages.set(
339
486
  pendingKey(ctx, event.turnId),
package/src/reminders.ts CHANGED
@@ -42,6 +42,7 @@ export type {
42
42
  ReminderDefinition,
43
43
  ReminderFireContext,
44
44
  ReminderFireResult,
45
+ ReminderFollowupOptions,
45
46
  ReminderHandlerConfig,
46
47
  ReminderHostApi,
47
48
  ReminderInfo,
package/src/types.ts CHANGED
@@ -3000,6 +3000,34 @@ export interface ReminderUntilContext {
3000
3000
  mcp: HostMcpRegistry;
3001
3001
  }
3002
3002
 
3003
+ /**
3004
+ * Send fields a reminder followup may carry onto the turn it starts, the
3005
+ * subset of {@link SendMessageOptions} a host pipeline needs when the fire
3006
+ * itself prepares the turn (channel state for a later `defineResult.commit`,
3007
+ * a session title, a cloud-attach reset). Continuation addressing stays the
3008
+ * reminder's stored binding and is not overridable here.
3009
+ */
3010
+ export interface ReminderFollowupOptions {
3011
+ /** Initial channel state when the followup creates the session. */
3012
+ state?: JsonValue;
3013
+ /** Shallow-merged into an existing session's channel state (see {@link SendMessageOptions.refreshState}). */
3014
+ refreshState?: JsonValue;
3015
+ /** Session display title (applied when creating a new session). */
3016
+ title?: string;
3017
+ /** See {@link SendMessageOptions.clearCloudOverride}. */
3018
+ clearCloudOverride?: boolean;
3019
+ /**
3020
+ * Open a new session under this principal when the stored continuation
3021
+ * has none anywhere (pod or durable storage) — the session an inbound
3022
+ * delivery on that key would create, so later deliveries under the same
3023
+ * principal keep joining it. A durable session that exists but cannot be
3024
+ * materialized still throws, matching prompt fires: opening over it would
3025
+ * shadow the record. Omitted, a missing session also keeps the existing
3026
+ * behavior: the followup throws.
3027
+ */
3028
+ openAuth?: AuthContext;
3029
+ }
3030
+
3003
3031
  export interface ReminderFireContext extends ReminderUntilContext {
3004
3032
  appAuth: AuthContext;
3005
3033
  /**
@@ -3013,7 +3041,9 @@ export interface ReminderFireContext extends ReminderUntilContext {
3013
3041
  * completes, because the pod may stop once the fire's outcome is applied;
3014
3042
  * locally and on v1 it resolves as soon as the turn is admitted.
3015
3043
  */
3016
- followup(args: { message: string }): Promise<ChannelSession>;
3044
+ followup(
3045
+ args: { message: string } & ReminderFollowupOptions
3046
+ ): Promise<ChannelSession>;
3017
3047
  cancel(reason?: string): Promise<void>;
3018
3048
  /**
3019
3049
  * Durable artifacts (unbound — the reminder is bound to a continuation