@cursor/july 0.1.9 → 0.1.10

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 (96) hide show
  1. package/dist/bin/agent-serve.js +9 -0
  2. package/dist/docs/404.html +2 -2
  3. package/dist/docs/ab.html +3 -3
  4. package/dist/docs/assets/{app.Ciosf24M.js → app.CY2buaEE.js} +1 -1
  5. package/dist/docs/assets/chunks/@localSearchIndexroot.OfDZ5CXg.js +1 -0
  6. package/dist/docs/assets/chunks/{VPLocalSearchBox.BcRDly7n.js → VPLocalSearchBox.Ix954SFJ.js} +1 -1
  7. package/dist/docs/assets/chunks/{theme.CMpbjdfx.js → theme.PDwuJbyi.js} +2 -2
  8. package/dist/docs/assets/{reference_cli.md.D189RBCH.js → reference_cli.md.CjfiV2Q-.js} +2 -1
  9. package/dist/docs/assets/{reference_connections.md.zaEYCLHT.js → reference_connections.md.FbWupyzW.js} +1 -1
  10. package/dist/docs/building-with-agents.html +3 -3
  11. package/dist/docs/concepts.html +3 -3
  12. package/dist/docs/deployment.html +3 -3
  13. package/dist/docs/evals.html +3 -3
  14. package/dist/docs/example-agents/approval-buddy.html +3 -3
  15. package/dist/docs/example-agents/benny.html +3 -3
  16. package/dist/docs/example-agents/bugbot.html +3 -3
  17. package/dist/docs/example-agents/codebase-wiki.html +3 -3
  18. package/dist/docs/example-agents/codeowners-review.html +3 -3
  19. package/dist/docs/example-agents/concierge.html +3 -3
  20. package/dist/docs/example-agents/fsd.html +3 -3
  21. package/dist/docs/example-agents/index.html +3 -3
  22. package/dist/docs/example-agents/knowledge-base.html +3 -3
  23. package/dist/docs/example-agents/oncall.html +3 -3
  24. package/dist/docs/example-agents/security-reviewer.html +3 -3
  25. package/dist/docs/example-agents/slack-agent.html +3 -3
  26. package/dist/docs/example-agents/weather-agent.html +3 -3
  27. package/dist/docs/guides/agent-to-agent.html +3 -3
  28. package/dist/docs/guides/cloud-runtime.html +3 -3
  29. package/dist/docs/guides/github.html +3 -3
  30. package/dist/docs/guides/human-in-the-loop.html +3 -3
  31. package/dist/docs/guides/slack.html +3 -3
  32. package/dist/docs/guides/webhooks.html +3 -3
  33. package/dist/docs/hashmap.json +1 -1
  34. package/dist/docs/hillclimbing.html +3 -3
  35. package/dist/docs/index.html +3 -3
  36. package/dist/docs/quickstart.html +3 -3
  37. package/dist/docs/reference/agent-config.html +3 -3
  38. package/dist/docs/reference/channels.html +3 -3
  39. package/dist/docs/reference/cli.html +6 -5
  40. package/dist/docs/reference/connections.html +5 -5
  41. package/dist/docs/reference/hooks.html +3 -3
  42. package/dist/docs/reference/http-api.html +3 -3
  43. package/dist/docs/reference/instructions.html +3 -3
  44. package/dist/docs/reference/playground.html +3 -3
  45. package/dist/docs/reference/project-layout.html +3 -3
  46. package/dist/docs/reference/schedules.html +3 -3
  47. package/dist/docs/reference/sessions.html +3 -3
  48. package/dist/docs/reference/skills.html +3 -3
  49. package/dist/docs/reference/subagents.html +3 -3
  50. package/dist/docs/reference/tools.html +3 -3
  51. package/dist/docs/scaffolding-agents.html +3 -3
  52. package/dist/docs/storage.html +3 -3
  53. package/dist/docs/troubleshooting.html +3 -3
  54. package/dist/internal/cli-ax.d.ts.map +1 -1
  55. package/dist/internal/cli-ax.js +7 -4
  56. package/dist/internal/persistence-coordinator.d.ts +127 -0
  57. package/dist/internal/persistence-coordinator.d.ts.map +1 -0
  58. package/dist/internal/resolved-connections.d.ts +6 -0
  59. package/dist/internal/resolved-connections.d.ts.map +1 -1
  60. package/dist/internal/resolved-connections.js +6 -2
  61. package/dist/internal/server.d.ts.map +1 -1
  62. package/dist/internal/server.js +11 -1
  63. package/dist/internal/session-engine.d.ts +23 -1
  64. package/dist/internal/session-engine.d.ts.map +1 -1
  65. package/dist/internal/session-engine.js +71 -11
  66. package/dist/internal/sessions-client.d.ts +16 -2
  67. package/dist/internal/sessions-client.d.ts.map +1 -1
  68. package/dist/internal/sessions-client.js +28 -12
  69. package/dist/internal/storage-coordinator.d.ts +11 -0
  70. package/dist/internal/storage-coordinator.d.ts.map +1 -1
  71. package/dist/internal/storage-coordinator.js +30 -0
  72. package/dist/persistence.d.ts +184 -0
  73. package/dist/persistence.d.ts.map +1 -0
  74. package/dist/storage-backends/cursor-hosted.d.ts +10 -0
  75. package/dist/storage-backends/cursor-hosted.d.ts.map +1 -1
  76. package/dist/storage-backends/cursor-hosted.js +14 -0
  77. package/dist/types.d.ts +15 -0
  78. package/dist/types.d.ts.map +1 -1
  79. package/docs/reference/cli.md +2 -0
  80. package/docs/reference/connections.md +4 -2
  81. package/package.json +1 -1
  82. package/src/bin/agent-serve.ts +10 -0
  83. package/src/internal/cli-ax.ts +10 -4
  84. package/src/internal/resolved-connections.ts +14 -4
  85. package/src/internal/server.ts +15 -1
  86. package/src/internal/session-engine.ts +77 -2
  87. package/src/internal/sessions-client.ts +49 -14
  88. package/src/internal/storage-coordinator.ts +36 -0
  89. package/src/storage-backends/cursor-hosted.ts +21 -0
  90. package/src/types.ts +16 -0
  91. package/dist/channels/slack/post-update-delivery.js +0 -318
  92. package/dist/docs/assets/chunks/@localSearchIndexroot.D154Detj.js +0 -1
  93. package/dist/internal/json-dir-store.js +0 -100
  94. package/docs/.vitepress/cache/deps/package.json +0 -3
  95. /package/dist/docs/assets/{reference_cli.md.D189RBCH.lean.js → reference_cli.md.CjfiV2Q-.lean.js} +0 -0
  96. /package/dist/docs/assets/{reference_connections.md.zaEYCLHT.lean.js → reference_connections.md.FbWupyzW.lean.js} +0 -0
