@cursor/july 0.1.97 → 0.1.99

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 (179) hide show
  1. package/AGENTS.md +1 -1
  2. package/dist/bin/agent-serve.js +6 -7
  3. package/dist/channels/github/github-channel.d.ts.map +1 -1
  4. package/dist/channels/github/github-channel.js +16 -0
  5. package/dist/channels/origin/origin-channel.d.ts +2 -0
  6. package/dist/channels/origin/origin-channel.d.ts.map +1 -1
  7. package/dist/channels/origin/origin-channel.js +37 -4
  8. package/dist/channels/slack/defaults.d.ts.map +1 -1
  9. package/dist/channels/slack/defaults.js +76 -55
  10. package/dist/channels/slack/dispatch.d.ts.map +1 -1
  11. package/dist/channels/slack/dispatch.js +1 -1
  12. package/dist/channels/slack/live-delivery.d.ts +1 -1
  13. package/dist/channels/slack/live-delivery.d.ts.map +1 -1
  14. package/dist/channels/slack/live-delivery.js +4 -2
  15. package/dist/channels/slack/progress-delivery.d.ts +28 -0
  16. package/dist/channels/slack/progress-delivery.d.ts.map +1 -0
  17. package/dist/channels/slack/progress-delivery.js +115 -0
  18. package/dist/channels/slack/types.d.ts +9 -1
  19. package/dist/channels/slack/types.d.ts.map +1 -1
  20. package/dist/docs/404.html +2 -2
  21. package/dist/docs/ab.html +4 -4
  22. package/dist/docs/assets/{app.B3rWNYE1.js → app.BplEAZ3S.js} +1 -1
  23. package/dist/docs/assets/chunks/@localSearchIndexroot.DpuI4i7Z.js +1 -0
  24. package/dist/docs/assets/chunks/{VPLocalSearchBox.C1lhqJJR.js → VPLocalSearchBox.CHXyv4wP.js} +1 -1
  25. package/dist/docs/assets/chunks/{theme.B40_SXuv.js → theme.BTcsZmjQ.js} +2 -2
  26. package/dist/docs/assets/{reference_cli.md.RyZf5OTE.js → reference_cli.md.DfoeyvL0.js} +4 -4
  27. package/dist/docs/assets/{reference_cli.md.RyZf5OTE.lean.js → reference_cli.md.DfoeyvL0.lean.js} +1 -1
  28. package/dist/docs/assets/{reference_http-api.md.5zOAbV86.js → reference_http-api.md.BEJx9XVj.js} +1 -1
  29. package/dist/docs/assets/{reference_tools.md.B84gw9Ii.js → reference_tools.md.CvAHsdSp.js} +10 -2
  30. package/dist/docs/assets/{reference_tools.md.B84gw9Ii.lean.js → reference_tools.md.CvAHsdSp.lean.js} +1 -1
  31. package/dist/docs/building-with-agents.html +4 -4
  32. package/dist/docs/concepts.html +4 -4
  33. package/dist/docs/deployment.html +4 -4
  34. package/dist/docs/design/runtime-abstraction.md +1757 -0
  35. package/dist/docs/evals.html +4 -4
  36. package/dist/docs/guides/agent-to-agent.html +4 -4
  37. package/dist/docs/guides/cloud-runtime.html +4 -4
  38. package/dist/docs/guides/convert-automation.html +4 -4
  39. package/dist/docs/guides/github.html +4 -4
  40. package/dist/docs/guides/human-in-the-loop.html +4 -4
  41. package/dist/docs/guides/mcp-oauth.html +4 -4
  42. package/dist/docs/guides/opentelemetry.html +4 -4
  43. package/dist/docs/guides/slack.html +4 -4
  44. package/dist/docs/guides/webhooks.html +4 -4
  45. package/dist/docs/hashmap.json +1 -1
  46. package/dist/docs/hillclimbing.html +4 -4
  47. package/dist/docs/index.html +4 -4
  48. package/dist/docs/llms-full.txt +1789 -7
  49. package/dist/docs/llms.txt +1 -0
  50. package/dist/docs/quickstart.html +4 -4
  51. package/dist/docs/reference/agent-config.html +4 -4
  52. package/dist/docs/reference/artifacts.html +4 -4
  53. package/dist/docs/reference/channels.html +4 -4
  54. package/dist/docs/reference/cli.html +7 -7
  55. package/dist/docs/reference/cli.md +1 -7
  56. package/dist/docs/reference/connections.html +4 -4
  57. package/dist/docs/reference/hooks.html +4 -4
  58. package/dist/docs/reference/http-api.html +6 -6
  59. package/dist/docs/reference/http-api.md +1 -0
  60. package/dist/docs/reference/instructions.html +4 -4
  61. package/dist/docs/reference/playground.html +4 -4
  62. package/dist/docs/reference/project-layout.html +4 -4
  63. package/dist/docs/reference/prompt.html +4 -4
  64. package/dist/docs/reference/schedules.html +4 -4
  65. package/dist/docs/reference/sessions.html +4 -4
  66. package/dist/docs/reference/skills.html +4 -4
  67. package/dist/docs/reference/subagents.html +4 -4
  68. package/dist/docs/reference/tools.html +14 -6
  69. package/dist/docs/reference/tools.md +25 -0
  70. package/dist/docs/scaffolding-agents.html +4 -4
  71. package/dist/docs/storage.html +4 -4
  72. package/dist/docs/templates/agentic-owners.html +4 -4
  73. package/dist/docs/templates/agents-md.html +4 -4
  74. package/dist/docs/templates/code-wiki.html +4 -4
  75. package/dist/docs/templates/demo.html +4 -4
  76. package/dist/docs/templates/pr-autofixer.html +4 -4
  77. package/dist/docs/templates/security-help.html +4 -4
  78. package/dist/docs/templates/security-reviewer.html +4 -4
  79. package/dist/docs/templates/triage.html +4 -4
  80. package/dist/docs/troubleshooting.html +4 -4
  81. package/dist/files-backends/cursor-hosted.d.ts +3 -2
  82. package/dist/files-backends/cursor-hosted.d.ts.map +1 -1
  83. package/dist/files-backends/cursor-hosted.js +3 -2
  84. package/dist/index.d.ts +1 -0
  85. package/dist/index.d.ts.map +1 -1
  86. package/dist/index.js +1 -0
  87. package/dist/internal/as-of.d.ts +19 -0
  88. package/dist/internal/as-of.d.ts.map +1 -0
  89. package/dist/internal/as-of.js +82 -0
  90. package/dist/internal/cli-deploy.d.ts +2 -1
  91. package/dist/internal/cli-deploy.d.ts.map +1 -1
  92. package/dist/internal/cli-deploy.js +100 -5
  93. package/dist/internal/cursor-event-relay.d.ts.map +1 -1
  94. package/dist/internal/cursor-event-relay.js +5 -6
  95. package/dist/internal/framework-file-storage.d.ts +10 -16
  96. package/dist/internal/framework-file-storage.d.ts.map +1 -1
  97. package/dist/internal/framework-file-storage.js +11 -40
  98. package/dist/internal/framework-storage-selection.d.ts +13 -11
  99. package/dist/internal/framework-storage-selection.d.ts.map +1 -1
  100. package/dist/internal/framework-storage-selection.js +37 -26
  101. package/dist/internal/hosted-admission-adapter.d.ts +28 -0
  102. package/dist/internal/hosted-admission-adapter.d.ts.map +1 -0
  103. package/dist/internal/hosted-admission-adapter.js +7 -0
  104. package/dist/internal/hosted-delivery-protocol.d.ts +6 -1
  105. package/dist/internal/hosted-delivery-protocol.d.ts.map +1 -1
  106. package/dist/internal/hosted-delivery-protocol.js +1 -1
  107. package/dist/internal/hosted-delivery.d.ts +15 -1
  108. package/dist/internal/hosted-delivery.d.ts.map +1 -1
  109. package/dist/internal/hosted-delivery.js +47 -21
  110. package/dist/internal/hosted-execution-diag.d.ts +11 -0
  111. package/dist/internal/hosted-execution-diag.d.ts.map +1 -1
  112. package/dist/internal/hosted-execution-diag.js +39 -2
  113. package/dist/internal/http-channel.d.ts.map +1 -1
  114. package/dist/internal/http-channel.js +4 -1
  115. package/dist/internal/null-kv-storage.d.ts +48 -0
  116. package/dist/internal/null-kv-storage.d.ts.map +1 -0
  117. package/dist/internal/null-kv-storage.js +58 -0
  118. package/dist/internal/server.d.ts +3 -0
  119. package/dist/internal/server.d.ts.map +1 -1
  120. package/dist/internal/server.js +82 -19
  121. package/dist/internal/session-engine.d.ts.map +1 -1
  122. package/dist/internal/session-engine.js +24 -4
  123. package/dist/internal/session-run-log.d.ts +120 -0
  124. package/dist/internal/session-run-log.d.ts.map +1 -0
  125. package/dist/internal/session-run-log.js +359 -0
  126. package/dist/internal/storage-coordinator.d.ts +6 -0
  127. package/dist/internal/storage-coordinator.d.ts.map +1 -1
  128. package/dist/internal/storage-coordinator.js +9 -1
  129. package/dist/internal/tool-policy.d.ts +20 -8
  130. package/dist/internal/tool-policy.d.ts.map +1 -1
  131. package/dist/internal/tool-policy.js +11 -0
  132. package/dist/playground/assets/index-D7rGZNjy.css +1 -0
  133. package/dist/playground/assets/index-jzNbJENa.js +69 -0
  134. package/dist/playground/index.html +2 -2
  135. package/dist/storage-protocol.d.ts +8 -0
  136. package/dist/storage-protocol.d.ts.map +1 -1
  137. package/dist/storage-protocol.js +8 -0
  138. package/dist/tools.d.ts +12 -2
  139. package/dist/tools.d.ts.map +1 -1
  140. package/dist/types.d.ts +25 -0
  141. package/dist/types.d.ts.map +1 -1
  142. package/docs/design/runtime-abstraction.md +1757 -0
  143. package/docs/reference/cli.md +1 -7
  144. package/docs/reference/http-api.md +1 -0
  145. package/docs/reference/tools.md +25 -0
  146. package/package.json +1 -1
  147. package/src/bin/agent-serve.ts +6 -7
  148. package/src/channels/github/github-channel.ts +17 -0
  149. package/src/channels/origin/origin-channel.ts +74 -10
  150. package/src/channels/slack/defaults.ts +91 -63
  151. package/src/channels/slack/dispatch.ts +2 -0
  152. package/src/channels/slack/live-delivery.ts +5 -3
  153. package/src/channels/slack/progress-delivery.ts +133 -0
  154. package/src/channels/slack/types.ts +9 -1
  155. package/src/files-backends/cursor-hosted.ts +3 -2
  156. package/src/index.ts +4 -0
  157. package/src/internal/as-of.ts +107 -0
  158. package/src/internal/cli-deploy.ts +128 -8
  159. package/src/internal/cursor-event-relay.ts +5 -6
  160. package/src/internal/framework-file-storage.ts +11 -45
  161. package/src/internal/framework-storage-selection.ts +41 -25
  162. package/src/internal/hosted-admission-adapter.ts +42 -0
  163. package/src/internal/hosted-delivery-protocol.ts +7 -2
  164. package/src/internal/hosted-delivery.ts +88 -36
  165. package/src/internal/hosted-execution-diag.ts +45 -0
  166. package/src/internal/http-channel.ts +8 -0
  167. package/src/internal/null-kv-storage.ts +74 -0
  168. package/src/internal/server.ts +100 -2
  169. package/src/internal/session-engine.ts +32 -1
  170. package/src/internal/session-run-log.ts +584 -0
  171. package/src/internal/storage-coordinator.ts +11 -1
  172. package/src/internal/tool-policy.ts +31 -9
  173. package/src/storage-protocol.ts +10 -0
  174. package/src/tools.ts +12 -2
  175. package/src/types.ts +25 -0
  176. package/dist/docs/assets/chunks/@localSearchIndexroot.BU9nHdnQ.js +0 -1
  177. package/dist/playground/assets/index-DrkI6y5O.js +0 -88
  178. package/dist/playground/assets/index-DzNGwm7q.css +0 -1
  179. /package/dist/docs/assets/{reference_http-api.md.5zOAbV86.lean.js → reference_http-api.md.BEJx9XVj.lean.js} +0 -0
