@cursor/july 0.1.80 → 0.1.82

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 (145) hide show
  1. package/AGENTS.md +0 -12
  2. package/dist/channels/github/cursor-account.d.ts +11 -0
  3. package/dist/channels/github/cursor-account.d.ts.map +1 -1
  4. package/dist/channels/github/cursor-account.js +13 -0
  5. package/dist/channels/github/github-channel.d.ts.map +1 -1
  6. package/dist/channels/github/github-channel.js +29 -1
  7. package/dist/channels/github/types.d.ts +8 -0
  8. package/dist/channels/github/types.d.ts.map +1 -1
  9. package/dist/channels/origin/origin-channel.d.ts.map +1 -1
  10. package/dist/channels/origin/origin-channel.js +32 -10
  11. package/dist/channels/origin/types.d.ts +7 -0
  12. package/dist/channels/origin/types.d.ts.map +1 -1
  13. package/dist/channels/slack/cursor-account.d.ts.map +1 -1
  14. package/dist/channels/slack/cursor-account.js +2 -2
  15. package/dist/channels/slack/dispatch.d.ts +3 -0
  16. package/dist/channels/slack/dispatch.d.ts.map +1 -1
  17. package/dist/channels/slack/dispatch.js +18 -18
  18. package/dist/channels/slack/interactive.d.ts +2 -0
  19. package/dist/channels/slack/interactive.d.ts.map +1 -1
  20. package/dist/channels/slack/interactive.js +4 -16
  21. package/dist/channels/slack/slack-channel.d.ts +1 -1
  22. package/dist/channels/slack/slack-channel.d.ts.map +1 -1
  23. package/dist/channels/slack/slack-channel.js +21 -10
  24. package/dist/channels/slack/types.d.ts +7 -0
  25. package/dist/channels/slack/types.d.ts.map +1 -1
  26. package/dist/channels.d.ts.map +1 -1
  27. package/dist/channels.js +4 -0
  28. package/dist/continuation.d.ts +20 -0
  29. package/dist/continuation.d.ts.map +1 -0
  30. package/dist/continuation.js +18 -0
  31. package/dist/docs/404.html +1 -1
  32. package/dist/docs/ab.html +2 -2
  33. package/dist/docs/assets/{app.e4_wtjUS.js → app.BEHVaPKt.js} +1 -1
  34. package/dist/docs/assets/chunks/{@localSearchIndexroot.BGiMJpnL.js → @localSearchIndexroot.Vvio4stV.js} +1 -1
  35. package/dist/docs/assets/chunks/{VPLocalSearchBox.DbdzyEie.js → VPLocalSearchBox.D2-5PKih.js} +1 -1
  36. package/dist/docs/assets/chunks/{theme.D8HpLrLK.js → theme.uNqJ4MUO.js} +2 -2
  37. package/dist/docs/building-with-agents.html +2 -2
  38. package/dist/docs/concepts.html +2 -2
  39. package/dist/docs/deployment.html +2 -2
  40. package/dist/docs/evals.html +2 -2
  41. package/dist/docs/example-agents/approval-buddy.html +2 -2
  42. package/dist/docs/example-agents/benny.html +2 -2
  43. package/dist/docs/example-agents/bugbot.html +2 -2
  44. package/dist/docs/example-agents/codebase-wiki.html +2 -2
  45. package/dist/docs/example-agents/codeowners-review.html +2 -2
  46. package/dist/docs/example-agents/concierge.html +2 -2
  47. package/dist/docs/example-agents/fsd.html +2 -2
  48. package/dist/docs/example-agents/index.html +2 -2
  49. package/dist/docs/example-agents/knowledge-base.html +2 -2
  50. package/dist/docs/example-agents/oncall.html +2 -2
  51. package/dist/docs/example-agents/security-reviewer.html +2 -2
  52. package/dist/docs/example-agents/slack-agent.html +2 -2
  53. package/dist/docs/example-agents/weather-agent.html +2 -2
  54. package/dist/docs/guides/agent-to-agent.html +2 -2
  55. package/dist/docs/guides/cloud-runtime.html +2 -2
  56. package/dist/docs/guides/convert-automation.html +2 -2
  57. package/dist/docs/guides/github.html +2 -2
  58. package/dist/docs/guides/human-in-the-loop.html +2 -2
  59. package/dist/docs/guides/mcp-oauth.html +2 -2
  60. package/dist/docs/guides/opentelemetry.html +2 -2
  61. package/dist/docs/guides/slack.html +2 -2
  62. package/dist/docs/guides/webhooks.html +2 -2
  63. package/dist/docs/hillclimbing.html +2 -2
  64. package/dist/docs/index.html +2 -2
  65. package/dist/docs/quickstart.html +2 -2
  66. package/dist/docs/reference/agent-config.html +2 -2
  67. package/dist/docs/reference/artifacts.html +2 -2
  68. package/dist/docs/reference/channels.html +2 -2
  69. package/dist/docs/reference/cli.html +2 -2
  70. package/dist/docs/reference/connections.html +2 -2
  71. package/dist/docs/reference/hooks.html +2 -2
  72. package/dist/docs/reference/http-api.html +2 -2
  73. package/dist/docs/reference/instructions.html +2 -2
  74. package/dist/docs/reference/playground.html +2 -2
  75. package/dist/docs/reference/project-layout.html +2 -2
  76. package/dist/docs/reference/prompt.html +2 -2
  77. package/dist/docs/reference/schedules.html +2 -2
  78. package/dist/docs/reference/sessions.html +2 -2
  79. package/dist/docs/reference/skills.html +2 -2
  80. package/dist/docs/reference/subagents.html +2 -2
  81. package/dist/docs/reference/tools.html +2 -2
  82. package/dist/docs/scaffolding-agents.html +2 -2
  83. package/dist/docs/storage.html +2 -2
  84. package/dist/docs/templates/agentic-owners.html +2 -2
  85. package/dist/docs/templates/demo.html +2 -2
  86. package/dist/docs/templates/pr-autofixer.html +2 -2
  87. package/dist/docs/templates/security-reviewer.html +2 -2
  88. package/dist/docs/templates/triage.html +2 -2
  89. package/dist/docs/troubleshooting.html +2 -2
  90. package/dist/files.d.ts +9 -3
  91. package/dist/files.d.ts.map +1 -1
  92. package/dist/files.js +9 -3
  93. package/dist/index.d.ts +5 -1
  94. package/dist/index.d.ts.map +1 -1
  95. package/dist/index.js +4 -1
  96. package/dist/internal/continuation-channel.d.ts +29 -0
  97. package/dist/internal/continuation-channel.d.ts.map +1 -0
  98. package/dist/internal/continuation-channel.js +88 -0
  99. package/dist/internal/continuation-identity.d.ts +95 -0
  100. package/dist/internal/continuation-identity.d.ts.map +1 -0
  101. package/dist/internal/continuation-identity.js +445 -0
  102. package/dist/internal/deploy-manifest.d.ts +12 -2
  103. package/dist/internal/deploy-manifest.d.ts.map +1 -1
  104. package/dist/internal/deploy-manifest.js +15 -7
  105. package/dist/internal/discovery.d.ts.map +1 -1
  106. package/dist/internal/discovery.js +32 -5
  107. package/dist/internal/server.d.ts.map +1 -1
  108. package/dist/internal/server.js +18 -8
  109. package/dist/internal/session-engine.d.ts +24 -0
  110. package/dist/internal/session-engine.d.ts.map +1 -1
  111. package/dist/internal/session-engine.js +104 -34
  112. package/dist/internal/session-store.d.ts.map +1 -1
  113. package/dist/internal/session-store.js +6 -0
  114. package/dist/internal/skills-sync.d.ts +25 -0
  115. package/dist/internal/skills-sync.d.ts.map +1 -0
  116. package/dist/internal/skills-sync.js +96 -0
  117. package/dist/playground/assets/{index-DJzcIeGt.css → index-ChETdG-_.css} +1 -1
  118. package/dist/playground/assets/{index-K3Sxyx0I.js → index-CkuIrhg4.js} +3 -3
  119. package/dist/playground/index.html +2 -2
  120. package/dist/types.d.ts +44 -0
  121. package/dist/types.d.ts.map +1 -1
  122. package/package.json +1 -1
  123. package/src/channels/github/cursor-account.ts +20 -0
  124. package/src/channels/github/github-channel.ts +52 -1
  125. package/src/channels/github/types.ts +8 -0
  126. package/src/channels/origin/origin-channel.ts +48 -10
  127. package/src/channels/origin/types.ts +7 -0
  128. package/src/channels/slack/cursor-account.ts +1 -0
  129. package/src/channels/slack/dispatch.ts +44 -2
  130. package/src/channels/slack/interactive.ts +5 -0
  131. package/src/channels/slack/slack-channel.ts +23 -2
  132. package/src/channels/slack/types.ts +7 -0
  133. package/src/channels.ts +7 -0
  134. package/src/continuation.ts +49 -0
  135. package/src/files.ts +10 -3
  136. package/src/index.ts +30 -1
  137. package/src/internal/continuation-channel.ts +129 -0
  138. package/src/internal/continuation-identity.ts +597 -0
  139. package/src/internal/deploy-manifest.ts +36 -3
  140. package/src/internal/discovery.ts +40 -2
  141. package/src/internal/server.ts +23 -9
  142. package/src/internal/session-engine.ts +132 -37
  143. package/src/internal/session-store.ts +6 -0
  144. package/src/internal/skills-sync.ts +107 -0
  145. package/src/types.ts +55 -0
