@cursor/july 0.1.45 → 0.1.46

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 +4 -2
  2. package/README.md +3 -1
  3. package/dist/channels/github/api.d.ts +25 -0
  4. package/dist/channels/github/api.d.ts.map +1 -1
  5. package/dist/channels/github/api.js +48 -0
  6. package/dist/channels/github/cursor-account.d.ts +7 -0
  7. package/dist/channels/github/cursor-account.d.ts.map +1 -1
  8. package/dist/channels/github/cursor-account.js +18 -0
  9. package/dist/channels/github/defaults.d.ts +1 -1
  10. package/dist/channels/github/defaults.js +1 -1
  11. package/dist/channels/github/github-channel.d.ts.map +1 -1
  12. package/dist/channels/github/github-channel.js +2 -2
  13. package/dist/channels/github/progress.d.ts +6 -5
  14. package/dist/channels/github/progress.d.ts.map +1 -1
  15. package/dist/channels/github/progress.js +58 -16
  16. package/dist/channels/github/state.d.ts +11 -0
  17. package/dist/channels/github/state.d.ts.map +1 -1
  18. package/dist/channels/github/state.js +1 -1
  19. package/dist/channels/github/types.d.ts +3 -3
  20. package/dist/docs/404.html +2 -2
  21. package/dist/docs/ab.html +3 -3
  22. package/dist/docs/assets/{app.fHnNXEJ6.js → app.C2NFTb-h.js} +1 -1
  23. package/dist/docs/assets/chunks/@localSearchIndexroot.B-FxvuoI.js +1 -0
  24. package/dist/docs/assets/chunks/{VPLocalSearchBox.Cf6ghfFY.js → VPLocalSearchBox.o1_FJGip.js} +1 -1
  25. package/dist/docs/assets/chunks/{theme.B_5-VLe8.js → theme.BcGduE7g.js} +2 -2
  26. package/dist/docs/assets/{guides_github.md.DSg0j9qj.js → guides_github.md.B8QKtIpw.js} +7 -6
  27. package/dist/docs/assets/{guides_github.md.DSg0j9qj.lean.js → guides_github.md.B8QKtIpw.lean.js} +1 -1
  28. package/dist/docs/assets/{reference_agent-config.md.XRpxctsA.js → reference_agent-config.md.DHkkvcEZ.js} +4 -4
  29. package/dist/docs/assets/{reference_agent-config.md.XRpxctsA.lean.js → reference_agent-config.md.DHkkvcEZ.lean.js} +1 -1
  30. package/dist/docs/assets/{reference_hooks.md.4cbRRIIu.js → reference_hooks.md.DJE5DXcT.js} +1 -1
  31. package/dist/docs/assets/{storage.md.DcGSnIxX.js → storage.md.CJYd5YmN.js} +2 -2
  32. package/dist/docs/assets/{storage.md.DcGSnIxX.lean.js → storage.md.CJYd5YmN.lean.js} +1 -1
  33. package/dist/docs/building-with-agents.html +3 -3
  34. package/dist/docs/concepts.html +3 -3
  35. package/dist/docs/deployment.html +3 -3
  36. package/dist/docs/evals.html +3 -3
  37. package/dist/docs/example-agents/approval-buddy.html +3 -3
  38. package/dist/docs/example-agents/benny.html +3 -3
  39. package/dist/docs/example-agents/bugbot.html +3 -3
  40. package/dist/docs/example-agents/codebase-wiki.html +3 -3
  41. package/dist/docs/example-agents/codeowners-review.html +3 -3
  42. package/dist/docs/example-agents/concierge.html +3 -3
  43. package/dist/docs/example-agents/fsd.html +3 -3
  44. package/dist/docs/example-agents/index.html +3 -3
  45. package/dist/docs/example-agents/knowledge-base.html +3 -3
  46. package/dist/docs/example-agents/oncall.html +3 -3
  47. package/dist/docs/example-agents/security-reviewer.html +3 -3
  48. package/dist/docs/example-agents/slack-agent.html +3 -3
  49. package/dist/docs/example-agents/weather-agent.html +3 -3
  50. package/dist/docs/guides/agent-to-agent.html +3 -3
  51. package/dist/docs/guides/cloud-runtime.html +3 -3
  52. package/dist/docs/guides/github.html +10 -9
  53. package/dist/docs/guides/human-in-the-loop.html +3 -3
  54. package/dist/docs/guides/mcp-oauth.html +3 -3
  55. package/dist/docs/guides/opentelemetry.html +3 -3
  56. package/dist/docs/guides/slack.html +3 -3
  57. package/dist/docs/guides/webhooks.html +3 -3
  58. package/dist/docs/hashmap.json +1 -1
  59. package/dist/docs/hillclimbing.html +3 -3
  60. package/dist/docs/index.html +3 -3
  61. package/dist/docs/quickstart.html +3 -3
  62. package/dist/docs/reference/agent-config.html +6 -6
  63. package/dist/docs/reference/artifacts.html +3 -3
  64. package/dist/docs/reference/channels.html +3 -3
  65. package/dist/docs/reference/cli.html +3 -3
  66. package/dist/docs/reference/connections.html +3 -3
  67. package/dist/docs/reference/hooks.html +5 -5
  68. package/dist/docs/reference/http-api.html +3 -3
  69. package/dist/docs/reference/instructions.html +3 -3
  70. package/dist/docs/reference/playground.html +3 -3
  71. package/dist/docs/reference/project-layout.html +3 -3
  72. package/dist/docs/reference/prompt.html +3 -3
  73. package/dist/docs/reference/schedules.html +3 -3
  74. package/dist/docs/reference/sessions.html +3 -3
  75. package/dist/docs/reference/skills.html +3 -3
  76. package/dist/docs/reference/subagents.html +3 -3
  77. package/dist/docs/reference/tools.html +3 -3
  78. package/dist/docs/scaffolding-agents.html +3 -3
  79. package/dist/docs/storage.html +5 -5
  80. package/dist/docs/templates/triage.html +3 -3
  81. package/dist/docs/troubleshooting.html +3 -3
  82. package/dist/index.d.ts +1 -1
  83. package/dist/index.d.ts.map +1 -1
  84. package/dist/index.js +1 -1
  85. package/dist/internal/auth.d.ts +4 -0
  86. package/dist/internal/auth.d.ts.map +1 -1
  87. package/dist/internal/auth.js +7 -0
  88. package/dist/internal/durable-session-store.d.ts +35 -23
  89. package/dist/internal/durable-session-store.d.ts.map +1 -1
  90. package/dist/internal/durable-session-store.js +71 -26
  91. package/dist/internal/ledger-serving.d.ts +135 -67
  92. package/dist/internal/ledger-serving.d.ts.map +1 -1
  93. package/dist/internal/ledger-serving.js +326 -107
  94. package/dist/internal/server.d.ts.map +1 -1
  95. package/dist/internal/server.js +2 -2
  96. package/dist/internal/session-engine.d.ts +18 -17
  97. package/dist/internal/session-engine.d.ts.map +1 -1
  98. package/dist/internal/session-engine.js +60 -27
  99. package/dist/kv/memory-kv.d.ts +8 -0
  100. package/dist/kv/memory-kv.d.ts.map +1 -1
  101. package/dist/kv/memory-kv.js +8 -0
  102. package/dist/ledger/janitor.d.ts +60 -0
  103. package/dist/ledger/janitor.d.ts.map +1 -0
  104. package/dist/ledger/janitor.js +137 -0
  105. package/dist/ledger/session-ledger.d.ts +132 -43
  106. package/dist/ledger/session-ledger.d.ts.map +1 -1
  107. package/dist/ledger/session-ledger.js +282 -50
  108. package/dist/ledger.d.ts +9 -8
  109. package/dist/ledger.d.ts.map +1 -1
  110. package/dist/ledger.js +7 -7
  111. package/dist/playground/assets/{index-Dces8BDp.js → index-6wW6SAc4.js} +32 -32
  112. package/dist/playground/assets/index-CSRYDAFw.css +1 -0
  113. package/dist/playground/index.html +2 -2
  114. package/dist/storage-backends/cursor-hosted.d.ts +5 -0
  115. package/dist/storage-backends/cursor-hosted.d.ts.map +1 -1
  116. package/dist/storage-backends/cursor-hosted.js +6 -0
  117. package/dist/types.d.ts +19 -8
  118. package/dist/types.d.ts.map +1 -1
  119. package/docs/guides/github.md +23 -18
  120. package/docs/reference/agent-config.md +31 -36
  121. package/docs/reference/hooks.md +1 -1
  122. package/docs/storage.md +26 -9
  123. package/package.json +1 -1
  124. package/src/channels/github/api.ts +122 -0
  125. package/src/channels/github/cursor-account.ts +23 -0
  126. package/src/channels/github/defaults.ts +1 -1
  127. package/src/channels/github/github-channel.ts +5 -1
  128. package/src/channels/github/progress.ts +84 -19
  129. package/src/channels/github/state.ts +14 -0
  130. package/src/channels/github/types.ts +3 -3
  131. package/src/index.ts +1 -0
  132. package/src/internal/auth.ts +10 -0
  133. package/src/internal/durable-session-store.ts +66 -27
  134. package/src/internal/ledger-serving.ts +439 -148
  135. package/src/internal/server.ts +6 -2
  136. package/src/internal/session-engine.ts +95 -37
  137. package/src/kv/memory-kv.ts +8 -0
  138. package/src/ledger/janitor.ts +193 -0
  139. package/src/ledger/session-ledger.ts +372 -68
  140. package/src/ledger.ts +21 -6
  141. package/src/storage-backends/cursor-hosted.ts +17 -2
  142. package/src/types.ts +19 -8
  143. package/dist/docs/assets/chunks/@localSearchIndexroot.BMT0xM4I.js +0 -1
  144. package/dist/playground/assets/index-CZH9EFvO.css +0 -1
  145. /package/dist/docs/assets/{reference_hooks.md.4cbRRIIu.lean.js → reference_hooks.md.DJE5DXcT.lean.js} +0 -0
