@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
@@ -1,24 +1,35 @@
1
1
  /**
2
- * Ledger-mode serving seam (`serving: "ledger"` on `defineAgent`):
3
- * coordination only, no inbox. Every engine `send` acquires the session
4
- * lease before its turn runs and commits (head+1, lease released) after
5
- * it completes, so cross-pod mutual exclusion, generation fencing, and
6
- * the committed head sequence are real.
2
+ * Ledger-mode serving seam (`serving: "ledger"` on `defineAgent`): an event
3
+ * pump that routes every engine `send` through the KV session ledger
4
+ * instead of starting turns directly.
7
5
  *
8
- * **Durability trade-off (deliberate):** there is no durable mailbox.
9
- * An event that arrives while the lease is held does NOT park in the
10
- * store the delivering pod waits *in-process*: local concurrent sends
11
- * serialize on a per-session promise chain (no claim spinning), and a
12
- * lease held by another pod is waited out with bounded claim retries. A
13
- * crash therefore loses the in-flight event exactly as resident serving
14
- * does. A dead pod's lease simply expires (~30s TTL); the next delivery
15
- * claims at generation+1 no janitor is needed because nothing is
16
- * parked.
6
+ * Delivery: ensureSession (continuation key, as today) principal gate
7
+ * (an existing session rejects a sender that is not its principal BEFORE
8
+ * anything is parked; mirrors the resident `samePrincipal` check)
9
+ * appendEvent (payload + CAS the source ack point moves after this)
10
+ * claim drain run the drained turns through the resident engine path →
11
+ * commit (heartbeats while turns run) repeat while the commit reports
12
+ * pending. A deliver that finds the lease held walks away (the holder's
13
+ * commit sees the append); the janitor recovers sessions whose holder
14
+ * died. A foreign-principal envelope that reaches the mailbox anyway is
15
+ * consumed and committed away at drain (warn log), never replayed.
17
16
  *
18
- * Re-entrancy caveat (same shape as resident `waitForCompletion`): a
19
- * send issued from *inside* a running turn against the same session
20
- * chains behind that turn's serve, so awaiting its resolution from the
21
- * turn would deadlock. Fire-and-forget such sends.
17
+ * v1 scope: the mailbox envelope carries `message`, `auth`, `title`,
18
+ * `state`, `mode`, `purpose`, and the isolation-relevant per-send
19
+ * overrides `workspaceDir` and `cloud` the two options
20
+ * `resolveSessionRuntime` and harness-cwd selection read. Dropping
21
+ * those is a sandbox escape, not a feature gap: without them a drained
22
+ * GitHub/Slack turn falls back to the local runtime with the default
23
+ * harness cwd, so untrusted channel content drives tools on the serve
24
+ * host instead of the cloud VM / channel worktree. `coalesceSourceTs`
25
+ * (when present) is used as the drain-ordering `sourceTs`. Deferred
26
+ * (documented, not silently faked — deliver logs a warning naming what
27
+ * it dropped): `workspaceFiles` and per-turn `images` (content
28
+ * injection, and their base64 payloads can blow the mailbox blob past
29
+ * hosted KV value caps), `sdkAgentId` seeding, and `admission`
30
+ * semantics (both `"preempt"` and Slack `"coalesce"` nudge bookkeeping
31
+ * — ledger draining already serializes turns, but nothing is
32
+ * interrupted or coalesced through the mailbox).
22
33
  */
23
34
  var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
24
35
  function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
@@ -30,77 +41,277 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
30
41
  });
31
42
  };
32
43
  import { randomBytes, randomUUID } from "node:crypto";
33
- import { HEARTBEAT_INTERVAL_MS, LEASE_TTL_MS, LedgerFencedError, } from "../ledger/session-ledger.js";
44
+ import { createLedgerJanitor } from "../ledger/janitor.js";
45
+ import { HEARTBEAT_INTERVAL_MS, LedgerFencedError, } from "../ledger/session-ledger.js";
46
+ import { SessionAuthError, samePrincipal } from "./auth.js";
47
+ import { describeError } from "./describe-error.js";
34
48
  import { newContinuationKey } from "./ids.js";