@@ -17,17 +17,19 @@
17
17
  *
18
18
  * - **Cursor-managed hosting** (the deployment-generation environment
19
19
  * variable is set, same signal that registers `GET /v1/manifest`):
20
- * the Cursor-hosted backend. When the deployment carries a
21
- * **framework-level file store**
22
- * (`AGENT_SERVE_FRAMEWORK_STORE_SOURCE_ID`), session records, event
23
- * transcripts, and the continuation index live there — file-shaped state
24
- * on file storage, apart from the author-facing user store while
25
- * `host.kv`, eval runs, A/B samples, and conditional writes stay on the
26
- * control-plane KV proxy (see framework-file-storage.ts). Without one
27
- * (a deploy predating framework-store provisioning), everything goes to
28
- * the KV proxy as before. The API key resolves through the full
29
- * credential chain on every call because hosted freeze snapshots listen
30
- * unsigned and the binder drops the key file after restore.
20
+ * the deployment's **framework-level file store**
21
+ * (`AGENT_SERVE_FRAMEWORK_STORE_SOURCE_ID`) holds session records, event
22
+ * transcripts, and the continuation index — file-shaped state on file
23
+ * storage, apart from the author-facing user store. The control-plane KV
24
+ * proxy is *not* used: it authorizes by mapping a pod service account to
25
+ * a v1 deployment row, which no v2 deployment has, so every call 403s and
26
+ * fails the delivery that read it. The KV half is inert until the proxy
27
+ * resolves v2 deployments (see null-kv-storage.ts). Without a framework
28
+ * store (a deploy predating framework-store provisioning) there is
29
+ * nothing left to mirror to, so the state root is again the only copy.
30
+ * The API key resolves through the full credential chain on every call
31
+ * because hosted freeze snapshots listen unsigned and the binder drops
32
+ * the key file after restore.
31
33
  * - **Self-hosted**: no mirror. `--state-root` is the durable copy, which
