@cursor/july 0.1.43 → 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 (197) hide show
  1. package/AGENTS.md +5 -3
  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 +15 -7
  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 +11 -3
  20. package/dist/channels/github/types.d.ts.map +1 -1
  21. package/dist/channels.d.ts +17 -0
  22. package/dist/channels.d.ts.map +1 -1
  23. package/dist/channels.js +31 -0
  24. package/dist/docs/404.html +2 -2
  25. package/dist/docs/ab.html +3 -3
  26. package/dist/docs/assets/{app.DsTLo2j1.js → app.C2NFTb-h.js} +1 -1
  27. package/dist/docs/assets/chunks/@localSearchIndexroot.B-FxvuoI.js +1 -0
  28. package/dist/docs/assets/chunks/{VPLocalSearchBox.DoVfutcV.js → VPLocalSearchBox.o1_FJGip.js} +1 -1
  29. package/dist/docs/assets/chunks/{theme.DtuVtKkc.js → theme.BcGduE7g.js} +2 -2
  30. package/dist/docs/assets/{guides_github.md.DSg0j9qj.js → guides_github.md.B8QKtIpw.js} +7 -6
  31. package/dist/docs/assets/{guides_github.md.DSg0j9qj.lean.js → guides_github.md.B8QKtIpw.lean.js} +1 -1
  32. package/dist/docs/assets/guides_opentelemetry.md.Csn7ZI25.js +27 -0
  33. package/dist/docs/assets/guides_opentelemetry.md.Csn7ZI25.lean.js +1 -0
  34. package/dist/docs/assets/index.md.COeoSkcF.js +5 -0
  35. package/dist/docs/assets/index.md.COeoSkcF.lean.js +1 -0
  36. package/dist/docs/assets/{quickstart.md.rIbfahJM.js → quickstart.md.DZxBu44y.js} +18 -49
  37. package/dist/docs/assets/quickstart.md.DZxBu44y.lean.js +1 -0
  38. package/dist/docs/assets/{reference_agent-config.md.CRmkoxd6.js → reference_agent-config.md.DHkkvcEZ.js} +9 -4
  39. package/dist/docs/assets/{reference_agent-config.md.CRmkoxd6.lean.js → reference_agent-config.md.DHkkvcEZ.lean.js} +1 -1
  40. package/dist/docs/assets/{reference_cli.md.DqEqQpGn.js → reference_cli.md.BDmjPPAm.js} +2 -2
  41. package/dist/docs/assets/{reference_hooks.md.4cbRRIIu.js → reference_hooks.md.DJE5DXcT.js} +1 -1
  42. package/dist/docs/assets/{reference_tools.md.lSrsTxYJ.js → reference_tools.md.kJfJAP2m.js} +1 -1
  43. package/dist/docs/assets/{storage.md.DcGSnIxX.js → storage.md.CJYd5YmN.js} +2 -2
  44. package/dist/docs/assets/{storage.md.DcGSnIxX.lean.js → storage.md.CJYd5YmN.lean.js} +1 -1
  45. package/dist/docs/building-with-agents.html +3 -3
  46. package/dist/docs/concepts.html +3 -3
  47. package/dist/docs/deployment.html +3 -3
  48. package/dist/docs/evals.html +3 -3
  49. package/dist/docs/example-agents/approval-buddy.html +3 -3
  50. package/dist/docs/example-agents/benny.html +3 -3
  51. package/dist/docs/example-agents/bugbot.html +3 -3
  52. package/dist/docs/example-agents/codebase-wiki.html +3 -3
  53. package/dist/docs/example-agents/codeowners-review.html +3 -3
  54. package/dist/docs/example-agents/concierge.html +3 -3
  55. package/dist/docs/example-agents/fsd.html +3 -3
  56. package/dist/docs/example-agents/index.html +3 -3
  57. package/dist/docs/example-agents/knowledge-base.html +3 -3
  58. package/dist/docs/example-agents/oncall.html +3 -3
  59. package/dist/docs/example-agents/security-reviewer.html +3 -3
  60. package/dist/docs/example-agents/slack-agent.html +3 -3
  61. package/dist/docs/example-agents/weather-agent.html +3 -3
  62. package/dist/docs/guides/agent-to-agent.html +3 -3
  63. package/dist/docs/guides/cloud-runtime.html +3 -3
  64. package/dist/docs/guides/github.html +10 -9
  65. package/dist/docs/guides/human-in-the-loop.html +3 -3
  66. package/dist/docs/guides/mcp-oauth.html +3 -3
  67. package/dist/docs/guides/opentelemetry.html +12 -5
  68. package/dist/docs/guides/slack.html +3 -3
  69. package/dist/docs/guides/webhooks.html +3 -3
  70. package/dist/docs/hashmap.json +1 -1
  71. package/dist/docs/hillclimbing.html +3 -3
  72. package/dist/docs/index.html +8 -28
  73. package/dist/docs/quickstart.html +24 -55
  74. package/dist/docs/reference/agent-config.html +11 -6
  75. package/dist/docs/reference/artifacts.html +3 -3
  76. package/dist/docs/reference/channels.html +3 -3
  77. package/dist/docs/reference/cli.html +6 -6
  78. package/dist/docs/reference/connections.html +3 -3
  79. package/dist/docs/reference/hooks.html +5 -5
  80. package/dist/docs/reference/http-api.html +3 -3
  81. package/dist/docs/reference/instructions.html +3 -3
  82. package/dist/docs/reference/playground.html +3 -3
  83. package/dist/docs/reference/project-layout.html +3 -3
  84. package/dist/docs/reference/prompt.html +3 -3
  85. package/dist/docs/reference/schedules.html +3 -3
  86. package/dist/docs/reference/sessions.html +3 -3
  87. package/dist/docs/reference/skills.html +3 -3
  88. package/dist/docs/reference/subagents.html +3 -3
  89. package/dist/docs/reference/tools.html +5 -5
  90. package/dist/docs/scaffolding-agents.html +3 -3
  91. package/dist/docs/storage.html +5 -5
  92. package/dist/docs/templates/triage.html +3 -3
  93. package/dist/docs/troubleshooting.html +3 -3
  94. package/dist/index.d.ts +2 -2
  95. package/dist/index.d.ts.map +1 -1
  96. package/dist/index.js +2 -2
  97. package/dist/internal/auth.d.ts +4 -0
  98. package/dist/internal/auth.d.ts.map +1 -1
  99. package/dist/internal/auth.js +7 -0
  100. package/dist/internal/cli-ax.js +2 -2
  101. package/dist/internal/cli-deploy.d.ts.map +1 -1
  102. package/dist/internal/cli-deploy.js +38 -7
  103. package/dist/internal/deploy-client.d.ts +8 -0
  104. package/dist/internal/deploy-client.d.ts.map +1 -1
  105. package/dist/internal/deploy-client.js +5 -2
  106. package/dist/internal/discovery.d.ts.map +1 -1
  107. package/dist/internal/discovery.js +46 -9
  108. package/dist/internal/durable-session-store.d.ts +35 -23
  109. package/dist/internal/durable-session-store.d.ts.map +1 -1
  110. package/dist/internal/durable-session-store.js +71 -26
  111. package/dist/internal/host-otel.d.ts +25 -0
  112. package/dist/internal/host-otel.d.ts.map +1 -0
  113. package/dist/internal/host-otel.js +162 -0
  114. package/dist/internal/init-project.js +10 -10
  115. package/dist/internal/ledger-serving.d.ts +135 -67
  116. package/dist/internal/ledger-serving.d.ts.map +1 -1
  117. package/dist/internal/ledger-serving.js +326 -107
  118. package/dist/internal/otel/collector.d.ts +5 -0
  119. package/dist/internal/otel/collector.d.ts.map +1 -1
  120. package/dist/internal/otel/collector.js +68 -27
  121. package/dist/internal/sdk-runner.d.ts +1 -1
  122. package/dist/internal/sdk-runner.d.ts.map +1 -1
  123. package/dist/internal/sdk-runner.js +1 -1
  124. package/dist/internal/server.d.ts.map +1 -1
  125. package/dist/internal/server.js +3 -2
  126. package/dist/internal/session-engine.d.ts +23 -18
  127. package/dist/internal/session-engine.d.ts.map +1 -1
  128. package/dist/internal/session-engine.js +83 -32
  129. package/dist/kv/memory-kv.d.ts +8 -0
  130. package/dist/kv/memory-kv.d.ts.map +1 -1
  131. package/dist/kv/memory-kv.js +8 -0
  132. package/dist/ledger/janitor.d.ts +60 -0
  133. package/dist/ledger/janitor.d.ts.map +1 -0
  134. package/dist/ledger/janitor.js +137 -0
  135. package/dist/ledger/session-ledger.d.ts +132 -43
  136. package/dist/ledger/session-ledger.d.ts.map +1 -1
  137. package/dist/ledger/session-ledger.js +282 -50
  138. package/dist/ledger.d.ts +9 -8
  139. package/dist/ledger.d.ts.map +1 -1
  140. package/dist/ledger.js +7 -7
  141. package/dist/playground/assets/{index-D-DIYQzw.js → index-6wW6SAc4.js} +19 -19
  142. package/dist/playground/assets/index-CSRYDAFw.css +1 -0
  143. package/dist/playground/index.html +2 -2
  144. package/dist/storage-backends/cursor-hosted.d.ts +5 -0
  145. package/dist/storage-backends/cursor-hosted.d.ts.map +1 -1
  146. package/dist/storage-backends/cursor-hosted.js +6 -0
  147. package/dist/types.d.ts +100 -10
  148. package/dist/types.d.ts.map +1 -1
  149. package/docs/README.md +33 -106
  150. package/docs/guides/github.md +23 -18
  151. package/docs/guides/opentelemetry.md +30 -0
  152. package/docs/quickstart.md +89 -227
  153. package/docs/reference/agent-config.md +47 -0
  154. package/docs/reference/cli.md +2 -2
  155. package/docs/reference/hooks.md +1 -1
  156. package/docs/reference/tools.md +5 -4
  157. package/docs/storage.md +26 -9
  158. package/package.json +2 -2
  159. package/skills/otel/SKILL.md +20 -2
  160. package/src/channels/github/api.ts +122 -0
  161. package/src/channels/github/cursor-account.ts +23 -0
  162. package/src/channels/github/defaults.ts +1 -1
  163. package/src/channels/github/github-channel.ts +14 -1
  164. package/src/channels/github/progress.ts +84 -19
  165. package/src/channels/github/state.ts +14 -0
  166. package/src/channels/github/types.ts +7 -3
  167. package/src/channels.ts +38 -0
  168. package/src/index.ts +2 -0
  169. package/src/internal/auth.ts +10 -0
  170. package/src/internal/cli-ax.ts +2 -2
  171. package/src/internal/cli-deploy.ts +58 -3
  172. package/src/internal/deploy-client.ts +11 -0
  173. package/src/internal/discovery.ts +49 -0
  174. package/src/internal/durable-session-store.ts +66 -27
  175. package/src/internal/host-otel.ts +252 -0
  176. package/src/internal/init-project.ts +10 -10
  177. package/src/internal/ledger-serving.ts +439 -148
  178. package/src/internal/otel/collector.ts +135 -50
  179. package/src/internal/sdk-runner.ts +6 -0
  180. package/src/internal/server.ts +7 -2
  181. package/src/internal/session-engine.ts +127 -39
  182. package/src/kv/memory-kv.ts +8 -0
  183. package/src/ledger/janitor.ts +193 -0
  184. package/src/ledger/session-ledger.ts +372 -68
  185. package/src/ledger.ts +21 -6
  186. package/src/storage-backends/cursor-hosted.ts +17 -2
  187. package/src/types.ts +108 -10
  188. package/dist/docs/assets/chunks/@localSearchIndexroot.BZx4pmZL.js +0 -1
  189. package/dist/docs/assets/guides_opentelemetry.md.DmyUwC5R.js +0 -20
  190. package/dist/docs/assets/guides_opentelemetry.md.DmyUwC5R.lean.js +0 -1
  191. package/dist/docs/assets/index.md.CMc5FP7E.js +0 -25
  192. package/dist/docs/assets/index.md.CMc5FP7E.lean.js +0 -1
  193. package/dist/docs/assets/quickstart.md.rIbfahJM.lean.js +0 -1
  194. package/dist/playground/assets/index-DQTH622f.css +0 -1
  195. /package/dist/docs/assets/{reference_cli.md.DqEqQpGn.lean.js → reference_cli.md.BDmjPPAm.lean.js} +0 -0
  196. /package/dist/docs/assets/{reference_hooks.md.4cbRRIIu.lean.js → reference_hooks.md.DJE5DXcT.lean.js} +0 -0
  197. /package/dist/docs/assets/{reference_tools.md.lSrsTxYJ.lean.js → reference_tools.md.kJfJAP2m.lean.js} +0 -0