35
- /** Wait between claim retries while another holder has the lease. */
36
- export const CLAIM_RETRY_DELAY_MS = 250;
37
- /**
38
- * Give up waiting for the lease after this long — two full lease TTLs,
39
- * so a dead holder's expiry (~30s) is always survived with margin.
40
- */
41
- export const CLAIM_WAIT_TIMEOUT_MS = 2 * LEASE_TTL_MS;
42
49
  export class LedgerServing {
43
50
  constructor(options) {
44
- var _a, _b, _c, _d, _e, _f;
45
- /**
46
- * Per-ledger-session in-process serialization: local concurrent sends
47
- * queue here instead of spinning claim retries against our own lease.
48
- */
49
- this.chains = new Map();
51
+ var _a, _b, _c;
52
+ /** One pump per ledger session — the in-process face of the lease. */
53
+ this.pumps = new Map();
54
+ /** Fire-and-forget re-pump checks, awaited on close. */
55
+ this.background = new Set();
50
56
  this.ledger = options.ledger;
51
57
  this.deliverTurn = options.deliverTurn;
58
+ this.findSession = options.findSession;
52
59
  this.logger = (_a = options.logger) !== null && _a !== void 0 ? _a : (() => { });
53
60
  this.heartbeatIntervalMs = (_b = options.heartbeatIntervalMs) !== null && _b !== void 0 ? _b : HEARTBEAT_INTERVAL_MS;
54
- this.claimRetryDelayMs = (_c = options.claimRetryDelayMs) !== null && _c !== void 0 ? _c : CLAIM_RETRY_DELAY_MS;
55
- this.claimWaitTimeoutMs = (_d = options.claimWaitTimeoutMs) !== null && _d !== void 0 ? _d : CLAIM_WAIT_TIMEOUT_MS;
56
- this.clock = (_e = options.clock) !== null && _e !== void 0 ? _e : Date.now;
57
- this.sleep = (_f = options.sleep) !== null && _f !== void 0 ? _f : ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
61
+ this.clock = (_c = options.clock) !== null && _c !== void 0 ? _c : Date.now;
62
+ this.janitor = createLedgerJanitor(Object.assign(Object.assign({ ledger: options.ledger, onSession: (claimed) => this.serveClaimed(claimed),
63
+ // Consumed mailbox blobs (full message envelopes) would otherwise
64
+ // accumulate forever; the default TTL (1h) is far beyond any lease
65
+ // or turn horizon, so only truly dead blobs are collected.
66
+ orphanSweep: {} }, (options.janitorIntervalMs === undefined
67
+ ? {}
68
+ : { intervalMs: options.janitorIntervalMs })), { onError: (error, sessionId) => {
69
+ this.logger(`[ledger] janitor error${sessionId === undefined ? "" : ` (session ${sessionId})`}: ${describeError(error)}`);
70
+ }, clock: this.clock }));
58
71
  }
59
- /** Settle every queued and in-flight delivery. */
72
+ /** Start the janitor loop (recovers expired-lease sessions). */
73
+ start() {
74
+ this.janitor.start();
75
+ }
76
+ /** Stop the janitor and settle in-flight pumps and re-pump checks. */
60
77
  close() {
61
78
  return __awaiter(this, void 0, void 0, function* () {
62
- yield Promise.allSettled([...this.chains.values()]);
79
+ // Awaiting stop() matters: an in-flight janitor pass can claim a
80
+ // session and register a pump after this method snapshots the maps.
81
+ yield this.janitor.stop();
82
+ yield Promise.allSettled([...this.pumps.values()]);
83
+ yield Promise.allSettled([...this.background]);
63
84
  });
64
85
  }
65
86
  /**
66
- * Ledger-mode `send`: resolve the session identity, wait for the
67
- * lease (in-process queue first, then cross-pod claim retries), run
68
- * the turn, commit. Resolves with the engine session handle after the
69
- * commit released the lease.
87
+ * Ledger-mode `send`: append the message to the session mailbox, then
88
+ * serve pending events (or walk away if another holder is already
89
+ * serving them). Resolves with the engine session handle once one
90
+ * exists the ack point is the durable append, not turn completion —
91
+ * so HTTP create/follow-up can return a session id while the holder is
92
+ * still in flight. When this process wins the claim, the pump keeps
93
+ * draining in the background; callers that need the turn to finish use
94
+ * `waitForCompletion` (and the lease releases on the pump's commit).
95
+ * When another holder is serving the session and no engine session
96
+ * exists on this pod, the delivery is still a success — the event is
97
+ * durably parked and the holder's commit (or the janitor) drains it —
98
+ * so the resolved handle is a parked stand-in carrying the durable
99
+ * session identity.
70
100
  */
71
101
  deliver(channelId, message, options) {
72
102
  return __awaiter(this, void 0, void 0, function* () {
73
- var _a;
103
+ var _a, _b, _c;
74
104
  const continuationToken = (_a = options.continuationToken) !== null && _a !== void 0 ? _a : newContinuationKey();
105
+ this.warnOnDroppedOptions(channelId, options);
75
106
  const ensured = yield this.ledger.ensureSession(`${channelId}/${continuationToken}`);
76
- return this.enqueue(ensured.session.sessionId, () => this.serveOne(ensured.session.sessionId, {
107
+ const ledgerSessionId = ensured.session.sessionId;
108
+ // Principal gate BEFORE the durable append: once an envelope is
109
+ // parked it WILL be drained, so a foreign sender must be rejected
110
+ // here — not on the resident path after the mailbox write. A new
111
+ // ledger session has no owner yet: the creating sender becomes it,
112
+ // exactly as on resident session creation.
113
+ if (!ensured.created) {
114
+ const existing = yield this.findSession(channelId, continuationToken);
115
+ if (existing !== undefined) {
116
+ if (!samePrincipal(existing.info.auth, (_b = options.auth) !== null && _b !== void 0 ? _b : null)) {
117
+ throw new SessionAuthError(existing.id);
118
+ }
119
+ }
120
+ // No handle: the engine session does not exist yet (the first
121
+ // parked event has not run — the lease holder creates it) or a
122
+ // cross-pod restore transiently failed. Park rather than fail
123
+ // closed: rejecting here would bounce the legitimate owner racing
124
+ // the session's first turn. This is not fail-open — the resident
125
+ // path re-checks the principal when the event drains, and the
126
+ // drain treats a SessionAuthError as consume-and-commit, so a
127
+ // foreign envelope that parks through this window costs one warn
128
+ // line and never actuates, bricks, or replays the session.
129
+ }
130
+ const envelope = Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({ channelId,
131
+ continuationToken,
132
+ message, auth: (_c = options.auth) !== null && _c !== void 0 ? _c : null }, (options.title === undefined ? {} : { title: options.title })), (options.state === undefined ? {} : { state: options.state })), (options.mode === undefined ? {} : { mode: options.mode })), (options.purpose === undefined ? {} : { purpose: options.purpose })), (options.workspaceDir === undefined
133
+ ? {}
134
+ : { workspaceDir: options.workspaceDir })), (options.cloud === undefined ? {} : { cloud: options.cloud }));
135
+ yield this.ledger.appendEvent(ledgerSessionId, {
136
+ eventId: `evt_${randomBytes(12).toString("hex")}`,
137
+ envelope: envelope,
138
+ sourceTs: this.resolveSourceTs(options),
139
+ });
140
+ // Ack point: the event is durable (mailbox blob + ledger pending).
141
+ // Do not await the pump: awaiting turns here would block HTTP create
142
+ // until the first turn committed, so a client could never park a
143
+ // mid-turn follow-up and a second pod would see a released lease.
144
+ const serving = this.servePending(ledgerSessionId);
145
+ return this.resolveDeliverHandle({
77
146
  channelId,
78
147
  continuationToken,
79
- message,
80
- options,
81
- }));
148
+ ledgerSessionId,
149
+ envelope,
150
+ created: ensured.created,
151
+ serving,
152
+ });
82
153
  });
