@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.
- package/dist/bin/agent-serve.js +9 -0
- package/dist/docs/404.html +2 -2
- package/dist/docs/ab.html +3 -3
- package/dist/docs/assets/{app.Ciosf24M.js → app.CY2buaEE.js} +1 -1
- package/dist/docs/assets/chunks/@localSearchIndexroot.OfDZ5CXg.js +1 -0
- package/dist/docs/assets/chunks/{VPLocalSearchBox.BcRDly7n.js → VPLocalSearchBox.Ix954SFJ.js} +1 -1
- package/dist/docs/assets/chunks/{theme.CMpbjdfx.js → theme.PDwuJbyi.js} +2 -2
- package/dist/docs/assets/{reference_cli.md.D189RBCH.js → reference_cli.md.CjfiV2Q-.js} +2 -1
- package/dist/docs/assets/{reference_connections.md.zaEYCLHT.js → reference_connections.md.FbWupyzW.js} +1 -1
- package/dist/docs/building-with-agents.html +3 -3
- package/dist/docs/concepts.html +3 -3
- package/dist/docs/deployment.html +3 -3
- package/dist/docs/evals.html +3 -3
- package/dist/docs/example-agents/approval-buddy.html +3 -3
- package/dist/docs/example-agents/benny.html +3 -3
- package/dist/docs/example-agents/bugbot.html +3 -3
- package/dist/docs/example-agents/codebase-wiki.html +3 -3
- package/dist/docs/example-agents/codeowners-review.html +3 -3
- package/dist/docs/example-agents/concierge.html +3 -3
- package/dist/docs/example-agents/fsd.html +3 -3
- package/dist/docs/example-agents/index.html +3 -3
- package/dist/docs/example-agents/knowledge-base.html +3 -3
- package/dist/docs/example-agents/oncall.html +3 -3
- package/dist/docs/example-agents/security-reviewer.html +3 -3
- package/dist/docs/example-agents/slack-agent.html +3 -3
- package/dist/docs/example-agents/weather-agent.html +3 -3
- package/dist/docs/guides/agent-to-agent.html +3 -3
- package/dist/docs/guides/cloud-runtime.html +3 -3
- package/dist/docs/guides/github.html +3 -3
- package/dist/docs/guides/human-in-the-loop.html +3 -3
- package/dist/docs/guides/slack.html +3 -3
- package/dist/docs/guides/webhooks.html +3 -3
- package/dist/docs/hashmap.json +1 -1
- package/dist/docs/hillclimbing.html +3 -3
- package/dist/docs/index.html +3 -3
- package/dist/docs/quickstart.html +3 -3
- package/dist/docs/reference/agent-config.html +3 -3
- package/dist/docs/reference/channels.html +3 -3
- package/dist/docs/reference/cli.html +6 -5
- package/dist/docs/reference/connections.html +5 -5
- package/dist/docs/reference/hooks.html +3 -3
- package/dist/docs/reference/http-api.html +3 -3
- package/dist/docs/reference/instructions.html +3 -3
- package/dist/docs/reference/playground.html +3 -3
- package/dist/docs/reference/project-layout.html +3 -3
- package/dist/docs/reference/schedules.html +3 -3
- package/dist/docs/reference/sessions.html +3 -3
- package/dist/docs/reference/skills.html +3 -3
- package/dist/docs/reference/subagents.html +3 -3
- package/dist/docs/reference/tools.html +3 -3
- package/dist/docs/scaffolding-agents.html +3 -3
- package/dist/docs/storage.html +3 -3
- package/dist/docs/troubleshooting.html +3 -3
- package/dist/internal/cli-ax.d.ts.map +1 -1
- package/dist/internal/cli-ax.js +7 -4
- package/dist/internal/persistence-coordinator.d.ts +127 -0
- package/dist/internal/persistence-coordinator.d.ts.map +1 -0
- package/dist/internal/resolved-connections.d.ts +6 -0
- package/dist/internal/resolved-connections.d.ts.map +1 -1
- package/dist/internal/resolved-connections.js +6 -2
- package/dist/internal/server.d.ts.map +1 -1
- package/dist/internal/server.js +11 -1
- package/dist/internal/session-engine.d.ts +23 -1
- package/dist/internal/session-engine.d.ts.map +1 -1
- package/dist/internal/session-engine.js +71 -11
- package/dist/internal/sessions-client.d.ts +16 -2
- package/dist/internal/sessions-client.d.ts.map +1 -1
- package/dist/internal/sessions-client.js +28 -12
- package/dist/internal/storage-coordinator.d.ts +11 -0
- package/dist/internal/storage-coordinator.d.ts.map +1 -1
- package/dist/internal/storage-coordinator.js +30 -0
- package/dist/persistence.d.ts +184 -0
- package/dist/persistence.d.ts.map +1 -0
- package/dist/storage-backends/cursor-hosted.d.ts +10 -0
- package/dist/storage-backends/cursor-hosted.d.ts.map +1 -1
- package/dist/storage-backends/cursor-hosted.js +14 -0
- package/dist/types.d.ts +15 -0
- package/dist/types.d.ts.map +1 -1
- package/docs/reference/cli.md +2 -0
- package/docs/reference/connections.md +4 -2
- package/package.json +1 -1
- package/src/bin/agent-serve.ts +10 -0
- package/src/internal/cli-ax.ts +10 -4
- package/src/internal/resolved-connections.ts +14 -4
- package/src/internal/server.ts +15 -1
- package/src/internal/session-engine.ts +77 -2
- package/src/internal/sessions-client.ts +49 -14
- package/src/internal/storage-coordinator.ts +36 -0
- package/src/storage-backends/cursor-hosted.ts +21 -0
- package/src/types.ts +16 -0
- package/dist/channels/slack/post-update-delivery.js +0 -318
- package/dist/docs/assets/chunks/@localSearchIndexroot.D154Detj.js +0 -1
- package/dist/internal/json-dir-store.js +0 -100
- package/docs/.vitepress/cache/deps/package.json +0 -3
- /package/dist/docs/assets/{reference_cli.md.D189RBCH.lean.js → reference_cli.md.CjfiV2Q-.lean.js} +0 -0
- /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 {
|
|
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
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
|
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
|
-
}
|