@@ -384,6 +384,128 @@ export async function createGitHubCommitStatus(
384
384
  }
385
385
  }
386
386
 
387
+ export type GitHubCheckRunStatus = "queued" | "in_progress" | "completed";
388
+ export type GitHubCheckRunConclusion =
389
+ | "action_required"
390
+ | "cancelled"
391
+ | "failure"
392
+ | "neutral"
393
+ | "skipped"
394
+ | "success"
395
+ | "timed_out";
396
+
397
+ export interface GitHubCheckRun {
398
+ htmlUrl: string | undefined;
399
+ id: number;
400
+ raw: unknown;
401
+ }
402
+
403
+ export interface GitHubCheckRunWriteInput {
404
+ conclusion?: GitHubCheckRunConclusion;
405
+ detailsUrl?: string;
406
+ name: string;
407
+ output?: { summary: string; title: string };
408
+ status: GitHubCheckRunStatus;
409
+ }
410
+
411
+ /** Create a check run (Checks API). */
412
+ export async function createGitHubCheckRun(
413
+ input: GitHubResourceInput &
414
+ GitHubCheckRunWriteInput & {
415
+ sha: string;
416
+ }
417
+ ): Promise<GitHubCheckRun> {
418
+ const octokit = await createGitHubOctokit(input);
419
+ const now = new Date().toISOString();
420
+ try {
421
+ const response = await octokit.rest.checks.create({
422
+ owner: input.owner,
423
+ repo: input.repo,
424
+ name: input.name,
425
+ head_sha: input.sha,
426
+ status: input.status,
427
+ started_at: now,
428
+ ...checkRunCompletionFields(input, now),
429
+ ...checkRunDetailsUrl(input),
430
+ ...checkRunOutput(input),
431
+ });
432
+ return toCheckRun(response.data);
433
+ } catch (error) {
434
+ throw wrapOctokitError(
435
+ error,
436
+ "POST",
437
+ `/repos/${input.owner}/${input.repo}/check-runs`
438
+ );
439
+ }
440
+ }
441
+
442
+ /** Update a check run (Checks API). */
443
+ export async function updateGitHubCheckRun(
444
+ input: GitHubResourceInput &
445
+ GitHubCheckRunWriteInput & {
446
+ checkRunId: number;
447
+ }
448
+ ): Promise<GitHubCheckRun> {
449
+ const octokit = await createGitHubOctokit(input);
450
+ try {
451
+ const response = await octokit.rest.checks.update({
452
+ owner: input.owner,
453
+ repo: input.repo,
454
+ check_run_id: input.checkRunId,
455
+ name: input.name,
456
+ status: input.status,
457
+ ...checkRunCompletionFields(input, new Date().toISOString()),
458
+ ...checkRunDetailsUrl(input),
459
+ ...checkRunOutput(input),
460
+ });
461
+ return toCheckRun(response.data);
462
+ } catch (error) {
463
+ throw wrapOctokitError(
464
+ error,
465
+ "PATCH",
466
+ `/repos/${input.owner}/${input.repo}/check-runs/${input.checkRunId}`
467
+ );
468
+ }
469
+ }
470
+
471
+ function checkRunCompletionFields(
472
+ input: GitHubCheckRunWriteInput,
473
+ completedAt: string
474
+ ): {
475
+ completed_at?: string;
476
+ conclusion?: GitHubCheckRunConclusion;
477
+ } {
478
+ if (input.status !== "completed" || input.conclusion === undefined) {
479
+ return {};
480
+ }
481
+ return { completed_at: completedAt, conclusion: input.conclusion };
482
+ }
483
+
484
+ function checkRunDetailsUrl(input: GitHubCheckRunWriteInput): {
485
+ details_url?: string;
486
+ } {
487
+ return input.detailsUrl === undefined
488
+ ? {}
489
+ : { details_url: input.detailsUrl };
490
+ }
491
+
492
+ function checkRunOutput(input: GitHubCheckRunWriteInput): {
493
+ output?: { summary: string; title: string };
494
+ } {
495
+ return input.output === undefined ? {} : { output: input.output };
496
+ }
497
+
498
+ function toCheckRun(raw: {
499
+ html_url?: string | null;
500
+ id?: number;
501
+ }): GitHubCheckRun {
502
+ return {
503
+ htmlUrl: typeof raw.html_url === "string" ? raw.html_url : undefined,
504
+ id: typeof raw.id === "number" ? raw.id : 0,
505
+ raw,
506
+ };
507
+ }
508
+
387
509
  function toPostedComment(raw: {
388
510
  html_url?: string;
389
511
  id?: number;
@@ -117,6 +117,29 @@ export function parseCursorGithubRepoScope(
117
117
  };
118
118
  }
119
119
 
120
+ /**
121
+ * Hosted `cursorAccount` mints `checks:write` on the `contents-write` tier.
122
+ * Enabling `progress.commitStatus` opts a `pr-write` (or omitted) channel up
123
+ * to that tier so github-proxy / `/github-token` can post Checks API runs.
124
+ * `"read"` cannot post merge-box checks.
125
+ */
126
+ export function cursorGithubPermissionsForChannel(
127
+ options: Pick<GitHubChannelOptions, "progress">,
128
+ authored: CursorGithubPermissions | undefined = "pr-write"
129
+ ): CursorGithubPermissions {
130
+ const base = authored ?? "pr-write";
131
+ const commitStatus = options.progress?.commitStatus;
132
+ if (commitStatus === undefined || commitStatus === false) {
133
+ return base;
134
+ }
135
+ if (base === "read") {
136
+ throw new Error(
137
+ 'progress.commitStatus needs checks:write; cursorAccount permissions: "read" cannot post merge-box checks. Use "pr-write" or "contents-write".'
138
+ );
139
+ }
140
+ return "contents-write";
141
+ }
142
+
120
143
  /** One host credential: the most privileged requested tier wins. */
121
144
  export function resolveCursorGithubPermissions(
122
145
  values: readonly CursorGithubPermissions[]
@@ -19,7 +19,7 @@ export interface BuildDefaultGitHubEventsOptions extends GitHubBindingConfig {
19
19
  }
20
20
 
21
21
  /**
22
- * Built-in delivery: eyes reaction on turn start, optional commit status /
22
+ * Built-in delivery: eyes reaction on turn start, optional Checks API run /
23
23
  * sticky banner via {@link GitHubProgressConfig}, post assistant text on
24
24
  * `message.completed`, and a short error comment on turn/session failure.
25
25
  */
@@ -27,6 +27,7 @@ import {
27
27
  import {
28
28
  createCursorAccountGithubTokenResolver,
29
29
  createPendingCursorGithubCredentials,
30
+ cursorGithubPermissionsForChannel,
30
31
  type PendingCursorGithubCredentials,
31
32
  parseCursorGithubRepoScope,
32
33
  registerGithubCursorAccountRuntime,
@@ -608,7 +609,10 @@ export function githubChannel(
608
609
  typeof options.cursorAccount === "object"
609
610
  ? parseCursorGithubRepoScope(
610
611
  options.cursorAccount.repos,
611
- options.cursorAccount.permissions
612
+ cursorGithubPermissionsForChannel(
613
+ options,
614
+ options.cursorAccount.permissions
615
+ )
612
616
  )
613
617
  : undefined;
614
618
 
@@ -1,18 +1,20 @@
1
1
  /**
2
- * Opt-in PR progress surfaces: commit status + sticky banner comment.
2
+ * Opt-in PR progress surfaces: Checks API run + sticky banner comment.
3
3
  */
4
4
 
5
5
  import {
6
- createGitHubCommitStatus,
6
+ createGitHubCheckRun,
7
7
  createGitHubIssueComment,
8
8
  GitHubApiError,
9
+ type GitHubCheckRunWriteInput,
10
+ updateGitHubCheckRun,
9
11
  updateGitHubIssueComment,
10
12
  } from "./api.js";
11
13
  import type { GitHubBindingConfig } from "./binding.js";
12
14
  import { formatGitHubOutboundComment } from "./binding.js";
13
15
  import type { GitHubChannelState } from "./state.js";
14
16
 
15
- /** GitHub commit-status descriptions are capped at 140 characters. */
17
+ /** Merge-box check titles stay short; GitHub allows 1024, we clip to 140. */
16
18
  const STATUS_DESCRIPTION_MAX = 140;
17
19
 
18
20
  export type GitHubProgressPhase = "pending" | "success" | "failure";
@@ -24,7 +26,7 @@ export interface GitHubProgressPhaseCopy {
24
26
  }
25
27
 
26
28
  export interface GitHubCommitStatusProgress extends GitHubProgressPhaseCopy {
27
- /** Commit status context (unique per check in the merge box). */
29
+ /** Check run name (unique per check in the merge box). */
28
30
  context: string;
29
31
  }
30
32
 
@@ -34,9 +36,10 @@ export interface GitHubProgressConfig {
34
36
  /** When false, skip the eyes reaction on turn.started. Defaults to true. */
35
37
  reactions?: boolean;
36
38
  /**
37
- * Opt-in commit status on the PR head. Omit or `false` keeps today's
38
- * behavior (no statuses from default events). With `cursorAccount`,
39
- * needs `permissions: "contents-write"` (push-equivalent).
39
+ * Opt-in Checks API run on the PR head. Omit or `false` keeps today's
40
+ * behavior (no check from default events). With `cursorAccount`, enabling
41
+ * this opts the minted token up to `permissions: "contents-write"` so the
42
+ * hosted github-proxy lease includes `checks:write`.
40
43
  */
41
44
  commitStatus?: false | GitHubCommitStatusProgress;
42
45
  /**
@@ -127,7 +130,7 @@ export async function applyGitHubProgress(input: {
127
130
  const tasks: Array<Promise<void>> = [];
128
131
  if (commitStatus !== null) {
129
132
  tasks.push(
130
- postCommitStatus({
133
+ postProgressCheckRun({
131
134
  phase: input.phase,
132
135
  channel: input.channel,
133
136
  options: input.options,
@@ -149,7 +152,7 @@ export async function applyGitHubProgress(input: {
149
152
  await Promise.all(tasks);
150
153
  }
151
154
 
152
- async function postCommitStatus(input: {
155
+ async function postProgressCheckRun(input: {
153
156
  phase: GitHubProgressPhase;
154
157
  channel: { state: GitHubChannelState };
155
158
  options: GitHubProgressOptions;
@@ -164,6 +167,7 @@ async function postCommitStatus(input: {
164
167
  ) {
165
168
  return;
166
169
  }
170
+ const sha = state.headSha;
167
171
  const description = clipStatusDescription(
168
172
  progressPhaseCopy({
169
173
  copy: input.commitStatus,
@@ -171,23 +175,84 @@ async function postCommitStatus(input: {
171
175
  label: progressLabel(input.options, input.commitStatus.context),
172
176
  })
173
177
  );
178
+ const write: GitHubCheckRunWriteInput = {
179
+ name: input.commitStatus.context,
180
+ ...checkRunPhaseFields(input.phase),
181
+ output: { title: description, summary: description },
182
+ };
183
+ const resource = {
184
+ api: input.options.api,
185
+ credentials: input.options.credentials,
186
+ installationId: state.installationId ?? undefined,
187
+ owner: state.owner,
188
+ repo: state.repo,
189
+ };
190
+ const reuseId =
191
+ state.progressCheckRunId !== null && state.progressCheckRunSha === sha
192
+ ? state.progressCheckRunId
193
+ : null;
194
+ // Completed runs cannot go back to in_progress. GitHub often 200s and
195
+ // leaves the run completed (sometimes 422). Create a new run so a later
196
+ // turn on the same SHA shows pending again.
197
+ const completedBlocksReuse =
198
+ write.status === "in_progress" && state.progressCheckRunCompleted === true;
174
199
  try {
175
- await createGitHubCommitStatus({
176
- api: input.options.api,
177
- credentials: input.options.credentials,
178
- installationId: state.installationId ?? undefined,
179
- owner: state.owner,
180
- repo: state.repo,
181
- sha: state.headSha,
182
- state: input.phase,
183
- context: input.commitStatus.context,
184
- description,
200
+ if (reuseId !== null && !completedBlocksReuse) {
201
+ try {
202
+ await updateGitHubCheckRun({
203
+ ...resource,
204
+ ...write,
205
+ checkRunId: reuseId,
206
+ });
207
+ state.progressCheckRunCompleted = write.status === "completed";
208
+ return;
209
+ } catch (error) {
210
+ if (!shouldRecreateCheckRun(error, write.status)) {
211
+ return;
212
+ }
213
+ state.progressCheckRunId = null;
214
+ state.progressCheckRunSha = null;
215
+ state.progressCheckRunCompleted = false;
216
+ }
217
+ }
218
+ const created = await createGitHubCheckRun({
219
+ ...resource,
220
+ ...write,
221
+ sha,
185
222
  });
223
+ state.progressCheckRunId = created.id;
224
+ state.progressCheckRunSha = sha;
225
+ state.progressCheckRunCompleted = write.status === "completed";
186
226
  } catch {
187
227
  // Soft-fail: missing token / permission is fine.
188
228
  }
189
229
  }
190
230
 
231
+ function shouldRecreateCheckRun(
232
+ error: unknown,
233
+ status: GitHubCheckRunWriteInput["status"]
234
+ ): boolean {
235
+ if (!(error instanceof GitHubApiError)) {
236
+ return false;
237
+ }
238
+ if (error.status === 404) {
239
+ return true;
240
+ }
241
+ return error.status === 422 && status === "in_progress";
242
+ }
243
+
244
+ function checkRunPhaseFields(
245
+ phase: GitHubProgressPhase
246
+ ): Pick<GitHubCheckRunWriteInput, "conclusion" | "status"> {
247
+ if (phase === "pending") {
248
+ return { status: "in_progress" };
249
+ }
250
+ return {
251
+ status: "completed",
252
+ conclusion: phase === "success" ? "success" : "failure",
253
+ };
254
+ }
255
+
191
256
  async function upsertBanner(input: {
192
257
  phase: GitHubProgressPhase;
193
258
  channel: { state: GitHubChannelState };
@@ -13,6 +13,17 @@ export interface GitHubChannelState extends GitHubBindingState {
13
13
  headRef: string | null;
14
14
  /** Sticky progress banner comment id (issue/PR timeline). */
15
15
  progressBannerCommentId: number | null;
16
+ /** Latest Checks API run id for `progress.commitStatus`. */
17
+ progressCheckRunId: number | null;
18
+ /** Head SHA the latest check run was created against. */
19
+ progressCheckRunSha: string | null;
20
+ /**
21
+ * Whether that run is already `completed`. `null` means unknown (wake
22
+ * patches omit this so merge keeps the persisted value). GitHub rejects
23
+ * PATCH back to `in_progress`, so a later pending turn on the same SHA
24
+ * must create a new run.
25
+ */
26
+ progressCheckRunCompleted: boolean | null;
16
27
  }
17
28
 
18
29
  export function emptyGitHubChannelState(
@@ -35,6 +46,9 @@ export function emptyGitHubChannelState(
35
46
  baseRef: null,
36
47
  headRef: null,
37
48
  progressBannerCommentId: null,
49
+ progressCheckRunId: null,
50
+ progressCheckRunSha: null,
51
+ progressCheckRunCompleted: null,
38
52
  ...overrides,
39
53
  };
40
54
  }
@@ -452,7 +452,7 @@ export interface GitHubChannelOptions {
452
452
  * Token capability tier:
453
453
  * - `"read"` — PR/issue/status read
454
454
  * - `"pr-write"` (default) — comments / PR+issue write, no `contents:write`
455
- * - `"contents-write"` — push code and create commit statuses (host-held)
455
+ * - `"contents-write"` — push code and post merge-box check runs (host-held)
456
456
  */
457
457
  permissions?: "read" | "pr-write" | "contents-write";
458
458
  };
@@ -504,12 +504,12 @@ export interface GitHubChannelOptions {
504
504
  onEvent?: GitHubEventHook;
505
505
  /**
506
506
  * Override / extend default stream-event handlers (eyes reaction, reply
507
- * posting, opt-in commit status / banner). Handlers you provide replace
507
+ * posting, opt-in check run / banner). Handlers you provide replace
508
508
  * the matching defaults.
509
509
  */
510
510
  events?: ChannelEventHandlers<GitHubChannelState>;
511
511
  /**
512
- * Acknowledgement progress surfaces. Reactions default on; commit status
512
+ * Acknowledgement progress surfaces. Reactions default on; merge-box check
513
513
  * and sticky banner are opt-in via `commitStatus` / `banner`.
514
514
  */
515
515
  progress?: GitHubProgressConfig;
package/src/index.ts CHANGED
@@ -94,6 +94,7 @@ export {
94
94
  } from "./internal/session-engine.js";
95
95
  export { createMemoryKv, memoryCasTable } from "./kv/memory-kv.js";
96
96
  export {
97
+ createLedgerJanitor,
97
98
  LedgerCasExhaustedError,
98
99
  LedgerFencedError,
99
100
  SessionLedger,
@@ -20,6 +20,16 @@ export function isSafeSessionId(sessionId: string): boolean {
20
20
  );
21
21
  }
22
22
 
23
+ /** Thrown when a send addresses a session owned by a different principal. */
24
+ export class SessionAuthError extends Error {
25
+ constructor(sessionId: string) {
26
+ super(
27
+ `Session ${sessionId} belongs to a different principal; pass the original caller auth on follow-up`
28
+ );
29
+ this.name = "SessionAuthError";
30
+ }
31
+ }
32
+
23
33
  /** True when both sides identify the same principal (or both are null). */
24
34
  export function samePrincipal(
25
35
  a: AuthContext | null,
@@ -3,11 +3,13 @@
3
3
  *
4
4
  * Two APIs, one backend: `defineStorage` is the **storage API** (agent
5
5
  * state — session records, event chunks, author KV), and the session
6
- * ledger is the **durable session API** (session identity, leases,
7
- * fenced commits). The ledger consumes {@link SessionLedgerKv} — exactly
8
- * the three CAS methods it uses, nothing more; this module adapts a
9
- * storage backend's `cas` group into that interface, so a deployment's
10
- * one authored backend serves both APIs by default.
6
+ * ledger is the **durable session API** (session identity, mailboxes,
7
+ * leases, fenced commits). The ledger consumes {@link SessionLedgerKv} —
8
+ * the full CAS-capable KV surface, since the inbox reads/sweeps mailbox
9
+ * payload blobs (`get`/`put`/`delete`) and scans by prefix (`listKeys`)
10
+ * on top of the CAS trio; this module adapts a storage backend's methods
11
+ * into that interface, so a deployment's one authored backend serves
12
+ * both APIs by default.
11
13
  *
12
14
  * Calls go straight at the config's methods — NOT through the
13
15
  * StorageCoordinator: the coordinator's queue is bounded, serialized, and
@@ -22,6 +24,7 @@ import {
22
24
  type StorageConfig,
23
25
  type StorageContext,
24
26
  } from "../storage.js";
27
+ import type { JsonValue } from "../types.js";
25
28
 
26
29
  export interface StorageCasKvArgs {
27
30
  agentName: string;
@@ -29,10 +32,29 @@ export interface StorageCasKvArgs {
29
32
  }
30
33
 
31
34
  /**
32
- * Adapt a storage backend's `cas` group into the ledger's KV interface.
33
- * Throws when the backend does not implement `cas` — the caller turns
34
- * that into the fail-fast startup error for `serving: "ledger"`. Every
35
- * call carries a `reason: "coordination"` {@link StorageContext}.
35
+ * The one place the ledger's physical namespace is minted: the per-agent
36
+ * prefix for ledger state, following the {@link storageKeys} convention
37
+ * (`<root>/<agent>/<family>/…`) so a store shared by several ledger-mode
38
+ * agents (one fileKv dir, one hosted deployment) never mixes their
39
+ * sessions, leases, or mailboxes — and so each agent's janitor
40
+ * prefix-scan only ever sees its own ledgers. The extra
41
+ * `durable-session` segment keeps the ledger's `session/` family out of
42
+ * `storageKeys.sessionPrefix` scans (storage restore lists
43
+ * `<root>/<agent>/session/`). The ledger itself never sees this string:
44
+ * it speaks bare `session/…` / `ledger/…` / `mailbox/…` keys against a
45
+ * handle that {@link namespacedKv} has already scoped.
46
+ */
47
+ export function ledgerKeyPrefix(agentName: string): string {
48
+ return `${STORAGE_KEY_ROOT}/${keySegment(agentName)}/durable-session/`;
49
+ }
50
+
51
+ /**
52
+ * Adapt a storage backend into the ledger's KV interface. Throws when
53
+ * the backend does not implement the `cas` group, or omits `get` /
54
+ * `delete` (optional for resident-only sinks, but the ledger reads and
55
+ * sweeps mailbox payload blobs through the same store) — the caller
56
+ * turns that into the fail-fast startup error for `serving: "ledger"`.
57
+ * Every call carries a `reason: "coordination"` {@link StorageContext}.
36
58
  */
37
59
  export function storageCasKv(
38
60
  config: StorageConfig,
@@ -49,7 +71,23 @@ export function storageCasKv(
49
71
  "storageCasKv: the storage backend does not implement the cas group (conditional writes)"
50
72
  );
51
73
  }
74
+ const get = config.get;
75
+ const remove = config.delete;
76
+ if (get === undefined || remove === undefined) {
77
+ throw new Error(
78
+ "storageCasKv: the storage backend must implement get and delete (the session ledger reads and sweeps payload blobs through the same store)"
79
+ );
80
+ }
52
81
  return {
82
+ async get(key): Promise<JsonValue | undefined> {
83
+ return (await get(key, ctx)) ?? undefined;
84
+ },
85
+ async put(key, value): Promise<void> {
86
+ await config.put(key, value, ctx);
87
+ },
88
+ async delete(key): Promise<void> {
89
+ await remove(key, ctx);
90
+ },
53
91
  async getWithVersion(key) {
54
92
  return await cas.getWithVersion(key, ctx);
55
93
  },
@@ -59,37 +97,38 @@ export function storageCasKv(
59
97
  async putIfVersion(key, value, expectedVersion) {
60
98
  return await cas.putIfVersion(key, value, expectedVersion, ctx);
61
99
  },
100
+ async listKeys(prefix) {
101
+ return await cas.listKeys(prefix, ctx);
102
+ },
62
103
  };
63
104
  }
64
105
 
65
106
  /**
66
- * The one place the ledger's physical namespace is minted:
67
- * `agentkit/v1/<agent>/durable-session/` the {@link STORAGE_KEY_ROOT}
68
- * conventions with the agent segment encoded like every other
69
- * caller-controlled segment. The ledger itself never sees this string;
70
- * it speaks bare `session/…` / `ledger/…` keys against a handle that
71
- * {@link namespacedKv} has already scoped.
72
- */
73
- export function ledgerKeyPrefix(agentName: string): string {
74
- return `${STORAGE_KEY_ROOT}/${keySegment(agentName)}/durable-session/`;
75
- }
76
-
77
- /**
78
- * Scope a raw store handle under a namespace, prepending it to every
79
- * key on the way in. Applied exactly once, where the engine constructs
80
- * the ledger's store handle — the same conceptual boundary where
81
- * `host.kv` gets its agent scoping. Internal: not part of the public kv
82
- * interface, and callers (including the explicit `durableSessionStore`
83
- * override) always supply the raw store — they never write prefixes.
107
+ * Scope a raw store handle under a namespace: every key going in gets it
108
+ * prepended, and `listKeys` results come back with it **stripped**, so a
109
+ * caller that lists and then reads/deletes the returned keys stays fully
110
+ * prefix-free (the janitor scan and the orphan sweep do exactly that).
111
+ * Applied exactly once, where the engine constructs the ledger's store
112
+ * handle the same conceptual boundary where `host.kv` gets its agent
113
+ * scoping. Internal: not part of the public kv interface, and callers
114
+ * (including the explicit `durableSessionStore` override) always supply
115
+ * the raw store — they never write prefixes.
84
116
  */
85
117
  export function namespacedKv(
86
118
  store: SessionLedgerKv,
87
119
  namespace: string
88
120
  ): SessionLedgerKv {
89
121
  return {
122
+ get: (key) => store.get(namespace + key),
123
+ put: (key, value) => store.put(namespace + key, value),
124
+ delete: (key) => store.delete(namespace + key),
90
125
  getWithVersion: (key) => store.getWithVersion(namespace + key),
91
126
  putIfAbsent: (key, value) => store.putIfAbsent(namespace + key, value),
92
127
  putIfVersion: (key, value, expectedVersion) =>
93
128
  store.putIfVersion(namespace + key, value, expectedVersion),
129
+ listKeys: async (prefix) => {
130
+ const keys = await store.listKeys(namespace + prefix);
131
+ return keys.map((key) => key.slice(namespace.length));
132
+ },
94
133
  };
95
134
  }