@cursor/july 0.1.7 → 0.1.8

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 (173) hide show
  1. package/dist/bin/agent-serve.js +31 -2
  2. package/dist/channels/slack/channel-watch.d.ts +6 -1
  3. package/dist/channels/slack/channel-watch.d.ts.map +1 -1
  4. package/dist/channels/slack/channel-watch.js +22 -1
  5. package/dist/channels/slack/cursor-account.d.ts +4 -0
  6. package/dist/channels/slack/cursor-account.d.ts.map +1 -1
  7. package/dist/channels/slack/cursor-account.js +12 -2
  8. package/dist/channels/slack/defaults.d.ts +10 -5
  9. package/dist/channels/slack/defaults.d.ts.map +1 -1
  10. package/dist/channels/slack/defaults.js +39 -17
  11. package/dist/channels/slack/dispatch.d.ts +18 -0
  12. package/dist/channels/slack/dispatch.d.ts.map +1 -1
  13. package/dist/channels/slack/dispatch.js +141 -38
  14. package/dist/channels/slack/inbound.d.ts +13 -3
  15. package/dist/channels/slack/inbound.d.ts.map +1 -1
  16. package/dist/channels/slack/inbound.js +17 -9
  17. package/dist/channels/slack/index.d.ts +4 -0
  18. package/dist/channels/slack/index.d.ts.map +1 -1
  19. package/dist/channels/slack/index.js +4 -0
  20. package/dist/channels/slack/live-delivery.d.ts.map +1 -1
  21. package/dist/channels/slack/live-delivery.js +1 -0
  22. package/dist/channels/slack/manifest.d.ts.map +1 -1
  23. package/dist/channels/slack/manifest.js +3 -0
  24. package/dist/channels/slack/nudge-carrier.d.ts +19 -0
  25. package/dist/channels/slack/nudge-carrier.d.ts.map +1 -0
  26. package/dist/channels/slack/nudge-carrier.js +166 -0
  27. package/dist/channels/slack/nudge-store.d.ts +61 -0
  28. package/dist/channels/slack/nudge-store.d.ts.map +1 -0
  29. package/dist/channels/slack/nudge-store.js +266 -0
  30. package/dist/channels/slack/slack-channel.d.ts.map +1 -1
  31. package/dist/channels/slack/slack-channel.js +74 -25
  32. package/dist/channels/slack/stop.d.ts +14 -0
  33. package/dist/channels/slack/stop.d.ts.map +1 -0
  34. package/dist/channels/slack/stop.js +22 -0
  35. package/dist/channels/slack/thread-context.d.ts +81 -0
  36. package/dist/channels/slack/thread-context.d.ts.map +1 -0
  37. package/dist/channels/slack/thread-context.js +470 -0
  38. package/dist/channels/slack/types.d.ts +27 -0
  39. package/dist/channels/slack/types.d.ts.map +1 -1
  40. package/dist/docs/404.html +2 -2
  41. package/dist/docs/ab.html +4 -4
  42. package/dist/docs/assets/{app.COTN7wgo.js → app.BP8mAVMW.js} +1 -1
  43. package/dist/docs/assets/chunks/@localSearchIndexroot.DevOabKJ.js +1 -0
  44. package/dist/docs/assets/chunks/{VPLocalSearchBox.BW3TBdT0.js → VPLocalSearchBox.1FXIZbz8.js} +1 -1
  45. package/dist/docs/assets/chunks/{theme.BEJW0vE7.js → theme.Dm1R02v9.js} +2 -2
  46. package/dist/docs/assets/example-agents_index.md.CwtWu5ix.js +2 -0
  47. package/dist/docs/assets/{example-agents_index.md.CRqJlnIf.lean.js → example-agents_index.md.CwtWu5ix.lean.js} +1 -1
  48. package/dist/docs/assets/example-agents_oncall.md.BG_sUMly.js +10 -0
  49. package/dist/docs/assets/example-agents_oncall.md.BG_sUMly.lean.js +1 -0
  50. package/dist/docs/assets/{guides_slack.md.CCwqHvSV.js → guides_slack.md.FWale6Ip.js} +4 -3
  51. package/dist/docs/assets/{guides_slack.md.CCwqHvSV.lean.js → guides_slack.md.FWale6Ip.lean.js} +1 -1
  52. package/dist/docs/assets/{reference_sessions.md.DdjWabnN.js → reference_sessions.md.DLd6mvbv.js} +2 -2
  53. package/dist/docs/assets/{reference_sessions.md.DdjWabnN.lean.js → reference_sessions.md.DLd6mvbv.lean.js} +1 -1
  54. package/dist/docs/assets/{storage.md.CVnInNiN.js → storage.md.Cu3MwM2a.js} +3 -2
  55. package/dist/docs/assets/{storage.md.CVnInNiN.lean.js → storage.md.Cu3MwM2a.lean.js} +1 -1
  56. package/dist/docs/building-with-agents.html +4 -4
  57. package/dist/docs/concepts.html +4 -4
  58. package/dist/docs/deployment.html +4 -4
  59. package/dist/docs/evals.html +4 -4
  60. package/dist/docs/example-agents/approval-buddy.html +4 -4
  61. package/dist/docs/example-agents/benny.html +5 -5
  62. package/dist/docs/example-agents/bugbot.html +5 -5
  63. package/dist/docs/example-agents/codebase-wiki.html +4 -4
  64. package/dist/docs/example-agents/codeowners-review.html +4 -4
  65. package/dist/docs/example-agents/concierge.html +4 -4
  66. package/dist/docs/example-agents/fsd.html +4 -4
  67. package/dist/docs/example-agents/index.html +6 -6
  68. package/dist/docs/example-agents/knowledge-base.html +4 -4
  69. package/dist/docs/example-agents/oncall.html +34 -0
  70. package/dist/docs/example-agents/security-reviewer.html +4 -4
  71. package/dist/docs/example-agents/slack-agent.html +4 -4
  72. package/dist/docs/example-agents/weather-agent.html +4 -4
  73. package/dist/docs/guides/agent-to-agent.html +4 -4
  74. package/dist/docs/guides/cloud-runtime.html +4 -4
  75. package/dist/docs/guides/github.html +4 -4
  76. package/dist/docs/guides/human-in-the-loop.html +4 -4
  77. package/dist/docs/guides/slack.html +7 -6
  78. package/dist/docs/guides/webhooks.html +4 -4
  79. package/dist/docs/hashmap.json +1 -1
  80. package/dist/docs/hillclimbing.html +4 -4
  81. package/dist/docs/index.html +4 -4
  82. package/dist/docs/quickstart.html +4 -4
  83. package/dist/docs/reference/agent-config.html +4 -4
  84. package/dist/docs/reference/channels.html +4 -4
  85. package/dist/docs/reference/cli.html +4 -4
  86. package/dist/docs/reference/connections.html +4 -4
  87. package/dist/docs/reference/hooks.html +4 -4
  88. package/dist/docs/reference/http-api.html +4 -4
  89. package/dist/docs/reference/instructions.html +4 -4
  90. package/dist/docs/reference/playground.html +4 -4
  91. package/dist/docs/reference/project-layout.html +4 -4
  92. package/dist/docs/reference/schedules.html +4 -4
  93. package/dist/docs/reference/sessions.html +5 -5
  94. package/dist/docs/reference/skills.html +4 -4
  95. package/dist/docs/reference/subagents.html +4 -4
  96. package/dist/docs/reference/tools.html +4 -4
  97. package/dist/docs/scaffolding-agents.html +4 -4
  98. package/dist/docs/storage.html +7 -6
  99. package/dist/docs/troubleshooting.html +4 -4
  100. package/dist/internal/cli-mcp.d.ts +64 -0
  101. package/dist/internal/cli-mcp.d.ts.map +1 -0
  102. package/dist/internal/cli-mcp.js +570 -0
  103. package/dist/internal/distribution.d.ts.map +1 -1
  104. package/dist/internal/distribution.js +2 -0
  105. package/dist/internal/host-kv.d.ts +17 -0
  106. package/dist/internal/host-kv.d.ts.map +1 -0
  107. package/dist/internal/host-kv.js +77 -0
  108. package/dist/internal/server.d.ts.map +1 -1
  109. package/dist/internal/server.js +8 -0
  110. package/dist/internal/session-engine.d.ts +42 -7
  111. package/dist/internal/session-engine.d.ts.map +1 -1
  112. package/dist/internal/session-engine.js +247 -51
  113. package/dist/internal/storage-coordinator.d.ts +18 -1
  114. package/dist/internal/storage-coordinator.d.ts.map +1 -1
  115. package/dist/internal/storage-coordinator.js +43 -0
  116. package/dist/playground/assets/index-7GJIUFQj.css +1 -0
  117. package/dist/playground/assets/index-Bpr4Th4M.js +319 -0
  118. package/dist/playground/index.html +2 -2
  119. package/dist/storage-backends/file-kv.d.ts +42 -0
  120. package/dist/storage-backends/file-kv.d.ts.map +1 -0
  121. package/dist/storage-backends/file-kv.js +190 -0
  122. package/dist/storage-backends/postgres-kv.d.ts +77 -0
  123. package/dist/storage-backends/postgres-kv.d.ts.map +1 -0
  124. package/dist/storage-backends/postgres-kv.js +180 -0
  125. package/dist/storage.d.ts +10 -1
  126. package/dist/storage.d.ts.map +1 -1
  127. package/dist/storage.js +10 -1
  128. package/dist/types.d.ts +79 -0
  129. package/dist/types.d.ts.map +1 -1
  130. package/docs/example-agents/index.md +14 -9
  131. package/docs/example-agents/oncall.md +216 -0
  132. package/docs/guides/slack.md +11 -2
  133. package/docs/reference/sessions.md +13 -4
  134. package/docs/storage.md +27 -0
  135. package/package.json +15 -1
  136. package/skills/framework-map/SKILL.md +6 -4
  137. package/src/bin/agent-serve.ts +32 -2
  138. package/src/channels/slack/channel-watch.ts +28 -1
  139. package/src/channels/slack/cursor-account.ts +20 -0
  140. package/src/channels/slack/defaults.ts +51 -20
  141. package/src/channels/slack/dispatch.ts +215 -40
  142. package/src/channels/slack/inbound.ts +30 -10
  143. package/src/channels/slack/index.ts +4 -0
  144. package/src/channels/slack/live-delivery.ts +1 -0
  145. package/src/channels/slack/manifest.ts +3 -0
  146. package/src/channels/slack/nudge-carrier.ts +182 -0
  147. package/src/channels/slack/nudge-store.ts +308 -0
  148. package/src/channels/slack/slack-channel.ts +106 -31
  149. package/src/channels/slack/stop.ts +25 -0
  150. package/src/channels/slack/thread-context.ts +572 -0
  151. package/src/channels/slack/types.ts +27 -0
  152. package/src/internal/cli-mcp.ts +708 -0
  153. package/src/internal/distribution.ts +2 -0
  154. package/src/internal/host-kv.ts +85 -0
  155. package/src/internal/server.ts +31 -0
  156. package/src/internal/session-engine.ts +351 -50
  157. package/src/internal/storage-coordinator.ts +41 -0
  158. package/src/storage-backends/file-kv.ts +221 -0
  159. package/src/storage-backends/postgres-kv.ts +248 -0
  160. package/src/storage.ts +11 -1
  161. package/src/types.ts +83 -0
  162. package/dist/channels/slack/post-update-delivery.d.ts +0 -85
  163. package/dist/channels/slack/post-update-delivery.d.ts.map +0 -1
  164. package/dist/docs/assets/chunks/@localSearchIndexroot.B7UcKvIn.js +0 -1
  165. package/dist/docs/assets/example-agents_index.md.CRqJlnIf.js +0 -2
  166. package/dist/internal/json-dir-store.d.ts +0 -32
  167. package/dist/internal/json-dir-store.d.ts.map +0 -1
  168. package/dist/internal/persistence-coordinator.d.ts +0 -127
  169. package/dist/internal/persistence-coordinator.d.ts.map +0 -1
  170. package/dist/persistence.d.ts +0 -184
  171. package/dist/persistence.d.ts.map +0 -1
  172. package/dist/playground/assets/index-72vCOBWO.js +0 -86
  173. package/dist/playground/assets/index-BjnMwYoR.css +0 -1