83
154
  }
84
- /** Chain a delivery behind the session's in-flight ones. */
85
- enqueue(ledgerSessionId, run) {
155
+ /**
156
+ * The stand-in handle for a delivery whose event is parked while
157
+ * another holder serves the session: real durable identity, no local
158
+ * turn. `waitForCompletion` resolves immediately — the parked event's
159
+ * turn runs on the holder, so there is nothing local to await (the
160
+ * same contract as a coalesced send, whose outcome is not its own
161
+ * message's turn). The event stream and re-keying live wherever the
162
+ * turn runs, so both fail loudly here.
163
+ */
164
+ parkedSession(ledgerSessionId, envelope, isNew) {
86
165
  var _a;
87
- const prior = (_a = this.chains.get(ledgerSessionId)) !== null && _a !== void 0 ? _a : Promise.resolve();
88
- const next = prior.then(() => run(), () => run());
89
- this.chains.set(ledgerSessionId, next);
90
- void next
91
- .catch(() => { })
92
- .finally(() => {
93
- if (this.chains.get(ledgerSessionId) === next) {
94
- this.chains.delete(ledgerSessionId);
166
+ const unavailable = (what) => new Error(`ledger serving: ${what} is unavailable for session ${ledgerSessionId}: the parked event runs on the session's current lease holder, not this process`);
167
+ return {
168
+ id: ledgerSessionId,
169
+ continuationToken: envelope.continuationToken,
170
+ isNew,
171
+ info: Object.assign(Object.assign(Object.assign({ id: ledgerSessionId, channelId: envelope.channelId, mode: (_a = envelope.mode) !== null && _a !== void 0 ? _a : "chat", auth: envelope.auth }, (envelope.title === undefined ? {} : { title: envelope.title })), (envelope.purpose === undefined
172
+ ? {}
173
+ : { purpose: envelope.purpose })), { continuationKey: envelope.continuationToken }),
174
+ getEventStream: () => Promise.reject(unavailable("the event stream")),
175
+ waitForCompletion: () => __awaiter(this, void 0, void 0, function* () { return ({ status: "finished" }); }),
176
+ setContinuationToken: () => Promise.reject(unavailable("re-keying the continuation token")),
177
+ };
178
+ }
179
+ /**
180
+ * Return a handle as soon as the engine session exists, without waiting
181
+ * for the pump to commit. If the pump settles without creating a
182
+ * session (walk-away, or a throw before admit), surface that: rethrow
183
+ * a failed claimant turn, otherwise the parked stand-in.
184
+ */
185
+ resolveDeliverHandle(args) {
186
+ return __awaiter(this, void 0, void 0, function* () {
187
+ const { channelId, continuationToken, ledgerSessionId, envelope, created, serving, } = args;
188
+ for (;;) {
189
+ const handle = yield this.findSession(channelId, continuationToken);
190
+ if (handle !== undefined) {
191
+ return handle;
192
+ }
193
+ const pump = this.pumps.get(ledgerSessionId);
194
+ if (pump !== undefined) {
195
+ yield Promise.race([
196
+ new Promise((resolve) => {
197
+ setTimeout(resolve, 15);
198
+ }),
199
+ pump.then(() => undefined, () => undefined),
200
+ ]);
201
+ continue;
202
+ }
203
+ const createdLate = yield this.findSession(channelId, continuationToken);
204
+ if (createdLate !== undefined) {
205
+ return createdLate;
206
+ }
207
+ yield serving;
208
+ this.logger(`[ledger] deliver on channel "${channelId}": event parked for session ${ledgerSessionId} (continuation ${continuationToken}); another holder is serving it and no local session exists — its commit (or the janitor) drains the event`);
209
+ return this.parkedSession(ledgerSessionId, envelope, created);
210
+ }
211
+ });
212
+ }
213
+ /**
214
+ * Drain ordering: prefer the real source time over this pod's receipt
215
+ * time. `coalesceSourceTs` is a Slack-style seconds string
216
+ * (`"1712345678.000100"`); normalize sub-epoch-ms values to ms so they
217
+ * stay comparable with the `Date.now()` fallback.
218
+ */
219
+ resolveSourceTs(options) {
220
+ if (options.coalesceSourceTs !== undefined) {
221
+ const parsed = Number(options.coalesceSourceTs);
222
+ if (Number.isFinite(parsed) && parsed > 0) {
223
+ return parsed < 1e11 ? Math.round(parsed * 1000) : parsed;
224
+ }
225
+ }
226
+ return this.clock();
227
+ }
228
+ /**
229
+ * One warning per delivery naming the send options the mailbox does not
230
+ * replay — better a loud line than an author silently losing images or
231
+ * admission semantics on a ledger-mode agent.
232
+ */
233
+ warnOnDroppedOptions(channelId, options) {
234
+ const dropped = ["images", "sdkAgentId", "admission", "workspaceFiles"].filter((name) => options[name] !== undefined);
235
+ if (dropped.length > 0) {
236
+ this.logger(`[ledger] deliver on channel "${channelId}": dropping send options not replayed through the mailbox: ${dropped.join(", ")}`);
237
+ }
238
+ }
239
+ /**
240
+ * Serve a session's pending events. When a pump is already running the
241
+ * caller walks away (the holder's commit re-reads pending) — except
242
+ * that the holder's *final* commit may have released before this
243
+ * append landed, so a re-check is chained behind it. Deliver never
244
+ * awaits this promise for its full lifetime: awaiting a new pump would
245
+ * block HTTP until the turn committed, and awaiting a running pump
246
+ * would deadlock a send issued from inside one of its turns.
247
+ */
248
+ servePending(ledgerSessionId) {
249
+ const running = this.pumps.get(ledgerSessionId);
250
+ if (running !== undefined) {
251
+ const recheck = running
252
+ .catch(() => { })
253
+ .then(() => this.repumpIfPending(ledgerSessionId))
254
+ .catch(() => { });
255
+ this.background.add(recheck);
256
+ void recheck.finally(() => {
257
+ this.background.delete(recheck);
258
+ });
259
+ return Promise.resolve();
260
+ }
261
+ return this.track(ledgerSessionId, () => this.runPump(ledgerSessionId));
262
+ }
263
+ /** Claim-or-defer entry used after an append and after a pump settles. */
264
+ runPump(ledgerSessionId) {
265
+ return __awaiter(this, void 0, void 0, function* () {
266
+ const claim = yield this.ledger.claim(ledgerSessionId);
267
+ if (!claim.claimed) {
268
+ return;
269
+ }
270
+ yield this.runClaimedTurns(ledgerSessionId, claim.lease);
271
+ });
272
+ }
273
+ /** Janitor hand-off: the claim already happened, just run the turns. */
274
+ serveClaimed(claimed) {
275
+ return this.track(claimed.sessionId, () => this.runClaimedTurns(claimed.sessionId, claimed.lease));
276
+ }
277
+ /**
278
+ * Re-pump after the previous holder settled, covering the release window
279
+ * where an append landed after that holder's final commit.
280
+ */
281
+ repumpIfPending(ledgerSessionId) {
282
+ return __awaiter(this, void 0, void 0, function* () {
283
+ if (this.pumps.has(ledgerSessionId)) {
284
+ return;
285
+ }
286
+ const snapshot = yield this.ledger.readLedger(ledgerSessionId);
287
+ if (snapshot === undefined || snapshot.pending.length === 0) {
288
+ return;
289
+ }
290
+ if (snapshot.lease !== null && snapshot.lease.expiresAt > this.clock()) {
291
+ return;
292
+ }
293
+ yield this.track(ledgerSessionId, () => this.runPump(ledgerSessionId));
294
+ });
295
+ }
296
+ /** Register a pump in the per-session map for its lifetime. */
297
+ track(ledgerSessionId, run) {
298
+ const tracked = run().finally(() => {
299
+ if (this.pumps.get(ledgerSessionId) === tracked) {
300
+ this.pumps.delete(ledgerSessionId);
95
301
  }
96
302
  });
97
- return next;
303
+ this.pumps.set(ledgerSessionId, tracked);
304
+ return tracked;
98
305
  }
99
- /** claim (waiting) → resident turn → commit-release. */
100
- serveOne(ledgerSessionId, request) {
306
+ /**
307
+ * The drain → turn → commit loop for a held lease. Heartbeats extend the
308
+ * lease while turns run; a fenced heartbeat (or fenced commit) means a
309
+ * new holder took over — discard without committing; the new holder
310
+ * re-drains (at-least-once actuation, exactly-once ledger apply).
311
+ */
312
+ runClaimedTurns(ledgerSessionId, lease) {
101
313
  return __awaiter(this, void 0, void 0, function* () {
102
314
  var _a;
103
- const lease = yield this.claimWithWait(ledgerSessionId);
104
315
  let fenced = false;
105
316
  const heartbeat = setInterval(() => {
106
317
  this.ledger.heartbeat(ledgerSessionId, lease.generation).then((result) => {
@@ -113,60 +324,68 @@ export class LedgerServing {
113
324
  }, this.heartbeatIntervalMs);
114
325
  (_a = heartbeat.unref) === null || _a === void 0 ? void 0 : _a.call(heartbeat);
115
326
  try {
116
- let handle;
117
- try {
118
- handle = yield this.deliverTurn(request);
119
- yield handle.waitForCompletion();
120
- }
121
- catch (error) {
122
- // The turn never committed; free the session for the next
123
- // delivery instead of making it wait out the TTL. A fenced
124
- // holder's release is a no-op.
125
- yield this.ledger
126
- .release(ledgerSessionId, lease.generation)
127
- .catch(() => { });
128
- throw error;
327
+ for (;;) {
328
+ if (fenced) {
329
+ return;
330
+ }
331
+ const { head, events } = yield this.ledger.drain(ledgerSessionId);
332
+ if (events.length === 0) {
333
+ yield this.ledger.release(ledgerSessionId, lease.generation);
334
+ return;
335
+ }
336
+ for (const event of events) {
337
+ if (fenced) {
338
+ return;
339
+ }
340
+ const envelope = event.envelope;
341
+ try {
342
+ const session = yield this.deliverTurn(envelope);
343
+ yield session.waitForCompletion();
344
+ }
345
+ catch (error) {
346
+ if (!(error instanceof SessionAuthError)) {
347
+ throw error;
348
+ }
349
+ // Defense in depth behind deliver's principal gate: a parked
350
+ // envelope from a foreign principal (gate raced the session's
351
+ // first turn, or a pre-gate build wrote it) is consumed and
352
+ // committed away like any drained event. Aborting the batch
353
+ // instead would replay the already-actuated turns before it
354
+ // and re-throw on every future drain — one poison event must
355
+ // cost a log line, never a stuck session.
356
+ this.logger(`[ledger] session ${ledgerSessionId}: discarding parked event ${event.eventId} — its sender is not the session's principal (${describeError(error)})`);
357
+ }
358
+ }
359
+ const commit = yield this.ledger.commit(ledgerSessionId, {
360
+ drainedEventIds: events.map((event) => event.eventId),
361
+ generation: lease.generation,
362
+ expectedHead: head,
363
+ commitId: randomUUID(),
364
+ });
365
+ if (commit.leaseReleased) {
366
+ return;
367
+ }
129
368
  }
130
- if (fenced) {
131
- // A new holder took over mid-turn (our lease lapsed). Discard:
132
- // committing would fence anyway, and warm state is now stale.
133
- this.logger(`[ledger] session ${ledgerSessionId} fenced mid-turn (generation ${lease.generation}); discarding without commit`);
134
- throw new LedgerFencedError(`deliver(${ledgerSessionId}): fenced mid-turn at generation ${lease.generation}`);
369
+ }
370
+ catch (error) {
371
+ if (error instanceof LedgerFencedError) {
372
+ this.logger(`[ledger] session ${ledgerSessionId} fenced mid-turn (generation ${lease.generation}); discarding the new holder re-drains`);
373
+ return;
135
374
  }
136
- yield this.ledger.commit(ledgerSessionId, {
137
- generation: lease.generation,
138
- expectedHead: lease.head,
139
- commitId: randomUUID(),
140
- });
141
- return handle;
375
+ // The turn never committed; free the session for the next
376
+ // append-driven claim instead of making it wait out the lease TTL
377
+ // (and the janitor cadence). Best-effort: a fenced holder's
378
+ // release is a no-op, and the events stay pending either way.
379
+ yield this.ledger
380
+ .release(ledgerSessionId, lease.generation)
381
+ .catch(() => { });
382
+ throw error;
142
383
  }
143
384
  finally {
144
385
  clearInterval(heartbeat);
145
386
  }
146
387
  });
147
388
  }
148
- /**
149
- * Claim the lease, waiting out another holder with bounded retries.
150
- * The common waits are short: our own pod's deliveries never reach
151
- * here concurrently (the in-process chain serializes them), so a held
152
- * lease means another pod is serving — usually for one turn — or died
153
- * (expiry unblocks us at ~30s).
154
- */
155
- claimWithWait(ledgerSessionId) {
156
- return __awaiter(this, void 0, void 0, function* () {
157
- const start = this.clock();
158
- for (;;) {
159
- const claim = yield this.ledger.claim(ledgerSessionId);
160
- if (claim.claimed) {
161
- return { generation: claim.lease.generation, head: claim.head };
162
- }
163
- if (this.clock() - start >= this.claimWaitTimeoutMs) {
164
- throw new Error(`ledger serving: session ${ledgerSessionId} is still leased by ${claim.holder} after ${this.claimWaitTimeoutMs}ms; giving up on this delivery`);
165
- }
166
- yield this.sleep(this.claimRetryDelayMs);
167
- }
168
- });
169
- }
170
389
  }
171
390
  /** Mint the lease holder id for this process. */
172
391
  export function newLedgerPodId() {
@@ -1 +1 @@
1
- {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/internal/server.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AA4BH,OAAO,KAAK,EACV,YAAY,EACZ,gBAAgB,EAOhB,YAAY,EACb,MAAM,aAAa,CAAC;AAyGrB,OAAO,EAAE,KAAK,WAAW,EAAmB,MAAM,iBAAiB,CAAC;AAiBpE,8EAA8E;AAC9E,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,YAAY,CAAC;CACvB;AAaD,6EAA6E;AAC7E,MAAM,WAAW,0BAA0B;IACzC,oEAAoE;IACpE,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB;AAED;;;;GAIG;AACH,wBAAsB,WAAW,CAC/B,MAAM,EAAE,UAAU,EAAE,EACpB,OAAO,GAAE,YAAY,GAAG,0BAA+B,GACtD,OAAO,CAAC,gBAAgB,CAAC,CAkvB3B"}
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/internal/server.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AA6BH,OAAO,KAAK,EACV,YAAY,EACZ,gBAAgB,EAOhB,YAAY,EACb,MAAM,aAAa,CAAC;AAyGrB,OAAO,EAAE,KAAK,WAAW,EAAmB,MAAM,iBAAiB,CAAC;AAiBpE,8EAA8E;AAC9E,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,YAAY,CAAC;CACvB;AAaD,6EAA6E;AAC7E,MAAM,WAAW,0BAA0B;IACzC,oEAAoE;IACpE,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB;AAED;;;;GAIG;AACH,wBAAsB,WAAW,CAC/B,MAAM,EAAE,UAAU,EAAE,EACpB,OAAO,GAAE,YAAY,GAAG,0BAA+B,GACtD,OAAO,CAAC,gBAAgB,CAAC,CAkvB3B"}
@@ -35,7 +35,7 @@ import { Buffer } from "node:buffer";
35
35
  import { once } from "node:events";
36
36
  import { createServer, } from "node:http";
37
37
  import { join } from "node:path";
38
- import { createCursorAccountGithubTokenResolver, getGithubCursorAccountRuntime, parseCursorGithubRepoScope, resolveCursorGithubPermissions, resolveCursorGithubProxyUrl, } from "../channels/github/cursor-account.js";
38
+ import { createCursorAccountGithubTokenResolver, cursorGithubPermissionsForChannel, getGithubCursorAccountRuntime, parseCursorGithubRepoScope, resolveCursorGithubPermissions, resolveCursorGithubProxyUrl, } from "../channels/github/cursor-account.js";
39
39
  import { findGitHubChannelTargets } from "../channels/github/forward.js";
40
40
  import { dispatchCursorSlackEnvelope, getSlackCursorAccountRuntime, } from "../channels/slack/cursor-account.js";
41
41
  import { assertSlackCursorAgentName } from "../channels/slack/types.js";
@@ -1450,7 +1450,7 @@ function resolveCursorGithubEventConfig(input) {
1450
1450
  ...((_b = (_a = target.accountRuntime.scope) === null || _a === void 0 ? void 0 : _a.repos) !== null && _b !== void 0 ? _b : []),
1451
1451
  ]; }),
1452
1452
  ];
1453
- const permissions = resolveCursorGithubPermissions(input.targets.map((target) => { var _a; var _b; return (_b = (_a = target.accountRuntime.scope) === null || _a === void 0 ? void 0 : _a.permissions) !== null && _b !== void 0 ? _b : "pr-write"; }));
1453
+ const permissions = resolveCursorGithubPermissions(input.targets.map((target) => { var _a; return cursorGithubPermissionsForChannel(target.accountRuntime.options, (_a = target.accountRuntime.scope) === null || _a === void 0 ? void 0 : _a.permissions); }));
1454
1454
  const scope = parseCursorGithubRepoScope(repos, permissions);
1455
1455
  return {
1456
1456
  cursorEvents: Object.assign(Object.assign({}, input.configured), { repos: [...scope.repos] }),
@@ -23,9 +23,7 @@ export { SCHEDULE_CHANNEL_PREFIX } from "./prompt-context.js";
23
23
  export declare class SessionBusyError extends Error {
24
24
  constructor(sessionId: string);
25
25
  }
26
- export declare class SessionAuthError extends Error {
27
- constructor(sessionId: string);
28
- }
26
+ export { SessionAuthError } from "./auth.js";
29
27
  export declare class UnknownChannelError extends Error {
30
28
  constructor(channelId: string);
31
29
  }
@@ -71,10 +69,11 @@ export interface SessionEngineOptions {
71
69
  /**
72
70
  * Explicit durable-session store for `serving: "ledger"` agents — a KV
73
71
  * dedicated to the ledger (session identity, leases, commit heads),
74
- * separate from agent state. Must implement the ledger's CAS methods
75
- * (`getWithVersion` / `putIfAbsent` / `putIfVersion`); construction
76
- * fails fast otherwise. Supply the **raw** store: the engine owns key
77
- * scoping and namespaces every ledger key under
72
+ * separate from agent state. Must implement the full CAS-capable KV
73
+ * surface (`HostKvCasApi`: the CAS trio plus `get`/`put`/`delete` for
74
+ * mailbox payload blobs and `listKeys` for the janitor scan);
75
+ * construction fails fast otherwise. Supply the **raw** store: the
76
+ * engine owns key scoping and namespaces every ledger key under
78
77
  * `agentkit/v1/<agent>/durable-session/` itself — a self-hoster never
79
78
  * writes prefixes. Default when omitted: the authored storage backend
80
79
  * (`agent/storage.ts`) when it implements the `cas` group, else — with
@@ -115,12 +114,13 @@ export interface EngineResolvedConnection {
115
114
  export interface EngineSendOptions extends SendMessageOptions {
116
115
  mode?: "chat" | "task";
117
116
  /**
118
- * Internal (ledger serving): this send *is* a lease-holding turn
119
- * delivery, so it must run the resident path. Without the flag, every
120
- * send on a `serving: "ledger"` agent routes through the ledger
121
- * (claim turn commit) and the pump would recurse into itself.
117
+ * Internal (ledger serving): set only by the ledger pump when it
118
+ * executes an already-parked event under its lease, so the call must
119
+ * run the resident path. Without the flag, every send on a
120
+ * `serving: "ledger"` agent routes through the ledger (append claim
121
+ * → drain → turn → commit) and the pump would recurse into itself.
122
122
  */
123
- ledgerDelivery?: boolean;
123
+ fromLedgerPump?: boolean;
124
124
  }
125
125
  export declare class SessionEngine {
126
126
  readonly project: AgentProject;
@@ -201,7 +201,7 @@ export declare class SessionEngine {
201
201
  constructor(options: SessionEngineOptions);
202
202
  /**
203
203
  * Ledger-mode wiring (`serving: "ledger"`): resolve a CAS-capable
204
- * durable-session store and build the delivery seam. Resident agents
204
+ * durable-session store and build the event pump. Resident agents
205
205
  * (the default) return undefined and never touch ledger keys.
206
206
  */
207
207
  private buildLedgerServing;
@@ -218,13 +218,14 @@ export declare class SessionEngine {
218
218
  *
219
219
  * An authored backend without `cas` fails fast: silently falling back
220
220
  * to local files would fork coordination away from the deployment's
221
- * durable store. The check demands exactly the methods the
222
- * coordination-only ledger uses `listKeys` is not one of them.
221
+ * durable store. The check demands the full surface the inbox uses —
222
+ * the CAS trio plus `get`/`put`/`delete` for mailbox payload blobs and
223
+ * `listKeys` for the janitor scan and the orphan sweep.
223
224
  *
224
225
  * Whatever store wins, the returned handle is scoped here — once —
225
226
  * under `agentkit/v1/<agent>/durable-session/`. The ledger speaks bare
226
- * `session/…` / `ledger/…` keys; namespacing is this boundary's job,
227
- * exactly like `host.kv`'s agent scoping.
227
+ * `session/…` / `ledger/…` / `mailbox/…` keys; namespacing is this
228
+ * boundary's job, exactly like `host.kv`'s agent scoping.
228
229
  */
229
230
  private resolveDurableSessionStore;
230
231
  /**