@@ -10,6 +10,7 @@
10
10
  import { mkdir, rm, writeFile } from "node:fs/promises";
11
11
  import { dirname, join } from "node:path";
12
12
  import type { SDKCustomTool, SDKCustomToolResult } from "@cursor/sdk";
13
+ import type { Meter } from "@opentelemetry/api";
13
14
  import {
14
15
  type ABDefinition,
15
16
  mergeABInstructions,
@@ -75,7 +76,7 @@ import { foldABStream } from "./ab-fold.js";
75
76
  import { type ABSnapshot, buildABSnapshot } from "./ab-snapshot.js";
76
77
  import { ApprovalGate } from "./approval-gate.js";
77
78
  import { ArtifactsStore } from "./artifacts-store.js";
78
- import { samePrincipal } from "./auth.js";
79
+ import { SessionAuthError, samePrincipal } from "./auth.js";
79
80
  import { mergeCloudOptions, resolveSessionRuntime } from "./cloud-merge.js";
80
81
  import {
81
82
  applyCloudUsageToEvents,
@@ -95,6 +96,7 @@ import { TurnEventMapper } from "./event-mapper.js";
95
96
  import { selectFrameworkStorageDefinition } from "./framework-storage-selection.js";
96
97
  import { createHostFilesApi } from "./host-files.js";
97
98
  import { createHostKvApi } from "./host-kv.js";
99
+ import { createHostOtelApi, type HostOtelSessionApi } from "./host-otel.js";
98
100
  import {
99
101
  createHostPlatforms,
100
102
  createHostSlackClient,
@@ -147,14 +149,9 @@ export class SessionBusyError extends Error {
147
149
  }
148
150
  }
149
151
 
150
- export class SessionAuthError extends Error {
151
- constructor(sessionId: string) {
152
- super(
153
- `Session ${sessionId} belongs to a different principal; pass the original caller auth on follow-up`
154
- );
155
- this.name = "SessionAuthError";
156
- }
157
- }
152
+ // Defined in auth.js (the ledger pump needs it without importing this
153
+ // module); re-exported here because this is its historical public home.
154
+ export { SessionAuthError } from "./auth.js";
158
155
 
159
156
  export class UnknownChannelError extends Error {
160
157
  constructor(channelId: string) {
@@ -232,10 +229,11 @@ export interface SessionEngineOptions {
232
229
  /**
233
230
  * Explicit durable-session store for `serving: "ledger"` agents — a KV
234
231
  * dedicated to the ledger (session identity, leases, commit heads),
235
- * separate from agent state. Must implement the ledger's CAS methods
236
- * (`getWithVersion` / `putIfAbsent` / `putIfVersion`); construction
237
- * fails fast otherwise. Supply the **raw** store: the engine owns key
238
- * scoping and namespaces every ledger key under
232
+ * separate from agent state. Must implement the full CAS-capable KV
233
+ * surface (`HostKvCasApi`: the CAS trio plus `get`/`put`/`delete` for
234
+ * mailbox payload blobs and `listKeys` for the janitor scan);
235
+ * construction fails fast otherwise. Supply the **raw** store: the
236
+ * engine owns key scoping and namespaces every ledger key under
239
237
  * `agentkit/v1/<agent>/durable-session/` itself — a self-hoster never
240
238
  * writes prefixes. Default when omitted: the authored storage backend
241
239
  * (`agent/storage.ts`) when it implements the `cas` group, else — with
@@ -246,6 +244,8 @@ export interface SessionEngineOptions {
246
244
  durableSessionStore?: HostKvApi;
247
245
  /** Process-level OTel collector; observe-only, never fatal. */
248
246
  otel?: OtelCollector;
247
+ /** Process-level meter for {@link HostContext.otel}. Absent → no-op counters. */
248
+ otelMeter?: Meter;
249
249
  }
250
250
 
251
251
  /**
@@ -282,12 +282,13 @@ interface ActiveToolContext {
282
282
  export interface EngineSendOptions extends SendMessageOptions {
283
283
  mode?: "chat" | "task";
284
284
  /**
285
- * Internal (ledger serving): this send *is* a lease-holding turn
286
- * delivery, so it must run the resident path. Without the flag, every
287
- * send on a `serving: "ledger"` agent routes through the ledger
288
- * (claim turn commit) and the pump would recurse into itself.
285
+ * Internal (ledger serving): set only by the ledger pump when it
286
+ * executes an already-parked event under its lease, so the call must
287
+ * run the resident path. Without the flag, every send on a
288
+ * `serving: "ledger"` agent routes through the ledger (append claim
289
+ * → drain → turn → commit) and the pump would recurse into itself.
289
290
  */
290
- ledgerDelivery?: boolean;
291
+ fromLedgerPump?: boolean;
291
292
  }
292
293
 
293
294
  interface CoalescedQueuedTurn {
@@ -362,6 +363,7 @@ export class SessionEngine {
362
363
  private readonly approvals = new ApprovalGate();
363
364
  private readonly abCollector: ABCollector;
364
365
  private readonly otel: OtelCollector | undefined;
366
+ private readonly otelHost: HostOtelSessionApi;
365
367
  private reminderApi: HostContext["reminders"];
366
368
  private evalsApi: HostContext["evals"];
367
369
  private readonly kvApi: HostContext["kv"];
@@ -395,6 +397,11 @@ export class SessionEngine {
395
397
  this.getCloudUsage = options.getCloudUsage ?? defaultGetCloudUsage;
396
398
  this.estimateTurnCost = options.estimateTurnCost ?? estimateTurnCostFromApi;
397
399
  this.otel = options.otel;
400
+ this.otelHost = createHostOtelApi({
401
+ agentName: options.project.name,
402
+ meter: options.otelMeter,
403
+ collector: options.otel,
404
+ });
398
405
  this.turnGovernor = new TurnGovernor(options.project.agent.concurrency);
399
406
  this.sessions = new SessionStore(join(options.stateRoot, "sessions"));
400
407
  this.nudgeStore = new SlackNudgeStore(
@@ -472,11 +479,12 @@ export class SessionEngine {
472
479
  this.channelsById.set(channel.id, channel.definition);
473
480
  }
474
481
  this.ledgerServing = this.buildLedgerServing(options);
482
+ this.ledgerServing?.start();
475
483
  }
476
484
 
477
485
  /**
478
486
  * Ledger-mode wiring (`serving: "ledger"`): resolve a CAS-capable
479
- * durable-session store and build the delivery seam. Resident agents
487
+ * durable-session store and build the event pump. Resident agents
480
488
  * (the default) return undefined and never touch ledger keys.
481
489
  */
482
490
  private buildLedgerServing(
@@ -485,15 +493,46 @@ export class SessionEngine {
485
493
  if (options.project.agent.serving !== "ledger") {
486
494
  return undefined;
487
495
  }
496
+ // The handle is already agent-scoped (resolveDurableSessionStore):
497
+ // two ledger-mode agents sharing one backend never resolve to each
498
+ // other's sessions or janitor-claim each other's parked envelopes.
488
499
  const ledgerKv = this.resolveDurableSessionStore(options);
489
500
  return new LedgerServing({
490
501
  ledger: new SessionLedger({ kv: ledgerKv, podId: newLedgerPodId() }),
491
- deliverTurn: (request) =>
492
- this.send(request.channelId, request.message, {
493
- ...request.options,
494
- continuationToken: request.continuationToken,
495
- ledgerDelivery: true,
502
+ deliverTurn: (envelope) =>
503
+ this.send(envelope.channelId, envelope.message, {
504
+ continuationToken: envelope.continuationToken,
505
+ auth: envelope.auth,
506
+ ...(envelope.title === undefined ? {} : { title: envelope.title }),
507
+ ...(envelope.state === undefined ? {} : { state: envelope.state }),
508
+ ...(envelope.mode === undefined ? {} : { mode: envelope.mode }),
509
+ ...(envelope.purpose === undefined
510
+ ? {}
511
+ : { purpose: envelope.purpose }),
512
+ // Isolation overrides ride the envelope: without them the
513
+ // drained turn would run local with the default harness cwd
514
+ // instead of the cloud VM / channel worktree the sender bound.
515
+ ...(envelope.workspaceDir === undefined
516
+ ? {}
517
+ : { workspaceDir: envelope.workspaceDir }),
518
+ ...(envelope.cloud === undefined ? {} : { cloud: envelope.cloud }),
519
+ fromLedgerPump: true,
496
520
  }),
521
+ findSession: async (channelId, continuationToken) => {
522
+ let record = await this.sessions.findByContinuation(
523
+ channelId,
524
+ continuationToken
525
+ );
526
+ if (record === undefined) {
527
+ record = await this.restoreSessionByContinuation(
528
+ channelId,
529
+ continuationToken
530
+ );
531
+ }
532
+ return record === undefined
533
+ ? undefined
534
+ : this.channelSession(record.sessionId, record, false);
535
+ },
497
536
  logger: this.logger,
498
537
  });
499
538
  }
@@ -511,13 +550,14 @@ export class SessionEngine {
511
550
  *
512
551
  * An authored backend without `cas` fails fast: silently falling back
513
552
  * to local files would fork coordination away from the deployment's
514
- * durable store. The check demands exactly the methods the
515
- * coordination-only ledger uses `listKeys` is not one of them.
553
+ * durable store. The check demands the full surface the inbox uses —
554
+ * the CAS trio plus `get`/`put`/`delete` for mailbox payload blobs and
555
+ * `listKeys` for the janitor scan and the orphan sweep.
516
556
  *
517
557
  * Whatever store wins, the returned handle is scoped here — once —
518
558
  * under `agentkit/v1/<agent>/durable-session/`. The ledger speaks bare
519
- * `session/…` / `ledger/…` keys; namespacing is this boundary's job,
520
- * exactly like `host.kv`'s agent scoping.
559
+ * `session/…` / `ledger/…` / `mailbox/…` keys; namespacing is this
560
+ * boundary's job, exactly like `host.kv`'s agent scoping.
521
561
  */
522
562
  private resolveDurableSessionStore(
523
563
  options: SessionEngineOptions
@@ -527,7 +567,7 @@ export class SessionEngine {
527
567
  if (explicit !== undefined) {
528
568
  if (!supportsSessionLedgerKv(explicit)) {
529
569
  throw new Error(
530
- `Agent "${options.project.name}" declares serving: "ledger", but the configured durableSessionStore is not CAS-capable (getWithVersion / putIfAbsent / putIfVersion). Provide a store implementing those methods (see @cursor/july/kv).`
570
+ `Agent "${options.project.name}" declares serving: "ledger", but the configured durableSessionStore is not CAS-capable (getWithVersion / putIfAbsent / putIfVersion / listKeys). Provide a store implementing HostKvCasApi (see @cursor/july/kv).`
531
571
  );
532
572
  }
533
573
  return namespacedKv(explicit, namespace);
@@ -574,7 +614,7 @@ export class SessionEngine {
574
614
  this.cloudServerToolsConnection = connection;
575
615
  }
576
616
 
577
- /** Shared host services (MCP / GitHub / Slack / reminders / evals / kv / Slack nudges). */
617
+ /** Shared host services (MCP / GitHub / Slack / reminders / evals / kv / files / otel / Slack nudges). */
578
618
  get host(): HostContext {
579
619
  return {
580
620
  mcp: this.mcpHost,
@@ -584,6 +624,7 @@ export class SessionEngine {
584
624
  evals: this.evalsApi,
585
625
  kv: this.kvApi,
586
626
  files: this.filesApi,
627
+ otel: this.otelHost,
587
628
  slackNudges: this.slackNudgeHostApi(),
588
629
  };
589
630
  }
@@ -598,7 +639,11 @@ export class SessionEngine {
598
639
  const host = this.host;
599
640
  return sessionId === undefined
600
641
  ? host
601
- : { ...host, files: this.filesApi.forSession(sessionId) };
642
+ : {
643
+ ...host,
644
+ files: this.filesApi.forSession(sessionId),
645
+ otel: this.otelHost.forSession(sessionId),
646
+ };
602
647
  }
603
648
 
604
649
  /** Unbound `ctx.artifacts` facade (channel handlers, scratch tool calls). */
@@ -714,10 +759,10 @@ export class SessionEngine {
714
759
  message: string,
715
760
  options: EngineSendOptions = {}
716
761
  ): Promise<ChannelSession> {
717
- // Ledger-mode agents acquire the session lease before any turn runs;
718
- // only the seam's own lease-holding deliveries fall through to the
719
- // resident path.
720
- if (this.ledgerServing !== undefined && options.ledgerDelivery !== true) {
762
+ // Ledger-mode agents park the event durably and coordinate through
763
+ // the session lease before any turn runs; only the pump's own
764
+ // lease-holding deliveries fall through to the resident path.
765
+ if (this.ledgerServing !== undefined && options.fromLedgerPump !== true) {
721
766
  return this.ledgerServing.deliver(channelId, message, options);
722
767
  }
723
768
  const auth = options.auth ?? null;
@@ -791,13 +836,20 @@ export class SessionEngine {
791
836
  ),
792
837
  }));
793
838
  }
839
+ const since = (await this.logs.get(record.sessionId)).count;
794
840
  const coalesced = await this.startTurn(record.sessionId, message, {
795
841
  admission: options.admission ?? "preempt",
796
842
  ...(options.coalesceSourceTs === undefined
797
843
  ? {}
798
844
  : { sourceTs: options.coalesceSourceTs }),
799
845
  });
800
- return this.channelSession(record.sessionId, record, false, coalesced);
846
+ return this.channelSession(
847
+ record.sessionId,
848
+ record,
849
+ false,
850
+ coalesced,
851
+ since
852
+ );
801
853
  }
802
854
 
803
855
  const now = new Date().toISOString();
@@ -823,18 +875,26 @@ export class SessionEngine {
823
875
  };
824
876
  await this.sessions.save(record);
825
877
  this.storage?.sessionRecord(record);
878
+ // Ledger serving: another pod can follow up mid-turn, so the session
879
+ // identity (record + continuation index) must hit the shared store
880
+ // before we return a session id. Default turnEnd flush is too late.
881
+ if (this.ledgerServing !== undefined && this.storage !== undefined) {
882
+ this.storage.flushSession(record.sessionId);
883
+ await this.storage.whenIdle();
884
+ }
826
885
  await this.appendEvent(record.sessionId, {
827
886
  type: "session.started",
828
887
  data: { channelId },
829
888
  });
830
889
  record = await this.enrollABs(record);
890
+ const since = (await this.logs.get(record.sessionId)).count;
831
891
  await this.startTurn(record.sessionId, message, {
832
892
  admission: options.admission ?? "preempt",
833
893
  ...(options.coalesceSourceTs === undefined
834
894
  ? {}
835
895
  : { sourceTs: options.coalesceSourceTs }),
836
896
  });
837
- return this.channelSession(record.sessionId, record, true, false);
897
+ return this.channelSession(record.sessionId, record, true, false, since);
838
898
  }
839
899
 
840
900
  /** Whether a continuation token already maps to a session on this channel. */
@@ -1359,6 +1419,9 @@ export class SessionEngine {
1359
1419
  workspaceDir,
1360
1420
  runnerStateDir: join(this.stateRoot, "runner"),
1361
1421
  sandbox: agent.local?.sandbox === true,
1422
+ ...(agent.allowedTools === undefined
1423
+ ? {}
1424
+ : { tools: agent.allowedTools }),
1362
1425
  mcpServers: this.buildMcpServers("local"),
1363
1426
  subagents: buildRunnerSubagents(agent),
1364
1427
  ...(agent.model === undefined ? {} : { model: agent.model }),
@@ -1614,6 +1677,15 @@ export class SessionEngine {
1614
1677
  agentRuntime: agent.runtime,
1615
1678
  cloudOverride: record.cloudOverride,
1616
1679
  });
1680
+ // Fail closed: the SDK cannot enforce the built-in tool allowlist on
1681
+ // cloud agents yet, and a restriction that silently does not apply is
1682
+ // worse than a refused turn. Cloud-runtime agents already fail
1683
+ // discovery; this guards hybrid agents' per-send cloud sessions.
1684
+ if (runtime === "cloud" && agent.allowedTools !== undefined) {
1685
+ throw new Error(
1686
+ `Agent "${agent.name}" declares a built-in tool allowlist (tools), which cannot be enforced on cloud turns yet. Refusing the cloud session rather than running it unrestricted.`
1687
+ );
1688
+ }
1617
1689
  const mergedCloud =
1618
1690
  runtime === "cloud"
1619
1691
  ? mergeCloudOptions({
@@ -1668,6 +1740,9 @@ export class SessionEngine {
1668
1740
  : { sdkAgentId: record.sdkAgentId }),
1669
1741
  workspaceDir: this.harnessCwd(record),
1670
1742
  sandbox: this.project.agent.local?.sandbox === true,
1743
+ ...(runtime === "local" && agent.allowedTools !== undefined
1744
+ ? { tools: agent.allowedTools }
1745
+ : {}),
1671
1746
  runnerStateDir: join(this.stateRoot, "runner"),
1672
1747
  prompt: turnPrompt,
1673
1748
  isFirstTurn,
@@ -1996,6 +2071,7 @@ export class SessionEngine {
1996
2071
  }),
1997
2072
  getSession: (channelId, sessionId) =>
1998
2073
  this.getChannelSession(channelId, sessionId),
2074
+ emit: args.emit,
1999
2075
  };
2000
2076
  try {
2001
2077
  const needsGate = await evaluateNeedsApproval(
@@ -2284,6 +2360,16 @@ export class SessionEngine {
2284
2360
  this.logger(`[session] tool ${toolName} start (direct ${callId})`);
2285
2361
  let result: unknown;
2286
2362
  let isError: boolean;
2363
+ const live = record;
2364
+ const emit =
2365
+ live === undefined
2366
+ ? undefined
2367
+ : (payload: SessionEventPayload) => {
2368
+ void this.appendEvent(live.sessionId, {
2369
+ ...payload,
2370
+ turnId: callId,
2371
+ }).catch(() => {});
2372
+ };
2287
2373
  try {
2288
2374
  result = normalizeToolResult(
2289
2375
  await execute(validation.value as never, {
@@ -2310,6 +2396,7 @@ export class SessionEngine {
2310
2396
  }),
2311
2397
  getSession: (channelId, sessionId) =>
2312
2398
  this.getChannelSession(channelId, sessionId),
2399
+ emit,
2313
2400
  })
2314
2401
  );
2315
2402
  isError = isErrorEnvelope(result);
@@ -3009,7 +3096,8 @@ export class SessionEngine {
3009
3096
  sessionId: string,
3010
3097
  record: SessionRecord,
3011
3098
  isNew: boolean,
3012
- coalesced = false
3099
+ coalesced = false,
3100
+ since = 0
3013
3101
  ): ChannelSession {
3014
3102
  return {
3015
3103
  id: sessionId,
@@ -3018,7 +3106,7 @@ export class SessionEngine {
3018
3106
  ...(coalesced ? { coalesced: true } : {}),
3019
3107
  info: this.sessionInfo(record),
3020
3108
  getEventStream: (options?: { startIndex?: number }) =>
3021
- this.eventStream(sessionId, options?.startIndex ?? 0),
3109
+ this.eventStream(sessionId, options?.startIndex ?? since),
3022
3110
  waitForCompletion: () => this.waitForCompletion(sessionId),
3023
3111
  setContinuationToken: (token: string | null) =>
3024
3112
  this.setContinuationToken(sessionId, token),
@@ -86,6 +86,14 @@ export function createMemoryKv(): HostKvCasApi {
86
86
  * fixtures, an off-hosting no-op sink). Non-durable and process-local by
87
87
  * construction; a real deployment backend should implement CAS against
88
88
  * its own store instead.
89
+ *
90
+ * **Never pair this with `serving: "ledger"` on a real backend.** The
91
+ * ledger derives its coordination store from the whole storage config,
92
+ * so a durable plain backend with a memory `cas` group silently voids
93
+ * ack-after-durable: leases, heads, and pending indexes live only in
94
+ * this process and vanish on restart while payload blobs go through the
95
+ * plain methods. Ledger state must live in the same durable store as
96
+ * the blobs.
89
97
  */
90
98
  export function memoryCasTable(): StorageCasTable {
91
99
  const kv = createMemoryKv();
@@ -0,0 +1,193 @@
1
+ /**
2
+ * The ledger janitor: lazy crash recovery has a hole — a session with
3
+ * pending events and no future events would wait forever — so serving runs
4
+ * this loop. It scans `ledger/` by prefix, finds pending-with-expired-lease
5
+ * sessions, claims them, and hands each claim to the execution callback.
6
+ * Self-host runs the same loop; it is part of the library, not the control
7
+ * plane.
8
+ *
9
+ * Nothing starts on construction: call `start()` for the interval loop or
10
+ * `runOnce()` for an explicit pass (what unit tests use).
11
+ */
12
+
13
+ import type {
14
+ ClaimResult,
15
+ LedgerLease,
16
+ SessionLedger,
17
+ } from "./session-ledger.js";
18
+
19
+ export const DEFAULT_JANITOR_INTERVAL_MS = 15_000;
20
+
21
+ export interface JanitorClaimedSession {
22
+ sessionId: string;
23
+ lease: LedgerLease;
24
+ head: number;
25
+ pending: string[];
26
+ }
27
+
28
+ export interface JanitorRunResult {
29
+ /** Ledgers scanned this pass. */
30
+ scanned: number;
31
+ /** Sessions this pass claimed and handed to the callback. */
32
+ claimed: JanitorClaimedSession[];
33
+ /** Orphan blob ids deleted (only when the sweep is enabled). */
34
+ orphansDeleted: string[];
35
+ }
36
+
37
+ export interface LedgerJanitorOptions {
38
+ ledger: SessionLedger;
39
+ /**
40
+ * Executes a recovered session: drain, run the turn, commit. Awaited so
41
+ * one pass never claims more sessions than it can serve.
42
+ */
43
+ onSession: (session: JanitorClaimedSession) => Promise<void> | void;
44
+ /** Scan cadence for `start()` (default {@link DEFAULT_JANITOR_INTERVAL_MS}). */
45
+ intervalMs?: number;
46
+ /**
47
+ * Also sweep unreferenced mailbox blobs each pass. Off by default —
48
+ * enable with `{ olderThanMs }` (see `SessionLedger.sweepOrphans`).
49
+ */
50
+ orphanSweep?: { olderThanMs?: number };
51
+ /** Called with per-session or sweep failures; the pass continues. */
52
+ onError?: (error: unknown, sessionId?: string) => void;
53
+ /** Time source for lease-expiry checks (default `Date.now`). */
54
+ clock?: () => number;
55
+ }
56
+
57
+ export interface LedgerJanitor {
58
+ start(): void;
59
+ /**
60
+ * Clear the interval AND await any in-flight pass: a running pass can
61
+ * claim a session and spawn a turn, so shutdown must not proceed while
62
+ * one is mid-scan.
63
+ */
64
+ stop(): Promise<void>;
65
+ runOnce(): Promise<JanitorRunResult>;
66
+ }
67
+
68
+ export function createLedgerJanitor(
69
+ options: LedgerJanitorOptions
70
+ ): LedgerJanitor {
71
+ const intervalMs = options.intervalMs ?? DEFAULT_JANITOR_INTERVAL_MS;
72
+ const clock = options.clock ?? Date.now;
73
+ const reportError =
74
+ options.onError ??
75
+ ((): void => {
76
+ // Recovery is best-effort per pass; an unobserved failure is retried
77
+ // on the next scan.
78
+ });
79
+
80
+ let timer: ReturnType<typeof setInterval> | undefined;
81
+ let running = false;
82
+ /** The settled-shape promise of the pass currently scanning, if any. */
83
+ let inFlight: Promise<void> | undefined;
84
+
85
+ const runPass = async (): Promise<JanitorRunResult> => {
86
+ // An unparseable ledger record is reported and skipped: it degrades
87
+ // that one session's recovery, never the whole agent's pass.
88
+ const ledgers = await options.ledger.listLedgers((key, error) => {
89
+ reportError(
90
+ new Error(
91
+ `janitor scan: skipping unparseable ledger record at ${key}: ${String(
92
+ error instanceof Error ? error.message : error
93
+ )}`
94
+ )
95
+ );
96
+ });
97
+ const claimed: JanitorClaimedSession[] = [];
98
+ for (const snapshot of ledgers) {
99
+ if (snapshot.pending.length === 0) {
100
+ continue;
101
+ }
102
+ if (snapshot.lease !== null && snapshot.lease.expiresAt > clock()) {
103
+ continue;
104
+ }
105
+ let claim: ClaimResult;
106
+ try {
107
+ claim = await options.ledger.claim(snapshot.sessionId);
108
+ } catch (error) {
109
+ reportError(error, snapshot.sessionId);
110
+ continue;
111
+ }
112
+ if (!claim.claimed) {
113
+ continue;
114
+ }
115
+ const session: JanitorClaimedSession = {
116
+ sessionId: snapshot.sessionId,
117
+ lease: claim.lease,
118
+ head: claim.head,
119
+ pending: claim.pending,
120
+ };
121
+ claimed.push(session);
122
+ try {
123
+ await options.onSession(session);
124
+ } catch (error) {
125
+ reportError(error, snapshot.sessionId);
126
+ }
127
+ }
128
+
129
+ let orphansDeleted: string[] = [];
130
+ if (options.orphanSweep !== undefined) {
131
+ try {
132
+ const swept = await options.ledger.sweepOrphans({
133
+ olderThanMs: options.orphanSweep.olderThanMs,
134
+ });
135
+ orphansDeleted = swept.deleted;
136
+ } catch (error) {
137
+ reportError(error);
138
+ }
139
+ }
140
+ return { scanned: ledgers.length, claimed, orphansDeleted };
141
+ };
142
+
143
+ const runOnce = (): Promise<JanitorRunResult> => {
144
+ const pass = runPass();
145
+ const settled = pass.then(
146
+ () => undefined,
147
+ () => undefined
148
+ );
149
+ inFlight = settled;
150
+ void settled.finally(() => {
151
+ if (inFlight === settled) {
152
+ inFlight = undefined;
153
+ }
154
+ });
155
+ return pass;
156
+ };
157
+
158
+ const tick = async (): Promise<void> => {
159
+ // Skip a tick rather than overlap passes when a scan outlasts the
160
+ // cadence — overlapping claims would just fence each other.
161
+ if (running) {
162
+ return;
163
+ }
164
+ running = true;
165
+ try {
166
+ await runOnce();
167
+ } catch (error) {
168
+ reportError(error);
169
+ } finally {
170
+ running = false;
171
+ }
172
+ };
173
+
174
+ return {
175
+ start(): void {
176
+ if (timer !== undefined) {
177
+ return;
178
+ }
179
+ timer = setInterval(() => {
180
+ void tick();
181
+ }, intervalMs);
182
+ timer.unref?.();
183
+ },
184
+ async stop(): Promise<void> {
185
+ if (timer !== undefined) {
186
+ clearInterval(timer);
187
+ timer = undefined;
188
+ }
189
+ await inFlight;
190
+ },
191
+ runOnce,
192
+ };
193
+ }