@@ -30,6 +30,7 @@ import { mkdir, rm, writeFile } from "node:fs/promises";
30
30
  import { dirname, join } from "node:path";
31
31
  import { mergeABInstructions, resolveABMaxPlaygroundSessions, resolveABSplit, } from "../ab.js";
32
32
  import { slackThreadPermalink } from "../channels/slack/links.js";
33
+ import { SlackAskDedupeStore, SlackNudgeStore, } from "../channels/slack/nudge-store.js";
33
34
  import { DIRECT_TOOL_CALL_CHANNEL_ID, isSymbolicConnectionTransport, } from "../types.js";
34
35
  import { ABCollector } from "./ab-collector.js";
35
36
  import { foldABStream } from "./ab-fold.js";
@@ -40,6 +41,7 @@ import { mergeCloudOptions, resolveSessionRuntime } from "./cloud-merge.js";
40
41
  import { isDevMode } from "./dev-mode.js";
41
42
  import { EventLogStore } from "./event-log.js";
42
43
  import { TurnEventMapper } from "./event-mapper.js";
44
+ import { createHostKvApi } from "./host-kv.js";
43
45
  import { createHostPlatforms } from "./host-platforms.js";
44
46
  import { formatContinuationToken, newContinuationKey, newSessionId, newToolCallId, } from "./ids.js";
