@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.
Files changed (143) hide show
  1. package/dist/channels/origin/origin-webhook.d.ts +23 -2
  2. package/dist/channels/origin/origin-webhook.d.ts.map +1 -1
  3. package/dist/channels/origin/origin-webhook.js +82 -20
  4. package/dist/docs/404.html +2 -2
  5. package/dist/docs/ab.html +3 -3
  6. package/dist/docs/assets/{app.b8bhB2SV.js → app.BsA7t7rS.js} +1 -1
  7. package/dist/docs/assets/chunks/@localSearchIndexroot.Dcs6boxD.js +1 -0
  8. package/dist/docs/assets/chunks/{VPLocalSearchBox.lLSeIyWw.js → VPLocalSearchBox.B1hHhktM.js} +1 -1
  9. package/dist/docs/assets/chunks/{theme.C_owesa-.js → theme.DLpLSsQh.js} +2 -2
  10. package/dist/docs/assets/{storage.md.D8NQ389l.js → storage.md.ks1u64_R.js} +3 -3
  11. package/dist/docs/assets/storage.md.ks1u64_R.lean.js +1 -0
  12. package/dist/docs/building-with-agents.html +3 -3
  13. package/dist/docs/concepts.html +3 -3
  14. package/dist/docs/deployment.html +3 -3
  15. package/dist/docs/evals.html +3 -3
  16. package/dist/docs/example-agents/approval-buddy.html +3 -3
  17. package/dist/docs/example-agents/benny.html +3 -3
  18. package/dist/docs/example-agents/bugbot.html +3 -3
  19. package/dist/docs/example-agents/codebase-wiki.html +3 -3
  20. package/dist/docs/example-agents/codeowners-review.html +3 -3
  21. package/dist/docs/example-agents/concierge.html +3 -3
  22. package/dist/docs/example-agents/fsd.html +3 -3
  23. package/dist/docs/example-agents/index.html +3 -3
  24. package/dist/docs/example-agents/knowledge-base.html +3 -3
  25. package/dist/docs/example-agents/oncall.html +3 -3
  26. package/dist/docs/example-agents/security-reviewer.html +3 -3
  27. package/dist/docs/example-agents/slack-agent.html +3 -3
  28. package/dist/docs/example-agents/weather-agent.html +3 -3
  29. package/dist/docs/guides/agent-to-agent.html +3 -3
  30. package/dist/docs/guides/cloud-runtime.html +3 -3
  31. package/dist/docs/guides/convert-automation.html +3 -3
  32. package/dist/docs/guides/github.html +3 -3
  33. package/dist/docs/guides/human-in-the-loop.html +3 -3
  34. package/dist/docs/guides/mcp-oauth.html +3 -3
  35. package/dist/docs/guides/opentelemetry.html +3 -3
  36. package/dist/docs/guides/slack.html +3 -3
  37. package/dist/docs/guides/webhooks.html +3 -3
  38. package/dist/docs/hashmap.json +1 -1
  39. package/dist/docs/hillclimbing.html +3 -3
  40. package/dist/docs/index.html +3 -3
  41. package/dist/docs/quickstart.html +3 -3
  42. package/dist/docs/reference/agent-config.html +3 -3
  43. package/dist/docs/reference/artifacts.html +3 -3
  44. package/dist/docs/reference/channels.html +3 -3
  45. package/dist/docs/reference/cli.html +3 -3
  46. package/dist/docs/reference/connections.html +3 -3
  47. package/dist/docs/reference/hooks.html +3 -3
  48. package/dist/docs/reference/http-api.html +3 -3
  49. package/dist/docs/reference/instructions.html +3 -3
  50. package/dist/docs/reference/playground.html +3 -3
  51. package/dist/docs/reference/project-layout.html +3 -3
  52. package/dist/docs/reference/prompt.html +3 -3
  53. package/dist/docs/reference/schedules.html +3 -3
  54. package/dist/docs/reference/sessions.html +3 -3
  55. package/dist/docs/reference/skills.html +3 -3
  56. package/dist/docs/reference/subagents.html +3 -3
  57. package/dist/docs/reference/tools.html +3 -3
  58. package/dist/docs/scaffolding-agents.html +3 -3
  59. package/dist/docs/storage.html +7 -7
  60. package/dist/docs/templates/agentic-owners.html +3 -3
  61. package/dist/docs/templates/demo.html +3 -3
  62. package/dist/docs/templates/pr-autofixer.html +3 -3
  63. package/dist/docs/templates/security-reviewer.html +3 -3
  64. package/dist/docs/templates/triage.html +3 -3
  65. package/dist/docs/troubleshooting.html +3 -3
  66. package/dist/index.d.ts +0 -1
  67. package/dist/index.d.ts.map +1 -1
  68. package/dist/index.js +0 -1
  69. package/dist/internal/authored-alias-hooks.d.ts +12 -0
  70. package/dist/internal/authored-alias-hooks.d.ts.map +1 -1
  71. package/dist/internal/authored-alias-hooks.js +60 -8
  72. package/dist/internal/authored-loaders.d.ts +5 -3
  73. package/dist/internal/authored-loaders.d.ts.map +1 -1
  74. package/dist/internal/authored-loaders.js +13 -4
  75. package/dist/internal/deployment-scm-event-stream.js +4 -4
  76. package/dist/internal/discovery.d.ts.map +1 -1
  77. package/dist/internal/discovery.js +3 -21
  78. package/dist/internal/distribution.d.ts.map +1 -1
  79. package/dist/internal/distribution.js +0 -1
  80. package/dist/internal/review-comments.d.ts +62 -0
  81. package/dist/internal/review-comments.d.ts.map +1 -1
  82. package/dist/internal/review-comments.js +199 -0
  83. package/dist/internal/session-engine.d.ts +1 -60
  84. package/dist/internal/session-engine.d.ts.map +1 -1
  85. package/dist/internal/session-engine.js +10 -109
  86. package/dist/kv/memory-kv.d.ts +1 -10
  87. package/dist/kv/memory-kv.d.ts.map +1 -1
  88. package/dist/kv/memory-kv.js +1 -10
  89. package/dist/kv.d.ts +2 -3
  90. package/dist/kv.d.ts.map +1 -1
  91. package/dist/kv.js +2 -3
  92. package/dist/playground/assets/index-DJzcIeGt.css +1 -0
  93. package/dist/playground/assets/{index-BJy9UJrF.js → index-K3Sxyx0I.js} +48 -45
  94. package/dist/playground/index.html +2 -2
  95. package/dist/storage-backends/cursor-hosted.js +3 -3
  96. package/dist/storage-protocol.d.ts +3 -8
  97. package/dist/storage-protocol.d.ts.map +1 -1
  98. package/dist/storage-protocol.js +4 -10
  99. package/dist/storage.d.ts +5 -11
  100. package/dist/storage.d.ts.map +1 -1
  101. package/dist/storage.js +3 -4
  102. package/dist/types.d.ts +5 -34
  103. package/dist/types.d.ts.map +1 -1
  104. package/docs/storage.md +11 -44
  105. package/package.json +1 -8
  106. package/src/channels/origin/origin-webhook.ts +111 -25
  107. package/src/index.ts +0 -6
  108. package/src/internal/authored-alias-hooks.ts +75 -7
  109. package/src/internal/authored-loaders.ts +15 -4
  110. package/src/internal/deployment-scm-event-stream.ts +4 -4
  111. package/src/internal/discovery.ts +3 -27
  112. package/src/internal/distribution.ts +0 -1
  113. package/src/internal/review-comments.ts +286 -0
  114. package/src/internal/session-engine.ts +1 -168
  115. package/src/kv/memory-kv.ts +1 -10
  116. package/src/kv.ts +2 -3
  117. package/src/storage-backends/cursor-hosted.ts +3 -3
  118. package/src/storage-protocol.ts +4 -10
  119. package/src/storage.ts +6 -12
  120. package/src/types.ts +5 -35
  121. package/dist/docs/assets/chunks/@localSearchIndexroot.DGZeXVqn.js +0 -1
  122. package/dist/docs/assets/storage.md.D8NQ389l.lean.js +0 -1
  123. package/dist/internal/durable-session-store.d.ts +0 -60
  124. package/dist/internal/durable-session-store.d.ts.map +0 -1
  125. package/dist/internal/durable-session-store.js +0 -131
  126. package/dist/internal/ledger-serving.d.ts +0 -169
  127. package/dist/internal/ledger-serving.d.ts.map +0 -1
  128. package/dist/internal/ledger-serving.js +0 -393
  129. package/dist/ledger/janitor.d.ts +0 -60
  130. package/dist/ledger/janitor.d.ts.map +0 -1
  131. package/dist/ledger/janitor.js +0 -137
  132. package/dist/ledger/session-ledger.d.ts +0 -293
  133. package/dist/ledger/session-ledger.d.ts.map +0 -1
  134. package/dist/ledger/session-ledger.js +0 -614
  135. package/dist/ledger.d.ts +0 -12
  136. package/dist/ledger.d.ts.map +0 -1
  137. package/dist/ledger.js +0 -9
  138. package/dist/playground/assets/index-BrSd5vff.css +0 -1
  139. package/src/internal/durable-session-store.ts +0 -134
  140. package/src/internal/ledger-serving.ts +0 -531
  141. package/src/ledger/janitor.ts +0 -193
  142. package/src/ledger/session-ledger.ts +0 -839
  143. package/src/ledger.ts +0 -44