32
34
  * is correct when the operator owns the disk.
33
35
  *
@@ -45,15 +47,17 @@ import {
45
47
  cursorHostedFiles,
46
48
  } from "../files-backends/cursor-hosted.js";
47
49
  import { defineStorage, type StorageDefinition } from "../storage.js";
48
- import { cursorHostedStorage } from "../storage-backends/cursor-hosted.js";
50
+ import { CURSOR_HOSTED_STORAGE } from "../storage-backends/cursor-hosted.js";
49
51
  import type { AgentArchitecture } from "../types.js";
50
52
  import { resolveApiKeySync } from "./cursor/credentials.js";
51
53
  import { AGENT_SERVE_GENERATION_ENV_VAR } from "./cursor-relay-core.js";
52
54
  import {
53
55
  composeFrameworkStorage,
54
56
  fileSinkStorageCore,
55
- frameworkGuardedCas,
56
57
  } from "./framework-file-storage.js";
58
+ import { nullKvStorage } from "./null-kv-storage.js";
59
+
60
+ const cursorHostedMarker = { [CURSOR_HOSTED_STORAGE]: true } as const;
57
61
 
58
62
  export interface FrameworkStorageSelectionInput {
59
63
  /** `agent/storage.ts`, when the author wrote one (v1 only: discovery rejects it on v2). */
@@ -96,23 +100,38 @@ export function selectFrameworkStorageDefinition(
96
100
  // comes up). Follow-ups restore lazily by continuation or session id;
97
101
  // GET /v1/sessions lists from the sink either way.
98
102
  const policy = { restore: "off" } as const;
99
- // Full chain (env → key file → stored login): frozen engines listen
100
- // unsigned and bind CURSOR_API_KEY_FILE at task time, after this
101
- // definition is constructed.
102
- const resolveApiKey = (): string | undefined => resolveApiKeySync()?.apiKey;
103
- const kv = cursorHostedStorage({ resolveApiKey });
104
103
  const frameworkStoreId = cursorFrameworkStoreSourceId(
105
104
  env as NodeJS.ProcessEnv
106
105
  );
107
106
  if (frameworkStoreId === undefined) {
108
- return defineStorage({ name: "cursor-hosted", policy, ...kv });
107
+ // Nothing to mirror to: the KV proxy rejects v2 service accounts, so a
108
+ // definition over it would fail every delivery that reads it. The state
109
+ // root is the durable copy, as when self-hosted.
110
+ return undefined;
109
111
  }
110
- // Framework session state on the framework-level file store; author kv,
111
- // evals, and A/B tables stay on the KV proxy (spread from `kv`, which
112
- // also carries the cursor-hosted marker for GET /v1/sessions).
112
+ // Full chain (env key file stored login): frozen engines listen
113
+ // unsigned and bind CURSOR_API_KEY_FILE at task time, after this
114
+ // definition is constructed.
115
+ const resolveApiKey = (): string | undefined => resolveApiKeySync()?.apiKey;
116
+ // Framework session state on the framework-level file store, which
117
+ // authorizes by store source id and so works for v2. Everything the KV
118
+ // proxy used to serve — author kv, eval runs, A/B samples, conditional
119
+ // writes — is inert; `defineStorage` derives the eval/A/B tables over the
120
+ // core, which means that bookkeeping reads back empty rather than 403ing.
121
+ //
122
+ // Restoring `cursorHostedStorage()` here once the proxy resolves v2
123
+ // deployments also means restoring the guard its `cas` group needs: a
124
+ // conditional write over a `session/` or `continuation/` key would run on
125
+ // the KV database while the plain core routes that key to the file store.
126
+ const kv = nullKvStorage();
113
127
  return defineStorage({
114
128
  name: "cursor-hosted (framework files)",
115
129
  policy,
130
+ // GET /v1/sessions still prefers the durable list: session records live
131
+ // on the framework store, which the composed core lists. Spread rather
132
+ // than named, the way cursorHostedStorage carries it: the marker is a
133
+ // platform stamp, not part of the authored config surface.
134
+ ...cursorHostedMarker,
116
135
  ...kv,
117
136
  ...composeFrameworkStorage({
118
137
  kv,
@@ -120,8 +139,5 @@ export function selectFrameworkStorageDefinition(
120
139
  cursorHostedFiles({ storeSourceId: frameworkStoreId, resolveApiKey })
121
140
  ),
122
141
  }),
123
- // CAS stays on the KV proxy but must refuse framework-file keys, or a
124
- // future coordination path would split-brain against the plain core.
125
- cas: frameworkGuardedCas(kv.cas),
126
142
  });
127
143
  }
@@ -0,0 +1,42 @@
1
+ import type { ChannelHandlerArgs } from "../types.js";
2
+
3
+ /**
4
+ * Hosted-admission hook a channel pack attaches to its definition.
5
+ *
6
+ * Hosted delivery looks this up from the mounted definition — it does not
7
+ * switch on `channelKind`. A new forge implements `dispatch` / `getOutcome`
8
+ * next to its webhook handler and calls {@link attachHostedAdmissionAdapter}
9
+ * from the channel constructor.
10
+ */
11
+ export type HostedAdmissionDispatchOutcome = "skip" | "task";
12
+
13
+ export type HostedAdmissionAdapter = {
14
+ kind: string;
15
+ /**
16
+ * Optional wrap of the capturing `send` / `waitUntil` args (GitHub uses
17
+ * this to propagate background-task errors). Identity when omitted.
18
+ */
19
+ prepareArgs?(args: ChannelHandlerArgs): ChannelHandlerArgs;
20
+ dispatch(input: {
21
+ eventType: string;
22
+ payload: unknown;
23
+ deliveryId: string;
24
+ args: ChannelHandlerArgs;
25
+ }): Promise<Response>;
26
+ getOutcome(response: Response): HostedAdmissionDispatchOutcome | undefined;
27
+ };
28
+
29
+ const adapters = new WeakMap<object, HostedAdmissionAdapter>();
30
+
31
+ export function attachHostedAdmissionAdapter(
32
+ definition: object,
33
+ adapter: HostedAdmissionAdapter
34
+ ): void {
35
+ adapters.set(definition, adapter);
36
+ }
37
+
38
+ export function getHostedAdmissionAdapter(
39
+ definition: object
40
+ ): HostedAdmissionAdapter | undefined {
41
+ return adapters.get(definition);
42
+ }
@@ -18,7 +18,12 @@ export type HostedAdmissionManagedAutomation = {
18
18
  export type HostedAdmission = {
19
19
  v: 2;
20
20
  channelId: string;
21
- channelKind: "github";
21
+ /**
22
+ * Mailbox / subscription channel kind (`github`, `origin`, …). The guest
23
+ * resolves dispatch from the mounted definition's hosted-admission
24
+ * adapter; this field is not an allowlist.
25
+ */
26
+ channelKind: string;
22
27
  eventType: string;
23
28
  deliveryId: string;
24
29
  sessionMode: "event" | "continuation";
@@ -107,7 +112,7 @@ export function parseHostedAdmission(
107
112
  !Object.hasOwn(body, "payload") ||
108
113
  body.v !== HOSTED_DELIVERY_PROTOCOL_VERSION ||
109
114
  !boundedString(body.channelId, 128) ||
110
- body.channelKind !== "github" ||
115
+ !boundedString(body.channelKind, 128) ||
111
116
  !boundedString(body.eventType, 128) ||
112
117
  !boundedString(body.deliveryId, 256) ||
113
118
  (body.sessionMode !== "event" && body.sessionMode !== "continuation") ||
@@ -1,10 +1,4 @@
1
1
  import { randomBytes } from "node:crypto";
2
- import {
3
- dispatchGitHubWebhook,
4
- getGitHubChannelOptions,
5
- getGitHubDispatchOutcome,
6
- withPropagatingGitHubBackgroundErrors,
7
- } from "../channels/github/github-channel.js";
8
2
  import type {
9
3
  AuthPolicy,
10
4
  ChannelHandlerArgs,
@@ -14,6 +8,10 @@ import {
14
8
  createDeferredChannelSession,
15
9
  DeferredChannelSessionError,
16
10
  } from "./deferred-channel-session.js";
11
+ import {
12
+ getHostedAdmissionAdapter,
13
+ type HostedAdmissionAdapter,
14
+ } from "./hosted-admission-adapter.js";
17
15
  import {
18
16
  HostedAdmissionLocalSendError,
19
17
  runDuringHostedAdmission,
@@ -30,12 +28,19 @@ import {
30
28
  import {
31
29
  abortReasonFromTurnStatus,
32
30
  HOSTED_EXECUTION_DIAG_RUNNING_STATUS,
31
+ hostedErrorStackSnippet,
33
32
  hostedExecutionDiagId,
34
33
  writeHostedExecutionDiag,
35
34
  } from "./hosted-execution-diag.js";
36
35
  import { recordHostedManagedAutomation } from "./hosted-managed-automation.js";
37
36
  import { errorResponse, readJsonObject } from "./http-channel.js";
37
+ import type { LogRingEntry } from "./log-ring.js";
38
38
  import type { Router } from "./router.js";
39
+ import {
40
+ type SessionRunExecutionWrite,
41
+ serializeSessionRunError,
42
+ snapshotSessionRunLogs,
43
+ } from "./session-run-log.js";
39
44
 
40
45
  type DispatchInput = {
41
46
  delivery: HostedAdmission;
@@ -115,17 +120,20 @@ function nonRetryableRejection(error: unknown): AdmissionDispatch | undefined {
115
120
  }
116
121
 
117
122
  /**
118
- * Run authored admission without creating a framework session. V1 GitHub
123
+ * Run authored admission without creating a framework session. Authored
119
124
  * `task` callbacks execute as sessionless work; the first `send` is captured
120
125
  * and executed only after the control plane allocates `ses_…`. Capturing
121
126
  * does not terminate the caller — `send` returns an inert deferred handle so
122
127
  * a task body runs to completion before the admission response is computed.
128
+ *
129
+ * Dispatch is the adapter the mounted channel attached. Hosted delivery
130
+ * does not switch on `channelKind`.
123
131
  */
124
132
  export async function dispatchHostedAdmission(
125
133
  input: DispatchInput
126
134
  ): Promise<AdmissionDispatch> {
127
- const github = getGitHubChannelOptions(input.definition);
128
- if (github === undefined) {
135
+ const adapter = getHostedAdmissionAdapter(input.definition);
136
+ if (adapter === undefined || adapter.kind !== input.delivery.channelKind) {
129
137
  return {
130
138
  response: Response.json(
131
139
  { ok: false, error: "channel_kind_mismatch" },
@@ -135,16 +143,16 @@ export async function dispatchHostedAdmission(
135
143
  }
136
144
  // The admission window marker follows the async graph into task bodies so
137
145
  // the engine can refuse local sends that would bypass the capture stub.
138
- return runDuringHostedAdmission(() => runAdmission(input, github));
146
+ return runDuringHostedAdmission(() => runAdmission(input, adapter));
139
147
  }
140
148
 
141
149
  async function runAdmission(
142
150
  input: DispatchInput,
143
- github: NonNullable<ReturnType<typeof getGitHubChannelOptions>>
151
+ adapter: HostedAdmissionAdapter
144
152
  ): Promise<AdmissionDispatch> {
145
153
  const background: Promise<unknown>[] = [];
146
154
  let captured: { message: string; options: SendMessageOptions } | undefined;
147
- const args = withPropagatingGitHubBackgroundErrors({
155
+ const capturingArgs: ChannelHandlerArgs = {
148
156
  ...input.args,
149
157
  send: async (message, options = {}) => {
150
158
  if (captured !== undefined) {
@@ -156,12 +164,12 @@ async function runAdmission(
156
164
  waitUntil: (promise) => {
157
165
  background.push(promise);
158
166
  },
159
- });
167
+ };
168
+ const args = adapter.prepareArgs?.(capturingArgs) ?? capturingArgs;
160
169
  let dispatched: Response;
161
170
  try {
162
- dispatched = await dispatchGitHubWebhook({
163
- options: github,
164
- eventName: input.delivery.eventType,
171
+ dispatched = await adapter.dispatch({
172
+ eventType: input.delivery.eventType,
165
173
  payload: input.delivery.payload,
166
174
  deliveryId: input.delivery.deliveryId,
167
175
  args,
@@ -212,7 +220,7 @@ async function runAdmission(
212
220
  },
213
221
  };
214
222
  }
215
- const outcome = getGitHubDispatchOutcome(dispatched);
223
+ const outcome = adapter.getOutcome(dispatched);
216
224
  if (outcome === undefined) {
217
225
  return {
218
226
  response: Response.json(
@@ -264,6 +272,15 @@ export function registerHostedDeliveryRoute(input: {
264
272
  * `hosted-execution.json` from that path after the POST returns.
265
273
  */
266
274
  stateRoot?: string;
275
+ /**
276
+ * Upsert this execution into the v2 session-level framework-store log.
277
+ * Called for the pre-turn `running` claim and every terminal outcome so
278
+ * a dead Node process still leaves a leftover. Soft-fail inside the
279
+ * store; this path still awaits so a parked pod cannot shed the write.
280
+ */
281
+ persistSessionRun?(write: SessionRunExecutionWrite): Promise<void>;
282
+ /** Snapshot of the process log ring at persist time. */
283
+ snapshotLogs?(): LogRingEntry[];
267
284
  }): void {
268
285
  const pending = new Map<
269
286
  string,
@@ -404,30 +421,64 @@ export function registerHostedDeliveryRoute(input: {
404
421
  abortReason?:
405
422
  | ReturnType<typeof abortReasonFromTurnStatus>
406
423
  | "execute_throw";
424
+ error?: unknown;
407
425
  }): Promise<void> => {
408
- if (input.stateRoot === undefined) {
409
- return;
410
- }
426
+ const durationMs = Math.max(
427
+ 0,
428
+ Math.round(performance.now() - startedAt)
429
+ );
411
430
  const message =
412
431
  fields.message === undefined
413
432
  ? undefined
414
433
  : boundedMessage(fields.message);
415
- await writeHostedExecutionDiag(input.stateRoot, {
416
- v: 1,
417
- executionId,
418
- writtenAt: new Date().toISOString(),
419
- sessionPublicId: execution.sessionPublicId,
420
- sessionId: fields.sessionId,
421
- turnStatus: fields.turnStatus,
422
- durationMs: Math.max(0, Math.round(performance.now() - startedAt)),
423
- ...(message === undefined ? {} : { message }),
424
- ...(message !== undefined && /abort/i.test(message)
425
- ? { signalAborted: true }
426
- : {}),
427
- ...(fields.abortReason === undefined
428
- ? {}
429
- : { abortReason: fields.abortReason }),
430
- });
434
+ const stack =
435
+ fields.error === undefined
436
+ ? undefined
437
+ : hostedErrorStackSnippet(fields.error);
438
+ if (input.stateRoot !== undefined) {
439
+ await writeHostedExecutionDiag(input.stateRoot, {
440
+ v: 1,
441
+ executionId,
442
+ writtenAt: new Date().toISOString(),
443
+ sessionPublicId: execution.sessionPublicId,
444
+ sessionId: fields.sessionId,
445
+ turnStatus: fields.turnStatus,
446
+ durationMs,
447
+ ...(message === undefined ? {} : { message }),
448
+ ...(stack === undefined || stack.length === 0 ? {} : { stack }),
449
+ ...(message !== undefined && /abort/i.test(message)
450
+ ? { signalAborted: true }
451
+ : {}),
452
+ ...(fields.abortReason === undefined
453
+ ? {}
454
+ : { abortReason: fields.abortReason }),
455
+ });
456
+ }
457
+ if (input.persistSessionRun === undefined) {
458
+ return;
459
+ }
460
+ const error =
461
+ fields.error === undefined
462
+ ? undefined
463
+ : serializeSessionRunError(fields.error);
464
+ try {
465
+ await input.persistSessionRun({
466
+ executionId,
467
+ writtenAt: new Date().toISOString(),
468
+ sessionPublicId: execution.sessionPublicId,
469
+ sessionId: fields.sessionId,
470
+ turnStatus: fields.turnStatus,
471
+ durationMs,
472
+ ...(fields.abortReason === undefined
473
+ ? {}
474
+ : { abortReason: fields.abortReason }),
475
+ ...(message === undefined ? {} : { message }),
476
+ ...(error === undefined ? {} : { error }),
477
+ logs: snapshotSessionRunLogs(input.snapshotLogs?.() ?? []),
478
+ });
479
+ } catch {
480
+ // Soft-fail: a store outage must not change the HTTP reply.
481
+ }
431
482
  };
432
483
  // Claim the file before the turn runs. The host reads it after its
433
484
  // own POST returns, which for a 9-minute timeout is while this turn
@@ -486,6 +537,7 @@ export function registerHostedDeliveryRoute(input: {
486
537
  turnStatus: "error",
487
538
  message: error instanceof Error ? error.message : String(error),
488
539
  abortReason: "execute_throw",
540
+ error,
489
541
  });
490
542
  throw error;
491
543
  }
@@ -46,6 +46,8 @@ export type HostedExecutionDiag = {
46
46
  turnStatus: string;
47
47
  durationMs: number;
48
48
  message?: string;
49
+ /** First frames of a thrown Error; omitted on soft turn failures. */
50
+ stack?: string;
49
51
  signalAborted?: boolean;
50
52
  requestId?: string;
51
53
  abortReason?: HostedExecutionDiagAbortReason;
@@ -63,6 +65,7 @@ const DIAG_KEYS = new Set([
63
65
  "turnStatus",
64
66
  "durationMs",
65
67
  "message",
68
+ "stack",
66
69
  "signalAborted",
67
70
  "requestId",
68
71
  "abortReason",
@@ -86,6 +89,42 @@ function boundedOptionalString(
86
89
  : undefined;
87
90
  }
88
91
 
92
+ /** First line + frames; factory `last_error` is 1024 chars with a TAS prefix. */
93
+ export const HOSTED_ERROR_STACK_LINE_COUNT = 5;
94
+ export const HOSTED_ERROR_SNIPPET_MAX_CHARS = 700;
95
+
96
+ /**
97
+ * Bounded throw text for the hosted 500 body and the on-disk diag `stack`.
98
+ * Prefers `Error.stack` (message line + `at` frames) so `last_error` is not
99
+ * the generic "Internal server error." when the throw has a stack.
100
+ */
101
+ export function hostedErrorStackSnippet(error: unknown): string {
102
+ if (error instanceof Error) {
103
+ const stack = error.stack?.trim();
104
+ if (stack !== undefined && stack.length > 0) {
105
+ const lines = stack
106
+ .split("\n")
107
+ .map((line) => line.trimEnd())
108
+ .filter((line) => line.trim().length > 0)
109
+ .slice(0, HOSTED_ERROR_STACK_LINE_COUNT);
110
+ const text = lines.join("\n");
111
+ return text.length > HOSTED_ERROR_SNIPPET_MAX_CHARS
112
+ ? `${text.slice(0, HOSTED_ERROR_SNIPPET_MAX_CHARS)}…`
113
+ : text;
114
+ }
115
+ return error.message.length > HOSTED_ERROR_SNIPPET_MAX_CHARS
116
+ ? `${error.message.slice(0, HOSTED_ERROR_SNIPPET_MAX_CHARS)}…`
117
+ : error.message;
118
+ }
119
+ const text = String(error);
120
+ if (text.length === 0) {
121
+ return "";
122
+ }
123
+ return text.length > HOSTED_ERROR_SNIPPET_MAX_CHARS
124
+ ? `${text.slice(0, HOSTED_ERROR_SNIPPET_MAX_CHARS)}…`
125
+ : text;
126
+ }
127
+
89
128
  export function hostedExecutionDiagPath(stateRoot: string): string {
90
129
  return join(stateRoot, HOSTED_EXECUTION_DIAG_FILENAME);
91
130
  }
@@ -147,6 +186,10 @@ export function parseHostedExecutionDiag(
147
186
  return undefined;
148
187
  }
149
188
  const message = boundedOptionalString(body.message, 1024);
189
+ const stack = boundedOptionalString(
190
+ body.stack,
191
+ HOSTED_ERROR_SNIPPET_MAX_CHARS
192
+ );
150
193
  const requestId = boundedOptionalString(body.requestId, 128);
151
194
  const abortReason =
152
195
  body.abortReason === undefined
@@ -173,6 +216,7 @@ export function parseHostedExecutionDiag(
173
216
  turnStatus: body.turnStatus,
174
217
  durationMs: body.durationMs,
175
218
  ...(message === undefined ? {} : { message }),
219
+ ...(stack === undefined ? {} : { stack }),
176
220
  ...(body.signalAborted === true ? { signalAborted: true } : {}),
177
221
  ...(requestId === undefined ? {} : { requestId }),
178
222
  ...(abortReason === undefined ? {} : { abortReason }),
@@ -196,6 +240,7 @@ export async function writeHostedExecutionDiag(
196
240
  serialized.length > HOSTED_EXECUTION_DIAG_MAX_BYTES
197
241
  ? JSON.stringify({
198
242
  ...diag,
243
+ stack: undefined,
199
244
  message: boundedOptionalString(diag.message, 256),
200
245
  })
201
246
  : serialized;
@@ -95,6 +95,13 @@ export function buildHttpChannelRoutes(args: {
95
95
  "Expected a JSON body with a string `message`."
96
96
  );
97
97
  }
98
+ if (body.asOf !== undefined && typeof body.asOf !== "string") {
99
+ return errorResponse(
100
+ 400,
101
+ "invalid_request",
102
+ "`asOf` must be an ISO-8601 instant with a timezone when provided."
103
+ );
104
+ }
98
105
  const message = await transformMessage(body.message, ctx.auth);
99
106
  const workspaceFiles = readWorkspaceFiles(body.workspaceFiles);
100
107
  // Per-session cloud attach (create only). Eval harness uses this to pin
@@ -105,6 +112,7 @@ export function buildHttpChannelRoutes(args: {
105
112
  ...(typeof body.title === "string" ? { title: body.title } : {}),
106
113
  ...(body.purpose === "eval" ? { purpose: "eval" as const } : {}),
107
114
  ...(body.dryRun === true ? { dryRun: true } : {}),
115
+ asOf: body.asOf,
108
116
  ...(workspaceFiles === undefined ? {} : { workspaceFiles }),
109
117
  ...(cloud === undefined ? {} : { cloud }),
110
118
  });
@@ -0,0 +1,74 @@
1
+ /**
2
+ * The inert KV half of hosted v2 storage.
3
+ *
4
+ * The control-plane KV proxy (`/internal/agent-serve/storage/*`) authorizes a
5
+ * request by mapping the caller's pod service account to a row in the v1
6
+ * deployment table. Architecture v2 registers its deployments in the factory
7
+ * control plane instead, so every proxy call from a v2 pod answers
8
+ * `403 No Agent Serve deployment is bound to this service account`. The
9
+ * lazy-restore path deliberately propagates read failures (a store outage must
10
+ * not read as "unknown continuation token"), so those 403s fail follow-up
11
+ * deliveries outright.
12
+ *
13
+ * This backend stands in for the proxy until it can resolve v2 deployments:
14
+ * writes are dropped, reads are definitive misses, and lists are empty. It is
15
+ * paired with the framework file store (see framework-file-storage.ts), which
16
+ * authorizes by store source id rather than by deployment row and therefore
17
+ * still carries session records, event transcripts, and the continuation
18
+ * index — the state sessions are restored from.
19
+ *
20
+ * Author KV is the one thing that must not silently accept writes here, since
21
+ * handlers read their own writes back. {@link AUTHOR_KV_UNSUPPORTED} marks the
22
+ * definition so `ctx.host.kv` falls back to `--state-root` disk instead, the
23
+ * same fallback a deployment with no configured backend gets.
24
+ */
25
+
26
+ import type { StorageConfig } from "../storage.js";
27
+ import type { JsonValue } from "../types.js";
28
+
29
+ /**
30
+ * Marker stamped on a definition whose KV core cannot serve `ctx.host.kv`,
31
+ * even though `get` is present for framework session state. Explicit
32
+ * `unique symbol` annotation required for `--isolatedDeclarations` (TS9010).
33
+ */
34
+ export const AUTHOR_KV_UNSUPPORTED: unique symbol = Symbol.for(
35
+ "@cursor/july.storage.author-kv-unsupported"
36
+ );
37
+
38
+ /** True when `value` was built from {@link nullKvStorage}. */
39
+ export function isAuthorKvUnsupported(value: unknown): boolean {
40
+ return (
41
+ typeof value === "object" &&
42
+ value !== null &&
43
+ (value as Record<symbol, unknown>)[AUTHOR_KV_UNSUPPORTED] === true
44
+ );
45
+ }
46
+
47
+ type NullKvStorageMethods = Required<
48
+ Pick<StorageConfig, "put" | "get" | "delete" | "list">
49
+ > & {
50
+ readonly [AUTHOR_KV_UNSUPPORTED]: true;
51
+ };
52
+
53
+ /**
54
+ * A KV core that stores nothing. `get` resolving `undefined` is honest here
55
+ * rather than dangerous: nothing was ever written, so a miss cannot shadow a
56
+ * session that exists elsewhere.
57
+ *
58
+ * No `cas` group and no `listNewest`: conditional writes over a store that
59
+ * forgets would report a lock acquired, and write-time ordering means nothing
60
+ * without writes.
61
+ */
62
+ export function nullKvStorage(): NullKvStorageMethods {
63
+ return {
64
+ [AUTHOR_KV_UNSUPPORTED]: true,
65
+ put(): void {},
66
+ get(): JsonValue | undefined {
67
+ return undefined;
68
+ },
69
+ delete(): void {},
70
+ list(): [] {
71
+ return [];
72
+ },
73
+ };
74
+ }