@cursor/july 0.1.39 → 0.1.41

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 (259) hide show
  1. package/AGENTS.md +4 -3
  2. package/dist/bin/agent-serve.js +28 -9
  3. package/dist/channels/slack/api.d.ts.map +1 -1
  4. package/dist/channels/slack/api.js +18 -8
  5. package/dist/channels/slack/attachments.d.ts +82 -0
  6. package/dist/channels/slack/attachments.d.ts.map +1 -0
  7. package/dist/channels/slack/attachments.js +520 -0
  8. package/dist/channels/slack/cursor-account.d.ts +7 -1
  9. package/dist/channels/slack/cursor-account.d.ts.map +1 -1
  10. package/dist/channels/slack/cursor-account.js +2 -2
  11. package/dist/channels/slack/defaults.d.ts.map +1 -1
  12. package/dist/channels/slack/defaults.js +26 -17
  13. package/dist/channels/slack/dispatch.d.ts +4 -3
  14. package/dist/channels/slack/dispatch.d.ts.map +1 -1
  15. package/dist/channels/slack/dispatch.js +84 -21
  16. package/dist/channels/slack/inbound.d.ts +6 -0
  17. package/dist/channels/slack/inbound.d.ts.map +1 -1
  18. package/dist/channels/slack/inbound.js +48 -2
  19. package/dist/channels/slack/index.d.ts +1 -0
  20. package/dist/channels/slack/index.d.ts.map +1 -1
  21. package/dist/channels/slack/index.js +1 -0
  22. package/dist/channels/slack/live-delivery.d.ts.map +1 -1
  23. package/dist/channels/slack/live-delivery.js +16 -4
  24. package/dist/channels/slack/manifest.d.ts.map +1 -1
  25. package/dist/channels/slack/manifest.js +1 -0
  26. package/dist/channels/slack/slack-channel.d.ts.map +1 -1
  27. package/dist/channels/slack/slack-channel.js +7 -4
  28. package/dist/channels/slack/types.d.ts +49 -1
  29. package/dist/channels/slack/types.d.ts.map +1 -1
  30. package/dist/docs/404.html +2 -2
  31. package/dist/docs/ab.html +6 -6
  32. package/dist/docs/assets/{ab.md.6cLOW7--.js → ab.md.hdxsvX5M.js} +2 -2
  33. package/dist/docs/assets/{app.BQ8Hihdf.js → app.mIw24pb1.js} +1 -1
  34. package/dist/docs/assets/{building-with-agents.md.txrcGU2B.js → building-with-agents.md.PeZaZA1P.js} +1 -1
  35. package/dist/docs/assets/chunks/@localSearchIndexroot.BA7V8dEJ.js +1 -0
  36. package/dist/docs/assets/chunks/{VPLocalSearchBox.BBCr8Yuy.js → VPLocalSearchBox.hOzpPwrG.js} +1 -1
  37. package/dist/docs/assets/chunks/{theme.CK_NiGC-.js → theme.BtFhUYmh.js} +2 -2
  38. package/dist/docs/assets/{deployment.md.CuK5SNjN.js → deployment.md.BHVjVqRa.js} +4 -4
  39. package/dist/docs/assets/{deployment.md.CuK5SNjN.lean.js → deployment.md.BHVjVqRa.lean.js} +1 -1
  40. package/dist/docs/assets/{evals.md.BQXI3rXy.js → evals.md.qeoGgPw0.js} +3 -3
  41. package/dist/docs/assets/example-agents_index.md.xmJ87d_3.js +2 -0
  42. package/dist/docs/assets/{guides_webhooks.md.BERuBSJW.js → guides_webhooks.md.DB-r_er9.js} +47 -4
  43. package/dist/docs/assets/{guides_webhooks.md.BERuBSJW.lean.js → guides_webhooks.md.DB-r_er9.lean.js} +1 -1
  44. package/dist/docs/assets/index.md.BKpbkCa6.js +24 -0
  45. package/dist/docs/assets/{index.md.CmhptOmN.lean.js → index.md.BKpbkCa6.lean.js} +1 -1
  46. package/dist/docs/assets/{quickstart.md.C_b6ESpD.js → quickstart.md.rIbfahJM.js} +4 -4
  47. package/dist/docs/assets/{reference_cli.md.Byvrg8eu.js → reference_cli.md.DqEqQpGn.js} +5 -5
  48. package/dist/docs/assets/{reference_cli.md.Byvrg8eu.lean.js → reference_cli.md.DqEqQpGn.lean.js} +1 -1
  49. package/dist/docs/assets/{reference_hooks.md.BGDw4VLm.js → reference_hooks.md.BHeG-vjK.js} +2 -2
  50. package/dist/docs/assets/{reference_hooks.md.BGDw4VLm.lean.js → reference_hooks.md.BHeG-vjK.lean.js} +1 -1
  51. package/dist/docs/assets/{storage.md.mQDtIULc.js → storage.md.DcGSnIxX.js} +10 -4
  52. package/dist/docs/assets/{storage.md.mQDtIULc.lean.js → storage.md.DcGSnIxX.lean.js} +1 -1
  53. package/dist/docs/assets/troubleshooting.md.CkgwmVI0.js +1 -0
  54. package/dist/docs/assets/{troubleshooting.md.B5RVX_tL.lean.js → troubleshooting.md.CkgwmVI0.lean.js} +1 -1
  55. package/dist/docs/building-with-agents.html +5 -5
  56. package/dist/docs/concepts.html +3 -3
  57. package/dist/docs/deployment.html +6 -6
  58. package/dist/docs/evals.html +7 -7
  59. package/dist/docs/example-agents/approval-buddy.html +3 -3
  60. package/dist/docs/example-agents/benny.html +3 -3
  61. package/dist/docs/example-agents/bugbot.html +3 -3
  62. package/dist/docs/example-agents/codebase-wiki.html +3 -3
  63. package/dist/docs/example-agents/codeowners-review.html +3 -3
  64. package/dist/docs/example-agents/concierge.html +3 -3
  65. package/dist/docs/example-agents/fsd.html +3 -3
  66. package/dist/docs/example-agents/index.html +5 -5
  67. package/dist/docs/example-agents/knowledge-base.html +3 -3
  68. package/dist/docs/example-agents/oncall.html +3 -3
  69. package/dist/docs/example-agents/security-reviewer.html +3 -3
  70. package/dist/docs/example-agents/slack-agent.html +3 -3
  71. package/dist/docs/example-agents/weather-agent.html +3 -3
  72. package/dist/docs/guides/agent-to-agent.html +3 -3
  73. package/dist/docs/guides/cloud-runtime.html +3 -3
  74. package/dist/docs/guides/github.html +3 -3
  75. package/dist/docs/guides/human-in-the-loop.html +3 -3
  76. package/dist/docs/guides/mcp-oauth.html +3 -3
  77. package/dist/docs/guides/slack.html +3 -3
  78. package/dist/docs/guides/webhooks.html +49 -6
  79. package/dist/docs/hashmap.json +1 -1
  80. package/dist/docs/hillclimbing.html +3 -3
  81. package/dist/docs/index.html +7 -7
  82. package/dist/docs/quickstart.html +8 -8
  83. package/dist/docs/reference/agent-config.html +3 -3
  84. package/dist/docs/reference/artifacts.html +3 -3
  85. package/dist/docs/reference/channels.html +3 -3
  86. package/dist/docs/reference/cli.html +8 -8
  87. package/dist/docs/reference/connections.html +3 -3
  88. package/dist/docs/reference/hooks.html +5 -5
  89. package/dist/docs/reference/http-api.html +3 -3
  90. package/dist/docs/reference/instructions.html +3 -3
  91. package/dist/docs/reference/playground.html +3 -3
  92. package/dist/docs/reference/project-layout.html +3 -3
  93. package/dist/docs/reference/prompt.html +3 -3
  94. package/dist/docs/reference/schedules.html +3 -3
  95. package/dist/docs/reference/sessions.html +3 -3
  96. package/dist/docs/reference/skills.html +3 -3
  97. package/dist/docs/reference/subagents.html +3 -3
  98. package/dist/docs/reference/tools.html +3 -3
  99. package/dist/docs/scaffolding-agents.html +3 -3
  100. package/dist/docs/storage.html +14 -8
  101. package/dist/docs/troubleshooting.html +5 -5
  102. package/dist/index.d.ts +4 -2
  103. package/dist/index.d.ts.map +1 -1
  104. package/dist/index.js +3 -1
  105. package/dist/internal/cli-ax.d.ts.map +1 -1
  106. package/dist/internal/cli-ax.js +22 -2
  107. package/dist/internal/cli-deploy.d.ts +6 -0
  108. package/dist/internal/cli-deploy.d.ts.map +1 -1
  109. package/dist/internal/cli-deploy.js +14 -4
  110. package/dist/internal/cli-mcp-oauth.d.ts.map +1 -1
  111. package/dist/internal/cli-mcp-oauth.js +3 -1
  112. package/dist/internal/cli-mcp.js +4 -4
  113. package/dist/internal/cursor-event-relay.d.ts +45 -0
  114. package/dist/internal/cursor-event-relay.d.ts.map +1 -1
  115. package/dist/internal/cursor-event-relay.js +96 -2
  116. package/dist/internal/deploy-client.js +1 -1
  117. package/dist/internal/discovery.d.ts.map +1 -1
  118. package/dist/internal/discovery.js +35 -7
  119. package/dist/internal/distribution.d.ts.map +1 -1
  120. package/dist/internal/distribution.js +3 -1
  121. package/dist/internal/durable-session-store.d.ts +48 -0
  122. package/dist/internal/durable-session-store.d.ts.map +1 -0
  123. package/dist/internal/durable-session-store.js +86 -0
  124. package/dist/internal/hosting.d.ts +1 -1
  125. package/dist/internal/hosting.js +2 -2
  126. package/dist/internal/init-project.d.ts +34 -0
  127. package/dist/internal/init-project.d.ts.map +1 -1
  128. package/dist/internal/init-project.js +121 -2
  129. package/dist/internal/ledger-serving.d.ts +101 -0
  130. package/dist/internal/ledger-serving.d.ts.map +1 -0
  131. package/dist/internal/ledger-serving.js +174 -0
  132. package/dist/internal/listen-port.d.ts +22 -0
  133. package/dist/internal/listen-port.d.ts.map +1 -0
  134. package/dist/internal/listen-port.js +77 -0
  135. package/dist/internal/mcp-oauth.d.ts +12 -1
  136. package/dist/internal/mcp-oauth.d.ts.map +1 -1
  137. package/dist/internal/mcp-oauth.js +25 -3
  138. package/dist/internal/playground/static.d.ts +21 -4
  139. package/dist/internal/playground/static.d.ts.map +1 -1
  140. package/dist/internal/playground/static.js +82 -18
  141. package/dist/internal/sdk-runner.d.ts.map +1 -1
  142. package/dist/internal/sdk-runner.js +10 -1
  143. package/dist/internal/server.d.ts.map +1 -1
  144. package/dist/internal/server.js +67 -49
  145. package/dist/internal/session-engine.d.ts +57 -1
  146. package/dist/internal/session-engine.d.ts.map +1 -1
  147. package/dist/internal/session-engine.js +117 -28
  148. package/dist/internal/slack-provision-client.js +1 -1
  149. package/dist/internal/workspace.d.ts +6 -2
  150. package/dist/internal/workspace.d.ts.map +1 -1
  151. package/dist/internal/workspace.js +10 -2
  152. package/dist/kv/memory-kv.d.ts +23 -0
  153. package/dist/kv/memory-kv.d.ts.map +1 -0
  154. package/dist/kv/memory-kv.js +103 -0
  155. package/dist/kv.d.ts +22 -0
  156. package/dist/kv.d.ts.map +1 -0
  157. package/dist/kv.js +20 -0
  158. package/dist/ledger/session-ledger.d.ts +204 -0
  159. package/dist/ledger/session-ledger.d.ts.map +1 -0
  160. package/dist/ledger/session-ledger.js +382 -0
  161. package/dist/ledger.d.ts +11 -0
  162. package/dist/ledger.d.ts.map +1 -0
  163. package/dist/ledger.js +9 -0
  164. package/dist/playground/assets/index-B3HBneZZ.js +85 -0
  165. package/dist/playground/assets/index-Bqk9nplh.css +1 -0
  166. package/dist/playground/index.html +2 -2
  167. package/dist/storage-backends/cursor-hosted.d.ts +25 -3
  168. package/dist/storage-backends/cursor-hosted.d.ts.map +1 -1
  169. package/dist/storage-backends/cursor-hosted.js +50 -2
  170. package/dist/storage-backends/file-kv.d.ts +5 -2
  171. package/dist/storage-backends/file-kv.d.ts.map +1 -1
  172. package/dist/storage-backends/file-kv.js +107 -7
  173. package/dist/storage.d.ts +60 -5
  174. package/dist/storage.d.ts.map +1 -1
  175. package/dist/storage.js +28 -4
  176. package/dist/types.d.ts +123 -3
  177. package/dist/types.d.ts.map +1 -1
  178. package/dist/types.js +14 -0
  179. package/docs/README.md +10 -7
  180. package/docs/ab.md +2 -2
  181. package/docs/building-with-agents.md +1 -1
  182. package/docs/deployment.md +34 -11
  183. package/docs/evals.md +3 -3
  184. package/docs/example-agents/index.md +1 -1
  185. package/docs/guides/webhooks.md +87 -0
  186. package/docs/quickstart.md +7 -8
  187. package/docs/reference/cli.md +15 -6
  188. package/docs/reference/hooks.md +8 -2
  189. package/docs/storage.md +67 -7
  190. package/docs/troubleshooting.md +10 -2
  191. package/package.json +20 -7
  192. package/skills/create-agent/SKILL.md +1 -1
  193. package/src/bin/agent-serve.ts +27 -11
  194. package/src/channels/slack/api.ts +19 -14
  195. package/src/channels/slack/attachments.ts +635 -0
  196. package/src/channels/slack/cursor-account.ts +8 -0
  197. package/src/channels/slack/defaults.ts +34 -18
  198. package/src/channels/slack/dispatch.ts +121 -6
  199. package/src/channels/slack/inbound.ts +60 -0
  200. package/src/channels/slack/index.ts +1 -0
  201. package/src/channels/slack/live-delivery.ts +17 -3
  202. package/src/channels/slack/manifest.ts +1 -0
  203. package/src/channels/slack/slack-channel.ts +7 -0
  204. package/src/channels/slack/types.ts +47 -1
  205. package/src/index.ts +9 -0
  206. package/src/internal/cli-ax.ts +25 -1
  207. package/src/internal/cli-deploy.ts +22 -4
  208. package/src/internal/cli-mcp-oauth.ts +3 -1
  209. package/src/internal/cli-mcp.ts +4 -4
  210. package/src/internal/cursor-event-relay.ts +168 -0
  211. package/src/internal/deploy-client.ts +1 -1
  212. package/src/internal/discovery.ts +42 -5
  213. package/src/internal/distribution.ts +3 -1
  214. package/src/internal/durable-session-store.ts +95 -0
  215. package/src/internal/hosting.ts +2 -2
  216. package/src/internal/init-project.ts +151 -2
  217. package/src/internal/ledger-serving.ts +240 -0
  218. package/src/internal/listen-port.ts +85 -0
  219. package/src/internal/mcp-oauth.ts +29 -4
  220. package/src/internal/playground/static.ts +111 -20
  221. package/src/internal/sdk-runner.ts +11 -1
  222. package/src/internal/server.ts +76 -62
  223. package/src/internal/session-engine.ts +192 -16
  224. package/src/internal/slack-provision-client.ts +1 -1
  225. package/src/internal/workspace.ts +14 -3
  226. package/src/kv/memory-kv.ts +99 -0
  227. package/src/kv.ts +27 -0
  228. package/src/ledger/session-ledger.ts +535 -0
  229. package/src/ledger.ts +29 -0
  230. package/src/storage-backends/cursor-hosted.ts +108 -3
  231. package/src/storage-backends/file-kv.ts +128 -12
  232. package/src/storage.ts +90 -6
  233. package/src/types.ts +130 -4
  234. package/dist/channels/slack/post-update-delivery.d.ts +0 -85
  235. package/dist/channels/slack/post-update-delivery.d.ts.map +0 -1
  236. package/dist/docs/assets/chunks/@localSearchIndexroot.B-VTH4As.js +0 -1
  237. package/dist/docs/assets/example-agents_index.md.DgGBwckv.js +0 -2
  238. package/dist/docs/assets/index.md.CmhptOmN.js +0 -24
  239. package/dist/docs/assets/troubleshooting.md.B5RVX_tL.js +0 -1
  240. package/dist/internal/json-dir-store.d.ts +0 -32
  241. package/dist/internal/json-dir-store.d.ts.map +0 -1
  242. package/dist/internal/persistence-coordinator.d.ts +0 -127
  243. package/dist/internal/persistence-coordinator.d.ts.map +0 -1
  244. package/dist/multi-tenant.d.ts +0 -80
  245. package/dist/multi-tenant.d.ts.map +0 -1
  246. package/dist/multi-tenant.js +0 -69
  247. package/dist/persistence.d.ts +0 -184
  248. package/dist/persistence.d.ts.map +0 -1
  249. package/dist/playground/assets/index-BRhBbnd4.css +0 -1
  250. package/dist/playground/assets/index-_2WKw9a-.js +0 -85
  251. package/dist/storage-backends/postgres-kv.d.ts +0 -82
  252. package/dist/storage-backends/postgres-kv.d.ts.map +0 -1
  253. package/dist/storage-backends/postgres-kv.js +0 -185
  254. package/src/storage-backends/postgres-kv.ts +0 -253
  255. /package/dist/docs/assets/{ab.md.6cLOW7--.lean.js → ab.md.hdxsvX5M.lean.js} +0 -0
  256. /package/dist/docs/assets/{building-with-agents.md.txrcGU2B.lean.js → building-with-agents.md.PeZaZA1P.lean.js} +0 -0
  257. /package/dist/docs/assets/{evals.md.BQXI3rXy.lean.js → evals.md.qeoGgPw0.lean.js} +0 -0
  258. /package/dist/docs/assets/{example-agents_index.md.DgGBwckv.lean.js → example-agents_index.md.xmJ87d_3.lean.js} +0 -0
  259. /package/dist/docs/assets/{quickstart.md.C_b6ESpD.lean.js → quickstart.md.rIbfahJM.lean.js} +0 -0