@@ -0,0 +1,597 @@
1
+ /**
2
+ * Continuation identity is a closed selector string. Omitted `continuation`
3
+ * on a channel constructor stays `/v1` (engine pack minting) so existing
4
+ * agents do not change: `githubChannel()` → `github.pr/v1`, `slackChannel()`
5
+ * → `slack.thread/v1`, `originChannel()` → `origin.pr/v1`, `httpChannel()`
6
+ * → `none/v1`.
7
+ *
8
+ * Opt the whole agent into the v2 control plane with
9
+ * `defineAgent({ architecture: "v2" })`. That stamps `/v2` defaults from
10
+ * the channel kind and declared events (`github.pr/v2`, `github.issue/v2`,
11
+ * `github.none/v2`, `slack.thread/v2`, …) onto every channel that omitted
12
+ * a selector. An explicit per-channel selector still overrides. JSONPath
13
+ * and per-agent format strings are rejected.
14
+ */
15
+
16
+ import { randomBytes } from "node:crypto";
17
+
18
+ export type ConversationType = "pr" | "issue" | "thread" | "origin-pr";
19
+
20
+ export const CONTINUATION_SELECTORS = [
21
+ "github.pr/v1",
22
+ "github.pr/v2",
23
+ "github.issue/v1",
24
+ "github.issue/v2",
25
+ "github.none/v2",
26
+ "slack.thread/v1",
27
+ "slack.thread/v2",
28
+ "slack.none/v2",
29
+ "origin.pr/v1",
30
+ "origin.pr/v2",
31
+ "origin.none/v2",
32
+ "none/v1",
33
+ "none/v2",
34
+ ] as const;
35
+
36
+ export type ContinuationSelector = (typeof CONTINUATION_SELECTORS)[number];
37
+ export type ContinuationIdentity = ContinuationSelector;
38
+ /** Agent-root protocol version. Omit / `"v1"` keeps engine minting. */
39
+ export type ContinuationProtocolVersion = "v1" | "v2";
40
+
41
+ export type ParsedContinuation = {
42
+ selector: ContinuationSelector;
43
+ version: 1 | 2;
44
+ type: ConversationType | "none";
45
+ kind: "github" | "slack" | "origin" | "http";
46
+ };
47
+
48
+ export type ContinuationClaim = {
49
+ type: ConversationType;
50
+ resource:
51
+ | { owner: string; repo: string; number: number }
52
+ | { channelId: string; threadTs: string }
53
+ | { fullName: string; number: number };
54
+ };
55
+
56
+ export type MailboxContinuation = {
57
+ key: string;
58
+ };
59
+
60
+ export type ContinuationBuiltin = {
61
+ type: ConversationType;
62
+ kind: "github" | "slack" | "origin";
63
+ events: readonly string[];
64
+ format: string;
65
+ };
66
+
67
+ export const BUILTINS: readonly ContinuationBuiltin[] = [
68
+ {
69
+ type: "pr",
70
+ kind: "github",
71
+ events: [
72
+ "pull_request",
73
+ "issue_comment",
74
+ "pull_request_review",
75
+ "pull_request_review_comment",
76
+ "check_suite",
77
+ "check_run",
78
+ ],
79
+ format: "github-pr:{fullName}#{number}",
80
+ },
81
+ {
82
+ type: "issue",
83
+ kind: "github",
84
+ events: ["issues", "issue_comment"],
85
+ format: "github-issue:{fullName}#{number}",
86
+ },
87
+ {
88
+ type: "thread",
89
+ kind: "slack",
90
+ events: ["message", "app_mention"],
91
+ format: "{channelId}:{threadTs}",
92
+ },
93
+ {
94
+ type: "origin-pr",
95
+ kind: "origin",
96
+ events: [
97
+ "pull_request.created",
98
+ "pull_request.published",
99
+ "pull_request.head_ref.pushed",
100
+ "pull_request.base_ref.updated",
101
+ "pull_request.closed",
102
+ "pull_request.merged",
103
+ "pull_request.comment.created",
104
+ "pull_request.review.submitted",
105
+ ],
106
+ format: "origin-pr:{fullName}#{number}",
107
+ },
108
+ ] as const;
109
+
110
+ export class V2EngineError extends Error {
111
+ constructor(message: string) {
112
+ super(message);
113
+ this.name = "V2EngineError";
114
+ }
115
+ }
116
+
117
+ const SELECTOR_FIELDS: {
118
+ [K in ContinuationSelector]: Omit<ParsedContinuation, "selector">;
119
+ } = {
120
+ "github.pr/v1": { version: 1, type: "pr", kind: "github" },
121
+ "github.pr/v2": { version: 2, type: "pr", kind: "github" },
122
+ "github.issue/v1": { version: 1, type: "issue", kind: "github" },
123
+ "github.issue/v2": { version: 2, type: "issue", kind: "github" },
124
+ "github.none/v2": { version: 2, type: "none", kind: "github" },
125
+ "slack.thread/v1": { version: 1, type: "thread", kind: "slack" },
126
+ "slack.thread/v2": { version: 2, type: "thread", kind: "slack" },
127
+ "slack.none/v2": { version: 2, type: "none", kind: "slack" },
128
+ "origin.pr/v1": { version: 1, type: "origin-pr", kind: "origin" },
129
+ "origin.pr/v2": { version: 2, type: "origin-pr", kind: "origin" },
130
+ "origin.none/v2": { version: 2, type: "none", kind: "origin" },
131
+ "none/v1": { version: 1, type: "none", kind: "http" },
132
+ "none/v2": { version: 2, type: "none", kind: "http" },
133
+ };
134
+
135
+ export function isContinuationSelector(
136
+ value: string
137
+ ): value is ContinuationSelector {
138
+ return Object.hasOwn(SELECTOR_FIELDS, value);
139
+ }
140
+
141
+ export function defaultContinuationSelector(
142
+ kind?: string,
143
+ version: ContinuationProtocolVersion = "v1",
144
+ eventTypes?: readonly string[]
145
+ ): ContinuationSelector {
146
+ if (version !== "v2") {
147
+ switch (kind) {
148
+ case "github":
149
+ return "github.pr/v1";
150
+ case "slack":
151
+ return "slack.thread/v1";
152
+ case "origin":
153
+ return "origin.pr/v1";
154
+ default:
155
+ return "none/v1";
156
+ }
157
+ }
158
+ switch (kind) {
159
+ case "github":
160
+ return githubV2Default(eventTypes);
161
+ case "slack":
162
+ return "slack.thread/v2";
163
+ case "origin":
164
+ return originV2Default(eventTypes);
165
+ default:
166
+ return "none/v2";
167
+ }
168
+ }
169
+
170
+ function githubV2Default(eventTypes?: readonly string[]): ContinuationSelector {
171
+ const events = eventTypes ?? [];
172
+ const issueEvents = new Set(builtinForType("issue")?.events ?? []);
173
+ const prEvents = new Set(builtinForType("pr")?.events ?? []);
174
+ if (events.length > 0 && events.every((event) => issueEvents.has(event))) {
175
+ return "github.issue/v2";
176
+ }
177
+ if (events.some((event) => !prEvents.has(event))) {
178
+ return "github.none/v2";
179
+ }
180
+ return "github.pr/v2";
181
+ }
182
+
183
+ function originV2Default(eventTypes?: readonly string[]): ContinuationSelector {
184
+ const events = eventTypes ?? [];
185
+ const prEvents = new Set(builtinForType("origin-pr")?.events ?? []);
186
+ if (events.some((event) => !prEvents.has(event))) {
187
+ return "origin.none/v2";
188
+ }
189
+ return "origin.pr/v2";
190
+ }
191
+
192
+ export function parseContinuationSelector(
193
+ selector: string
194
+ ): ParsedContinuation | undefined {
195
+ if (!isContinuationSelector(selector)) {
196
+ return undefined;
197
+ }
198
+ return { selector, ...SELECTOR_FIELDS[selector] };
199
+ }
200
+
201
+ export function resolveContinuation(
202
+ raw?: ContinuationIdentity,
203
+ kind?: string
204
+ ): ParsedContinuation {
205
+ const selector = raw ?? defaultContinuationSelector(kind);
206
+ const parsed = parseContinuationSelector(selector);
207
+ if (parsed === undefined) {
208
+ throw new V2EngineError(
209
+ `v2: invalid continuation selector ${JSON.stringify(selector)}`
210
+ );
211
+ }
212
+ return parsed;
213
+ }
214
+
215
+ export function normalizeIdentity(
216
+ raw?: ContinuationIdentity,
217
+ kind?: string
218
+ ): ContinuationIdentity {
219
+ return resolveContinuation(raw, kind).selector;
220
+ }
221
+
222
+ export function continuationSource(
223
+ identity?: ContinuationIdentity,
224
+ kind?: string
225
+ ): "engine" | "control-plane" {
226
+ return resolveContinuation(identity, kind).version === 2
227
+ ? "control-plane"
228
+ : "engine";
229
+ }
230
+
231
+ export function isV2DurableContinuationKey(key: string): boolean {
232
+ return key.startsWith("cont:") || key.startsWith("evt:");
233
+ }
234
+
235
+ export function newEvtKey(): string {
236
+ return `evt:${randomBytes(16).toString("hex")}`;
237
+ }
238
+
239
+ export function builtinForType(
240
+ type: ConversationType
241
+ ): ContinuationBuiltin | undefined {
242
+ return BUILTINS.find((builtin) => builtin.type === type);
243
+ }
244
+
245
+ export function formatConversationKey(
246
+ type: ConversationType,
247
+ fields: {
248
+ fullName?: string;
249
+ number?: number;
250
+ channelId?: string;
251
+ threadTs?: string;
252
+ }
253
+ ): string {
254
+ switch (type) {
255
+ case "pr":
256
+ return `github-pr:${fields.fullName}#${fields.number}`;
257
+ case "issue":
258
+ return `github-issue:${fields.fullName}#${fields.number}`;
259
+ case "origin-pr":
260
+ return `origin-pr:${fields.fullName}#${fields.number}`;
261
+ case "thread":
262
+ return `${fields.channelId}:${fields.threadTs}`;
263
+ }
264
+ }
265
+
266
+ export function mintClaim(claim: ContinuationClaim): string {
267
+ const resource = claim.resource;
268
+ if ("channelId" in resource && "threadTs" in resource) {
269
+ if (claim.type !== "thread") {
270
+ throw new V2EngineError(
271
+ `v2: claim type ${claim.type} does not match thread resource`
272
+ );
273
+ }
274
+ return `cont:${formatConversationKey("thread", resource)}`;
275
+ }
276
+ const fullName =
277
+ "fullName" in resource
278
+ ? resource.fullName
279
+ : `${resource.owner}/${resource.repo}`;
280
+ if (claim.type === "thread") {
281
+ throw new V2EngineError("v2: thread claim requires channelId and threadTs");
282
+ }
283
+ return `cont:${formatConversationKey(claim.type, {
284
+ fullName,
285
+ number: resource.number,
286
+ })}`;
287
+ }
288
+
289
+ export function handshakeContinuation(input: {
290
+ kind?: string;
291
+ eventTypes?: readonly string[];
292
+ identity?: ContinuationIdentity;
293
+ }): { ok: true } | { ok: false; error: string } {
294
+ if (input.identity === undefined) {
295
+ return { ok: true };
296
+ }
297
+ const parsed = parseContinuationSelector(input.identity);
298
+ if (parsed === undefined) {
299
+ return {
300
+ ok: false,
301
+ error: `v2: invalid continuation selector ${JSON.stringify(input.identity)}`,
302
+ };
303
+ }
304
+ if (parsed.version !== 2 || parsed.type === "none") {
305
+ return { ok: true };
306
+ }
307
+ if (
308
+ input.kind !== undefined &&
309
+ parsed.kind !== "http" &&
310
+ input.kind !== parsed.kind
311
+ ) {
312
+ return {
313
+ ok: false,
314
+ error: `v2: continuation ${parsed.selector} does not match channel kind ${input.kind}`,
315
+ };
316
+ }
317
+ const builtin = builtinForType(parsed.type);
318
+ if (builtin === undefined) {
319
+ return {
320
+ ok: false,
321
+ error: `v2: unknown continuation type ${parsed.type}`,
322
+ };
323
+ }
324
+ const eventTypes = input.eventTypes ?? [];
325
+ const allowed = new Set(builtin.events);
326
+ const unexpected = eventTypes.filter((event) => !allowed.has(event));
327
+ if (unexpected.length > 0) {
328
+ return {
329
+ ok: false,
330
+ error: `v2: eventTypes ${unexpected.join(", ")} are not in the ${parsed.type} membership`,
331
+ };
332
+ }
333
+ return { ok: true };
334
+ }
335
+
336
+ /**
337
+ * CP/pack extract. Returns a `cont:` key when the payload binds the
338
+ * identity type; undefined on membership miss or missing number (caller
339
+ * stamps `evt:` and does not join the conversation index).
340
+ */
341
+ export function extractContinuation(
342
+ type: ConversationType,
343
+ eventName: string,
344
+ payload: unknown
345
+ ): string | undefined {
346
+ const builtin = builtinForType(type);
347
+ if (builtin === undefined) {
348
+ return undefined;
349
+ }
350
+ if (!builtin.events.includes(eventName)) {
351
+ return undefined;
352
+ }
353
+ switch (type) {
354
+ case "pr":
355
+ case "issue":
356
+ return extractGitHub(type, eventName, payload);
357
+ case "thread":
358
+ return extractSlack(payload);
359
+ case "origin-pr":
360
+ return extractOrigin(payload);
361
+ }
362
+ }
363
+
364
+ export function stampMailboxContinuation(
365
+ identity: ContinuationIdentity,
366
+ eventName: string,
367
+ payload: unknown,
368
+ kind?: string
369
+ ): MailboxContinuation {
370
+ const parsed = resolveContinuation(identity, kind);
371
+ if (parsed.version !== 2) {
372
+ throw new V2EngineError("v2: refusing to stamp a mailbox key for v1");
373
+ }
374
+ if (parsed.type === "none") {
375
+ return { key: newEvtKey() };
376
+ }
377
+ const extracted = extractContinuation(parsed.type, eventName, payload);
378
+ if (extracted === undefined) {
379
+ return { key: newEvtKey() };
380
+ }
381
+ return { key: extracted };
382
+ }
383
+
384
+ export function resolveV2Key(
385
+ identity: ContinuationIdentity,
386
+ options: {
387
+ claim?: ContinuationClaim;
388
+ continuationToken?: string;
389
+ mailboxContinuation?: MailboxContinuation;
390
+ },
391
+ kind?: string
392
+ ): string {
393
+ const parsed = resolveContinuation(identity, kind);
394
+ if (parsed.version !== 2) {
395
+ throw new V2EngineError("v2: resolveV2Key requires a /v2 selector");
396
+ }
397
+ if (options.continuationToken !== undefined) {
398
+ throw new V2EngineError(
399
+ "v2: do not pass continuationToken; use mailboxContinuation or claim"
400
+ );
401
+ }
402
+ if (options.claim !== undefined) {
403
+ if (parsed.type === "none") {
404
+ throw new V2EngineError("v2: type none cannot take a claim");
405
+ }
406
+ if (options.claim.type !== parsed.type) {
407
+ throw new V2EngineError(
408
+ `v2: claim type ${options.claim.type} does not match identity type ${parsed.type}`
409
+ );
410
+ }
411
+ return mintClaim(options.claim);
412
+ }
413
+ if (options.mailboxContinuation !== undefined) {
414
+ return options.mailboxContinuation.key;
415
+ }
416
+ if (parsed.type === "none") {
417
+ return newEvtKey();
418
+ }
419
+ throw new V2EngineError(
420
+ "v2: no CP key on mailbox and no claim; refusing to mint in the engine"
421
+ );
422
+ }
423
+
424
+ export function bindMailboxContinuationSend<
425
+ TSession,
426
+ TOptions extends { mailboxContinuation?: MailboxContinuation },
427
+ >(
428
+ send: (message: string, options?: TOptions) => Promise<TSession>,
429
+ mailbox: MailboxContinuation | undefined
430
+ ): (message: string, options?: TOptions) => Promise<TSession> {
431
+ if (mailbox === undefined) {
432
+ return send;
433
+ }
434
+ return (message, options) =>
435
+ send(message, {
436
+ ...(options as TOptions),
437
+ mailboxContinuation: options?.mailboxContinuation ?? mailbox,
438
+ } as TOptions);
439
+ }
440
+
441
+ function extractGitHub(
442
+ type: "pr" | "issue",
443
+ eventName: string,
444
+ payload: unknown
445
+ ): string | undefined {
446
+ const record = asRecord(payload);
447
+ if (record === undefined) {
448
+ return undefined;
449
+ }
450
+ const fullName = githubFullName(record);
451
+ if (fullName === undefined) {
452
+ return undefined;
453
+ }
454
+ const bound = githubBoundConversation(eventName, record);
455
+ if (bound === undefined || bound.type !== type) {
456
+ return undefined;
457
+ }
458
+ return `cont:${formatConversationKey(type, {
459
+ fullName,
460
+ number: bound.number,
461
+ })}`;
462
+ }
463
+
464
+ function githubBoundConversation(
465
+ eventName: string,
466
+ payload: Record<string, unknown>
467
+ ): { type: "pr" | "issue"; number: number } | undefined {
468
+ switch (eventName) {
469
+ case "pull_request":
470
+ case "pull_request_review":
471
+ case "pull_request_review_comment":
472
+ case "pull_request_review_thread": {
473
+ const number = positiveInt(asRecord(payload.pull_request)?.number);
474
+ return number === undefined ? undefined : { type: "pr", number };
475
+ }
476
+ case "issues": {
477
+ const number = positiveInt(asRecord(payload.issue)?.number);
478
+ return number === undefined ? undefined : { type: "issue", number };
479
+ }
480
+ case "issue_comment": {
481
+ const issue = asRecord(payload.issue);
482
+ const number = positiveInt(issue?.number);
483
+ if (number === undefined) {
484
+ return undefined;
485
+ }
486
+ return issue?.pull_request !== undefined && issue.pull_request !== null
487
+ ? { type: "pr", number }
488
+ : { type: "issue", number };
489
+ }
490
+ case "check_suite": {
491
+ const number = firstPullRequestNumber(asRecord(payload.check_suite));
492
+ return number === undefined ? undefined : { type: "pr", number };
493
+ }
494
+ case "check_run": {
495
+ const checkRun = asRecord(payload.check_run);
496
+ const number =
497
+ firstPullRequestNumber(checkRun) ??
498
+ firstPullRequestNumber(asRecord(checkRun?.check_suite));
499
+ return number === undefined ? undefined : { type: "pr", number };
500
+ }
501
+ default:
502
+ return undefined;
503
+ }
504
+ }
505
+
506
+ function extractSlack(payload: unknown): string | undefined {
507
+ const record = asRecord(payload);
508
+ if (record === undefined) {
509
+ return undefined;
510
+ }
511
+ const channelId =
512
+ nonEmpty(record.channelId) ??
513
+ nonEmpty(record.channel_id) ??
514
+ nonEmpty(record.channel);
515
+ const threadTs =
516
+ nonEmpty(record.threadTs) ??
517
+ nonEmpty(record.thread_ts) ??
518
+ nonEmpty(record.ts);
519
+ if (channelId === undefined || threadTs === undefined) {
520
+ return undefined;
521
+ }
522
+ return `cont:${formatConversationKey("thread", { channelId, threadTs })}`;
523
+ }
524
+
525
+ function extractOrigin(payload: unknown): string | undefined {
526
+ const record = asRecord(payload);
527
+ if (record === undefined) {
528
+ return undefined;
529
+ }
530
+ const inner = asRecord(record.pullRequest) ?? asRecord(record.pull_request);
531
+ const number = positiveInt(inner?.number);
532
+ const repository =
533
+ asRecord(record.repository) ??
534
+ asRecord(asRecord(asRecord(record.event)?.payload)?.repository);
535
+ const fullName =
536
+ nonEmpty(asRecord(repository)?.fullName) ??
537
+ nonEmpty(asRecord(repository)?.full_name) ??
538
+ originFullName(repository);
539
+ if (number === undefined || fullName === undefined) {
540
+ return undefined;
541
+ }
542
+ return `cont:${formatConversationKey("origin-pr", { fullName, number })}`;
543
+ }
544
+
545
+ function githubFullName(payload: Record<string, unknown>): string | undefined {
546
+ const repository = asRecord(payload.repository);
547
+ return nonEmpty(repository?.full_name) ?? originFullName(repository);
548
+ }
549
+
550
+ function originFullName(
551
+ repository: Record<string, unknown> | undefined
552
+ ): string | undefined {
553
+ if (repository === undefined) {
554
+ return undefined;
555
+ }
556
+ const owner =
557
+ nonEmpty(repository.owner) ??
558
+ nonEmpty(asRecord(repository.owner)?.login) ??
559
+ nonEmpty(asRecord(repository.owner)?.name);
560
+ const name = nonEmpty(repository.name);
561
+ if (owner === undefined || name === undefined) {
562
+ return undefined;
563
+ }
564
+ return `${owner}/${name}`;
565
+ }
566
+
567
+ function firstPullRequestNumber(
568
+ record: Record<string, unknown> | undefined
569
+ ): number | undefined {
570
+ const pullRequests = record?.pull_requests;
571
+ if (!Array.isArray(pullRequests) || pullRequests.length === 0) {
572
+ return undefined;
573
+ }
574
+ return positiveInt(asRecord(pullRequests[0])?.number);
575
+ }
576
+
577
+ function asRecord(value: unknown): Record<string, unknown> | undefined {
578
+ return typeof value === "object" && value !== null && !Array.isArray(value)
579
+ ? (value as Record<string, unknown>)
580
+ : undefined;
581
+ }
582
+
583
+ function nonEmpty(value: unknown): string | undefined {
584
+ return typeof value === "string" && value.trim() !== ""
585
+ ? value.trim()
586
+ : undefined;
587
+ }
588
+
589
+ function positiveInt(value: unknown): number | undefined {
590
+ if (typeof value === "number" && Number.isInteger(value) && value > 0) {
591
+ return value;
592
+ }
593
+ if (typeof value === "string" && /^[1-9]\d*$/.test(value)) {
594
+ return Number(value);
595
+ }
596
+ return undefined;
597
+ }
@@ -31,8 +31,15 @@
31
31
 