45
47
  import { validateToolInput } from "./json-schema.js";
@@ -120,6 +122,16 @@ export class SessionEngine {
120
122
  this.turnAborts = new Map();
121
123
  /** Serializes turn admission per session (latest follow-up wins). */
122
124
  this.admissionChains = new Map();
125
+ /**
126
+ * Per-session coalesce queue for `admission: "coalesce"` (Slack). Drained
127
+ * into one follow-up turn when the active turn finishes without interrupt.
128
+ */
129
+ this.coalesceQueues = new Map();
130
+ /**
131
+ * Sessions whose turn lifecycle is draining coalesced follow-ups. Counts as
132
+ * busy so inbound coalesce admits enqueue instead of racing `clearCoalesced`.
133
+ */
134
+ this.coalesceDrains = new Map();
123
135
  this.lastOutcomes = new Map();
124
136
  this.updateChains = new Map();
125
137
  this.dispatchChains = new Map();
@@ -132,6 +144,8 @@ export class SessionEngine {
132
144
  this.runner = options.runner;
133
145
  this.logger = (_a = options.logger) !== null && _a !== void 0 ? _a : (() => { });
134
146
  this.sessions = new SessionStore(join(options.stateRoot, "sessions"));
147
+ this.nudgeStore = new SlackNudgeStore(join(options.stateRoot, "slack-nudges"));
148
+ this.askDedupe = new SlackAskDedupeStore(join(options.stateRoot, "slack-asks"));
135
149
  this.logs = new EventLogStore((sessionId) => this.sessions.eventFilePath(sessionId), (sessionId, error) => this.logger(`[agentkit] failed to persist events for ${sessionId}: ${describeError(error)}`));
136
150
  this.storage =
137
151
  options.project.storage === undefined
@@ -142,6 +156,7 @@ export class SessionEngine {
142
156
  projectRoot: options.project.rootDir,
143
157
  logger: this.logger,
144
158
  });
159
+ this.kvApi = createHostKvApi(Object.assign({ agentName: options.project.name, stateRoot: options.stateRoot }, (this.storage === undefined ? {} : { storage: this.storage })));
145
160
  this.abCollector = new ABCollector(options.project.abs, this.logger, (sessionId) => __awaiter(this, void 0, void 0, function* () { return (yield this.logs.get(sessionId)).snapshot(0); }),
146
161
  // Samples flow to the defineStorage `abs` table (no-op without one).
147
162
  { persistSample: (sample) => { var _a; return (_a = this.storage) === null || _a === void 0 ? void 0 : _a.abSample(sample); } });
@@ -171,13 +186,26 @@ export class SessionEngine {
171
186
  });