@@ -23,6 +23,12 @@ import {
23
23
  } from "../channels/slack/nudge-store.js";
24
24
  import { resolveDedicatedSlackHostBinding } from "../channels/slack/slack-channel.js";
25
25
  import { isCursorHostedFilesAvailable } from "../files-backends/cursor-hosted.js";
26
+ import {
27
+ SessionLedger,
28
+ type SessionLedgerKv,
29
+ supportsSessionLedgerKv,
30
+ } from "../ledger/session-ledger.js";
31
+ import { fileKv } from "../storage-backends/file-kv.js";
26
32
  import {
27
33
  type AgentProject,
28
34
  type ApprovalDecision,
@@ -40,6 +46,7 @@ import {
40
46
  DIRECT_TOOL_CALL_CHANNEL_ID,
41
47
  type HookHandler,
42
48
  type HostContext,
49
+ type HostKvApi,
43
50
  type HttpChannelDefinition,
44
51
  isHostOauthConnectionTransport,
45
52
  isSymbolicConnectionTransport,
@@ -78,6 +85,11 @@ import {
78
85
  mapSdkTokenUsage,
79
86
  } from "./cloud-turn-cost.js";
80
87
  import { isDevMode } from "./dev-mode.js";
88
+ import {
89
+ ledgerKeyPrefix,
90
+ namespacedKv,
91
+ storageCasKv,
92
+ } from "./durable-session-store.js";
81
93
  import { EventLogStore } from "./event-log.js";
82
94
  import { TurnEventMapper } from "./event-mapper.js";
83
95
  import { selectFrameworkStorageDefinition } from "./framework-storage-selection.js";
@@ -95,6 +107,7 @@ import {
95
107
  newToolCallId,
96
108
  } from "./ids.js";
97
109
  import { validateToolInput } from "./json-schema.js";
110
+ import { LedgerServing, newLedgerPodId } from "./ledger-serving.js";
98
111
  import { McpHost } from "./mcp-host.js";
99
112
  import {
100
113
  buildSessionContextBlock,
@@ -215,6 +228,21 @@ export interface SessionEngineOptions {
215
228
  * stub; production uses billed usage events when an agent id is known.
216
229
  */
217
230
  estimateTurnCost?: EstimateTurnCostFn;
231
+ /**
232
+ * Explicit durable-session store for `serving: "ledger"` agents — a KV
233
+ * dedicated to the ledger (session identity, leases, commit heads),
234
+ * separate from agent state. Must implement the ledger's CAS methods
235
+ * (`getWithVersion` / `putIfAbsent` / `putIfVersion`); construction
236
+ * fails fast otherwise. Supply the **raw** store: the engine owns key
237
+ * scoping and namespaces every ledger key under
238
+ * `agentkit/v1/<agent>/durable-session/` itself — a self-hoster never
239
+ * writes prefixes. Default when omitted: the authored storage backend
240
+ * (`agent/storage.ts`) when it implements the `cas` group, else — with
241
+ * no authored storage — a file store under `<stateRoot>/ledger-kv`
242
+ * (single-process correctness only). An authored storage backend
243
+ * *without* `cas` fails fast. Ignored for resident-mode agents.
244
+ */
245
+ durableSessionStore?: HostKvApi;
218
246
  }
219
247
 
220
248
  /**
@@ -250,6 +278,13 @@ interface ActiveToolContext {
250
278
 
251
279
  export interface EngineSendOptions extends SendMessageOptions {
252
280
  mode?: "chat" | "task";
281
+ /**
282
+ * Internal (ledger serving): this send *is* a lease-holding turn
283
+ * delivery, so it must run the resident path. Without the flag, every
284
+ * send on a `serving: "ledger"` agent routes through the ledger
285
+ * (claim → turn → commit) and the pump would recurse into itself.
286
+ */
287
+ ledgerDelivery?: boolean;
253
288
  }
254
289
 
255
290
  interface CoalescedQueuedTurn {
@@ -341,6 +376,12 @@ export class SessionEngine {
341
376
  private readonly cloudCostBackfillDone = new Set<string>();
342
377
  /** Deduped in-flight cloud cost backfills, keyed by session id. */
343
378
  private readonly cloudCostBackfill = new Map<string, Promise<void>>();
379
+ /**
380
+ * Ledger-mode delivery seam; set only when the agent declares
381
+ * `serving: "ledger"`. When present, {@link send} routes every
382
+ * non-delivery call through the ledger's claim → turn → commit.
383
+ */
384
+ private readonly ledgerServing: LedgerServing | undefined;
344
385
 
345
386
  constructor(options: SessionEngineOptions) {
346
387
  this.project = options.project;
@@ -425,6 +466,82 @@ export class SessionEngine {
425
466
  for (const channel of options.project.channels) {
426
467
  this.channelsById.set(channel.id, channel.definition);
427
468
  }
469
+ this.ledgerServing = this.buildLedgerServing(options);
470
+ }
471
+
472
+ /**
473
+ * Ledger-mode wiring (`serving: "ledger"`): resolve a CAS-capable
474
+ * durable-session store and build the delivery seam. Resident agents
475
+ * (the default) return undefined and never touch ledger keys.
476
+ */
477
+ private buildLedgerServing(
478
+ options: SessionEngineOptions
479
+ ): LedgerServing | undefined {
480
+ if (options.project.agent.serving !== "ledger") {
481
+ return undefined;
482
+ }
483
+ const ledgerKv = this.resolveDurableSessionStore(options);
484
+ return new LedgerServing({
485
+ ledger: new SessionLedger({ kv: ledgerKv, podId: newLedgerPodId() }),
486
+ deliverTurn: (request) =>
487
+ this.send(request.channelId, request.message, {
488
+ ...request.options,
489
+ continuationToken: request.continuationToken,
490
+ ledgerDelivery: true,
491
+ }),
492
+ logger: this.logger,
493
+ });
494
+ }
495
+
496
+ /**
497
+ * Pick the KV the ledger coordinates on, in precedence order:
498
+ *
499
+ * 1. `options.durableSessionStore` — an explicit store separate from
500
+ * agent state (advanced deployments, tests).
501
+ * 2. The authored storage backend (`agent/storage.ts`) when it
502
+ * implements the `cas` group — one backend serves both the storage
503
+ * API and the durable session API.
504
+ * 3. No authored storage: a file store under `<stateRoot>/ledger-kv`
505
+ * (same place resident state lives; single-process only).
506
+ *
507
+ * An authored backend without `cas` fails fast: silently falling back
508
+ * to local files would fork coordination away from the deployment's
509
+ * durable store. The check demands exactly the methods the
510
+ * coordination-only ledger uses — `listKeys` is not one of them.
511
+ *
512
+ * Whatever store wins, the returned handle is scoped here — once —
513
+ * under `agentkit/v1/<agent>/durable-session/`. The ledger speaks bare
514
+ * `session/…` / `ledger/…` keys; namespacing is this boundary's job,
515
+ * exactly like `host.kv`'s agent scoping.
516
+ */
517
+ private resolveDurableSessionStore(
518
+ options: SessionEngineOptions
519
+ ): SessionLedgerKv {
520
+ const namespace = ledgerKeyPrefix(options.project.name);
521
+ const explicit = options.durableSessionStore;
522
+ if (explicit !== undefined) {
523
+ if (!supportsSessionLedgerKv(explicit)) {
524
+ throw new Error(
525
+ `Agent "${options.project.name}" declares serving: "ledger", but the configured durableSessionStore is not CAS-capable (getWithVersion / putIfAbsent / putIfVersion). Provide a store implementing those methods (see @cursor/july/kv).`
526
+ );
527
+ }
528
+ return namespacedKv(explicit, namespace);
529
+ }
530
+ const casArgs = {
531
+ agentName: options.project.name,
532
+ projectRoot: options.project.rootDir,
533
+ };
534
+ const authored = options.project.storage;
535
+ if (authored !== undefined) {
536
+ if (authored.cas === undefined) {
537
+ throw new Error(
538
+ `Agent "${options.project.name}" declares serving: "ledger", which needs conditional writes, but the storage backend in agent/storage.ts does not implement the cas group. Add cas: {...} to defineStorage (fileKv and cursorHostedStorage already include it), or pass an explicit durableSessionStore.`
539
+ );
540
+ }
541
+ return namespacedKv(storageCasKv(authored, casArgs), namespace);
542
+ }
543
+ const fallback = fileKv({ dir: join(options.stateRoot, "ledger-kv") });
544
+ return namespacedKv(storageCasKv(fallback, casArgs), namespace);
428
545
  }
429
546
 
430
547
  /**
@@ -592,6 +709,12 @@ export class SessionEngine {
592
709
  message: string,
593
710
  options: EngineSendOptions = {}
594
711
  ): Promise<ChannelSession> {
712
+ // Ledger-mode agents acquire the session lease before any turn runs;
713
+ // only the seam's own lease-holding deliveries fall through to the
714
+ // resident path.
715
+ if (this.ledgerServing !== undefined && options.ledgerDelivery !== true) {
716
+ return this.ledgerServing.deliver(channelId, message, options);
717
+ }
595
718
  const auth = options.auth ?? null;
596
719
  let existing =
597
720
  options.continuationToken === undefined
@@ -616,20 +739,27 @@ export class SessionEngine {
616
739
  // Refresh harness cwd when the caller supplies a newer worktree (e.g. PR
617
740
  // synchronize). Leave the durable continuation key unchanged.
618
741
  let record = existing;
619
- if (
742
+ const refreshWorkspace =
620
743
  options.workspaceDir !== undefined &&
621
- options.workspaceDir !== existing.workspaceOverride
744
+ options.workspaceDir !== existing.workspaceOverride;
745
+ if (
746
+ refreshWorkspace ||
747
+ options.workspaceFiles !== undefined ||
748
+ options.images !== undefined
622
749
  ) {
623
750
  record = await this.updateRecord(existing.sessionId, (current) => ({
624
751
  ...current,
625
- workspaceOverride: options.workspaceDir,
626
- pendingWorkspaceFiles:
627
- options.workspaceFiles ?? current.pendingWorkspaceFiles,
628
- }));
629
- } else if (options.workspaceFiles !== undefined) {
630
- record = await this.updateRecord(existing.sessionId, (current) => ({
631
- ...current,
632
- pendingWorkspaceFiles: options.workspaceFiles,
752
+ workspaceOverride: refreshWorkspace
753
+ ? options.workspaceDir
754
+ : current.workspaceOverride,
755
+ pendingWorkspaceFiles: mergePendingWorkspaceFiles(
756
+ current.pendingWorkspaceFiles,
757
+ options.workspaceFiles
758
+ ),
759
+ pendingImages: mergePendingImages(
760
+ current.pendingImages,
761
+ options.images
762
+ ),
633
763
  }));
634
764
  }
635
765
  // Refresh the cloud attach the same way: a follow-up send carries the
@@ -668,6 +798,7 @@ export class SessionEngine {
668
798
  channelState,
669
799
  workspaceOverride: options.workspaceDir,
670
800
  pendingWorkspaceFiles: options.workspaceFiles,
801
+ pendingImages: options.images,
671
802
  cloudOverride: options.cloud,
672
803
  sdkAgentId: options.sdkAgentId,
673
804
  turnCount: 0,
@@ -1299,15 +1430,29 @@ export class SessionEngine {
1299
1430
  stateRoot: this.stateRoot,
1300
1431
  });
1301
1432
  const pendingFiles = record.pendingWorkspaceFiles;
1433
+ const pendingImages = record.pendingImages;
1302
1434
  if (
1303
1435
  pendingFiles !== undefined &&
1304
1436
  Object.keys(pendingFiles).length > 0
1305
1437
  ) {
1306
1438
  await writeWorkspaceFiles(harnessCwd, pendingFiles);
1307
- await this.updateRecord(sessionId, (current) => {
1308
- const { pendingWorkspaceFiles: _cleared, ...rest } = current;
1309
- return rest;
1310
- });
1439
+ }
1440
+ if (
1441
+ (pendingFiles !== undefined &&
1442
+ Object.keys(pendingFiles).length > 0) ||
1443
+ (pendingImages !== undefined && pendingImages.length > 0)
1444
+ ) {
1445
+ await this.updateRecord(sessionId, (current) => ({
1446
+ ...current,
1447
+ pendingWorkspaceFiles:
1448
+ current.pendingWorkspaceFiles === pendingFiles
1449
+ ? undefined
1450
+ : current.pendingWorkspaceFiles,
1451
+ pendingImages:
1452
+ current.pendingImages === pendingImages
1453
+ ? undefined
1454
+ : current.pendingImages,
1455
+ }));
1311
1456
  }
1312
1457
  // The harness only loads the scaffolded AGENTS.md when the session
1313
1458
  // workspace is its own project root. When the harness cwd is borrowed
@@ -1340,7 +1485,8 @@ export class SessionEngine {
1340
1485
  await yieldEventLoop();
1341
1486
  }
1342
1487
  },
1343
- signal
1488
+ signal,
1489
+ pendingImages
1344
1490
  );
1345
1491
  this.activeToolContexts.set(sessionId, { record, turnId, emit });
1346
1492
  try {
@@ -1443,7 +1589,8 @@ export class SessionEngine {
1443
1589
  instructionsInvisibleToHarness: boolean,
1444
1590
  emit: (payload: SessionEventPayload) => void,
1445
1591
  onUpdate: RunnerTurnRequest["onUpdate"],
1446
- signal: AbortSignal
1592
+ signal: AbortSignal,
1593
+ images?: Array<{ data: string; mimeType: string }>
1447
1594
  ): RunnerTurnRequest {
1448
1595
  // Per-session cloud attach (channel/tool handoff) forces cloud runtime
1449
1596
  // even when the agent defaults to local for chat server tools.
@@ -1508,6 +1655,7 @@ export class SessionEngine {
1508
1655
  runnerStateDir: join(this.stateRoot, "runner"),
1509
1656
  prompt: turnPrompt,
1510
1657
  isFirstTurn,
1658
+ ...(images !== undefined && images.length > 0 ? { images } : {}),
1511
1659
  ...(agent.model === undefined ? {} : { model: agent.model }),
1512
1660
  runtime,
1513
1661
  ...(mergedCloud === undefined ? {} : { cloud: mergedCloud }),
@@ -3193,6 +3341,8 @@ export class SessionEngine {
3193
3341
  /** Drain running turns, dispatches, and pending writes, then dispose. */
3194
3342
  async close(): Promise<void> {
3195
3343
  this.approvals.cancelAll("deny");
3344
+ // Ledger deliveries spawn turns, so settle them before activeTurns.
3345
+ await this.ledgerServing?.close();
3196
3346
  await Promise.allSettled([...this.activeTurns.values()]);
3197
3347
  await Promise.allSettled([...this.backgroundWork]);
3198
3348
  await Promise.allSettled([...this.dispatchChains.values()]);
@@ -3208,6 +3358,32 @@ export class SessionEngine {
3208
3358
  const RESTORE_FETCH_CONCURRENCY = 8;
3209
3359
 
3210
3360
  /** NDJSON payload for a restored event stream; undefined when empty. */
3361
+ function mergePendingImages(
3362
+ current: Array<{ data: string; mimeType: string }> | undefined,
3363
+ incoming: Array<{ data: string; mimeType: string }> | undefined
3364
+ ): Array<{ data: string; mimeType: string }> | undefined {
3365
+ if (incoming === undefined) {
3366
+ return current;
3367
+ }
3368
+ if (current === undefined || current.length === 0) {
3369
+ return incoming;
3370
+ }
3371
+ return [...current, ...incoming];
3372
+ }
3373
+
3374
+ function mergePendingWorkspaceFiles(
3375
+ current: SessionRecord["pendingWorkspaceFiles"],
3376
+ incoming: SessionRecord["pendingWorkspaceFiles"]
3377
+ ): SessionRecord["pendingWorkspaceFiles"] {
3378
+ if (incoming === undefined) {
3379
+ return current;
3380
+ }
3381
+ if (current === undefined) {
3382
+ return incoming;
3383
+ }
3384
+ return { ...current, ...incoming };
3385
+ }
3386
+
3211
3387
  function eventsNdjson(events: SessionEvent[]): string | undefined {
3212
3388
  if (events.length === 0) {
3213
3389
  return undefined;
@@ -386,7 +386,7 @@ function mapErrorResponse(
386
386
  );
387
387
  case 403:
388
388
  return new SlackProvisionApiError(
389
- `${serverError ?? "Not authorized"} — Slack provisioning needs team-admin permission and the cloud-agent entitlement on that team.`,
389
+ `${serverError ?? "Not authorized"} — Slack provisioning needs team membership and the cloud-agent entitlement on that team.`,
390
390
  { status: 403 }
391
391
  );
392
392
  case 404: {
@@ -421,16 +421,27 @@ export function buildSkillMd(skill: DiscoveredSkill): string {
421
421
 
422
422
  /**
423
423
  * Write relative path → contents under `workspaceDir`, refusing paths that
424
- * escape the directory.
424
+ * escape the directory. Values may be utf8 strings or base64 blob envelopes.
425
425
  */
426
426
  export async function writeWorkspaceFiles(
427
427
  workspaceDir: string,
428
- files: Record<string, string>
428
+ files: Record<
429
+ string,
430
+ string | { encoding: "base64"; data: string; mimeType?: string }
431
+ >
429
432
  ): Promise<void> {
430
433
  for (const [relativePath, contents] of Object.entries(files)) {
431
434
  const target = resolveWorkspaceWritePath(workspaceDir, relativePath);
432
435
  await mkdir(dirname(target), { recursive: true });
433
- await writeFile(target, contents, "utf8");
436
+ if (typeof contents === "string") {
437
+ await writeFile(target, contents, "utf8");
438
+ } else if (contents.encoding === "base64") {
439
+ await writeFile(target, Buffer.from(contents.data, "base64"));
440
+ } else {
441
+ throw new Error(
442
+ `Unsupported workspace file encoding for ${relativePath}`
443
+ );
444
+ }
434
445
  }
435
446
  }
436
447
 
@@ -0,0 +1,99 @@
1
+ /**
2
+ * In-memory {@link HostKvCasApi} — the test/dev implementation of the
3
+ * interface module (`@cursor/july/kv`), exported at
4
+ * `@cursor/july/kv/memory`.
5
+ *
6
+ * The memory KV *is* the dev shim: tests and local runs exercise the
7
+ * same CAS coordination paths that hosted serving uses, not a mock of an
8
+ * RPC. Values are deep-copied on both write and
9
+ * read so callers can never observe (or cause) shared-reference mutation.
10
+ */
11
+
12
+ import type { StorageCasTable } from "../storage.js";
13
+ import type {
14
+ HostKvCasApi,
15
+ JsonValue,
16
+ KvCasResult,
17
+ KvVersionedEntry,
18
+ } from "../types.js";
19
+
20
+ /** Create an empty in-memory CAS-capable KV. */
21
+ export function createMemoryKv(): HostKvCasApi {
22
+ const entries = new Map<string, { value: JsonValue; version: string }>();
23
+ let versionCounter = 0;
24
+
25
+ const nextVersion = (): string => {
26
+ versionCounter += 1;
27
+ return `v${versionCounter}`;
28
+ };
29
+
30
+ const snapshot = (key: string): KvVersionedEntry | undefined => {
31
+ const entry = entries.get(key);
32
+ if (entry === undefined) {
33
+ return undefined;
34
+ }
35
+ return { value: structuredClone(entry.value), version: entry.version };
36
+ };
37
+
38
+ return {
39
+ async get(key): Promise<JsonValue | undefined> {
40
+ return snapshot(key)?.value;
41
+ },
42
+
43
+ async put(key, value): Promise<void> {
44
+ entries.set(key, {
45
+ value: structuredClone(value),
46
+ version: nextVersion(),
47
+ });
48
+ },
49
+
50
+ async delete(key): Promise<void> {
51
+ entries.delete(key);
52
+ },
53
+
54
+ async getWithVersion(key): Promise<KvVersionedEntry | undefined> {
55
+ return snapshot(key);
56
+ },
57
+
58
+ async putIfAbsent(key, value): Promise<KvCasResult> {
59
+ if (entries.has(key)) {
60
+ return { ok: false, current: snapshot(key) };
61
+ }
62
+ const version = nextVersion();
63
+ entries.set(key, { value: structuredClone(value), version });
64
+ return { ok: true, version };
65
+ },
66
+
67
+ async putIfVersion(key, value, expectedVersion): Promise<KvCasResult> {
68
+ const entry = entries.get(key);
69
+ if (entry === undefined || entry.version !== expectedVersion) {
70
+ return { ok: false, current: snapshot(key) };
71
+ }
72
+ const version = nextVersion();
73
+ entries.set(key, { value: structuredClone(value), version });
74
+ return { ok: true, version };
75
+ },
76
+
77
+ async listKeys(prefix): Promise<string[]> {
78
+ return [...entries.keys()].filter((key) => key.startsWith(prefix)).sort();
79
+ },
80
+ };
81
+ }
82
+
83
+ /**
84
+ * In-memory `cas` group for `defineStorage` — a complete conditional-write
85
+ * group for sinks whose plain methods are inert or live elsewhere (test
86
+ * fixtures, an off-hosting no-op sink). Non-durable and process-local by
87
+ * construction; a real deployment backend should implement CAS against
88
+ * its own store instead.
89
+ */
90
+ export function memoryCasTable(): StorageCasTable {
91
+ const kv = createMemoryKv();
92
+ return {
93
+ getWithVersion: (key) => kv.getWithVersion(key),
94
+ putIfAbsent: (key, value) => kv.putIfAbsent(key, value),
95
+ putIfVersion: (key, value, expectedVersion) =>
96
+ kv.putIfVersion(key, value, expectedVersion),
97
+ listKeys: (prefix) => kv.listKeys(prefix),
98
+ };
99
+ }
package/src/kv.ts ADDED
@@ -0,0 +1,27 @@
1
+ /**
2
+ * The CAS-capable KV **interface** — `@cursor/july/kv`.
3
+ *
4
+ * This module is implementation-free on purpose: it is the contract a
5
+ * bring-your-own KV implements so coordination protocols (conditional
6
+ * writes are the substrate for the upcoming session ledger) can run on
7
+ * it. {@link HostKvCasApi} is the conditional-write superset of the plain
8
+ * {@link HostKvApi}; {@link supportsKvCas} is the runtime probe for
9
+ * stores of unknown provenance — a `defineStorage` backend declares CAS
10
+ * through its `cas` group instead, so it never needs probing.
11
+ *
12
+ * Implementations live elsewhere:
13
+ *
14
+ * - `@cursor/july/kv/memory` — `createMemoryKv`, the test/dev shim.
15
+ * - The storage backends implement the same operations as the `cas`
16
+ * group on `defineStorage` (see `StorageCasTable` in
17
+ * `@cursor/july/storage`): `fileKv` (single-process) and
18
+ * `cursorHostedStorage` (control-plane proxy). See `docs/storage.md`.
19
+ */
20
+
21
+ export type {
22
+ HostKvApi,
23
+ HostKvCasApi,
24
+ KvCasResult,
25
+ KvVersionedEntry,
26
+ } from "./types.js";
27
+ export { supportsKvCas } from "./types.js";