32
32
  import { z } from "zod";
33
33
  import { isPublicEndpointPolicy } from "../channels.js";
34
+ import type { ContinuationIdentity } from "../continuation.js";
34
35
  import { isCursorHostedStorage } from "../storage-backends/cursor-hosted.js";
35
36
  import type { AgentProject, AgentProjectInfo } from "../types.js";
37
+ import {
38
+ applyAgentContinuationDefaults,
39
+ channelContinuationIdentity,
40
+ handshakeChannelContinuation,
41
+ } from "./continuation-channel.js";
42
+ import { CONTINUATION_SELECTORS } from "./continuation-identity.js";
36
43
  import { projectInfo } from "./discovery.js";
37
44
  import { packageVersion } from "./distribution.js";
38
45
 
@@ -43,6 +50,8 @@ export const DEPLOY_MANIFEST_VERSION: 1 = 1;
43
50
  * module-private (the package builds with isolated declarations); consumers
44
51
  * use {@link parseAgentDeployManifest} and the explicit interfaces below.
45
52
  */
53
+ const continuationIdentitySchema = z.enum(CONTINUATION_SELECTORS);
54
+
46
55
  const agentDeployManifestV1Schema = z.object({
47
56
  manifestVersion: z.literal(DEPLOY_MANIFEST_VERSION),
48
57
  generatedAtMs: z.number().int().nonnegative(),
@@ -51,12 +60,14 @@ const agentDeployManifestV1Schema = z.object({
51
60
  egressDomains: z.array(z.string().min(1)).max(64),
52
61
  publicRoutePaths: z.array(z.string().min(1)).max(64),
53
62
  frameworkStorage: z.enum(["cursor-hosted", "other", "none"]).optional(),
63
+ architecture: z.enum(["v1", "v2"]).optional(),
54
64
  channels: z
55
65
  .array(
56
66
  z.object({
57
67
  id: z.string().min(1).max(128),
58
68
  kind: z.string().min(1).max(32).optional(),
59
69
  eventTypes: z.array(z.string().min(1).max(64)).max(64).optional(),
70
+ continuation: continuationIdentitySchema,
60
71
  })
61
72
  )
62
73
  .max(64),
@@ -70,13 +81,26 @@ export interface AgentDeployManifestDeclarations {
70
81
  publicRoutePaths: string[];
71
82
  /** Where this runtime mirrors framework-owned sessions and event chunks. */
72
83
  frameworkStorage?: "cursor-hosted" | "other" | "none";
84
+ /**
85
+ * Hosted architecture this release deploys onto. `"v2"` selects the v2
86
+ * control plane; omitted channel constructors inherit kind `/v2`
87
+ * continuation defaults. Omit or `"v1"` keeps engine minting.
88
+ */
89
+ architecture?: "v1" | "v2";
73
90
  /**
74
91
  * Mounted channels with their kinds and per-channel event capability.
75
92
  * Scope keys are installation config and are never extracted here.
76
- * Continuation is opt-in: without a deterministic selector key, every
77
- * inbound event starts a fresh session. Selector encoding remains open.
93
+ * Omitted channel `continuation` is `/v1` (engine minting) unless the
94
+ * agent set `architecture: "v2"`, which stamps kind `/v2` defaults
95
+ * (`github.pr/v2`, `slack.thread/v2`, `origin.pr/v2`). Per-channel
96
+ * selectors still override type.
78
97
  */
79
- channels: Array<{ id: string; kind?: string; eventTypes?: string[] }>;
98
+ channels: Array<{
99
+ id: string;
100
+ kind?: string;
101
+ eventTypes?: string[];
102
+ continuation: ContinuationIdentity;
103
+ }>;
80
104
  }
81
105
 
82
106
  export interface AgentDeployManifestV1 {
@@ -101,7 +125,12 @@ export function buildAgentDeployManifest(
101
125
  project: AgentProject,
102
126
  options?: { nowMs?: number; cliVersion?: string }
103
127
  ): AgentDeployManifestV1 {
128
+ applyAgentContinuationDefaults(project);
104
129
  const channels = project.channels.map((channel) => {
130
+ const handshake = handshakeChannelContinuation(channel);
131
+ if (!handshake.ok) {
132
+ throw new Error(handshake.error);
133
+ }
105
134
  const kind = channel.definition.meta?.kind;
106
135
  const githubEvents = channel.definition.meta?.githubWebhookEvents;
107
136
  const originEvents = channel.definition.meta?.originWebhookEvents;
@@ -115,6 +144,7 @@ export function buildAgentDeployManifest(
115
144
  id: channel.id,
116
145
  kind,
117
146
  eventTypes,
147
+ continuation: channelContinuationIdentity(channel.definition),
118
148
  };
119
149
  });
120
150
 
@@ -139,6 +169,9 @@ export function buildAgentDeployManifest(
139
169
  : isCursorHostedStorage(project.storage)
140
170
  ? "cursor-hosted"
141
171
  : "other",
172
+ ...(project.agent.architecture === undefined
173
+ ? {}
174
+ : { architecture: project.agent.architecture }),
142
175
  channels,
143
176
  },
144
177
  info: projectInfo(project),