@@ -1,614 +0,0 @@
1
- /**
2
- * Session ledger — KV coordination with a durable inbox.
3
- *
4
- * Three key families, all driven with `get` + CAS on a
5
- * {@link SessionLedgerKv}; agents never touch these keys directly:
6
- *
7
- * ```
8
- * session/<continuationKey> identity, created with put-if-absent
9
- * ledger/<sessionId> head + pending + lease — every mutation one CAS
10
- * mailbox/<sessionId>/<eventId> immutable payload blobs, plain put
11
- * ```
12
- *
13
- * The keys are deliberately bare: namespacing (per agent, per
14
- * deployment) is not the ledger's concern. The store handle it is given
15
- * must already be scoped — the engine applies the namespace exactly once
16
- * where it constructs the handle (see `durable-session-store.ts`), the
17
- * same boundary where `host.kv` gets its agent scoping.
18
- *
19
- * The guarantee: **exactly-once ledger apply, at-least-once actuation.**
20
- * A commit is one CAS carrying the read version, the expected `head`, and
21
- * the holder's `generation` — one committed transition per turn, atomic by
22
- * construction. What the CAS cannot fence (a zombie's plain `host.kv`
23
- * writes, external tool calls) is a stated bound, not this module's job.
24
- *
25
- * The inbox makes events durable before their turn runs: `appendEvent`
26
- * writes the payload blob, then one CAS indexes the eventId into
27
- * `pending` — that CAS is the ack point. A delivery that finds the lease
28
- * held walks away; the holder's commit sees the appended id in the same
29
- * CAS value, keeps the lease, and drains again. Sessions whose holder
30
- * died with events pending are recovered by the janitor
31
- * (`./janitor.js`), which is also why the store must implement
32
- * `listKeys`: the inbox brings the prefix scans (janitor + orphan
33
- * sweep) that the coordination-only ledger did not need.
34
- *
35
- * Clocks: lease expiry compares `expiresAt` against {@link
36
- * SessionLedgerOptions.clock} (default `Date.now`). Self-hosted N-pod
37
- * deployments accept NTP-synced pods as a documented requirement; a
38
- * backend-clock primitive can replace the default without touching the
39
- * protocol code.
40
- */
41
- var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
42
- function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
43
- return new (P || (P = Promise))(function (resolve, reject) {
44
- function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
45
- function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
46
- function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
47
- step((generator = generator.apply(thisArg, _arguments || [])).next());
48
- });
49
- };
50
- import { newSessionId } from "../internal/ids.js";
51
- import { keySegment } from "../storage.js";
52
- // Lease mechanics: TTL ~30s, heartbeat ~10s, generation increments per
53
- // acquisition.
54
- export const LEASE_TTL_MS = 30000;
55
- export const HEARTBEAT_INTERVAL_MS = 10000;
56
- /** Unreferenced mailbox blobs older than this are sweepable. */
57
- export const DEFAULT_ORPHAN_TTL_MS = 60 * 60000;
58
- // Sized against the two-pod simulation running at this default (no
59
- // override): 64 exhausts appendEvent under a hot session's worst-case
60
- // interleaving, 128 passes with margin. Exhaustion mid-lease stalls the
61
- // session for a lease TTL, so the budget errs generous — the backoff
62
- // keeps late attempts cheap.
63
- const DEFAULT_MAX_CAS_ATTEMPTS = 128;
64
- /** Runtime probe for {@link SessionLedgerKv} on stores of unknown shape. */
65
- export function supportsSessionLedgerKv(kv) {
66
- const candidate = kv;
67
- return (typeof candidate.get === "function" &&
68
- typeof candidate.put === "function" &&
69
- typeof candidate.delete === "function" &&
70
- typeof candidate.getWithVersion === "function" &&
71
- typeof candidate.putIfAbsent === "function" &&
72
- typeof candidate.putIfVersion === "function" &&
73
- typeof candidate.listKeys === "function");
74
- }
75
- /**
76
- * The turn was fenced: a stale generation, a moved head, or a missing
77
- * ledger. The caller must abort the turn and discard warm state.
78
- */
79
- export class LedgerFencedError extends Error {
80
- constructor(message) {
81
- super(message);
82
- this.name = "LedgerFencedError";
83
- }
84
- }
85
- /** A CAS retry loop ran out of attempts (sustained contention or outage). */
86
- export class LedgerCasExhaustedError extends Error {
87
- constructor(operation, attempts) {
88
- super(`${operation}: CAS did not settle after ${attempts} attempts`);
89
- this.name = "LedgerCasExhaustedError";
90
- }
91
- }
92
- function defaultBackoff(attempt) {
93
- return __awaiter(this, void 0, void 0, function* () {
94
- // The common case is a single conflicting writer (one heartbeat or one
95
- // append); retrying immediately wins the next round. Sleep only when
96
- // contention persists.
97
- if (attempt < 2) {
98
- return;
99
- }
100
- const cap = Math.min(5 * Math.pow(2, attempt), 250);
101
- const waitMs = cap / 2 + Math.random() * (cap / 2);
102
- yield new Promise((resolve) => setTimeout(resolve, waitMs));
103
- });
104
- }
105
- /** Drives the three ledger key families over a CAS-capable KV. */
106
- export class SessionLedger {
107
- constructor(options) {
108
- var _a, _b, _c, _d, _e;
109
- this.kv = options.kv;
110
- this.podId = options.podId;
111
- this.leaseTtlMs = (_a = options.leaseTtlMs) !== null && _a !== void 0 ? _a : LEASE_TTL_MS;
112
- this.clock = (_b = options.clock) !== null && _b !== void 0 ? _b : Date.now;
113
- this.maxCasAttempts = (_c = options.maxCasAttempts) !== null && _c !== void 0 ? _c : DEFAULT_MAX_CAS_ATTEMPTS;
114
- this.backoff = (_d = options.backoff) !== null && _d !== void 0 ? _d : defaultBackoff;
115
- this.mintSessionId = (_e = options.newSessionId) !== null && _e !== void 0 ? _e : newSessionId;
116
- }
117
- sessionKey(continuationKey) {
118
- return `session/${keySegment(continuationKey)}`;
119
- }
120
- ledgerKey(sessionId) {
121
- return `ledger/${keySegment(sessionId)}`;
122
- }
123
- mailboxKey(sessionId, eventId) {
124
- return `${this.mailboxPrefix(sessionId)}${keySegment(eventId)}`;
125
- }
126
- mailboxPrefix(sessionId) {
127
- return `mailbox/${keySegment(sessionId)}/`;
128
- }
129
- emptyLedger(sessionId) {
130
- return {
131
- sessionId,
132
- head: 0,
133
- pending: [],
134
- lease: null,
135
- generation: 0,
136
- lastCommitId: null,
137
- };
138
- }
139
- /**
140
- * Create-or-read the session identity for a continuation key. The create
141
- * race has one winner (put-if-absent); losers read the winner's identity
142
- * and append there.
143
- */
144
- ensureSession(continuationKey, init) {
145
- return __awaiter(this, void 0, void 0, function* () {
146
- var _a;
147
- const key = this.sessionKey(continuationKey);
148
- for (let attempt = 0; attempt < this.maxCasAttempts; attempt++) {
149
- const existing = yield this.kv.getWithVersion(key);
150
- if (existing !== undefined) {
151
- return {
152
- created: false,
153
- session: parseSessionIdentity(existing.value),
154
- };
155
- }
156
- const session = {
157
- sessionId: this.mintSessionId(),
158
- sdkSessionRef: (_a = init === null || init === void 0 ? void 0 : init.sdkSessionRef) !== null && _a !== void 0 ? _a : null,
159
- createdAt: this.clock(),
160
- };
161
- const result = yield this.kv.putIfAbsent(key, toJson(session));
162
- if (result.ok) {
163
- return { created: true, session };
164
- }
165
- if (result.current !== undefined) {
166
- return {
167
- created: false,
168
- session: parseSessionIdentity(result.current.value),
169
- };
170
- }
171
- // Conflict with no current entry (created then deleted underneath
172
- // us) — retry from the read.
173
- yield this.backoff(attempt);
174
- }
175
- throw new LedgerCasExhaustedError("ensureSession", this.maxCasAttempts);
176
- });
177
- }
178
- /**
179
- * Make one event durable: payload blob first, then one CAS indexing its
180
- * id into `pending`. The blob must land before the index — a pending id
181
- * with no payload would break drain, while the reverse (blob without
182
- * index, i.e. a crash between the two writes) is a sweepable orphan.
183
- * Idempotent by `eventId`: `pending` is a set, so redelivery re-runs are
184
- * no-ops. The CAS in step two is the ack point — callers ack the source
185
- * only after this resolves.
186
- */
187
- appendEvent(sessionId, event) {
188
- return __awaiter(this, void 0, void 0, function* () {
189
- var _a;
190
- const blob = {
191
- sessionId,
192
- eventId: event.eventId,
193
- envelope: event.envelope,
194
- hookContext: (_a = event.hookContext) !== null && _a !== void 0 ? _a : null,
195
- sourceTs: event.sourceTs,
196
- storedAt: this.clock(),
197
- };
198
- yield this.kv.put(this.mailboxKey(sessionId, event.eventId), blob);
199
- const key = this.ledgerKey(sessionId);
200
- for (let attempt = 0; attempt < this.maxCasAttempts; attempt++) {
201
- const entry = yield this.kv.getWithVersion(key);
202
- if (entry === undefined) {
203
- const initial = Object.assign(Object.assign({}, this.emptyLedger(sessionId)), { pending: [event.eventId] });
204
- const result = yield this.kv.putIfAbsent(key, toJson(initial));
205
- if (result.ok) {
206
- return { appended: true };
207
- }
208
- yield this.backoff(attempt);
209
- continue;
210
- }
211
- const ledger = parseLedgerSnapshot(entry.value);
212
- if (ledger.pending.includes(event.eventId)) {
213
- return { appended: false };
214
- }
215
- const next = Object.assign(Object.assign({}, ledger), { pending: [...ledger.pending, event.eventId] });
216
- const result = yield this.kv.putIfVersion(key, toJson(next), entry.version);
217
- if (result.ok) {
218
- return { appended: true };
219
- }
220
- yield this.backoff(attempt);
221
- }
222
- throw new LedgerCasExhaustedError("appendEvent", this.maxCasAttempts);
223
- });
224
- }
225
- /**
226
- * Try to take the session lease, creating the ledger record on first
227
- * contact. Succeeds only when the lease is null or expired; the CAS
228
- * bumps the acquisition generation, fencing every prior holder. A
229
- * losing caller walks away — the holder's commit sees any appended
230
- * events in the same CAS value, and the janitor recovers sessions
231
- * whose holder died.
232
- */
233
- claim(sessionId) {
234
- return __awaiter(this, void 0, void 0, function* () {
235
- const key = this.ledgerKey(sessionId);
236
- for (let attempt = 0; attempt < this.maxCasAttempts; attempt++) {
237
- const entry = yield this.kv.getWithVersion(key);
238
- if (entry === undefined) {
239
- const result = yield this.kv.putIfAbsent(key, toJson(this.emptyLedger(sessionId)));
240
- // Won or lost, the record now exists — loop back and claim it.
241
- if (!result.ok) {
242
- yield this.backoff(attempt);
243
- }
244
- continue;
245
- }
246
- const ledger = parseLedgerSnapshot(entry.value);
247
- const now = this.clock();
248
- if (ledger.lease !== null && ledger.lease.expiresAt > now) {
249
- return {
250
- claimed: false,
251
- reason: "held",
252
- holder: ledger.lease.holder,
253
- expiresAt: ledger.lease.expiresAt,
254
- };
255
- }
256
- const lease = {
257
- holder: this.podId,
258
- generation: ledger.generation + 1,
259
- expiresAt: now + this.leaseTtlMs,
260
- };
261
- const next = Object.assign(Object.assign({}, ledger), { generation: lease.generation, lease });
262
- const result = yield this.kv.putIfVersion(key, toJson(next), entry.version);
263
- if (result.ok) {
264
- return { claimed: true, lease, head: next.head, pending: next.pending };
265
- }
266
- yield this.backoff(attempt);
267
- }
268
- throw new LedgerCasExhaustedError("claim", this.maxCasAttempts);
269
- });
270
- }
271
- /**
272
- * Extend the lease. Fails when the generation is no longer ours — the
273
- * caller must abort the turn and discard warm state; the session has a
274
- * new legitimate holder.
275
- */
276
- heartbeat(sessionId, generation) {
277
- return __awaiter(this, void 0, void 0, function* () {
278
- const key = this.ledgerKey(sessionId);
279
- for (let attempt = 0; attempt < this.maxCasAttempts; attempt++) {
280
- const entry = yield this.kv.getWithVersion(key);
281
- if (entry === undefined) {
282
- return { ok: false, reason: "not-found" };
283
- }
284
- const ledger = parseLedgerSnapshot(entry.value);
285
- if (ledger.lease === null ||
286
- ledger.lease.holder !== this.podId ||
287
- ledger.lease.generation !== generation) {
288
- return { ok: false, reason: "fenced" };
289
- }
290
- const expiresAt = this.clock() + this.leaseTtlMs;
291
- const next = Object.assign(Object.assign({}, ledger), { lease: Object.assign(Object.assign({}, ledger.lease), { expiresAt }) });
292
- const result = yield this.kv.putIfVersion(key, toJson(next), entry.version);
293
- if (result.ok) {
294
- return { ok: true, expiresAt };
295
- }
296
- // Version conflict here is a concurrent append, not a lost lease —
297
- // the re-read revalidates the generation before trying again.
298
- yield this.backoff(attempt);
299
- }
300
- throw new LedgerCasExhaustedError("heartbeat", this.maxCasAttempts);
301
- });
302
- }
303
- /**
304
- * Read the pending mailbox: fetch each indexed payload blob and return
305
- * them sorted by `sourceTs`, then `eventId` (mailbox order ≠ source
306
- * order is a stated non-guarantee; this is the drain order). The
307
- * returned `head` is the `expectedHead` for the matching commit.
308
- */
309
- drain(sessionId) {
310
- return __awaiter(this, void 0, void 0, function* () {
311
- const entry = yield this.kv.getWithVersion(this.ledgerKey(sessionId));
312
- if (entry === undefined) {
313
- return { head: 0, events: [] };
314
- }
315
- const ledger = parseLedgerSnapshot(entry.value);
316
- const events = [];
317
- for (const eventId of ledger.pending) {
318
- const blob = yield this.kv.get(this.mailboxKey(sessionId, eventId));
319
- // Append order (blob before index) makes a missing blob impossible;
320
- // skip defensively rather than wedge the session on corruption.
321
- if (blob === undefined) {
322
- continue;
323
- }
324
- events.push(parseLedgerEvent(blob));
325
- }
326
- events.sort((a, b) => a.sourceTs - b.sourceTs || compareStrings(a.eventId, b.eventId));
327
- return { head: ledger.head, events };
328
- });
329
- }
330
- /**
331
- * Commit one turn. The single CAS is the commit point: it bumps `head`,
332
- * removes ONLY the drained ids from `pending`, releases the lease iff
333
- * nothing is left pending, and records `commitId` — atomically. A
334
- * non-empty result `pending` means events landed mid-turn: the lease was
335
- * kept and the caller drains again.
336
- *
337
- * Ambiguous outcomes (the KV threw and we never saw the CAS result) are
338
- * resolved by re-reading, never by blind-retrying: `head` advanced to
339
- * `expectedHead + 1` with our `commitId` ⇒ our write landed, report
340
- * success; `head` still at `expectedHead` with our lease ⇒ it did not,
341
- * retry against the fresh version; anything else ⇒ fenced.
342
- */
343
- commit(sessionId, args) {
344
- return __awaiter(this, void 0, void 0, function* () {
345
- const key = this.ledgerKey(sessionId);
346
- const drained = new Set(args.drainedEventIds);
347
- for (let attempt = 0; attempt < this.maxCasAttempts; attempt++) {
348
- const entry = yield this.kv.getWithVersion(key);
349
- if (entry === undefined) {
350
- throw new LedgerFencedError(`commit(${sessionId}): ledger key is missing`);
351
- }
352
- const ledger = parseLedgerSnapshot(entry.value);
353
- if (ledger.head === args.expectedHead + 1 &&
354
- ledger.lastCommitId === args.commitId) {
355
- // A previous attempt landed even though its result was lost. The
356
- // lease is released *for us* unless the observed lease is exactly
357
- // ours (our commit kept it for a non-empty pending): any other
358
- // non-null lease belongs to a claimant who took the session after
359
- // our releasing write, and reporting it as "still held" would
360
- // keep the caller's turn loop alive alongside the new holder's.
361
- const stillOurs = ledger.lease !== null &&
362
- ledger.lease.holder === this.podId &&
363
- ledger.lease.generation === args.generation;
364
- return {
365
- head: ledger.head,
366
- pending: ledger.pending,
367
- leaseReleased: !stillOurs,
368
- };
369
- }
370
- if (ledger.head !== args.expectedHead) {
371
- throw new LedgerFencedError(`commit(${sessionId}): head is ${ledger.head}, expected ${args.expectedHead}`);
372
- }
373
- if (ledger.lease === null ||
374
- ledger.lease.holder !== this.podId ||
375
- ledger.lease.generation !== args.generation) {
376
- throw new LedgerFencedError(`commit(${sessionId}): lease generation ${args.generation} is stale`);
377
- }
378
- const pending = ledger.pending.filter((id) => !drained.has(id));
379
- const leaseReleased = pending.length === 0;
380
- const next = Object.assign(Object.assign({}, ledger), { head: args.expectedHead + 1, pending, lease: leaseReleased ? null : ledger.lease, lastCommitId: args.commitId });
381
- let result;
382
- try {
383
- result = yield this.kv.putIfVersion(key, toJson(next), entry.version);
384
- }
385
- catch (_a) {
386
- // Unknown outcome — loop back to the re-read, which either finds
387
- // our commitId (success) or a fresh version to CAS against.
388
- yield this.backoff(attempt);
389
- continue;
390
- }
391
- if (result.ok) {
392
- return { head: next.head, pending, leaseReleased };
393
- }
394
- // Version conflict: a concurrent append. Re-read and retry — the
395
- // fencing predicates re-run against the fresh value.
396
- yield this.backoff(attempt);
397
- }
398
- throw new LedgerCasExhaustedError("commit", this.maxCasAttempts);
399
- });
400
- }
401
- /**
402
- * Release a held lease without committing — graceful shutdown, a turn
403
- * that failed before its commit, or a claim that found nothing to
404
- * drain. Fenced by generation: a stale holder's release is a no-op
405
- * (the session has moved on). `head` and the acquisition counter are
406
- * untouched, so prior holders stay fenced.
407
- */
408
- release(sessionId, generation) {
409
- return __awaiter(this, void 0, void 0, function* () {
410
- const key = this.ledgerKey(sessionId);
411
- for (let attempt = 0; attempt < this.maxCasAttempts; attempt++) {
412
- const entry = yield this.kv.getWithVersion(key);
413
- if (entry === undefined) {
414
- return { released: false };
415
- }
416
- const ledger = parseLedgerSnapshot(entry.value);
417
- if (ledger.lease === null ||
418
- ledger.lease.holder !== this.podId ||
419
- ledger.lease.generation !== generation) {
420
- return { released: false };
421
- }
422
- const next = Object.assign(Object.assign({}, ledger), { lease: null });
423
- const result = yield this.kv.putIfVersion(key, toJson(next), entry.version);
424
- if (result.ok) {
425
- return { released: true };
426
- }
427
- yield this.backoff(attempt);
428
- }
429
- throw new LedgerCasExhaustedError("release", this.maxCasAttempts);
430
- });
431
- }
432
- /** Parsed ledger value for one session, or `undefined` when never created. */
433
- readLedger(sessionId) {
434
- return __awaiter(this, void 0, void 0, function* () {
435
- const entry = yield this.kv.getWithVersion(this.ledgerKey(sessionId));
436
- return entry === undefined ? undefined : parseLedgerSnapshot(entry.value);
437
- });
438
- }
439
- /**
440
- * All ledgers under this prefix (the janitor's scan). An unparseable
441
- * record is skipped — reported through `onSkip` — never thrown: one
442
- * corrupt value must degrade that one session, not abort the scan and
443
- * kill the whole agent's recovery pass.
444
- */
445
- listLedgers(onSkip) {
446
- return __awaiter(this, void 0, void 0, function* () {
447
- const keys = yield this.kv.listKeys("ledger/");
448
- const ledgers = [];
449
- for (const key of keys) {
450
- const entry = yield this.kv.getWithVersion(key);
451
- if (entry === undefined) {
452
- continue;
453
- }
454
- try {
455
- ledgers.push(parseLedgerSnapshot(entry.value));
456
- }
457
- catch (error) {
458
- onSkip === null || onSkip === void 0 ? void 0 : onSkip(key, error);
459
- }
460
- }
461
- return ledgers;
462
- });
463
- }
464
- /**
465
- * Delete mailbox blobs that are (a) not referenced by their session's
466
- * `pending` and (b) older than the TTL. Covers both true orphans (a
467
- * crash between the payload put and the ledger CAS) and consumed blobs
468
- * whose ids a commit already removed. Membership is checked against a
469
- * ledger read taken *after* listing the blobs, so a blob whose append
470
- * CAS is in flight is never swept: its id is either already in
471
- * `pending`, or the blob is younger than any sane TTL.
472
- */
473
- sweepOrphans(args) {
474
- return __awaiter(this, void 0, void 0, function* () {
475
- var _a, _b, _c;
476
- const olderThanMs = (_a = args === null || args === void 0 ? void 0 : args.olderThanMs) !== null && _a !== void 0 ? _a : DEFAULT_ORPHAN_TTL_MS;
477
- const prefix = (args === null || args === void 0 ? void 0 : args.sessionId) === undefined
478
- ? "mailbox/"
479
- : this.mailboxPrefix(args.sessionId);
480
- const keys = yield this.kv.listKeys(prefix);
481
- const now = this.clock();
482
- // Blob contents carry sessionId/eventId (key segments may be digested
483
- // and are not invertible), so group by reading each blob.
484
- const bySession = new Map();
485
- for (const key of keys) {
486
- const value = yield this.kv.get(key);
487
- if (value === undefined) {
488
- continue;
489
- }
490
- const event = parseLedgerEvent(value);
491
- const sessionId = readBlobSessionId(value);
492
- const group = (_b = bySession.get(sessionId)) !== null && _b !== void 0 ? _b : [];
493
- group.push({ key, event });
494
- bySession.set(sessionId, group);
495
- }
496
- const deleted = [];
497
- for (const [sessionId, blobs] of bySession) {
498
- const ledger = yield this.readLedger(sessionId);
499
- const pending = new Set((_c = ledger === null || ledger === void 0 ? void 0 : ledger.pending) !== null && _c !== void 0 ? _c : []);
500
- for (const { key, event } of blobs) {
501
- if (pending.has(event.eventId)) {
502
- continue;
503
- }
504
- if (event.storedAt + olderThanMs > now) {
505
- continue;
506
- }
507
- yield this.kv.delete(key);
508
- deleted.push(`${sessionId}/${event.eventId}`);
509
- }
510
- }
511
- return { deleted };
512
- });
513
- }
514
- }
515
- function compareStrings(a, b) {
516
- if (a < b) {
517
- return -1;
518
- }
519
- return a > b ? 1 : 0;
520
- }
521
- function toJson(value) {
522
- return value;
523
- }
524
- function parseSessionIdentity(value) {
525
- const record = asRecord(value, "session identity");
526
- const sessionId = record.sessionId;
527
- const createdAt = record.createdAt;
528
- if (typeof sessionId !== "string" || typeof createdAt !== "number") {
529
- throw new Error("session ledger: malformed session identity value");
530
- }
531
- const sdkSessionRef = record.sdkSessionRef;
532
- return {
533
- sessionId,
534
- sdkSessionRef: typeof sdkSessionRef === "string" ? sdkSessionRef : null,
535
- createdAt,
536
- };
537
- }
538
- function parseLedgerSnapshot(value) {
539
- const record = asRecord(value, "ledger");
540
- const { sessionId, head, lease, generation, lastCommitId } = record;
541
- if (typeof sessionId !== "string" ||
542
- typeof head !== "number" ||
543
- typeof generation !== "number") {
544
- throw new Error("session ledger: malformed ledger value");
545
- }
546
- return {
547
- sessionId,
548
- head,
549
- pending: parsePending(record.pending),
550
- lease: parseLease(lease),
551
- generation,
552
- lastCommitId: typeof lastCommitId === "string" ? lastCommitId : null,
553
- };
554
- }
555
- /**
556
- * Records written by the pre-inbox ledger have no `pending` field; read
557
- * them as an empty inbox instead of rejecting them — one legacy record
558
- * must not brick `claim`/`readLedger` (or a whole `listLedgers` scan)
559
- * after an upgrade. Every write from the inbox on includes the field.
560
- */
561
- function parsePending(value) {
562
- if (value === undefined) {
563
- return [];
564
- }
565
- if (!Array.isArray(value) ||
566
- !value.every((id) => typeof id === "string")) {
567
- throw new Error("session ledger: malformed ledger value");
568
- }
569
- return value;
570
- }
571
- function parseLease(value) {
572
- if (value === null || value === undefined) {
573
- return null;
574
- }
575
- const record = asRecord(value, "lease");
576
- const { holder, generation, expiresAt } = record;
577
- if (typeof holder !== "string" ||
578
- typeof generation !== "number" ||
579
- typeof expiresAt !== "number") {
580
- throw new Error("session ledger: malformed lease value");
581
- }
582
- return { holder, generation, expiresAt };
583
- }
584
- function parseLedgerEvent(value) {
585
- var _a, _b;
586
- const record = asRecord(value, "mailbox blob");
587
- const { eventId, sourceTs, storedAt } = record;
588
- if (typeof eventId !== "string" ||
589
- typeof sourceTs !== "number" ||
590
- typeof storedAt !== "number") {
591
- throw new Error("session ledger: malformed mailbox blob");
592
- }
593
- return {
594
- eventId,
595
- envelope: (_a = record.envelope) !== null && _a !== void 0 ? _a : null,
596
- hookContext: (_b = record.hookContext) !== null && _b !== void 0 ? _b : null,
597
- sourceTs,
598
- storedAt,
599
- };
600
- }
601
- function readBlobSessionId(value) {
602
- const record = asRecord(value, "mailbox blob");
603
- const sessionId = record.sessionId;
604
- if (typeof sessionId !== "string") {
605
- throw new Error("session ledger: mailbox blob is missing sessionId");
606
- }
607
- return sessionId;
608
- }
609
- function asRecord(value, what) {
610
- if (value === null || typeof value !== "object" || Array.isArray(value)) {
611
- throw new Error(`session ledger: ${what} value is not an object`);
612
- }
613
- return value;
614
- }
package/dist/ledger.d.ts DELETED
@@ -1,12 +0,0 @@
1
- /**
2
- * Session ledger — KV coordination with a durable inbox. The protocol
3
- * library that drives the `session/`, `ledger/`, and `mailbox/` key
4
- * families over a CAS-capable KV: identity creation, event append,
5
- * lease claim/heartbeat/release, fenced commit, drain, the janitor
6
- * loop, and the orphan sweep.
7
- */
8
- export type { JanitorClaimedSession, JanitorRunResult, LedgerJanitor, LedgerJanitorOptions, } from "./ledger/janitor.js";
9
- export { createLedgerJanitor, DEFAULT_JANITOR_INTERVAL_MS, } from "./ledger/janitor.js";
10
- export type { ClaimResult, CommitArgs, CommitResult, DrainResult, EnsureSessionResult, HeartbeatResult, LedgerEvent, LedgerEventInput, LedgerLease, LedgerSnapshot, SessionIdentity, SessionLedgerKv, SessionLedgerOptions, SweepOrphansArgs, SweepOrphansResult, } from "./ledger/session-ledger.js";
11
- export { DEFAULT_ORPHAN_TTL_MS, HEARTBEAT_INTERVAL_MS, LEASE_TTL_MS, LedgerCasExhaustedError, LedgerFencedError, SessionLedger, supportsSessionLedgerKv, } from "./ledger/session-ledger.js";
12
- //# sourceMappingURL=ledger.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"ledger.d.ts","sourceRoot":"","sources":["../src/ledger.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,YAAY,EACV,qBAAqB,EACrB,gBAAgB,EAChB,aAAa,EACb,oBAAoB,GACrB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EACL,mBAAmB,EACnB,2BAA2B,GAC5B,MAAM,qBAAqB,CAAC;AAC7B,YAAY,EACV,WAAW,EACX,UAAU,EACV,YAAY,EACZ,WAAW,EACX,mBAAmB,EACnB,eAAe,EACf,WAAW,EACX,gBAAgB,EAChB,WAAW,EACX,cAAc,EACd,eAAe,EACf,eAAe,EACf,oBAAoB,EACpB,gBAAgB,EAChB,kBAAkB,GACnB,MAAM,4BAA4B,CAAC;AACpC,OAAO,EACL,qBAAqB,EACrB,qBAAqB,EACrB,YAAY,EACZ,uBAAuB,EACvB,iBAAiB,EACjB,aAAa,EACb,uBAAuB,GACxB,MAAM,4BAA4B,CAAC"}
package/dist/ledger.js DELETED
@@ -1,9 +0,0 @@
1
- /**
2
- * Session ledger — KV coordination with a durable inbox. The protocol
3
- * library that drives the `session/`, `ledger/`, and `mailbox/` key
4
- * families over a CAS-capable KV: identity creation, event append,
5
- * lease claim/heartbeat/release, fenced commit, drain, the janitor
6
- * loop, and the orphan sweep.
7
- */
8
- export { createLedgerJanitor, DEFAULT_JANITOR_INTERVAL_MS, } from "./ledger/janitor.js";
9
- export { DEFAULT_ORPHAN_TTL_MS, HEARTBEAT_INTERVAL_MS, LEASE_TTL_MS, LedgerCasExhaustedError, LedgerFencedError, SessionLedger, supportsSessionLedgerKv, } from "./ledger/session-ledger.js";