@@ -45,6 +45,7 @@ import {
45
45
  type SessionEvent,
46
46
  type SessionEventPayload,
47
47
  type SessionInfo,
48
+ type SessionListSource,
48
49
  type SessionRecord,
49
50
  type SessionSummary,
50
51
  type SlackNudgeHostApi,
@@ -1827,13 +1828,88 @@ export class SessionEngine {
1827
1828
  * observability surface across every principal (channel-driven runs,
1828
1829
  * schedule/task sessions, other callers). The route auth chain still
1829
1830
  * gates who may call this at all.
1831
+ *
1832
+ * When the project uses `cursorHostedStorage`, prefers durable Bugbot rows
1833
+ * (merged with any newer local records not flushed yet). Falls back to the
1834
+ * local `--state-root` store when the sink is unavailable.
1830
1835
  */
1831
1836
  async listSessions(
1832
1837
  caller: AuthContext | null,
1833
1838
  options?: { includeAll?: boolean }
1834
1839
  ): Promise<SessionSummary[]> {
1840
+ return (await this.listSessionsWithSource(caller, options)).sessions;
1841
+ }
1842
+
1843
+ /**
1844
+ * Same as {@link listSessions}, plus which store answered
1845
+ * (`cursor-hosted` = durable DB via the control-plane proxy; `local` =
1846
+ * `--state-root`).
1847
+ */
1848
+ async listSessionsWithSource(
1849
+ caller: AuthContext | null,
1850
+ options?: { includeAll?: boolean }
1851
+ ): Promise<{ sessions: SessionSummary[]; source: SessionListSource }> {
1835
1852
  const includeAll = options?.includeAll ?? false;
1853
+ if (this.storage?.isCursorHosted === true) {
1854
+ try {
1855
+ const records = await this.mergeStoredAndLocalSessions();
1856
+ return {
1857
+ sessions: this.summarizeSessionRecords(records, caller, includeAll),
1858
+ source: "cursor-hosted",
1859
+ };
1860
+ } catch (error) {
1861
+ this.logger(
1862
+ `[agentkit] cursor-hosted session list failed; falling back to local: ${describeError(error)}`
1863
+ );
1864
+ }
1865
+ }
1836
1866
  const records = await this.sessions.list();
1867
+ // Local store is createdAt-asc; reverse so newest created leads.
1868
+ return {
1869
+ sessions: this.summarizeSessionRecords(
1870
+ [...records].reverse(),
1871
+ caller,
1872
+ includeAll
1873
+ ),
1874
+ source: "local",
1875
+ };
1876
+ }
1877
+
1878
+ /**
1879
+ * Durable sink rows plus local records (local wins on the same id when
1880
+ * newer — covers debounce windows that have not flushed yet). Newest
1881
+ * `updatedAt` first.
1882
+ */
1883
+ private async mergeStoredAndLocalSessions(): Promise<SessionRecord[]> {
1884
+ const storage = this.storage;
1885
+ if (storage === undefined) {
1886
+ throw new Error("storage is not configured");
1887
+ }
1888
+ const stored = await storage.listSessionsFromSink();
1889
+ const local = await this.sessions.list();
1890
+ const byId = new Map<string, SessionRecord>();
1891
+ for (const record of stored) {
1892
+ byId.set(record.sessionId, record);
1893
+ }
1894
+ for (const record of local) {
1895
+ const existing = byId.get(record.sessionId);
1896
+ if (
1897
+ existing === undefined ||
1898
+ record.updatedAt.localeCompare(existing.updatedAt) >= 0
1899
+ ) {
1900
+ byId.set(record.sessionId, record);
1901
+ }
1902
+ }
1903
+ return [...byId.values()].sort((a, b) =>
1904
+ b.updatedAt.localeCompare(a.updatedAt)
1905
+ );
1906
+ }
1907
+
1908
+ private summarizeSessionRecords(
1909
+ records: readonly SessionRecord[],
1910
+ caller: AuthContext | null,
1911
+ includeAll: boolean
1912
+ ): SessionSummary[] {
1837
1913
  return records
1838
1914
  .filter((record) => includeAll || samePrincipal(record.auth, caller))
1839
1915
  .map((record) => {
@@ -1858,8 +1934,7 @@ export class SessionEngine {
1858
1934
  ),
1859
1935
  ...(externalUrl === undefined ? {} : { externalUrl }),
1860
1936
  };
1861
- })
1862
- .reverse();
1937
+ });
1863
1938
  }
