@cursor/july 0.1.75 → 0.1.78
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.
- package/dist/channels/origin/origin-webhook.d.ts +23 -2
- package/dist/channels/origin/origin-webhook.d.ts.map +1 -1
- package/dist/channels/origin/origin-webhook.js +82 -20
- package/dist/docs/404.html +2 -2
- package/dist/docs/ab.html +3 -3
- package/dist/docs/assets/{app.b8bhB2SV.js → app.BsA7t7rS.js} +1 -1
- package/dist/docs/assets/chunks/@localSearchIndexroot.Dcs6boxD.js +1 -0
- package/dist/docs/assets/chunks/{VPLocalSearchBox.lLSeIyWw.js → VPLocalSearchBox.B1hHhktM.js} +1 -1
- package/dist/docs/assets/chunks/{theme.C_owesa-.js → theme.DLpLSsQh.js} +2 -2
- package/dist/docs/assets/{storage.md.D8NQ389l.js → storage.md.ks1u64_R.js} +3 -3
- package/dist/docs/assets/storage.md.ks1u64_R.lean.js +1 -0
- package/dist/docs/building-with-agents.html +3 -3
- package/dist/docs/concepts.html +3 -3
- package/dist/docs/deployment.html +3 -3
- package/dist/docs/evals.html +3 -3
- package/dist/docs/example-agents/approval-buddy.html +3 -3
- package/dist/docs/example-agents/benny.html +3 -3
- package/dist/docs/example-agents/bugbot.html +3 -3
- package/dist/docs/example-agents/codebase-wiki.html +3 -3
- package/dist/docs/example-agents/codeowners-review.html +3 -3
- package/dist/docs/example-agents/concierge.html +3 -3
- package/dist/docs/example-agents/fsd.html +3 -3
- package/dist/docs/example-agents/index.html +3 -3
- package/dist/docs/example-agents/knowledge-base.html +3 -3
- package/dist/docs/example-agents/oncall.html +3 -3
- package/dist/docs/example-agents/security-reviewer.html +3 -3
- package/dist/docs/example-agents/slack-agent.html +3 -3
- package/dist/docs/example-agents/weather-agent.html +3 -3
- package/dist/docs/guides/agent-to-agent.html +3 -3
- package/dist/docs/guides/cloud-runtime.html +3 -3
- package/dist/docs/guides/convert-automation.html +3 -3
- package/dist/docs/guides/github.html +3 -3
- package/dist/docs/guides/human-in-the-loop.html +3 -3
- package/dist/docs/guides/mcp-oauth.html +3 -3
- package/dist/docs/guides/opentelemetry.html +3 -3
- package/dist/docs/guides/slack.html +3 -3
- package/dist/docs/guides/webhooks.html +3 -3
- package/dist/docs/hashmap.json +1 -1
- package/dist/docs/hillclimbing.html +3 -3
- package/dist/docs/index.html +3 -3
- package/dist/docs/quickstart.html +3 -3
- package/dist/docs/reference/agent-config.html +3 -3
- package/dist/docs/reference/artifacts.html +3 -3
- package/dist/docs/reference/channels.html +3 -3
- package/dist/docs/reference/cli.html +3 -3
- package/dist/docs/reference/connections.html +3 -3
- package/dist/docs/reference/hooks.html +3 -3
- package/dist/docs/reference/http-api.html +3 -3
- package/dist/docs/reference/instructions.html +3 -3
- package/dist/docs/reference/playground.html +3 -3
- package/dist/docs/reference/project-layout.html +3 -3
- package/dist/docs/reference/prompt.html +3 -3
- package/dist/docs/reference/schedules.html +3 -3
- package/dist/docs/reference/sessions.html +3 -3
- package/dist/docs/reference/skills.html +3 -3
- package/dist/docs/reference/subagents.html +3 -3
- package/dist/docs/reference/tools.html +3 -3
- package/dist/docs/scaffolding-agents.html +3 -3
- package/dist/docs/storage.html +7 -7
- package/dist/docs/templates/agentic-owners.html +3 -3
- package/dist/docs/templates/demo.html +3 -3
- package/dist/docs/templates/pr-autofixer.html +3 -3
- package/dist/docs/templates/security-reviewer.html +3 -3
- package/dist/docs/templates/triage.html +3 -3
- package/dist/docs/troubleshooting.html +3 -3
- package/dist/index.d.ts +0 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +0 -1
- package/dist/internal/authored-alias-hooks.d.ts +12 -0
- package/dist/internal/authored-alias-hooks.d.ts.map +1 -1
- package/dist/internal/authored-alias-hooks.js +60 -8
- package/dist/internal/authored-loaders.d.ts +5 -3
- package/dist/internal/authored-loaders.d.ts.map +1 -1
- package/dist/internal/authored-loaders.js +13 -4
- package/dist/internal/deployment-scm-event-stream.js +4 -4
- package/dist/internal/discovery.d.ts.map +1 -1
- package/dist/internal/discovery.js +3 -21
- package/dist/internal/distribution.d.ts.map +1 -1
- package/dist/internal/distribution.js +0 -1
- package/dist/internal/review-comments.d.ts +62 -0
- package/dist/internal/review-comments.d.ts.map +1 -1
- package/dist/internal/review-comments.js +199 -0
- package/dist/internal/session-engine.d.ts +1 -60
- package/dist/internal/session-engine.d.ts.map +1 -1
- package/dist/internal/session-engine.js +10 -109
- package/dist/kv/memory-kv.d.ts +1 -10
- package/dist/kv/memory-kv.d.ts.map +1 -1
- package/dist/kv/memory-kv.js +1 -10
- package/dist/kv.d.ts +2 -3
- package/dist/kv.d.ts.map +1 -1
- package/dist/kv.js +2 -3
- package/dist/playground/assets/index-DJzcIeGt.css +1 -0
- package/dist/playground/assets/{index-BJy9UJrF.js → index-K3Sxyx0I.js} +48 -45
- package/dist/playground/index.html +2 -2
- package/dist/storage-backends/cursor-hosted.js +3 -3
- package/dist/storage-protocol.d.ts +3 -8
- package/dist/storage-protocol.d.ts.map +1 -1
- package/dist/storage-protocol.js +4 -10
- package/dist/storage.d.ts +5 -11
- package/dist/storage.d.ts.map +1 -1
- package/dist/storage.js +3 -4
- package/dist/types.d.ts +5 -34
- package/dist/types.d.ts.map +1 -1
- package/docs/storage.md +11 -44
- package/package.json +1 -8
- package/src/channels/origin/origin-webhook.ts +111 -25
- package/src/index.ts +0 -6
- package/src/internal/authored-alias-hooks.ts +75 -7
- package/src/internal/authored-loaders.ts +15 -4
- package/src/internal/deployment-scm-event-stream.ts +4 -4
- package/src/internal/discovery.ts +3 -27
- package/src/internal/distribution.ts +0 -1
- package/src/internal/review-comments.ts +286 -0
- package/src/internal/session-engine.ts +1 -168
- package/src/kv/memory-kv.ts +1 -10
- package/src/kv.ts +2 -3
- package/src/storage-backends/cursor-hosted.ts +3 -3
- package/src/storage-protocol.ts +4 -10
- package/src/storage.ts +6 -12
- package/src/types.ts +5 -35
- package/dist/docs/assets/chunks/@localSearchIndexroot.DGZeXVqn.js +0 -1
- package/dist/docs/assets/storage.md.D8NQ389l.lean.js +0 -1
- package/dist/internal/durable-session-store.d.ts +0 -60
- package/dist/internal/durable-session-store.d.ts.map +0 -1
- package/dist/internal/durable-session-store.js +0 -131
- package/dist/internal/ledger-serving.d.ts +0 -169
- package/dist/internal/ledger-serving.d.ts.map +0 -1
- package/dist/internal/ledger-serving.js +0 -393
- package/dist/ledger/janitor.d.ts +0 -60
- package/dist/ledger/janitor.d.ts.map +0 -1
- package/dist/ledger/janitor.js +0 -137
- package/dist/ledger/session-ledger.d.ts +0 -293
- package/dist/ledger/session-ledger.d.ts.map +0 -1
- package/dist/ledger/session-ledger.js +0 -614
- package/dist/ledger.d.ts +0 -12
- package/dist/ledger.d.ts.map +0 -1
- package/dist/ledger.js +0 -9
- package/dist/playground/assets/index-BrSd5vff.css +0 -1
- package/src/internal/durable-session-store.ts +0 -134
- package/src/internal/ledger-serving.ts +0 -531
- package/src/ledger/janitor.ts +0 -193
- package/src/ledger/session-ledger.ts +0 -839
- package/src/ledger.ts +0 -44
|
@@ -110,6 +110,43 @@ export type ReviewCommentReaderImplementation = {
|
|
|
110
110
|
) => Promise<ExistingReviewComment[]>;
|
|
111
111
|
};
|
|
112
112
|
|
|
113
|
+
export type ResolveReviewThreadsArgs = {
|
|
114
|
+
target: ScmReviewTarget;
|
|
115
|
+
/** REST comment ids whose threads should be resolved. */
|
|
116
|
+
commentIds: readonly number[];
|
|
117
|
+
signal?: AbortSignal;
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Outcome of resolving review threads for a set of comment ids. Callers
|
|
122
|
+
* treat this as best-effort actuation: a failed resolve must not fail the
|
|
123
|
+
* review that already posted.
|
|
124
|
+
*/
|
|
125
|
+
export type ResolveReviewThreadsResult = {
|
|
126
|
+
requested: number;
|
|
127
|
+
resolved: number;
|
|
128
|
+
alreadyResolved: number;
|
|
129
|
+
notFound: number;
|
|
130
|
+
failed: number;
|
|
131
|
+
detail: string;
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Write side of thread resolution, the counterpart of
|
|
136
|
+
* {@link ReviewCommentReaderImplementation}. Bugbot's equivalent is
|
|
137
|
+
* `IGitProvider.resolveReviewThreadForComment` / `resolveThreads`.
|
|
138
|
+
*
|
|
139
|
+
* GitHub only exposes resolve via GraphQL (`resolveReviewThread`). REST
|
|
140
|
+
* has no equivalent. The mutation runs as whatever identity minted the
|
|
141
|
+
* bound GitHub client — for `githubChannel({ cursorAccount })` that is
|
|
142
|
+
* the Cursor installation, the same identity that posted the comments.
|
|
143
|
+
*/
|
|
144
|
+
export type ReviewThreadResolverImplementation = {
|
|
145
|
+
resolveReviewThreadsForComments: (
|
|
146
|
+
args: ResolveReviewThreadsArgs
|
|
147
|
+
) => Promise<ResolveReviewThreadsResult>;
|
|
148
|
+
};
|
|
149
|
+
|
|
113
150
|
/** Parse a PR URL into the SCM target to read comments from. */
|
|
114
151
|
export function resolveScmReviewTarget(
|
|
115
152
|
prUrl: string
|
|
@@ -141,9 +178,65 @@ export function createReviewCommentReader(args: {
|
|
|
141
178
|
}
|
|
142
179
|
}
|
|
143
180
|
|
|
181
|
+
/**
|
|
182
|
+
* Resolver for a resolved target. Returns undefined when the host cannot
|
|
183
|
+
* serve that provider, so callers degrade instead of throwing.
|
|
184
|
+
*/
|
|
185
|
+
export function createReviewThreadResolver(args: {
|
|
186
|
+
target: ScmReviewTarget;
|
|
187
|
+
github?: HostGitHubClient;
|
|
188
|
+
}): ReviewThreadResolverImplementation | undefined {
|
|
189
|
+
switch (args.target.provider) {
|
|
190
|
+
case "github":
|
|
191
|
+
return args.github === undefined
|
|
192
|
+
? undefined
|
|
193
|
+
: createHostGithubReviewThreadResolver(args.github);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Resolve the review threads that own `commentIds` on `prUrl`.
|
|
199
|
+
*
|
|
200
|
+
* No-ops (never throws) when the PR is on an SCM this host cannot write,
|
|
201
|
+
* no client is bound, or `commentIds` is empty. Same degrade-don't-fail
|
|
202
|
+
* posture as {@link createReviewCommentReader}.
|
|
203
|
+
*/
|
|
204
|
+
export async function resolveReviewThreadsForComments(args: {
|
|
205
|
+
prUrl: string;
|
|
206
|
+
commentIds: readonly number[];
|
|
207
|
+
github?: HostGitHubClient;
|
|
208
|
+
resolver?: ReviewThreadResolverImplementation;
|
|
209
|
+
signal?: AbortSignal;
|
|
210
|
+
}): Promise<ResolveReviewThreadsResult> {
|
|
211
|
+
if (args.commentIds.length === 0) {
|
|
212
|
+
return skippedResolve("no comments to resolve");
|
|
213
|
+
}
|
|
214
|
+
const target = resolveScmReviewTarget(args.prUrl);
|
|
215
|
+
if (target === undefined) {
|
|
216
|
+
return skippedResolve("no SCM resolver for this PR URL");
|
|
217
|
+
}
|
|
218
|
+
const resolver =
|
|
219
|
+
args.resolver ??
|
|
220
|
+
createReviewThreadResolver({
|
|
221
|
+
target,
|
|
222
|
+
github: args.github,
|
|
223
|
+
});
|
|
224
|
+
if (resolver === undefined) {
|
|
225
|
+
return skippedResolve("no GitHub client bound to resolve threads");
|
|
226
|
+
}
|
|
227
|
+
return resolver.resolveReviewThreadsForComments({
|
|
228
|
+
target,
|
|
229
|
+
commentIds: args.commentIds,
|
|
230
|
+
signal: args.signal,
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
|
|
144
234
|
/** Cap on pages pulled per PR; GitHub serves 100 comments per page. */
|
|
145
235
|
const REVIEW_COMMENTS_MAX_PAGES = 5;
|
|
146
236
|
const REVIEW_COMMENTS_PAGE_SIZE = 100;
|
|
237
|
+
/** Cap on review-thread pages when mapping comment ids to GraphQL thread ids. */
|
|
238
|
+
const REVIEW_THREADS_MAX_PAGES = 20;
|
|
239
|
+
const REVIEW_THREADS_PAGE_SIZE = 100;
|
|
147
240
|
|
|
148
241
|
/**
|
|
149
242
|
* Reads inline review comments through the host GitHub client (Cursor
|
|
@@ -182,6 +275,188 @@ export function createHostGithubReviewCommentReader(
|
|
|
182
275
|
};
|
|
183
276
|
}
|
|
184
277
|
|
|
278
|
+
type GithubReviewThreadNode = {
|
|
279
|
+
id?: string;
|
|
280
|
+
isResolved?: boolean;
|
|
281
|
+
comments?: {
|
|
282
|
+
nodes?: Array<{ databaseId?: number | null } | null>;
|
|
283
|
+
};
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
type GithubReviewThreadsQuery = {
|
|
287
|
+
repository?: {
|
|
288
|
+
pullRequest?: {
|
|
289
|
+
reviewThreads?: {
|
|
290
|
+
nodes?: Array<GithubReviewThreadNode | null>;
|
|
291
|
+
pageInfo?: { hasNextPage?: boolean; endCursor?: string | null };
|
|
292
|
+
};
|
|
293
|
+
} | null;
|
|
294
|
+
} | null;
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
type GithubResolveThreadMutation = {
|
|
298
|
+
resolveReviewThread?: {
|
|
299
|
+
thread?: { isResolved?: boolean };
|
|
300
|
+
};
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Resolves inline review threads through the host GitHub client (Cursor
|
|
305
|
+
* account lease, App installation token, or PAT). GitHub GraphQL has no
|
|
306
|
+
* comment→thread field, so this pages the PR's review threads and matches
|
|
307
|
+
* each REST comment id by `databaseId` — the same approach as Bugbot's
|
|
308
|
+
* `resolveReviewThreadForComment`.
|
|
309
|
+
*/
|
|
310
|
+
export function createHostGithubReviewThreadResolver(
|
|
311
|
+
github: HostGitHubClient
|
|
312
|
+
): ReviewThreadResolverImplementation {
|
|
313
|
+
return {
|
|
314
|
+
async resolveReviewThreadsForComments({ target, commentIds, signal }) {
|
|
315
|
+
const uniqueIds = [...new Set(commentIds.filter((id) => id > 0))];
|
|
316
|
+
if (uniqueIds.length === 0) {
|
|
317
|
+
return skippedResolve("no comments to resolve");
|
|
318
|
+
}
|
|
319
|
+
const [owner, repo] = target.repo.split("/", 2);
|
|
320
|
+
if (owner === undefined || repo === undefined || repo === "") {
|
|
321
|
+
return skippedResolve("unparseable owner/repo from PR URL");
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
try {
|
|
325
|
+
const octokit = await github.getOctokit();
|
|
326
|
+
const wanted = new Set(uniqueIds);
|
|
327
|
+
const matched = new Map<
|
|
328
|
+
number,
|
|
329
|
+
{ threadId: string; isResolved: boolean }
|
|
330
|
+
>();
|
|
331
|
+
|
|
332
|
+
let cursor: string | null = null;
|
|
333
|
+
for (let page = 0; page < REVIEW_THREADS_MAX_PAGES; page++) {
|
|
334
|
+
signal?.throwIfAborted();
|
|
335
|
+
const response = (await octokit.graphql(REVIEW_THREADS_FOR_PR_QUERY, {
|
|
336
|
+
owner,
|
|
337
|
+
repo,
|
|
338
|
+
number: target.prNumber,
|
|
339
|
+
cursor,
|
|
340
|
+
})) as GithubReviewThreadsQuery;
|
|
341
|
+
const reviewThreads = response.repository?.pullRequest?.reviewThreads;
|
|
342
|
+
for (const thread of reviewThreads?.nodes ?? []) {
|
|
343
|
+
if (thread?.id === undefined || thread.id === "") {
|
|
344
|
+
continue;
|
|
345
|
+
}
|
|
346
|
+
for (const comment of thread.comments?.nodes ?? []) {
|
|
347
|
+
const databaseId = comment?.databaseId;
|
|
348
|
+
if (
|
|
349
|
+
databaseId !== undefined &&
|
|
350
|
+
databaseId !== null &&
|
|
351
|
+
wanted.has(databaseId) &&
|
|
352
|
+
!matched.has(databaseId)
|
|
353
|
+
) {
|
|
354
|
+
matched.set(databaseId, {
|
|
355
|
+
threadId: thread.id,
|
|
356
|
+
isResolved: thread.isResolved === true,
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
if (matched.size === wanted.size) {
|
|
362
|
+
break;
|
|
363
|
+
}
|
|
364
|
+
const pageInfo = reviewThreads?.pageInfo;
|
|
365
|
+
if (pageInfo?.hasNextPage !== true || !pageInfo.endCursor) {
|
|
366
|
+
break;
|
|
367
|
+
}
|
|
368
|
+
cursor = pageInfo.endCursor;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
const toResolve = [
|
|
372
|
+
...new Map(
|
|
373
|
+
[...matched.values()]
|
|
374
|
+
.filter((thread) => !thread.isResolved)
|
|
375
|
+
.map((thread) => [thread.threadId, thread.threadId])
|
|
376
|
+
).values(),
|
|
377
|
+
];
|
|
378
|
+
let resolved = 0;
|
|
379
|
+
let failed = 0;
|
|
380
|
+
for (const threadId of toResolve) {
|
|
381
|
+
signal?.throwIfAborted();
|
|
382
|
+
try {
|
|
383
|
+
const mutation = (await octokit.graphql(
|
|
384
|
+
RESOLVE_REVIEW_THREAD_MUTATION,
|
|
385
|
+
{ threadId }
|
|
386
|
+
)) as GithubResolveThreadMutation;
|
|
387
|
+
if (mutation.resolveReviewThread?.thread?.isResolved === true) {
|
|
388
|
+
resolved += 1;
|
|
389
|
+
} else {
|
|
390
|
+
failed += 1;
|
|
391
|
+
}
|
|
392
|
+
} catch {
|
|
393
|
+
failed += 1;
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
const alreadyResolved = new Set(
|
|
398
|
+
[...matched.values()]
|
|
399
|
+
.filter((thread) => thread.isResolved)
|
|
400
|
+
.map((thread) => thread.threadId)
|
|
401
|
+
).size;
|
|
402
|
+
const notFound = wanted.size - matched.size;
|
|
403
|
+
return {
|
|
404
|
+
requested: uniqueIds.length,
|
|
405
|
+
resolved,
|
|
406
|
+
alreadyResolved,
|
|
407
|
+
notFound,
|
|
408
|
+
failed,
|
|
409
|
+
detail: `resolved ${resolved} of ${toResolve.length} open thread(s) for ${uniqueIds.length} comment(s) via host GitHub (${alreadyResolved} already resolved, ${notFound} not found, ${failed} failed)`,
|
|
410
|
+
};
|
|
411
|
+
} catch (error) {
|
|
412
|
+
if (signal?.aborted) {
|
|
413
|
+
throw error;
|
|
414
|
+
}
|
|
415
|
+
return {
|
|
416
|
+
requested: uniqueIds.length,
|
|
417
|
+
resolved: 0,
|
|
418
|
+
alreadyResolved: 0,
|
|
419
|
+
notFound: 0,
|
|
420
|
+
failed: uniqueIds.length,
|
|
421
|
+
detail: `host GitHub thread resolve failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
},
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
const REVIEW_THREADS_FOR_PR_QUERY = `
|
|
429
|
+
query ReviewThreadsForPr(
|
|
430
|
+
$owner: String!
|
|
431
|
+
$repo: String!
|
|
432
|
+
$number: Int!
|
|
433
|
+
$cursor: String
|
|
434
|
+
) {
|
|
435
|
+
repository(owner: $owner, name: $repo) {
|
|
436
|
+
pullRequest(number: $number) {
|
|
437
|
+
reviewThreads(first: ${REVIEW_THREADS_PAGE_SIZE}, after: $cursor) {
|
|
438
|
+
nodes {
|
|
439
|
+
id
|
|
440
|
+
isResolved
|
|
441
|
+
comments(first: ${REVIEW_THREADS_PAGE_SIZE}) {
|
|
442
|
+
nodes { databaseId }
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
pageInfo { hasNextPage endCursor }
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
`;
|
|
451
|
+
|
|
452
|
+
const RESOLVE_REVIEW_THREAD_MUTATION = `
|
|
453
|
+
mutation ResolveReviewThread($threadId: ID!) {
|
|
454
|
+
resolveReviewThread(input: { threadId: $threadId }) {
|
|
455
|
+
thread { isResolved }
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
`;
|
|
459
|
+
|
|
185
460
|
type GithubReviewCommentLike = {
|
|
186
461
|
id: number;
|
|
187
462
|
path: string;
|
|
@@ -504,6 +779,17 @@ function skippedHost(detail: string): ReviewCommentPostResult {
|
|
|
504
779
|
};
|
|
505
780
|
}
|
|
506
781
|
|
|
782
|
+
function skippedResolve(detail: string): ResolveReviewThreadsResult {
|
|
783
|
+
return {
|
|
784
|
+
requested: 0,
|
|
785
|
+
resolved: 0,
|
|
786
|
+
alreadyResolved: 0,
|
|
787
|
+
notFound: 0,
|
|
788
|
+
failed: 0,
|
|
789
|
+
detail,
|
|
790
|
+
};
|
|
791
|
+
}
|
|
792
|
+
|
|
507
793
|
function parseGithubPrUrl(
|
|
508
794
|
prUrl: string
|
|
509
795
|
):
|
|
@@ -25,12 +25,6 @@ import {
|
|
|
25
25
|
} from "../channels/slack/nudge-store.js";
|
|
26
26
|
import { resolveDedicatedSlackHostBinding } from "../channels/slack/slack-channel.js";
|
|
27
27
|
import { isCursorHostedFilesAvailable } from "../files-backends/cursor-hosted.js";
|
|
28
|
-
import {
|
|
29
|
-
SessionLedger,
|
|
30
|
-
type SessionLedgerKv,
|
|
31
|
-
supportsSessionLedgerKv,
|
|
32
|
-
} from "../ledger/session-ledger.js";
|
|
33
|
-
import { fileKv } from "../storage-backends/file-kv.js";
|
|
34
28
|
import {
|
|
35
29
|
type AgentProject,
|
|
36
30
|
type ApprovalDecision,
|
|
@@ -48,7 +42,6 @@ import {
|
|
|
48
42
|
DIRECT_TOOL_CALL_CHANNEL_ID,
|
|
49
43
|
type HookHandler,
|
|
50
44
|
type HostContext,
|
|
51
|
-
type HostKvApi,
|
|
52
45
|
type HttpChannelDefinition,
|
|
53
46
|
isHostOauthConnectionTransport,
|
|
54
47
|
isSymbolicConnectionTransport,
|
|
@@ -88,11 +81,6 @@ import {
|
|
|
88
81
|
mapSdkTokenUsage,
|
|
89
82
|
} from "./cloud-turn-cost.js";
|
|
90
83
|
import { isDevMode } from "./dev-mode.js";
|
|
91
|
-
import {
|
|
92
|
-
ledgerKeyPrefix,
|
|
93
|
-
namespacedKv,
|
|
94
|
-
storageCasKv,
|
|
95
|
-
} from "./durable-session-store.js";
|
|
96
84
|
import { type EventLogMemoryStats, EventLogStore } from "./event-log.js";
|
|
97
85
|
import { TurnEventMapper } from "./event-mapper.js";
|
|
98
86
|
import { selectFrameworkStorageDefinition } from "./framework-storage-selection.js";
|
|
@@ -111,7 +99,6 @@ import {
|
|
|
111
99
|
newToolCallId,
|
|
112
100
|
} from "./ids.js";
|
|
113
101
|
import { validateToolInput } from "./json-schema.js";
|
|
114
|
-
import { LedgerServing, newLedgerPodId } from "./ledger-serving.js";
|
|
115
102
|
import { McpHost } from "./mcp-host.js";
|
|
116
103
|
import type { OtelCollector } from "./otel/collector.js";
|
|
117
104
|
import {
|
|
@@ -158,7 +145,7 @@ export class SessionBusyError extends Error {
|
|
|
158
145
|
}
|
|
159
146
|
}
|
|
160
147
|
|
|
161
|
-
// Defined in auth.js (
|
|
148
|
+
// Defined in auth.js (HTTP/session-store callers import it without this
|
|
162
149
|
// module); re-exported here because this is its historical public home.
|
|
163
150
|
export { SessionAuthError } from "./auth.js";
|
|
164
151
|
|
|
@@ -235,22 +222,6 @@ export interface SessionEngineOptions {
|
|
|
235
222
|
* stub; production uses billed usage events when an agent id is known.
|
|
236
223
|
*/
|
|
237
224
|
estimateTurnCost?: EstimateTurnCostFn;
|
|
238
|
-
/**
|
|
239
|
-
* Explicit durable-session store for `serving: "ledger"` agents — a KV
|
|
240
|
-
* dedicated to the ledger (session identity, leases, commit heads),
|
|
241
|
-
* separate from agent state. Must implement the full CAS-capable KV
|
|
242
|
-
* surface (`HostKvCasApi`: the CAS trio plus `get`/`put`/`delete` for
|
|
243
|
-
* mailbox payload blobs and `listKeys` for the janitor scan);
|
|
244
|
-
* construction fails fast otherwise. Supply the **raw** store: the
|
|
245
|
-
* engine owns key scoping and namespaces every ledger key under
|
|
246
|
-
* `agentkit/v1/<agent>/durable-session/` itself — a self-hoster never
|
|
247
|
-
* writes prefixes. Default when omitted: the authored storage backend
|
|
248
|
-
* (`agent/storage.ts`) when it implements the `cas` group, else — with
|
|
249
|
-
* no authored storage — a file store under `<stateRoot>/ledger-kv`
|
|
250
|
-
* (single-process correctness only). An authored storage backend
|
|
251
|
-
* *without* `cas` fails fast. Ignored for resident-mode agents.
|
|
252
|
-
*/
|
|
253
|
-
durableSessionStore?: HostKvApi;
|
|
254
225
|
/** Process-level OTel collector; observe-only, never fatal. */
|
|
255
226
|
otel?: OtelCollector;
|
|
256
227
|
/** Process-level meter for {@link HostContext.otel}. Absent → no-op counters. */
|
|
@@ -290,14 +261,6 @@ interface ActiveToolContext {
|
|
|
290
261
|
|
|
291
262
|
export interface EngineSendOptions extends SendMessageOptions {
|
|
292
263
|
mode?: "chat" | "task";
|
|
293
|
-
/**
|
|
294
|
-
* Internal (ledger serving): set only by the ledger pump when it
|
|
295
|
-
* executes an already-parked event under its lease, so the call must
|
|
296
|
-
* run the resident path. Without the flag, every send on a
|
|
297
|
-
* `serving: "ledger"` agent routes through the ledger (append → claim
|
|
298
|
-
* → drain → turn → commit) and the pump would recurse into itself.
|
|
299
|
-
*/
|
|
300
|
-
fromLedgerPump?: boolean;
|
|
301
264
|
}
|
|
302
265
|
|
|
303
266
|
interface CoalescedQueuedTurn {
|
|
@@ -400,12 +363,6 @@ export class SessionEngine {
|
|
|
400
363
|
private readonly cloudCostBackfillDone = new Set<string>();
|
|
401
364
|
/** Deduped in-flight cloud cost backfills, keyed by session id. */
|
|
402
365
|
private readonly cloudCostBackfill = new Map<string, Promise<void>>();
|
|
403
|
-
/**
|
|
404
|
-
* Ledger-mode delivery seam; set only when the agent declares
|
|
405
|
-
* `serving: "ledger"`. When present, {@link send} routes every
|
|
406
|
-
* non-delivery call through the ledger's claim → turn → commit.
|
|
407
|
-
*/
|
|
408
|
-
private readonly ledgerServing: LedgerServing | undefined;
|
|
409
366
|
|
|
410
367
|
constructor(options: SessionEngineOptions) {
|
|
411
368
|
this.project = options.project;
|
|
@@ -499,8 +456,6 @@ export class SessionEngine {
|
|
|
499
456
|
for (const channel of options.project.channels) {
|
|
500
457
|
this.channelsById.set(channel.id, channel.definition);
|
|
501
458
|
}
|
|
502
|
-
this.ledgerServing = this.buildLedgerServing(options);
|
|
503
|
-
this.ledgerServing?.start();
|
|
504
459
|
this.stopMemoryTelemetry = shouldStartProcessMemoryTelemetry()
|
|
505
460
|
? startProcessMemoryTelemetry({
|
|
506
461
|
agentName: options.project.name,
|
|
@@ -511,113 +466,6 @@ export class SessionEngine {
|
|
|
511
466
|
: undefined;
|
|
512
467
|
}
|
|
513
468
|
|
|
514
|
-
/**
|
|
515
|
-
* Ledger-mode wiring (`serving: "ledger"`): resolve a CAS-capable
|
|
516
|
-
* durable-session store and build the event pump. Resident agents
|
|
517
|
-
* (the default) return undefined and never touch ledger keys.
|
|
518
|
-
*/
|
|
519
|
-
private buildLedgerServing(
|
|
520
|
-
options: SessionEngineOptions
|
|
521
|
-
): LedgerServing | undefined {
|
|
522
|
-
if (options.project.agent.serving !== "ledger") {
|
|
523
|
-
return undefined;
|
|
524
|
-
}
|
|
525
|
-
// The handle is already agent-scoped (resolveDurableSessionStore):
|
|
526
|
-
// two ledger-mode agents sharing one backend never resolve to each
|
|
527
|
-
// other's sessions or janitor-claim each other's parked envelopes.
|
|
528
|
-
const ledgerKv = this.resolveDurableSessionStore(options);
|
|
529
|
-
return new LedgerServing({
|
|
530
|
-
ledger: new SessionLedger({ kv: ledgerKv, podId: newLedgerPodId() }),
|
|
531
|
-
deliverTurn: (envelope) =>
|
|
532
|
-
this.send(envelope.channelId, envelope.message, {
|
|
533
|
-
continuationToken: envelope.continuationToken,
|
|
534
|
-
auth: envelope.auth,
|
|
535
|
-
...(envelope.title === undefined ? {} : { title: envelope.title }),
|
|
536
|
-
...(envelope.state === undefined ? {} : { state: envelope.state }),
|
|
537
|
-
...(envelope.mode === undefined ? {} : { mode: envelope.mode }),
|
|
538
|
-
...(envelope.purpose === undefined
|
|
539
|
-
? {}
|
|
540
|
-
: { purpose: envelope.purpose }),
|
|
541
|
-
// Isolation overrides ride the envelope: without them the
|
|
542
|
-
// drained turn would run local with the default harness cwd
|
|
543
|
-
// instead of the cloud VM / channel worktree the sender bound.
|
|
544
|
-
...(envelope.workspaceDir === undefined
|
|
545
|
-
? {}
|
|
546
|
-
: { workspaceDir: envelope.workspaceDir }),
|
|
547
|
-
...(envelope.cloud === undefined ? {} : { cloud: envelope.cloud }),
|
|
548
|
-
fromLedgerPump: true,
|
|
549
|
-
}),
|
|
550
|
-
findSession: async (channelId, continuationToken) => {
|
|
551
|
-
let record = await this.sessions.findByContinuation(
|
|
552
|
-
channelId,
|
|
553
|
-
continuationToken
|
|
554
|
-
);
|
|
555
|
-
if (record === undefined) {
|
|
556
|
-
record = await this.restoreSessionByContinuation(
|
|
557
|
-
channelId,
|
|
558
|
-
continuationToken
|
|
559
|
-
);
|
|
560
|
-
}
|
|
561
|
-
return record === undefined
|
|
562
|
-
? undefined
|
|
563
|
-
: this.channelSession(record.sessionId, record, false);
|
|
564
|
-
},
|
|
565
|
-
logger: this.logger,
|
|
566
|
-
});
|
|
567
|
-
}
|
|
568
|
-
|
|
569
|
-
/**
|
|
570
|
-
* Pick the KV the ledger coordinates on, in precedence order:
|
|
571
|
-
*
|
|
572
|
-
* 1. `options.durableSessionStore` — an explicit store separate from
|
|
573
|
-
* agent state (advanced deployments, tests).
|
|
574
|
-
* 2. The authored storage backend (`agent/storage.ts`) when it
|
|
575
|
-
* implements the `cas` group — one backend serves both the storage
|
|
576
|
-
* API and the durable session API.
|
|
577
|
-
* 3. No authored storage: a file store under `<stateRoot>/ledger-kv`
|
|
578
|
-
* (same place resident state lives; single-process only).
|
|
579
|
-
*
|
|
580
|
-
* An authored backend without `cas` fails fast: silently falling back
|
|
581
|
-
* to local files would fork coordination away from the deployment's
|
|
582
|
-
* durable store. The check demands the full surface the inbox uses —
|
|
583
|
-
* the CAS trio plus `get`/`put`/`delete` for mailbox payload blobs and
|
|
584
|
-
* `listKeys` for the janitor scan and the orphan sweep.
|
|
585
|
-
*
|
|
586
|
-
* Whatever store wins, the returned handle is scoped here — once —
|
|
587
|
-
* under `agentkit/v1/<agent>/durable-session/`. The ledger speaks bare
|
|
588
|
-
* `session/…` / `ledger/…` / `mailbox/…` keys; namespacing is this
|
|
589
|
-
* boundary's job, exactly like `host.kv`'s agent scoping.
|
|
590
|
-
*/
|
|
591
|
-
private resolveDurableSessionStore(
|
|
592
|
-
options: SessionEngineOptions
|
|
593
|
-
): SessionLedgerKv {
|
|
594
|
-
const namespace = ledgerKeyPrefix(options.project.name);
|
|
595
|
-
const explicit = options.durableSessionStore;
|
|
596
|
-
if (explicit !== undefined) {
|
|
597
|
-
if (!supportsSessionLedgerKv(explicit)) {
|
|
598
|
-
throw new Error(
|
|
599
|
-
`Agent "${options.project.name}" declares serving: "ledger", but the configured durableSessionStore is not CAS-capable (getWithVersion / putIfAbsent / putIfVersion / listKeys). Provide a store implementing HostKvCasApi (see @cursor/july/kv).`
|
|
600
|
-
);
|
|
601
|
-
}
|
|
602
|
-
return namespacedKv(explicit, namespace);
|
|
603
|
-
}
|
|
604
|
-
const casArgs = {
|
|
605
|
-
agentName: options.project.name,
|
|
606
|
-
projectRoot: options.project.rootDir,
|
|
607
|
-
};
|
|
608
|
-
const authored = options.project.storage;
|
|
609
|
-
if (authored !== undefined) {
|
|
610
|
-
if (authored.cas === undefined) {
|
|
611
|
-
throw new Error(
|
|
612
|
-
`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.`
|
|
613
|
-
);
|
|
614
|
-
}
|
|
615
|
-
return namespacedKv(storageCasKv(authored, casArgs), namespace);
|
|
616
|
-
}
|
|
617
|
-
const fallback = fileKv({ dir: join(options.stateRoot, "ledger-kv") });
|
|
618
|
-
return namespacedKv(storageCasKv(fallback, casArgs), namespace);
|
|
619
|
-
}
|
|
620
|
-
|
|
621
469
|
/**
|
|
622
470
|
* Attach serve-time resolved connections (peers, Cursor account bridge).
|
|
623
471
|
* They join the per-turn SDK `mcpServers` map (loopback URL for local
|
|
@@ -892,12 +740,6 @@ export class SessionEngine {
|
|
|
892
740
|
message: string,
|
|
893
741
|
options: EngineSendOptions = {}
|
|
894
742
|
): Promise<ChannelSession> {
|
|
895
|
-
// Ledger-mode agents park the event durably and coordinate through
|
|
896
|
-
// the session lease before any turn runs; only the pump's own
|
|
897
|
-
// lease-holding deliveries fall through to the resident path.
|
|
898
|
-
if (this.ledgerServing !== undefined && options.fromLedgerPump !== true) {
|
|
899
|
-
return this.ledgerServing.deliver(channelId, message, options);
|
|
900
|
-
}
|
|
901
743
|
const auth = options.auth ?? null;
|
|
902
744
|
let existing =
|
|
903
745
|
options.continuationToken === undefined
|
|
@@ -1008,13 +850,6 @@ export class SessionEngine {
|
|
|
1008
850
|
};
|
|
1009
851
|
await this.sessions.save(record);
|
|
1010
852
|
this.storage?.sessionRecord(record);
|
|
1011
|
-
// Ledger serving: another pod can follow up mid-turn, so the session
|
|
1012
|
-
// identity (record + continuation index) must hit the shared store
|
|
1013
|
-
// before we return a session id. Default turnEnd flush is too late.
|
|
1014
|
-
if (this.ledgerServing !== undefined && this.storage !== undefined) {
|
|
1015
|
-
this.storage.flushSession(record.sessionId);
|
|
1016
|
-
await this.storage.whenIdle();
|
|
1017
|
-
}
|
|
1018
853
|
await this.appendEvent(record.sessionId, {
|
|
1019
854
|
type: "session.started",
|
|
1020
855
|
data: { channelId },
|
|
@@ -3841,8 +3676,6 @@ export class SessionEngine {
|
|
|
3841
3676
|
async close(): Promise<void> {
|
|
3842
3677
|
this.stopMemoryTelemetry?.();
|
|
3843
3678
|
this.approvals.cancelAll("deny");
|
|
3844
|
-
// Ledger deliveries spawn turns, so settle them before activeTurns.
|
|
3845
|
-
await this.ledgerServing?.close();
|
|
3846
3679
|
await Promise.allSettled([...this.activeTurns.values()]);
|
|
3847
3680
|
await Promise.allSettled([...this.backgroundWork]);
|
|
3848
3681
|
await Promise.allSettled([...this.dispatchChains.values()]);
|
package/src/kv/memory-kv.ts
CHANGED
|
@@ -4,8 +4,7 @@
|
|
|
4
4
|
* `@cursor/july/kv/memory`.
|
|
5
5
|
*
|
|
6
6
|
* The memory KV *is* the dev shim: tests and local runs exercise the
|
|
7
|
-
*
|
|
8
|
-
* RPC. Values are deep-copied on both write and
|
|
7
|
+
* CAS contract, not a mock of an RPC. Values are deep-copied on both write and
|
|
9
8
|
* read so callers can never observe (or cause) shared-reference mutation.
|
|
10
9
|
*/
|
|
11
10
|
|
|
@@ -86,14 +85,6 @@ export function createMemoryKv(): HostKvCasApi {
|
|
|
86
85
|
* fixtures, an off-hosting no-op sink). Non-durable and process-local by
|
|
87
86
|
* construction; a real deployment backend should implement CAS against
|
|
88
87
|
* its own store instead.
|
|
89
|
-
*
|
|
90
|
-
* **Never pair this with `serving: "ledger"` on a real backend.** The
|
|
91
|
-
* ledger derives its coordination store from the whole storage config,
|
|
92
|
-
* so a durable plain backend with a memory `cas` group silently voids
|
|
93
|
-
* ack-after-durable: leases, heads, and pending indexes live only in
|
|
94
|
-
* this process and vanish on restart while payload blobs go through the
|
|
95
|
-
* plain methods. Ledger state must live in the same durable store as
|
|
96
|
-
* the blobs.
|
|
97
88
|
*/
|
|
98
89
|
export function memoryCasTable(): StorageCasTable {
|
|
99
90
|
const kv = createMemoryKv();
|
package/src/kv.ts
CHANGED
|
@@ -2,9 +2,8 @@
|
|
|
2
2
|
* The CAS-capable KV **interface** — `@cursor/july/kv`.
|
|
3
3
|
*
|
|
4
4
|
* This module is implementation-free on purpose: it is the contract a
|
|
5
|
-
* bring-your-own KV implements so
|
|
6
|
-
*
|
|
7
|
-
* it. {@link HostKvCasApi} is the conditional-write superset of the plain
|
|
5
|
+
* bring-your-own KV implements so conditional writes can run on it.
|
|
6
|
+
* {@link HostKvCasApi} is the conditional-write superset of the plain
|
|
8
7
|
* {@link HostKvApi}; {@link supportsKvCas} is the runtime probe for
|
|
9
8
|
* stores of unknown provenance — a `defineStorage` backend declares CAS
|
|
10
9
|
* through its `cas` group instead, so it never needs probing.
|
|
@@ -313,9 +313,9 @@ export function cursorHostedStorage(
|
|
|
313
313
|
{ prefix }
|
|
314
314
|
);
|
|
315
315
|
if (result.truncated === true) {
|
|
316
|
-
// Callers
|
|
317
|
-
//
|
|
318
|
-
//
|
|
316
|
+
// Callers treat the result as the complete key set; a silently
|
|
317
|
+
// capped scan hides missing keys. Fail loudly until pagination
|
|
318
|
+
// lands.
|
|
319
319
|
throw new Error(
|
|
320
320
|
`cursorHostedStorage: listKeys("${prefix}") exceeded the server's row cap and was truncated; the result is incomplete`
|
|
321
321
|
);
|
package/src/storage-protocol.ts
CHANGED
|
@@ -4,9 +4,7 @@
|
|
|
4
4
|
* `backend/server/src/agentServe/durableSessionProtocol.ts`) so a dashboard
|
|
5
5
|
* list can read Bugbot KV without importing `@cursor/july`.
|
|
6
6
|
*
|
|
7
|
-
* Additive JSON only — unknown fields are ignored.
|
|
8
|
-
* (`durable-session/`) types here; those are coordination-only and must not
|
|
9
|
-
* appear in the CP session list.
|
|
7
|
+
* Additive JSON only — unknown fields are ignored.
|
|
10
8
|
*/
|
|
11
9
|
|
|
12
10
|
import { createHash } from "node:crypto";
|
|
@@ -19,8 +17,7 @@ export const STORAGE_KEY_ROOT = "agentkit/v1" as const;
|
|
|
19
17
|
|
|
20
18
|
/**
|
|
21
19
|
* Exact session-record keys: `agentkit/v1/{agent}/session/{sessionId}`.
|
|
22
|
-
* Rejects `session-events
|
|
23
|
-
* agent name.
|
|
20
|
+
* Rejects `session-events/` and the CP-owned `_cp` agent name.
|
|
24
21
|
*/
|
|
25
22
|
const DURABLE_SESSION_RECORD_KEY_RE =
|
|
26
23
|
/^agentkit\/v1\/([^/]+)\/session\/([^/]+)$/;
|
|
@@ -109,9 +106,6 @@ export const MAX_KEY_SEGMENT_BYTES = 256;
|
|
|
109
106
|
* digest of the raw value when the encoding exceeds
|
|
110
107
|
* {@link MAX_KEY_SEGMENT_BYTES}. Deterministic, so key construction on the
|
|
111
108
|
* write path and the continuation-lookup path always agree.
|
|
112
|
-
*
|
|
113
|
-
* Exported for the session ledger, which builds its `session/` and
|
|
114
|
-
* `ledger/` keys with the same encoding rules.
|
|
115
109
|
*/
|
|
116
110
|
export function keySegment(raw: string): string {
|
|
117
111
|
const encoded = encodeURIComponent(raw);
|
|
@@ -163,8 +157,8 @@ function stringValue(value: unknown): string | undefined {
|
|
|
163
157
|
}
|
|
164
158
|
|
|
165
159
|
/**
|
|
166
|
-
* Tolerant `SessionRecord` decoder. Extra fields (`auth`, `purpose`,
|
|
167
|
-
*
|
|
160
|
+
* Tolerant `SessionRecord` decoder. Extra fields (`auth`, `purpose`,
|
|
161
|
+
* future keys) are ignored. Returns undefined when required list
|
|
168
162
|
* fields are missing or the wrong type.
|
|
169
163
|
*/
|
|
170
164
|
export function parseDurableSessionRecord(
|