172
187
  }
173
188
  }
174
- /** Shared host services (MCP / GitHub / Slack / reminders). */
189
+ /** Shared host services (MCP / GitHub / Slack / reminders / kv / Slack nudges). */
175
190
  get host() {
176
191
  return {
177
192
  mcp: this.mcpHost,
178
193
  github: this.platforms.github,
179
194
  slack: this.platforms.slack,
180
195
  reminders: this.reminderApi,
196
+ kv: this.kvApi,
197
+ slackNudges: this.slackNudgeHostApi(),
198
+ };
199
+ }
200
+ slackNudgeHostApi() {
201
+ return {
202
+ append: (input) => this.nudgeStore.append(input),
203
+ drainFormatted: (continuationKey) => this.nudgeStore.drainFormatted(continuationKey),
204
+ clear: (continuationKey) => this.nudgeStore.clear(continuationKey),
205
+ hasUndelivered: (continuationKey) => this.nudgeStore.hasUndelivered(continuationKey),
206
+ isDelivered: (continuationKey, sourceTs) => this.nudgeStore.isDelivered(continuationKey, sourceTs),
207
+ tryClaimAsk: (channelId, messageTs) => this.askDedupe.tryClaim(channelId, messageTs),
208
+ releaseAsk: (channelId, messageTs) => this.askDedupe.release(channelId, messageTs),
181
209
  };
182
210
  }
183
211
  /** Attach the serve-host reminder API (called from startServer). */