1864
1939
 
1865
1940
  /**
@@ -4,15 +4,33 @@
4
4
  * Customer-facing errors never include hosting URLs or infra jargon.
5
5
  */
6
6
 
7
- import type { SessionEvent, SessionSummary } from "../types.js";
7
+ import type {
8
+ SessionEvent,
9
+ SessionListSource,
10
+ SessionSummary,
11
+ } from "../types.js";
8
12
  import { sanitizeCustomerError } from "./logs-client.js";
9
13
  import { mergeRequestHeaders, type RequestAuth } from "./request-headers.js";
10
14
 
15
+ export interface ListedSessions {
16
+ sessions: SessionSummary[];
17
+ /** Present when the server reports which store answered. */
18
+ source?: SessionListSource;
19
+ }
20
+
11
21
  export async function listSessions(args: {
12
22
  baseUrl: string;
13
23
  auth?: RequestAuth;
14
24
  fetchImpl?: typeof fetch;
15
25
  }): Promise<SessionSummary[]> {
26
+ return (await listSessionsWithSource(args)).sessions;
27
+ }
28
+
29
+ export async function listSessionsWithSource(args: {
30
+ baseUrl: string;
31
+ auth?: RequestAuth;
32
+ fetchImpl?: typeof fetch;
33
+ }): Promise<ListedSessions> {
16
34
  const baseUrl = args.baseUrl.replace(/\/$/, "");
17
35
  const fetchImpl = args.fetchImpl ?? fetch;
18
36
  let resp: Response;
@@ -30,18 +48,21 @@ export async function listSessions(args: {
30
48
  if (!resp.ok) {
31
49
  throw new Error(`Could not list sessions (HTTP ${resp.status}).`);
32
50
  }
33
- const body = (await resp.json()) as { sessions?: unknown };
34
- if (!Array.isArray(body.sessions)) {
35
- return [];
36
- }
37
- const out: SessionSummary[] = [];
38
- for (const row of body.sessions) {
39
- const parsed = parseSessionSummary(row);
40
- if (parsed !== undefined) {
41
- out.push(parsed);
51
+ const body = (await resp.json()) as { sessions?: unknown; source?: unknown };
52
+ const sessions: SessionSummary[] = [];
53
+ if (Array.isArray(body.sessions)) {
54
+ for (const row of body.sessions) {
55
+ const parsed = parseSessionSummary(row);
56
+ if (parsed !== undefined) {
57
+ sessions.push(parsed);
58
+ }
42
59
  }
43
60
  }
44
- return out;
61
+ const source =
62
+ body.source === "cursor-hosted" || body.source === "local"
63
+ ? body.source
64
+ : undefined;
65
+ return source === undefined ? { sessions } : { sessions, source };
45
66
  }
46
67
 
47
68
  export async function fetchSessionEvents(args: {
@@ -186,10 +207,24 @@ export async function resolvePreviousSessionId(args: {
186
207
  return pickPreviousSession(sessions)?.sessionId;
187
208
  }
188
209
 
210
+ /** Human label for {@link SessionListSource} in CLI output. */
211
+ export function formatSessionListSource(source: SessionListSource): string {
212
+ return source === "cursor-hosted"
213
+ ? "Listing from cursor-hosted storage (durable DB)."
214
+ : "Listing from local session store.";
215
+ }
216
+
189
217
  /** Format a sessions table (id, channel, mode, turns, running, updated). */
190
- export function formatSessionsTable(sessions: SessionSummary[]): string {
218
+ export function formatSessionsTable(
219
+ sessions: SessionSummary[],
220
+ options?: { source?: SessionListSource }
221
+ ): string {
222
+ const sourceLine =
223
+ options?.source === undefined
224
+ ? ""
225
+ : `${formatSessionListSource(options.source)}\n`;
191
226
  if (sessions.length === 0) {
192
- return "No sessions.\n";
227
+ return `${sourceLine}No sessions.\n`;
193
228
  }
194
229
  const header = ["SESSION", "CHANNEL", "MODE", "TURNS", "RUNNING", "UPDATED"];
195
230
  const rows = sessions.map((s) => [
@@ -200,7 +235,7 @@ export function formatSessionsTable(sessions: SessionSummary[]): string {
200
235
  s.running ? "yes" : "no",
201
236
  s.updatedAt,
202
237
  ]);
203
- return `${renderTable(header, rows)}\n`;
238
+ return `${sourceLine}${renderTable(header, rows)}\n`;
204
239
  }
205
240
 
206
241
  function renderTable(header: string[], rows: string[][]): string {
@@ -24,6 +24,7 @@ import {
24
24
  type StorageDefinition,
25
25
  storageKeys,
26
26
  } from "../storage.js";
27
+ import { isCursorHostedStorage } from "../storage-backends/cursor-hosted.js";
27
28
  import type { JsonValue, SessionEvent, SessionRecord } from "../types.js";
28
29
  import type { ABSnapshot } from "./ab-snapshot.js";
29
30
  import { describeError } from "./describe-error.js";
@@ -261,6 +262,14 @@ export class StorageCoordinator {
261
262
  return this.definition.list !== undefined;
262
263
  }
263
264
 
265
+ /**
266
+ * True when the authored sink is `cursorHostedStorage` (Bugbot via the
267
+ * control-plane proxy). `GET /v1/sessions` prefers this durable list.
268
+ */
269
+ get isCursorHosted(): boolean {
270
+ return isCursorHostedStorage(this.definition);
271
+ }
272
+
264
273
  /**
265
274
  * Saved session records for this agent (startup restore). Errors are
266
275
  * logged and read as "nothing saved" — a broken store must never block
@@ -275,6 +284,33 @@ export class StorageCoordinator {
275
284
  .filter((record) => record != null);
276
285
  }
277
286
 
287
+ /**
288
+ * Session records from the sink for the sessions API. Unlike
289
+ * {@link listSessions}, missing `list` or a thrown sink error **propagates**
290
+ * so the caller can fall back to the local session store.
291
+ */
292
+ async listSessionsFromSink(): Promise<SessionRecord[]> {
293
+ const list = this.definition.list;
294
+ if (list === undefined) {
295
+ throw new Error("storage sink has no list");
296
+ }
297
+ const entries = await list(
298
+ storageKeys.sessionPrefix(this.agentName),
299
+ this.context("restore")
300
+ );
301
+ if (!Array.isArray(entries)) {
302
+ throw new Error("storage list returned a non-array");
303
+ }
304
+ return entries
305
+ .map((entry) => entry.value as unknown as SessionRecord)
306
+ .filter(
307
+ (record) =>
308
+ record != null &&
309
+ typeof record.sessionId === "string" &&
310
+ record.sessionId !== ""
311
+ );
312
+ }
313
+
278
314
  /**
279
315
  * Saved event stream for one session: chunk values concatenated in key
280
316
  * order (chunks are keyed by their first event's index, zero-padded, so
@@ -80,9 +80,29 @@ export interface CursorHostedStorageOptions {
80
80
  fetch?: typeof fetch;
81
81
  }
82
82
 
83
+ /**
84
+ * Marker stamped on {@link cursorHostedStorage} so the engine can prefer
85
+ * durable Bugbot rows for `GET /v1/sessions` (survives `defineStorage` spread).
86
+ * Explicit `unique symbol` annotation required for `--isolatedDeclarations`
87
+ * (TS9010).
88
+ */
89
+ export const CURSOR_HOSTED_STORAGE: unique symbol = Symbol.for(
90
+ "@cursor/july.storage.cursor-hosted"
91
+ );
92
+
93
+ /** True when `value` is (or was built from) {@link cursorHostedStorage}. */
94
+ export function isCursorHostedStorage(value: unknown): boolean {
95
+ return (
96
+ typeof value === "object" &&
97
+ value !== null &&
98
+ (value as Record<symbol, unknown>)[CURSOR_HOSTED_STORAGE] === true
99
+ );
100
+ }
101
+
83
102
  type CursorHostedStorageMethods = Required<
84
103
  Pick<StorageConfig, "put" | "get" | "delete" | "list">
85
104
  > & {
105
+ readonly [CURSOR_HOSTED_STORAGE]: true;
86
106
  evals: StorageEvalsTable;
87
107
  abs: StorageABTable;
88
108
  };
@@ -155,6 +175,7 @@ export function cursorHostedStorage(
155
175
  options: CursorHostedStorageOptions = {}
156
176
  ): CursorHostedStorageMethods {
157
177
  return {
178
+ [CURSOR_HOSTED_STORAGE]: true,
158
179
  async put(key, value): Promise<void> {
159
180
  await storageRpc(options, "/internal/agent-serve/storage/kv/put", {
160
181
  key,
package/src/types.ts CHANGED
@@ -886,6 +886,13 @@ export interface SessionSummary {
886
886
  externalUrl?: string;
887
887
  }
888
888
 
889
+ /**
890
+ * Where `GET /v1/sessions` loaded rows from.
891
+ * - `cursor-hosted` — durable Bugbot `agent_serve_kv` via `cursorHostedStorage`
892
+ * - `local` — the serve process `--state-root` session store
893
+ */
894
+ export type SessionListSource = "cursor-hosted" | "local";
895
+
889
896
  /**
890
897
  * A session handle scoped to the channel that owns it. This is what channel
891
898
  * route handlers get back from `send()` / `getSession()`.
@@ -1897,6 +1904,15 @@ export interface ServeOptions {
1897
1904
  * SSO/Okta proxy in front of the host).
1898
1905
  */
1899
1906
  allowAnonymousCursorGithub?: boolean;
1907
+ /**
1908
+ * Allow {@link allowAnonymous} together with
1909
+ * `defineConnection({ cursorAccount: true })`. Without this opt-in, `serve`
1910
+ * refuses to start because every admitted anonymous caller could drive the
1911
+ * signed-in account's MCP connectors. Set it only when the network boundary
1912
+ * already authenticates callers (e.g. an SSO/Okta proxy, or the hosted
1913
+ * agent-serve alias token).
1914
+ */
1915
+ allowAnonymousCursorAccountMcp?: boolean;
1900
1916
  /**
1901
1917
  * Public base URL where this server is reachable from outside the host
1902
1918
  * (e.g. `https://agents.example.com`). Used to resolve peer
@@ -1,318 +0,0 @@
1
- /**
2
- * Live-reply delivery strategies for the Slack default handlers.
3
- *
4
- * A single strategy owns how an in-progress reply is opened, updated,
5
- * finalized, and cleaned up so the handlers in `defaults.ts` don't fork on the
6
- * delivery mode at every event:
7
- *
8
- * - `stream`: Slack chat.startStream / appendStream / stopStream.
9
- * - `post-update`: a chat.postMessage skeleton edited with chat.update. This is
10
- * the only path that preserves Cursor-account identity pinning (which only
11
- * covers postMessage).
12
- *
13
- * `buildDefaultEvents` selects one via {@link selectLiveDeliveryStrategy} up
14
- * front and calls into it. All calls keep the same soft-fail / log behavior as
15
- * the inlined logic they replaced.
16
- */
17
- var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
18
- function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
19
- return new (P || (P = Promise))(function (resolve, reject) {
20
- function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
21
- function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
22
- function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
23
- step((generator = generator.apply(thisArg, _arguments || [])).next());
24
- });
25
- };
26
- import { appendChatStream, deleteMessage, postMessage, setThreadStatus, startChatStream, stopChatStream, updateMessage, } from "./api.js";
27
- import { SLACK_POST_UPDATE_MIN_INTERVAL_MS } from "./constants.js";
28
- import { computeStreamAppend } from "./stream.js";
29
- function createPostUpdateStrategy(deps) {
30
- const { botToken, getLog } = deps;
31
- return {
32
- inlineToolSteps: false,
33
- start(_a) {
34
- return __awaiter(this, arguments, void 0, function* ({ channel, channelId, threadTs, sessionId }) {
35
- const log = getLog();
36
- try {
37
- const posted = yield postMessage({
38
- botToken,
39
- channel: channelId,
40
- text: "…",
41
- threadTs,
42
- });
43
- if (posted.ts !== "") {
44
- channel.state.streamTs = posted.ts;
45
- channel.state.streamChannelId = channelId;
46
- channel.state.lastPostedText = "";
47
- log.info("postMessage skeleton ok", {
48
- channel: channelId,
49
- message_ts: posted.ts,
50
- session: sessionId,
51
- });
52
- }
53
- }
54
- catch (error) {
55
- log.warn("postMessage skeleton failed", {
56
- error: error instanceof Error ? error.message : String(error),
57
- channel: channelId,
58
- session: sessionId,
59
- });
60
- // Transient proxy/network failures must not permanently disable
61
- // live delivery — only the stream path sets streamingUnavailable
62
- // when Slack reports the feature unsupported.
63
- }
64
- });
65
- },
66
- append(_a) {
67
- return __awaiter(this, arguments, void 0, function* ({ channel, channelId, streamTs, text, sessionId }) {
68
- var _b;
69
- const now = Date.now();
70
- const isFirstUpdate = channel.state.lastPostedText === null ||
71
- channel.state.lastPostedText === "";
72
- const lastAt = (_b = channel.state.lastPostUpdateAtMs) !== null && _b !== void 0 ? _b : 0;
73
- const due = isFirstUpdate || now - lastAt >= SLACK_POST_UPDATE_MIN_INTERVAL_MS;
74
- if (!due) {
75
- // Skip intermediate update; message.completed flushes full text.
76
- return;
77
- }
78
- const result = yield updateMessage({
79
- botToken,
80
- channel: channelId,
81
- ts: streamTs,
82
- text,
83
- });
84
- if (!result.ok) {
85
- getLog().warn("chat.update failed", {
86
- error: result.error,
87
- channel: channelId,
88
- message_ts: streamTs,
89
- session: sessionId,
90
- });
91
- return;
92
- }
93
- channel.state.lastPostUpdateAtMs = now;
94
- channel.state.lastPostedText = text;
95
- });
96
- },
97
- finalize(_a) {
98
- return __awaiter(this, arguments, void 0, function* ({ channel, channelId, streamTs, text, sessionId }) {
99
- const log = getLog();
100
- const result = yield updateMessage({
101
- botToken,
102
- channel: channelId,
103
- ts: streamTs,
104
- text,
105
- });
106
- channel.state.streamTs = null;
107
- if (result.ok) {
108
- channel.state.lastPostedText = text;
109
- log.info("reply delivered via chat.update", {
110
- channel: channelId,
111
- message_ts: streamTs,
112
- session: sessionId,
113
- chars: text.length,
114
- });
115
- return { delivered: true };
116
- }
117
- log.warn("chat.update finalize failed; falling back to postMessage", {
118
- error: result.error,
119
- channel: channelId,
120
- message_ts: streamTs,
121
- session: sessionId,
122
- });
123
- // Only suppress the fallback when the skeleton already holds the exact
124
- // final text. Partial updates must not permanently truncate.
125
- if (channel.state.lastPostedText === text) {
126
- return { delivered: true };
127
- }
128
- // The skeleton still shows the placeholder or a stale partial and we're
129
- // about to post the full reply as a fresh message. Best-effort delete the
130
- // orphan so the thread doesn't show a duplicate.
131
- yield deleteMessage({ botToken, channel: channelId, ts: streamTs });
132
- return { delivered: false };
133
- });
134
- },
135
- finalizeEmpty(_a) {
136
- return __awaiter(this, arguments, void 0, function* ({ channelId, streamTs }) {
137
- yield updateMessage({
138
- botToken,
139
- channel: channelId,
140
- ts: streamTs,
141
- text: " ",
142
- });
143
- });
144
- },
145
- cleanupOrphan(_a) {
146
- return __awaiter(this, arguments, void 0, function* ({ channel }) {
147
- var _b;
148
- // A skeleton that was never finalized (e.g. an empty turn that produced
149
- // no message.completed) would otherwise linger as a "…" placeholder
150
- // forever. Best-effort delete it and clear typing.
151
- const orphanTs = channel.state.streamTs;
152
- if (orphanTs === null || orphanTs === "") {
153
- return;
154
- }
155
- const orphanChannelId = (_b = channel.state.streamChannelId) !== null && _b !== void 0 ? _b : channel.state.channelId;
156
- if (orphanChannelId !== null && orphanChannelId !== "") {
157
- yield deleteMessage({
158
- botToken,
159
- channel: orphanChannelId,
160
- ts: orphanTs,
161
- });
162
- }
163
- channel.state.streamTs = null;
164
- if (channel.state.channelId !== null && channel.state.threadTs !== null) {
165
- yield setThreadStatus({
166
- botToken,
167
- channelId: channel.state.channelId,
168
- threadTs: channel.state.threadTs,
169
- status: "",
170
- });
171
- }
172
- });
173
- },
174
- fail(_a) {
175
- return __awaiter(this, arguments, void 0, function* ({ channel, channelId, streamTs, errorText }) {
176
- const updated = yield updateMessage({
177
- botToken,
178
- channel: channelId,
179
- ts: streamTs,
180
- text: errorText,
181
- });
182
- channel.state.streamTs = null;
183
- return { delivered: updated.ok };
184
- });
185
- },
186
- };
187
- }
188
- function createStreamStrategy(deps) {
189
- const { botToken, getLog } = deps;
190
- return {
191
- inlineToolSteps: true,
192
- start(_a) {
193
- return __awaiter(this, arguments, void 0, function* ({ channel, channelId, threadTs, sessionId }) {
194
- const log = getLog();
195
- const started = yield startChatStream({
196
- botToken,
197
- channel: channelId,
198
- thread_ts: threadTs,
199
- markdown_text: "",
200
- });
201
- if (!started.ok) {
202
- log.warn("chat.startStream unavailable", {
203
- error: started.error,
204
- channel: channelId,
205
- thread_ts: threadTs,
206
- session: sessionId,
207
- });
208
- if (started.unsupported === true) {
209
- channel.state.streamingUnavailable = true;
210
- }
211
- return;
212
- }
213
- channel.state.streamTs = started.ts;
214
- channel.state.streamChannelId = started.channel;
215
- channel.state.lastPostedText = "";
216
- log.info("chat.startStream ok", {
217
- channel: started.channel,
218
- stream_ts: started.ts,
219
- session: sessionId,
220
- });
221
- });
222
- },
223
- append(_a) {
224
- return __awaiter(this, arguments, void 0, function* ({ channel, channelId, streamTs, text, sessionId }) {
225
- const appendText = computeStreamAppend(channel.state.lastPostedText, text);
226
- if (appendText === "") {
227
- return;
228
- }
229
- const result = yield appendChatStream({
230
- botToken,
231
- channel: channelId,
232
- ts: streamTs,
233
- markdown_text: appendText,
234
- });
235
- if (!result.ok) {
236
- getLog().warn("appendStream failed", {
237
- error: result.error,
238
- channel: channelId,
239
- stream_ts: streamTs,
240
- session: sessionId,
241
- });
242
- if (result.unsupported === true) {
243
- channel.state.streamingUnavailable = true;
244
- }
245
- return;
246
- }
247
- channel.state.lastPostedText = text;
248
- });
249
- },
250
- finalize(_a) {
251
- return __awaiter(this, arguments, void 0, function* ({ channel, channelId, streamTs, text, sessionId }) {
252
- const log = getLog();
253
- // Only send the unsent suffix — full text was already appended.
254
- const appendText = computeStreamAppend(channel.state.lastPostedText, text);
255
- const stopped = yield stopChatStream({
256
- botToken,
257
- channel: channelId,
258
- ts: streamTs,
259
- markdown_text: appendText === "" ? undefined : appendText,
260
- });
261
- if (stopped.ok) {
262
- channel.state.streamTs = null;
263
- channel.state.lastPostedText = text;
264
- log.info("reply delivered via stream", {
265
- channel: channelId,
266
- stream_ts: streamTs,
267
- session: sessionId,
268
- chars: text.length,
269
- });
270
- return { delivered: true };
271
- }
272
- log.warn("chat.stopStream failed; falling back to postMessage", {
273
- error: stopped.error,
274
- channel: channelId,
275
- stream_ts: streamTs,
276
- session: sessionId,
277
- });
278
- if (stopped.unsupported === true) {
279
- channel.state.streamingUnavailable = true;
280
- }
281
- channel.state.streamTs = null;
282
- // If we already streamed content, do not post the full reply again.
283
- const alreadyStreamed = typeof channel.state.lastPostedText === "string" &&
284
- channel.state.lastPostedText.length > 0;
285
- return { delivered: alreadyStreamed };
286
- });
287
- },
288
- finalizeEmpty(_a) {
289
- return __awaiter(this, arguments, void 0, function* ({ channelId, streamTs }) {
290
- yield stopChatStream({ botToken, channel: channelId, ts: streamTs });
291
- });
292
- },
293
- cleanupOrphan() {
294
- return __awaiter(this, void 0, void 0, function* () {
295
- // The stream path closes its message in message.completed / turn.failed;
296
- // there is nothing to clean up after turn.completed.
297
- });
298
- },
299
- fail(_a) {
300
- return __awaiter(this, arguments, void 0, function* ({ channel, channelId, streamTs, errorText }) {
301
- const stopped = yield stopChatStream({
302
- botToken,
303
- channel: channelId,
304
- ts: streamTs,
305
- markdown_text: errorText,
306
- });
307
- channel.state.streamTs = null;
308
- return { delivered: stopped.ok };
309
- });
310
- },
311
- };
312
- }
313
- /** Pick the live-reply delivery strategy for a `buildDefaultEvents` instance. */
314
- export function selectLiveDeliveryStrategy(deps, usePostUpdate) {
315
- return usePostUpdate
316
- ? createPostUpdateStrategy(deps)
317
- : createStreamStrategy(deps);
318
- }