@@ -195,7 +223,7 @@ export class SessionEngine {
195
223
  send(channelId_1, message_1) {
196
224
  return __awaiter(this, arguments, void 0, function* (channelId, message, options = {}) {
197
225
  var _a;
198
- var _b, _c, _d;
226
+ var _b, _c, _d, _e, _f;
199
227
  const auth = (_b = options.auth) !== null && _b !== void 0 ? _b : null;
200
228
  let existing = options.continuationToken === undefined
201
229
  ? undefined
@@ -219,8 +247,10 @@ export class SessionEngine {
219
247
  else if (options.workspaceFiles !== undefined) {
220
248
  record = yield this.updateRecord(existing.sessionId, (current) => (Object.assign(Object.assign({}, current), { pendingWorkspaceFiles: options.workspaceFiles })));
221
249
  }
222
- yield this.startTurn(record.sessionId, message);
223
- return this.channelSession(record.sessionId, record, false);
250
+ const coalesced = yield this.startTurn(record.sessionId, message, Object.assign({ admission: (_c = options.admission) !== null && _c !== void 0 ? _c : "preempt" }, (options.coalesceSourceTs === undefined
251
+ ? {}
252
+ : { sourceTs: options.coalesceSourceTs })));
253
+ return this.channelSession(record.sessionId, record, false, coalesced);
224
254
  }
225
255
  const now = new Date().toISOString();
226
256
  const channelState = this.initialChannelState(channelId, options.state);
@@ -228,9 +258,9 @@ export class SessionEngine {
228
258
  let record = {
229
259
  sessionId: newSessionId(),
230
260
  channelId,
231
- mode: (_c = options.mode) !== null && _c !== void 0 ? _c : "chat",
261
+ mode: (_d = options.mode) !== null && _d !== void 0 ? _d : "chat",
232
262
  purpose,
233
- continuationKey: (_d = options.continuationToken) !== null && _d !== void 0 ? _d : newContinuationKey(),
263
+ continuationKey: (_e = options.continuationToken) !== null && _e !== void 0 ? _e : newContinuationKey(),
234
264
  auth,
235
265
  title: options.title,
236
266
  channelState,
@@ -249,8 +279,20 @@ export class SessionEngine {
249
279
  data: { channelId },
250
280
  });
251
281
  record = yield this.enrollABs(record);
252
- yield this.startTurn(record.sessionId, message);
253
- return this.channelSession(record.sessionId, record, true);
282
+ yield this.startTurn(record.sessionId, message, Object.assign({ admission: (_f = options.admission) !== null && _f !== void 0 ? _f : "preempt" }, (options.coalesceSourceTs === undefined
283
+ ? {}
284
+ : { sourceTs: options.coalesceSourceTs })));
285
+ return this.channelSession(record.sessionId, record, true, false);
286
+ });
287
+ }
288
+ /** Whether a continuation token already maps to a session on this channel. */
289
+ hasContinuationSession(channelId, continuationToken) {
290
+ return __awaiter(this, void 0, void 0, function* () {
291
+ let existing = yield this.sessions.findByContinuation(channelId, continuationToken);
292
+ if (existing === undefined) {
293
+ existing = yield this.restoreSessionByContinuation(channelId, continuationToken);
294
+ }
295
+ return existing !== undefined;
254
296
  });
255
297
  }
256
298
  /**
@@ -377,27 +419,95 @@ export class SessionEngine {
377
419
  // Turn execution
378
420
  // ==========================================================================
379
421
  /**
380
- * Admit a turn for `sessionId`. A follow-up that arrives while a turn is in
381
- * flight preempts it: the running turn is cancelled and its SDK run marked
382
- * terminal before the new turn starts. Admissions are serialized per session
383
- * so a burst of follow-ups can't race two runs onto the same SDK agent — the
384
- * last message wins. Resolves once the new turn has started (not finished).
422
+ * Admit a turn for `sessionId`.
423
+ *
424
+ * Default (`preempt`): a follow-up cancels the running turn and starts
425
+ * immediately (HTTP / MCP). With `admission: "coalesce"` (Slack), a busy
426
+ * session enqueues the message and returns without interrupting; the queue
427
+ * drains into one follow-up when the active turn finishes.
428
+ *
429
+ * Resolves to `true` when the message was coalesced (enqueued), `false`
430
+ * when a turn was started.
385
431
  */
386
- startTurn(sessionId, message) {
432
+ startTurn(sessionId, message, options = {}) {
387
433
  var _a;
388
434
  const prior = (_a = this.admissionChains.get(sessionId)) !== null && _a !== void 0 ? _a : Promise.resolve();
389
435
  const admitted = prior
390
436
  .catch(() => { })
391
- .then(() => this.admitTurn(sessionId, message));
437
+ .then(() => this.admitTurn(sessionId, message, options));
392
438
  this.admissionChains.set(sessionId, admitted.catch(() => { }));
393
439
  return admitted;
394
440
  }
395
- admitTurn(sessionId, message) {
441
+ enqueueCoalesced(sessionId, item) {
442
+ var _a;
443
+ const queue = (_a = this.coalesceQueues.get(sessionId)) !== null && _a !== void 0 ? _a : [];
444
+ if (item.sourceTs !== undefined &&
445
+ queue.some((entry) => entry.sourceTs === item.sourceTs)) {
446
+ return;
447
+ }
448
+ queue.push(item);
449
+ this.coalesceQueues.set(sessionId, queue);
450
+ }
451
+ clearCoalesced(sessionId) {
452
+ this.coalesceQueues.delete(sessionId);
453
+ }
454
+ formatCoalescedFollowups(items) {
455
+ if (items.length === 1) {
456
+ return items[0].message;
457
+ }
458
+ const parts = [
459
+ "<slack_followups>",
460
+ `ANSWER-EACH: ${items.length} asks arrived while you were busy. Address each in order; keep replies compact.`,
461
+ "",
462
+ ];
463
+ for (let i = 0; i < items.length; i++) {
464
+ parts.push(`### Ask ${i + 1}`);
465
+ parts.push(items[i].message);
466
+ parts.push("");
467
+ }
468
+ parts.push("</slack_followups>");
469
+ return parts.join("\n").trimEnd();
470
+ }
471
+ drainCoalescedFollowups(sessionId) {
396
472
  return __awaiter(this, void 0, void 0, function* () {
473
+ var _a;
474
+ const queued = (_a = this.coalesceQueues.get(sessionId)) !== null && _a !== void 0 ? _a : [];
475
+ if (queued.length === 0) {
476
+ return;
477
+ }
478
+ this.coalesceQueues.delete(sessionId);
479
+ const message = this.formatCoalescedFollowups(queued);
480
+ this.logger(`[session] coalesce drain starting follow-up (${queued.length}) ${sessionId}`);
481
+ yield this.startTurn(sessionId, message, {
482
+ admission: "preempt",
483
+ retainCoalesced: true,
484
+ });
485
+ });
486
+ }
487
+ admitTurn(sessionId_1, message_1) {
488
+ return __awaiter(this, arguments, void 0, function* (sessionId, message, options = {}) {
489
+ var _a;
490
+ const admission = (_a = options.admission) !== null && _a !== void 0 ? _a : "preempt";
491
+ if (admission === "coalesce" && this.isTurnRunning(sessionId)) {
492
+ this.enqueueCoalesced(sessionId, Object.assign({ message }, (options.sourceTs === undefined
493
+ ? {}
494
+ : { sourceTs: options.sourceTs })));
495
+ this.logger(`[session] coalesce enqueue ${sessionId}`);
496
+ return true;
497
+ }
397
498
  // Interrupt any turn in flight and wait for it to settle so the SDK run is
398
499
  // terminal before this turn's send (the follow-up also force-expires as a
399
- // belt-and-suspenders — see buildTurnRequest).
400
- yield this.interruptActiveTurn(sessionId);
500
+ // belt-and-suspenders — see buildTurnRequest). Drain-resume skips interrupt:
501
+ // the parent lifecycle already cleared activeTurns, and awaiting the in-
502
+ // flight coalesce drain here would deadlock on itself.
503
+ if (options.retainCoalesced !== true) {
504
+ yield this.interruptActiveTurn(sessionId);
505
+ }
506
+ // Preempt owns the conversation — drop stale coalesced asks. Coalesce
507
+ // admits and drain-resume keep the queue (retainCoalesced / coalesce path).
508
+ if (admission === "preempt" && options.retainCoalesced !== true) {
509
+ this.clearCoalesced(sessionId);
510
+ }
401
511
  this.pendingTurns.add(sessionId);
402
512
  try {
403
513
  let record = yield this.updateRecord(sessionId, (current) => (Object.assign(Object.assign({}, current), { turnCount: current.turnCount + 1 })));
@@ -417,26 +527,51 @@ export class SessionEngine {
417
527
  });
418
528
  const abort = new AbortController();
419
529
  this.turnAborts.set(sessionId, abort);
420
- const turnPromise = this.executeTurn(record, turnId, message, isFirstTurn, abort.signal);
421
- this.activeTurns.set(sessionId, turnPromise);
422
- void turnPromise
423
- .then((outcome) => {
530
+ // Lifecycle promise includes post-turn coalesce drain so waiters and
531
+ // interrupt settle after follow-ups are admitted (not only executeTurn).
532
+ // Assigned before the async body runs so self-identity checks typecheck.
533
+ let lifecycle;
534
+ lifecycle = (() => __awaiter(this, void 0, void 0, function* () {
535
+ let outcome;
536
+ try {
537
+ outcome = yield this.executeTurn(record, turnId, message, isFirstTurn, abort.signal);
538
+ }
539
+ catch (error) {
540
+ outcome = {
541
+ status: "error",
542
+ errorMessage: describeError(error),
543
+ };
544
+ }
424
545
  this.lastOutcomes.set(sessionId, outcome);
425
- })
426
- .catch(() => { })
427
- .finally(() => {
428
- // Guard against clobbering a newer turn that already replaced us.
429
- if (this.activeTurns.get(sessionId) === turnPromise) {
546
+ const wasCurrent = this.activeTurns.get(sessionId) === lifecycle;
547
+ if (wasCurrent) {
430
548
  this.activeTurns.delete(sessionId);
431
549
  }
432
550
  if (this.turnAborts.get(sessionId) === abort) {
433
551
  this.turnAborts.delete(sessionId);
434
552
  }
435
- });
553
+ if (wasCurrent && !abort.signal.aborted) {
554
+ const drain = this.drainCoalescedFollowups(sessionId).catch((error) => {
555
+ this.logger(`[session] coalesce drain failed for ${sessionId}: ${describeError(error)}`);
556
+ });
557
+ this.coalesceDrains.set(sessionId, drain);
558
+ try {
559
+ yield drain;
560
+ }
561
+ finally {
562
+ if (this.coalesceDrains.get(sessionId) === drain) {
563
+ this.coalesceDrains.delete(sessionId);
564
+ }
565
+ }
566
+ }
567
+ return outcome;
568
+ }))();
569
+ this.activeTurns.set(sessionId, lifecycle);
436
570
  }
437
571
  finally {
438
572
  this.pendingTurns.delete(sessionId);
439
573
  }
574
+ return false;
440
575
  });
441
576
  }
442
577
  /**
@@ -452,19 +587,80 @@ export class SessionEngine {
452
587
  if (active !== undefined) {
453
588
  yield active.catch(() => { });
454
589
  }
590
+ const drain = this.coalesceDrains.get(sessionId);
591
+ if (drain !== undefined) {
592
+ yield drain.catch(() => { });
593
+ }
455
594
  });
456
595
  }
457
596
  /**
458
597
  * Abort the in-flight turn without starting a follow-up. Returns whether
459
- * a turn was active (or pending abort) when called.
598
+ * a turn was active (or pending abort) when called. Also clears any
599
+ * coalesced follow-ups queued behind the session.
460
600
  */
461
601
  interrupt(sessionId) {
462
602
  return __awaiter(this, void 0, void 0, function* () {
463
- const hadWork = this.turnAborts.has(sessionId) || this.activeTurns.has(sessionId);
603
+ var _a;
604
+ var _b;
605
+ const hadWork = this.turnAborts.has(sessionId) ||
606
+ this.activeTurns.has(sessionId) ||
607
+ this.coalesceDrains.has(sessionId) ||
608
+ ((_b = (_a = this.coalesceQueues.get(sessionId)) === null || _a === void 0 ? void 0 : _a.length) !== null && _b !== void 0 ? _b : 0) > 0;
609
+ this.clearCoalesced(sessionId);
610
+ const record = yield this.sessions.get(sessionId);
611
+ const continuationKey = record === null || record === void 0 ? void 0 : record.continuationKey;
612
+ if (continuationKey !== undefined && continuationKey !== null) {
613
+ yield this.nudgeStore.clear(continuationKey);
614
+ }
464
615
  yield this.interruptActiveTurn(sessionId);
465
616
  return hadWork;
466
617
  });
467
618
  }
619
+ isContinuationBusy(channelId, continuationToken) {
620
+ return __awaiter(this, void 0, void 0, function* () {
621
+ let existing = yield this.sessions.findByContinuation(channelId, continuationToken);
622
+ if (existing === undefined) {
623
+ existing = yield this.restoreSessionByContinuation(channelId, continuationToken);
624
+ }
625
+ if (existing === undefined) {
626
+ return false;
627
+ }
628
+ return this.isTurnRunning(existing.sessionId);
629
+ });
630
+ }
631
+ /**
632
+ * Last agent-posted Slack message ts for warm `<slack_thread_delta>`, from
633
+ * the session's channel state when present.
634
+ */
635
+ getContinuationLastBotMessageTs(channelId, continuationToken) {
636
+ return __awaiter(this, void 0, void 0, function* () {
637
+ let existing = yield this.sessions.findByContinuation(channelId, continuationToken);
638
+ if (existing === undefined) {
639
+ existing = yield this.restoreSessionByContinuation(channelId, continuationToken);
640
+ }
641
+ if (existing === undefined) {
642
+ return undefined;
643
+ }
644
+ const state = existing.channelState;
645
+ if (state === null || state === undefined || typeof state !== "object") {
646
+ return undefined;
647
+ }
648
+ const ts = state.lastBotMessageTs;
649
+ return typeof ts === "string" && ts.trim() !== "" ? ts.trim() : undefined;
650
+ });
651
+ }
652
+ interruptContinuation(channelId, continuationToken) {
653
+ return __awaiter(this, void 0, void 0, function* () {
654
+ let existing = yield this.sessions.findByContinuation(channelId, continuationToken);
655
+ if (existing === undefined) {
656
+ existing = yield this.restoreSessionByContinuation(channelId, continuationToken);
657
+ }
658
+ if (existing === undefined) {
659
+ return false;
660
+ }
661
+ return this.interrupt(existing.sessionId);
662
+ });
663
+ }
468
664
  /**
469
665
  * Local harness cwd for a session: per-send override, else
470
666
  * `<agent.local.cwd>/<sessionId>`, else the durable session workspace.
@@ -1128,15 +1324,20 @@ export class SessionEngine {
1128
1324
  var _a;
1129
1325
  // Drain admission first so pendingTurns → activeTurns is visible before we
1130
1326
  // decide the session is idle (reminders use this for queueBehindTurn).
1131
- const chain = this.admissionChains.get(sessionId);
1132
- if (chain !== undefined) {
1133
- yield chain.catch(() => { });
1134
- }
1135
- const active = this.activeTurns.get(sessionId);
1136
- if (active !== undefined) {
1137
- return active;
1327
+ // The active lifecycle promise also awaits coalesce drain + the drained
1328
+ // follow-up's admission, so Slack coalesced waiters see the resume turn.
1329
+ for (;;) {
1330
+ const chain = this.admissionChains.get(sessionId);
1331
+ if (chain !== undefined) {
1332
+ yield chain.catch(() => { });
1333
+ }
1334
+ const active = this.activeTurns.get(sessionId);
1335
+ if (active !== undefined) {
1336
+ yield active;
1337
+ continue;
1338
+ }
1339
+ return (_a = this.lastOutcomes.get(sessionId)) !== null && _a !== void 0 ? _a : { status: "finished" };
1138
1340
  }
1139
- return (_a = this.lastOutcomes.get(sessionId)) !== null && _a !== void 0 ? _a : { status: "finished" };
1140
1341
  });
1141
1342
  }
1142
1343
  /**
@@ -1275,25 +1476,20 @@ export class SessionEngine {
1275
1476
  });
1276
1477
  }
1277
1478
  isTurnRunning(sessionId) {
1278
- // Include the admit window (pendingTurns) so busy policies do not treat a
1279
- // turn that has been accepted but not yet marked active as idle.
1280
- return this.activeTurns.has(sessionId) || this.pendingTurns.has(sessionId);
1479
+ // Include the admit window (pendingTurns) and coalesce drain so busy
1480
+ // policies do not treat a turn that has finished executeTurn but not yet
1481
+ // resumed queued follow-ups as idle (avoids clearCoalesced races).
1482
+ return (this.activeTurns.has(sessionId) ||
1483
+ this.pendingTurns.has(sessionId) ||
1484
+ this.coalesceDrains.has(sessionId));
1281
1485
  }
1282
1486
  sessionInfo(record) {
1283
1487
  return Object.assign(Object.assign(Object.assign({ id: record.sessionId, channelId: record.channelId, mode: record.mode, purpose: record.purpose === "eval" ? "eval" : "live", auth: record.auth, continuationKey: record.continuationKey }, (record.abs === undefined ? {} : { abs: record.abs })), (record.title === undefined ? {} : { title: record.title })), (record.sdkAgentId === undefined
1284
1488
  ? {}
1285
1489
  : { sdkAgentId: record.sdkAgentId }));
1286
1490
  }
1287
- channelSession(sessionId, record, isNew) {
1288
- return {
1289
- id: sessionId,
1290
- continuationToken: record.continuationKey,
1291
- isNew,
1292
- info: this.sessionInfo(record),
1293
- getEventStream: (options) => { var _a; return this.eventStream(sessionId, (_a = options === null || options === void 0 ? void 0 : options.startIndex) !== null && _a !== void 0 ? _a : 0); },
1294
- waitForCompletion: () => this.waitForCompletion(sessionId),
1295
- setContinuationToken: (token) => this.setContinuationToken(sessionId, token),
1296
- };
1491
+ channelSession(sessionId, record, isNew, coalesced = false) {
1492
+ return Object.assign(Object.assign({ id: sessionId, continuationToken: record.continuationKey, isNew }, (coalesced ? { coalesced: true } : {})), { info: this.sessionInfo(record), getEventStream: (options) => { var _a; return this.eventStream(sessionId, (_a = options === null || options === void 0 ? void 0 : options.startIndex) !== null && _a !== void 0 ? _a : 0); }, waitForCompletion: () => this.waitForCompletion(sessionId), setContinuationToken: (token) => this.setContinuationToken(sessionId, token) });
1297
1493
  }
1298
1494
  resolveChannelId(channel) {
1299
1495
  if (typeof channel === "string") {
@@ -16,7 +16,7 @@
16
16
  */
17
17
  import type { ABMetricSample } from "../ab.js";
18
18
  import { type ResolvedStoragePolicy, type StorageContext, type StorageDefinition } from "../storage.js";
19
- import type { SessionEvent, SessionRecord } from "../types.js";
19
+ import type { JsonValue, SessionEvent, SessionRecord } from "../types.js";
20
20
  import type { ABSnapshot } from "./ab-snapshot.js";
21
21
  import type { EvalRunStorage } from "./eval-run-store.js";
22
22
  import { type ReminderRecord } from "./reminder-store.js";
@@ -106,6 +106,23 @@ export declare class StorageCoordinator {
106
106
  * disk). Errors read as empty — same posture as session bulk restore.
107
107
  */
108
108
  listReminders(): Promise<ReminderRecord[]>;
109
+ /**
110
+ * Whether the sink can serve awaited author reads/writes (`get` is
111
+ * required — `put` is always present on {@link StorageConfig}).
112
+ */
113
+ get supportsAuthorKv(): boolean;
114
+ /**
115
+ * Awaited author KV read. Unlike framework session mirrors, this awaits
116
+ * the sink and propagates errors — handlers need read-after-write.
117
+ */
118
+ kvGet(authorKey: string): Promise<JsonValue | undefined>;
119
+ /**
120
+ * Awaited author KV write. Goes straight to the sink (not the
121
+ * at-most-once delivery queue) so callers observe durable success/failure.
122
+ */
123
+ kvPut(authorKey: string, value: JsonValue): Promise<void>;
124
+ /** Awaited author KV delete. No-op when the sink omits `delete`. */
125
+ kvDelete(authorKey: string): Promise<void>;
109
126
  /**
110
127
  * Adapter over the sink's dedicated `evals` table for the playground
111
128
  * eval-run store. Undefined when the table is not configured — eval
@@ -1 +1 @@
1
- {"version":3,"file":"storage-coordinator.d.ts","sourceRoot":"","sources":["../../src/internal/storage-coordinator.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"storage-coordinator.d.ts","sourceRoot":"","sources":["../../src/internal/storage-coordinator.ts"],"names":[],"mappings":"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"}
@@ -292,6 +292,49 @@ export class StorageCoordinator {
292
292
  });
293
293
  }
294
294
  // ==========================================================================
295
+ // Author KV ({@link HostContext.kv})
296
+ // ==========================================================================
297
+ /**
298
+ * Whether the sink can serve awaited author reads/writes (`get` is
299
+ * required — `put` is always present on {@link StorageConfig}).
300
+ */
301
+ get supportsAuthorKv() {
302
+ return this.definition.get !== undefined;
303
+ }
304
+ /**
305
+ * Awaited author KV read. Unlike framework session mirrors, this awaits
306
+ * the sink and propagates errors — handlers need read-after-write.
307
+ */
308
+ kvGet(authorKey) {
309
+ return __awaiter(this, void 0, void 0, function* () {
310
+ const get = this.definition.get;
311
+ if (get === undefined) {
312
+ throw new Error("storage get is not configured");
313
+ }
314
+ const key = storageKeys.kv(this.agentName, authorKey);
315
+ // Preserve stored JSON `null` (distinct from a missing key).
316
+ return yield get(key, this.context("restore"));
317
+ });
318
+ }
319
+ /**
320
+ * Awaited author KV write. Goes straight to the sink (not the
321
+ * at-most-once delivery queue) so callers observe durable success/failure.
322
+ */
323
+ kvPut(authorKey, value) {
324
+ return __awaiter(this, void 0, void 0, function* () {
325
+ const key = storageKeys.kv(this.agentName, authorKey);
326
+ yield this.definition.put(key, value, this.context("policy"));
327
+ });
328
+ }
329
+ /** Awaited author KV delete. No-op when the sink omits `delete`. */
330
+ kvDelete(authorKey) {
331
+ return __awaiter(this, void 0, void 0, function* () {
332
+ var _a, _b;
333
+ const key = storageKeys.kv(this.agentName, authorKey);
334
+ yield (_b = (_a = this.definition).delete) === null || _b === void 0 ? void 0 : _b.call(_a, key, this.context("policy"));
335
+ });
336
+ }
337
+ // ==========================================================================
295
338
  // Evals (dedicated `evals` table)
296
339
  // ==========================================================================
297
340
  /**