@cursor/july 0.1.76 → 0.1.78
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/channels/origin/origin-webhook.d.ts +23 -2
- package/dist/channels/origin/origin-webhook.d.ts.map +1 -1
- package/dist/channels/origin/origin-webhook.js +82 -20
- package/dist/docs/404.html +2 -2
- package/dist/docs/ab.html +3 -3
- package/dist/docs/assets/{app.b8bhB2SV.js → app.BsA7t7rS.js} +1 -1
- package/dist/docs/assets/chunks/@localSearchIndexroot.Dcs6boxD.js +1 -0
- package/dist/docs/assets/chunks/{VPLocalSearchBox.lLSeIyWw.js → VPLocalSearchBox.B1hHhktM.js} +1 -1
- package/dist/docs/assets/chunks/{theme.C_owesa-.js → theme.DLpLSsQh.js} +2 -2
- package/dist/docs/assets/{storage.md.D8NQ389l.js → storage.md.ks1u64_R.js} +3 -3
- package/dist/docs/assets/storage.md.ks1u64_R.lean.js +1 -0
- 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/convert-automation.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/mcp-oauth.html +3 -3
- package/dist/docs/guides/opentelemetry.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/artifacts.html +3 -3
- package/dist/docs/reference/channels.html +3 -3
- package/dist/docs/reference/cli.html +3 -3
- package/dist/docs/reference/connections.html +3 -3
- 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/prompt.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 +7 -7
- package/dist/docs/templates/agentic-owners.html +3 -3
- package/dist/docs/templates/demo.html +3 -3
- package/dist/docs/templates/pr-autofixer.html +3 -3
- package/dist/docs/templates/security-reviewer.html +3 -3
- package/dist/docs/templates/triage.html +3 -3
- package/dist/docs/troubleshooting.html +3 -3
- package/dist/index.d.ts +0 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +0 -1
- package/dist/internal/discovery.d.ts.map +1 -1
- package/dist/internal/discovery.js +3 -21
- package/dist/internal/distribution.d.ts.map +1 -1
- package/dist/internal/distribution.js +0 -1
- package/dist/internal/session-engine.d.ts +1 -60
- package/dist/internal/session-engine.d.ts.map +1 -1
- package/dist/internal/session-engine.js +10 -109
- package/dist/kv/memory-kv.d.ts +1 -10
- package/dist/kv/memory-kv.d.ts.map +1 -1
- package/dist/kv/memory-kv.js +1 -10
- package/dist/kv.d.ts +2 -3
- package/dist/kv.d.ts.map +1 -1
- package/dist/kv.js +2 -3
- package/dist/playground/assets/{index-Bfwz2yLN.css → index-DJzcIeGt.css} +1 -1
- package/dist/playground/assets/{index-BWqjJcRo.js → index-K3Sxyx0I.js} +48 -45
- package/dist/playground/index.html +2 -2
- package/dist/storage-backends/cursor-hosted.js +3 -3
- package/dist/storage-protocol.d.ts +3 -8
- package/dist/storage-protocol.d.ts.map +1 -1
- package/dist/storage-protocol.js +4 -10
- package/dist/storage.d.ts +5 -11
- package/dist/storage.d.ts.map +1 -1
- package/dist/storage.js +3 -4
- package/dist/types.d.ts +5 -34
- package/dist/types.d.ts.map +1 -1
- package/docs/storage.md +11 -44
- package/package.json +1 -8
- package/src/channels/origin/origin-webhook.ts +111 -25
- package/src/index.ts +0 -6
- package/src/internal/discovery.ts +3 -27
- package/src/internal/distribution.ts +0 -1
- package/src/internal/session-engine.ts +1 -168
- package/src/kv/memory-kv.ts +1 -10
- package/src/kv.ts +2 -3
- package/src/storage-backends/cursor-hosted.ts +3 -3
- package/src/storage-protocol.ts +4 -10
- package/src/storage.ts +6 -12
- package/src/types.ts +5 -35
- package/dist/docs/assets/chunks/@localSearchIndexroot.DGZeXVqn.js +0 -1
- package/dist/docs/assets/storage.md.D8NQ389l.lean.js +0 -1
- package/dist/internal/durable-session-store.d.ts +0 -60
- package/dist/internal/durable-session-store.d.ts.map +0 -1
- package/dist/internal/durable-session-store.js +0 -131
- package/dist/internal/ledger-serving.d.ts +0 -169
- package/dist/internal/ledger-serving.d.ts.map +0 -1
- package/dist/internal/ledger-serving.js +0 -393
- package/dist/ledger/janitor.d.ts +0 -60
- package/dist/ledger/janitor.d.ts.map +0 -1
- package/dist/ledger/janitor.js +0 -137
- package/dist/ledger/session-ledger.d.ts +0 -293
- package/dist/ledger/session-ledger.d.ts.map +0 -1
- package/dist/ledger/session-ledger.js +0 -614
- package/dist/ledger.d.ts +0 -12
- package/dist/ledger.d.ts.map +0 -1
- package/dist/ledger.js +0 -9
- package/src/internal/durable-session-store.ts +0 -134
- package/src/internal/ledger-serving.ts +0 -531
- package/src/ledger/janitor.ts +0 -193
- package/src/ledger/session-ledger.ts +0 -839
- package/src/ledger.ts +0 -44
|
@@ -32,8 +32,6 @@ const V1ED_TIMESTAMP_TOLERANCE_SECONDS = 5 * 60;
|
|
|
32
32
|
*/
|
|
33
33
|
const MAX_V1ED_CANDIDATE_SIGNATURES = 1;
|
|
34
34
|
|
|
35
|
-
const JWKS_CACHE_TTL_MS = 5 * 60 * 1000;
|
|
36
|
-
|
|
37
35
|
export type OriginJwk = Record<string, unknown>;
|
|
38
36
|
|
|
39
37
|
export type OriginWebhookHeaders = {
|
|
@@ -131,41 +129,115 @@ export function verifyV1edWebhookPayload(args: {
|
|
|
131
129
|
return false;
|
|
132
130
|
}
|
|
133
131
|
|
|
132
|
+
function cacheControlDeltaMs(
|
|
133
|
+
cacheControl: string | null | undefined,
|
|
134
|
+
directive: "max-age" | "stale-if-error"
|
|
135
|
+
): number | undefined {
|
|
136
|
+
if (cacheControl == null || cacheControl === "") {
|
|
137
|
+
return undefined;
|
|
138
|
+
}
|
|
139
|
+
const match = new RegExp(`(?:^|,)\\s*${directive}\\s*=\\s*(\\d+)`, "i").exec(
|
|
140
|
+
cacheControl
|
|
141
|
+
);
|
|
142
|
+
if (match === null) {
|
|
143
|
+
return undefined;
|
|
144
|
+
}
|
|
145
|
+
return Number(match[1]) * 1000;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* RFC 9111 `max-age` (seconds → ms) from a Cache-Control header.
|
|
150
|
+
* Does not read `s-maxage`. Missing or unparsable → `undefined`.
|
|
151
|
+
*/
|
|
152
|
+
export function cacheControlMaxAgeMs(
|
|
153
|
+
cacheControl: string | null | undefined
|
|
154
|
+
): number | undefined {
|
|
155
|
+
return cacheControlDeltaMs(cacheControl, "max-age");
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* RFC 5861 `stale-if-error` (seconds → ms). Missing → `undefined` (no
|
|
160
|
+
* fail-open after `max-age`).
|
|
161
|
+
*/
|
|
162
|
+
export function cacheControlStaleIfErrorMs(
|
|
163
|
+
cacheControl: string | null | undefined
|
|
164
|
+
): number | undefined {
|
|
165
|
+
return cacheControlDeltaMs(cacheControl, "stale-if-error");
|
|
166
|
+
}
|
|
167
|
+
|
|
134
168
|
let jwksCache:
|
|
135
|
-
| {
|
|
169
|
+
| {
|
|
170
|
+
apiBaseUrl: string;
|
|
171
|
+
keys: OriginJwk[];
|
|
172
|
+
freshUntilMs: number;
|
|
173
|
+
staleUntilMs: number;
|
|
174
|
+
}
|
|
136
175
|
| undefined;
|
|
137
176
|
|
|
138
|
-
|
|
177
|
+
export type FetchOriginJwksOptions = {
|
|
178
|
+
/** Skip the in-memory hit and call `/v1/origin/keys` (used after a verify miss). */
|
|
179
|
+
force?: boolean;
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Return the in-memory Origin signing JWKS.
|
|
184
|
+
*
|
|
185
|
+
* Freshness is the JWKS response `Cache-Control: max-age`. After that, or
|
|
186
|
+
* when `force` is set, refetch. A failed refetch returns the last keys only
|
|
187
|
+
* while `stale-if-error` still applies; otherwise it throws (including a
|
|
188
|
+
* cold-cache miss).
|
|
189
|
+
*/
|
|
139
190
|
export async function fetchOriginJwks(
|
|
140
191
|
apiBaseUrl: string,
|
|
141
|
-
fetchImpl: typeof fetch = fetch
|
|
192
|
+
fetchImpl: typeof fetch = fetch,
|
|
193
|
+
options?: FetchOriginJwksOptions
|
|
142
194
|
): Promise<readonly OriginJwk[]> {
|
|
143
195
|
const now = Date.now();
|
|
144
196
|
if (
|
|
197
|
+
options?.force !== true &&
|
|
145
198
|
jwksCache !== undefined &&
|
|
146
199
|
jwksCache.apiBaseUrl === apiBaseUrl &&
|
|
147
|
-
now
|
|
200
|
+
now < jwksCache.freshUntilMs
|
|
148
201
|
) {
|
|
149
202
|
return jwksCache.keys;
|
|
150
203
|
}
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
204
|
+
try {
|
|
205
|
+
const response = await fetchImpl(
|
|
206
|
+
`${apiBaseUrl.replace(/\/+$/, "")}/v1/origin/keys`
|
|
207
|
+
);
|
|
208
|
+
if (!response.ok) {
|
|
209
|
+
throw new Error(`Origin JWKS fetch failed: status=${response.status}`);
|
|
210
|
+
}
|
|
211
|
+
const body: unknown = await response.json().catch(() => undefined);
|
|
212
|
+
const keys =
|
|
213
|
+
typeof body === "object" &&
|
|
214
|
+
body !== null &&
|
|
215
|
+
Array.isArray((body as Record<string, unknown>).keys)
|
|
216
|
+
? ((body as Record<string, unknown>).keys as OriginJwk[])
|
|
217
|
+
: [];
|
|
218
|
+
if (keys.length === 0) {
|
|
219
|
+
throw new Error("Origin JWKS fetch returned no keys");
|
|
220
|
+
}
|
|
221
|
+
const cacheControl = response.headers.get("cache-control");
|
|
222
|
+
const maxAgeMs = cacheControlMaxAgeMs(cacheControl) ?? 0;
|
|
223
|
+
const staleIfErrorMs = cacheControlStaleIfErrorMs(cacheControl) ?? 0;
|
|
224
|
+
jwksCache = {
|
|
225
|
+
apiBaseUrl,
|
|
226
|
+
keys,
|
|
227
|
+
freshUntilMs: now + maxAgeMs,
|
|
228
|
+
staleUntilMs: now + maxAgeMs + staleIfErrorMs,
|
|
229
|
+
};
|
|
230
|
+
return keys;
|
|
231
|
+
} catch (error) {
|
|
232
|
+
if (
|
|
233
|
+
jwksCache !== undefined &&
|
|
234
|
+
jwksCache.apiBaseUrl === apiBaseUrl &&
|
|
235
|
+
now < jwksCache.staleUntilMs
|
|
236
|
+
) {
|
|
237
|
+
return jwksCache.keys;
|
|
238
|
+
}
|
|
239
|
+
throw error;
|
|
166
240
|
}
|
|
167
|
-
jwksCache = { apiBaseUrl, keys, fetchedAtMs: now };
|
|
168
|
-
return keys;
|
|
169
241
|
}
|
|
170
242
|
|
|
171
243
|
export interface VerifyOriginWebhookOptions {
|
|
@@ -183,14 +255,28 @@ export async function verifyOriginWebhook(
|
|
|
183
255
|
options?: VerifyOriginWebhookOptions
|
|
184
256
|
): Promise<void> {
|
|
185
257
|
const apiBaseUrl = options?.apiBaseUrl ?? DEFAULT_ORIGIN_API_BASE_URL;
|
|
186
|
-
const
|
|
258
|
+
const fetchImpl = options?.fetchImpl ?? fetch;
|
|
259
|
+
const keys = await fetchOriginJwks(apiBaseUrl, fetchImpl);
|
|
187
260
|
if (
|
|
188
|
-
|
|
261
|
+
verifyV1edWebhookPayload({
|
|
189
262
|
publicJwks: keys,
|
|
190
263
|
body,
|
|
191
264
|
headers,
|
|
192
265
|
nowMs: options?.nowMs,
|
|
193
266
|
})
|
|
267
|
+
) {
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
const refreshed = await fetchOriginJwks(apiBaseUrl, fetchImpl, {
|
|
271
|
+
force: true,
|
|
272
|
+
});
|
|
273
|
+
if (
|
|
274
|
+
!verifyV1edWebhookPayload({
|
|
275
|
+
publicJwks: refreshed,
|
|
276
|
+
body,
|
|
277
|
+
headers,
|
|
278
|
+
nowMs: options?.nowMs,
|
|
279
|
+
})
|
|
194
280
|
) {
|
|
195
281
|
throw new Error("Origin webhook signature verification failed");
|
|
196
282
|
}
|
package/src/index.ts
CHANGED
|
@@ -96,12 +96,6 @@ export {
|
|
|
96
96
|
UnknownToolError,
|
|
97
97
|
} from "./internal/session-engine.js";
|
|
98
98
|
export { createMemoryKv, memoryCasTable } from "./kv/memory-kv.js";
|
|
99
|
-
export {
|
|
100
|
-
LedgerCasExhaustedError,
|
|
101
|
-
LedgerFencedError,
|
|
102
|
-
SessionLedger,
|
|
103
|
-
supportsSessionLedgerKv,
|
|
104
|
-
} from "./ledger.js";
|
|
105
99
|
export type { MemoryBackend, TurnMemoryRecord } from "./memory.js";
|
|
106
100
|
export {
|
|
107
101
|
agentStoreMemoryBackend,
|
|
@@ -29,7 +29,6 @@ import {
|
|
|
29
29
|
type AgentProject,
|
|
30
30
|
type AgentProjectInfo,
|
|
31
31
|
type AgentRuntime,
|
|
32
|
-
type AgentServingMode,
|
|
33
32
|
type ArtifactsDefinition,
|
|
34
33
|
type BuiltinToolsConfig,
|
|
35
34
|
type ChannelDefinition,
|
|
@@ -194,9 +193,9 @@ export async function loadAgentProjects(
|
|
|
194
193
|
);
|
|
195
194
|
}
|
|
196
195
|
const results: Array<{ slug: string; project: AgentProject }> = [];
|
|
197
|
-
// Storage namespaces (session mirror, host kv
|
|
198
|
-
//
|
|
199
|
-
//
|
|
196
|
+
// Storage namespaces (session mirror, host kv) key on the project
|
|
197
|
+
// NAME, not the mount slug — two co-hosted agents sharing a name would
|
|
198
|
+
// silently share sessions and KV state.
|
|
200
199
|
const slugByName = new Map<string, string>();
|
|
201
200
|
for (const child of children) {
|
|
202
201
|
const project = await loadAgentProject(child.dir, options);
|
|
@@ -506,7 +505,6 @@ async function loadAgent(
|
|
|
506
505
|
};
|
|
507
506
|
const hosting = resolveHosting(ctx, agentDir, config?.hosting);
|
|
508
507
|
const concurrency = resolveConcurrency(ctx, agentDir, config?.concurrency);
|
|
509
|
-
const serving = resolveServing(ctx, agentDir, config?.serving);
|
|
510
508
|
|
|
511
509
|
if (runtime === "cloud") {
|
|
512
510
|
warnCloudRuntimeLimitations(ctx, agentDir, {
|
|
@@ -531,7 +529,6 @@ async function loadAgent(
|
|
|
531
529
|
...(allowedTools === undefined ? {} : { allowedTools }),
|
|
532
530
|
hosting,
|
|
533
531
|
concurrency,
|
|
534
|
-
serving,
|
|
535
532
|
tools,
|
|
536
533
|
skills,
|
|
537
534
|
connections,
|
|
@@ -540,27 +537,6 @@ async function loadAgent(
|
|
|
540
537
|
};
|
|
541
538
|
}
|
|
542
539
|
|
|
543
|
-
/**
|
|
544
|
-
* Validate the authored serving mode. An unrecognized value is an error
|
|
545
|
-
* diagnostic and falls back to undefined (resident) — a typo must not
|
|
546
|
-
* silently opt an agent into (or out of) ledger coordination.
|
|
547
|
-
*/
|
|
548
|
-
function resolveServing(
|
|
549
|
-
ctx: DiscoveryContext,
|
|
550
|
-
agentDir: string,
|
|
551
|
-
serving: AgentServingMode | undefined
|
|
552
|
-
): AgentServingMode | undefined {
|
|
553
|
-
if (serving === undefined || serving === "resident" || serving === "ledger") {
|
|
554
|
-
return serving;
|
|
555
|
-
}
|
|
556
|
-
ctx.diagnostics.push({
|
|
557
|
-
severity: "error",
|
|
558
|
-
path: displayPath(ctx, join(agentDir, "agent.ts")),
|
|
559
|
-
message: `serving must be "resident" or "ledger"; got ${JSON.stringify(serving)}.`,
|
|
560
|
-
});
|
|
561
|
-
return undefined;
|
|
562
|
-
}
|
|
563
|
-
|
|
564
540
|
/** Absolute harness cwd parent, or undefined when unset / blank. */
|
|
565
541
|
function resolveAgentCwd(
|
|
566
542
|
ctx: DiscoveryContext,
|
|
@@ -112,7 +112,6 @@ export const AUTHORING_ENTRY_FILES: ReadonlyArray<
|
|
|
112
112
|
["artifacts", "artifacts.js"],
|
|
113
113
|
["kv", "kv.js"],
|
|
114
114
|
["kv/memory", "kv/memory-kv.js"],
|
|
115
|
-
["ledger", "ledger.js"],
|
|
116
115
|
["storage", "storage.js"],
|
|
117
116
|
["storage/file-kv", "storage-backends/file-kv.js"],
|
|
118
117
|
["storage/cursor-hosted", "storage-backends/cursor-hosted.js"],
|
|
@@ -25,12 +25,6 @@ import {
|
|
|
25
25
|
} from "../channels/slack/nudge-store.js";
|
|
26
26
|
import { resolveDedicatedSlackHostBinding } from "../channels/slack/slack-channel.js";
|
|
27
27
|
import { isCursorHostedFilesAvailable } from "../files-backends/cursor-hosted.js";
|
|
28
|
-
import {
|
|
29
|
-
SessionLedger,
|
|
30
|
-
type SessionLedgerKv,
|
|
31
|
-
supportsSessionLedgerKv,
|
|
32
|
-
} from "../ledger/session-ledger.js";
|
|
33
|
-
import { fileKv } from "../storage-backends/file-kv.js";
|
|
34
28
|
import {
|
|
35
29
|
type AgentProject,
|
|
36
30
|
type ApprovalDecision,
|
|
@@ -48,7 +42,6 @@ import {
|
|
|
48
42
|
DIRECT_TOOL_CALL_CHANNEL_ID,
|
|
49
43
|
type HookHandler,
|
|
50
44
|
type HostContext,
|
|
51
|
-
type HostKvApi,
|
|
52
45
|
type HttpChannelDefinition,
|
|
53
46
|
isHostOauthConnectionTransport,
|
|
54
47
|
isSymbolicConnectionTransport,
|
|
@@ -88,11 +81,6 @@ import {
|
|
|
88
81
|
mapSdkTokenUsage,
|
|
89
82
|
} from "./cloud-turn-cost.js";
|
|
90
83
|
import { isDevMode } from "./dev-mode.js";
|
|
91
|
-
import {
|
|
92
|
-
ledgerKeyPrefix,
|
|
93
|
-
namespacedKv,
|
|
94
|
-
storageCasKv,
|
|
95
|
-
} from "./durable-session-store.js";
|
|
96
84
|
import { type EventLogMemoryStats, EventLogStore } from "./event-log.js";
|
|
97
85
|
import { TurnEventMapper } from "./event-mapper.js";
|
|
98
86
|
import { selectFrameworkStorageDefinition } from "./framework-storage-selection.js";
|
|
@@ -111,7 +99,6 @@ import {
|
|
|
111
99
|
newToolCallId,
|
|
112
100
|
} from "./ids.js";
|
|
113
101
|
import { validateToolInput } from "./json-schema.js";
|
|
114
|
-
import { LedgerServing, newLedgerPodId } from "./ledger-serving.js";
|
|
115
102
|
import { McpHost } from "./mcp-host.js";
|
|
116
103
|
import type { OtelCollector } from "./otel/collector.js";
|
|
117
104
|
import {
|
|
@@ -158,7 +145,7 @@ export class SessionBusyError extends Error {
|
|
|
158
145
|
}
|
|
159
146
|
}
|
|
160
147
|
|
|
161
|
-
// Defined in auth.js (
|
|
148
|
+
// Defined in auth.js (HTTP/session-store callers import it without this
|
|
162
149
|
// module); re-exported here because this is its historical public home.
|
|
163
150
|
export { SessionAuthError } from "./auth.js";
|
|
164
151
|
|
|
@@ -235,22 +222,6 @@ export interface SessionEngineOptions {
|
|
|
235
222
|
* stub; production uses billed usage events when an agent id is known.
|
|
236
223
|
*/
|
|
237
224
|
estimateTurnCost?: EstimateTurnCostFn;
|
|
238
|
-
/**
|
|
239
|
-
* Explicit durable-session store for `serving: "ledger"` agents — a KV
|
|
240
|
-
* dedicated to the ledger (session identity, leases, commit heads),
|
|
241
|
-
* separate from agent state. Must implement the full CAS-capable KV
|
|
242
|
-
* surface (`HostKvCasApi`: the CAS trio plus `get`/`put`/`delete` for
|
|
243
|
-
* mailbox payload blobs and `listKeys` for the janitor scan);
|
|
244
|
-
* construction fails fast otherwise. Supply the **raw** store: the
|
|
245
|
-
* engine owns key scoping and namespaces every ledger key under
|
|
246
|
-
* `agentkit/v1/<agent>/durable-session/` itself — a self-hoster never
|
|
247
|
-
* writes prefixes. Default when omitted: the authored storage backend
|
|
248
|
-
* (`agent/storage.ts`) when it implements the `cas` group, else — with
|
|
249
|
-
* no authored storage — a file store under `<stateRoot>/ledger-kv`
|
|
250
|
-
* (single-process correctness only). An authored storage backend
|
|
251
|
-
* *without* `cas` fails fast. Ignored for resident-mode agents.
|
|
252
|
-
*/
|
|
253
|
-
durableSessionStore?: HostKvApi;
|
|
254
225
|
/** Process-level OTel collector; observe-only, never fatal. */
|
|
255
226
|
otel?: OtelCollector;
|
|
256
227
|
/** Process-level meter for {@link HostContext.otel}. Absent → no-op counters. */
|
|
@@ -290,14 +261,6 @@ interface ActiveToolContext {
|
|
|
290
261
|
|
|
291
262
|
export interface EngineSendOptions extends SendMessageOptions {
|
|
292
263
|
mode?: "chat" | "task";
|
|
293
|
-
/**
|
|
294
|
-
* Internal (ledger serving): set only by the ledger pump when it
|
|
295
|
-
* executes an already-parked event under its lease, so the call must
|
|
296
|
-
* run the resident path. Without the flag, every send on a
|
|
297
|
-
* `serving: "ledger"` agent routes through the ledger (append → claim
|
|
298
|
-
* → drain → turn → commit) and the pump would recurse into itself.
|
|
299
|
-
*/
|
|
300
|
-
fromLedgerPump?: boolean;
|
|
301
264
|
}
|
|
302
265
|
|
|
303
266
|
interface CoalescedQueuedTurn {
|
|
@@ -400,12 +363,6 @@ export class SessionEngine {
|
|
|
400
363
|
private readonly cloudCostBackfillDone = new Set<string>();
|
|
401
364
|
/** Deduped in-flight cloud cost backfills, keyed by session id. */
|
|
402
365
|
private readonly cloudCostBackfill = new Map<string, Promise<void>>();
|
|
403
|
-
/**
|
|
404
|
-
* Ledger-mode delivery seam; set only when the agent declares
|
|
405
|
-
* `serving: "ledger"`. When present, {@link send} routes every
|
|
406
|
-
* non-delivery call through the ledger's claim → turn → commit.
|
|
407
|
-
*/
|
|
408
|
-
private readonly ledgerServing: LedgerServing | undefined;
|
|
409
366
|
|
|
410
367
|
constructor(options: SessionEngineOptions) {
|
|
411
368
|
this.project = options.project;
|
|
@@ -499,8 +456,6 @@ export class SessionEngine {
|
|
|
499
456
|
for (const channel of options.project.channels) {
|
|
500
457
|
this.channelsById.set(channel.id, channel.definition);
|
|
501
458
|
}
|
|
502
|
-
this.ledgerServing = this.buildLedgerServing(options);
|
|
503
|
-
this.ledgerServing?.start();
|
|
504
459
|
this.stopMemoryTelemetry = shouldStartProcessMemoryTelemetry()
|
|
505
460
|
? startProcessMemoryTelemetry({
|
|
506
461
|
agentName: options.project.name,
|
|
@@ -511,113 +466,6 @@ export class SessionEngine {
|
|
|
511
466
|
: undefined;
|
|
512
467
|
}
|
|
513
468
|
|
|
514
|
-
/**
|
|
515
|
-
* Ledger-mode wiring (`serving: "ledger"`): resolve a CAS-capable
|
|
516
|
-
* durable-session store and build the event pump. Resident agents
|
|
517
|
-
* (the default) return undefined and never touch ledger keys.
|
|
518
|
-
*/
|
|
519
|
-
private buildLedgerServing(
|
|
520
|
-
options: SessionEngineOptions
|
|
521
|
-
): LedgerServing | undefined {
|
|
522
|
-
if (options.project.agent.serving !== "ledger") {
|
|
523
|
-
return undefined;
|
|
524
|
-
}
|
|
525
|
-
// The handle is already agent-scoped (resolveDurableSessionStore):
|
|
526
|
-
// two ledger-mode agents sharing one backend never resolve to each
|
|
527
|
-
// other's sessions or janitor-claim each other's parked envelopes.
|
|
528
|
-
const ledgerKv = this.resolveDurableSessionStore(options);
|
|
529
|
-
return new LedgerServing({
|
|
530
|
-
ledger: new SessionLedger({ kv: ledgerKv, podId: newLedgerPodId() }),
|
|
531
|
-
deliverTurn: (envelope) =>
|
|
532
|
-
this.send(envelope.channelId, envelope.message, {
|
|
533
|
-
continuationToken: envelope.continuationToken,
|
|
534
|
-
auth: envelope.auth,
|
|
535
|
-
...(envelope.title === undefined ? {} : { title: envelope.title }),
|
|
536
|
-
...(envelope.state === undefined ? {} : { state: envelope.state }),
|
|
537
|
-
...(envelope.mode === undefined ? {} : { mode: envelope.mode }),
|
|
538
|
-
...(envelope.purpose === undefined
|
|
539
|
-
? {}
|
|
540
|
-
: { purpose: envelope.purpose }),
|
|
541
|
-
// Isolation overrides ride the envelope: without them the
|
|
542
|
-
// drained turn would run local with the default harness cwd
|
|
543
|
-
// instead of the cloud VM / channel worktree the sender bound.
|
|
544
|
-
...(envelope.workspaceDir === undefined
|
|
545
|
-
? {}
|
|
546
|
-
: { workspaceDir: envelope.workspaceDir }),
|
|
547
|
-
...(envelope.cloud === undefined ? {} : { cloud: envelope.cloud }),
|
|
548
|
-
fromLedgerPump: true,
|
|
549
|
-
}),
|
|
550
|
-
findSession: async (channelId, continuationToken) => {
|
|
551
|
-
let record = await this.sessions.findByContinuation(
|
|
552
|
-
channelId,
|
|
553
|
-
continuationToken
|
|
554
|
-
);
|
|
555
|
-
if (record === undefined) {
|
|
556
|
-
record = await this.restoreSessionByContinuation(
|
|
557
|
-
channelId,
|
|
558
|
-
continuationToken
|
|
559
|
-
);
|
|
560
|
-
}
|
|
561
|
-
return record === undefined
|
|
562
|
-
? undefined
|
|
563
|
-
: this.channelSession(record.sessionId, record, false);
|
|
564
|
-
},
|
|
565
|
-
logger: this.logger,
|
|
566
|
-
});
|
|
567
|
-
}
|
|
568
|
-
|
|
569
|
-
/**
|
|
570
|
-
* Pick the KV the ledger coordinates on, in precedence order:
|
|
571
|
-
*
|
|
572
|
-
* 1. `options.durableSessionStore` — an explicit store separate from
|
|
573
|
-
* agent state (advanced deployments, tests).
|
|
574
|
-
* 2. The authored storage backend (`agent/storage.ts`) when it
|
|
575
|
-
* implements the `cas` group — one backend serves both the storage
|
|
576
|
-
* API and the durable session API.
|
|
577
|
-
* 3. No authored storage: a file store under `<stateRoot>/ledger-kv`
|
|
578
|
-
* (same place resident state lives; single-process only).
|
|
579
|
-
*
|
|
580
|
-
* An authored backend without `cas` fails fast: silently falling back
|
|
581
|
-
* to local files would fork coordination away from the deployment's
|
|
582
|
-
* durable store. The check demands the full surface the inbox uses —
|
|
583
|
-
* the CAS trio plus `get`/`put`/`delete` for mailbox payload blobs and
|
|
584
|
-
* `listKeys` for the janitor scan and the orphan sweep.
|
|
585
|
-
*
|
|
586
|
-
* Whatever store wins, the returned handle is scoped here — once —
|
|
587
|
-
* under `agentkit/v1/<agent>/durable-session/`. The ledger speaks bare
|
|
588
|
-
* `session/…` / `ledger/…` / `mailbox/…` keys; namespacing is this
|
|
589
|
-
* boundary's job, exactly like `host.kv`'s agent scoping.
|
|
590
|
-
*/
|
|
591
|
-
private resolveDurableSessionStore(
|
|
592
|
-
options: SessionEngineOptions
|
|
593
|
-
): SessionLedgerKv {
|
|
594
|
-
const namespace = ledgerKeyPrefix(options.project.name);
|
|
595
|
-
const explicit = options.durableSessionStore;
|
|
596
|
-
if (explicit !== undefined) {
|
|
597
|
-
if (!supportsSessionLedgerKv(explicit)) {
|
|
598
|
-
throw new Error(
|
|
599
|
-
`Agent "${options.project.name}" declares serving: "ledger", but the configured durableSessionStore is not CAS-capable (getWithVersion / putIfAbsent / putIfVersion / listKeys). Provide a store implementing HostKvCasApi (see @cursor/july/kv).`
|
|
600
|
-
);
|
|
601
|
-
}
|
|
602
|
-
return namespacedKv(explicit, namespace);
|
|
603
|
-
}
|
|
604
|
-
const casArgs = {
|
|
605
|
-
agentName: options.project.name,
|
|
606
|
-
projectRoot: options.project.rootDir,
|
|
607
|
-
};
|
|
608
|
-
const authored = options.project.storage;
|
|
609
|
-
if (authored !== undefined) {
|
|
610
|
-
if (authored.cas === undefined) {
|
|
611
|
-
throw new Error(
|
|
612
|
-
`Agent "${options.project.name}" declares serving: "ledger", which needs conditional writes, but the storage backend in agent/storage.ts does not implement the cas group. Add cas: {...} to defineStorage (fileKv and cursorHostedStorage already include it), or pass an explicit durableSessionStore.`
|
|
613
|
-
);
|
|
614
|
-
}
|
|
615
|
-
return namespacedKv(storageCasKv(authored, casArgs), namespace);
|
|
616
|
-
}
|
|
617
|
-
const fallback = fileKv({ dir: join(options.stateRoot, "ledger-kv") });
|
|
618
|
-
return namespacedKv(storageCasKv(fallback, casArgs), namespace);
|
|
619
|
-
}
|
|
620
|
-
|
|
621
469
|
/**
|
|
622
470
|
* Attach serve-time resolved connections (peers, Cursor account bridge).
|
|
623
471
|
* They join the per-turn SDK `mcpServers` map (loopback URL for local
|
|
@@ -892,12 +740,6 @@ export class SessionEngine {
|
|
|
892
740
|
message: string,
|
|
893
741
|
options: EngineSendOptions = {}
|
|
894
742
|
): Promise<ChannelSession> {
|
|
895
|
-
// Ledger-mode agents park the event durably and coordinate through
|
|
896
|
-
// the session lease before any turn runs; only the pump's own
|
|
897
|
-
// lease-holding deliveries fall through to the resident path.
|
|
898
|
-
if (this.ledgerServing !== undefined && options.fromLedgerPump !== true) {
|
|
899
|
-
return this.ledgerServing.deliver(channelId, message, options);
|
|
900
|
-
}
|
|
901
743
|
const auth = options.auth ?? null;
|
|
902
744
|
let existing =
|
|
903
745
|
options.continuationToken === undefined
|
|
@@ -1008,13 +850,6 @@ export class SessionEngine {
|
|
|
1008
850
|
};
|
|
1009
851
|
await this.sessions.save(record);
|
|
1010
852
|
this.storage?.sessionRecord(record);
|
|
1011
|
-
// Ledger serving: another pod can follow up mid-turn, so the session
|
|
1012
|
-
// identity (record + continuation index) must hit the shared store
|
|
1013
|
-
// before we return a session id. Default turnEnd flush is too late.
|
|
1014
|
-
if (this.ledgerServing !== undefined && this.storage !== undefined) {
|
|
1015
|
-
this.storage.flushSession(record.sessionId);
|
|
1016
|
-
await this.storage.whenIdle();
|
|
1017
|
-
}
|
|
1018
853
|
await this.appendEvent(record.sessionId, {
|
|
1019
854
|
type: "session.started",
|
|
1020
855
|
data: { channelId },
|
|
@@ -3841,8 +3676,6 @@ export class SessionEngine {
|
|
|
3841
3676
|
async close(): Promise<void> {
|
|
3842
3677
|
this.stopMemoryTelemetry?.();
|
|
3843
3678
|
this.approvals.cancelAll("deny");
|
|
3844
|
-
// Ledger deliveries spawn turns, so settle them before activeTurns.
|
|
3845
|
-
await this.ledgerServing?.close();
|
|
3846
3679
|
await Promise.allSettled([...this.activeTurns.values()]);
|
|
3847
3680
|
await Promise.allSettled([...this.backgroundWork]);
|
|
3848
3681
|
await Promise.allSettled([...this.dispatchChains.values()]);
|
package/src/kv/memory-kv.ts
CHANGED
|
@@ -4,8 +4,7 @@
|
|
|
4
4
|
* `@cursor/july/kv/memory`.
|
|
5
5
|
*
|
|
6
6
|
* The memory KV *is* the dev shim: tests and local runs exercise the
|
|
7
|
-
*
|
|
8
|
-
* RPC. Values are deep-copied on both write and
|
|
7
|
+
* CAS contract, not a mock of an RPC. Values are deep-copied on both write and
|
|
9
8
|
* read so callers can never observe (or cause) shared-reference mutation.
|
|
10
9
|
*/
|
|
11
10
|
|
|
@@ -86,14 +85,6 @@ export function createMemoryKv(): HostKvCasApi {
|
|
|
86
85
|
* fixtures, an off-hosting no-op sink). Non-durable and process-local by
|
|
87
86
|
* construction; a real deployment backend should implement CAS against
|
|
88
87
|
* its own store instead.
|
|
89
|
-
*
|
|
90
|
-
* **Never pair this with `serving: "ledger"` on a real backend.** The
|
|
91
|
-
* ledger derives its coordination store from the whole storage config,
|
|
92
|
-
* so a durable plain backend with a memory `cas` group silently voids
|
|
93
|
-
* ack-after-durable: leases, heads, and pending indexes live only in
|
|
94
|
-
* this process and vanish on restart while payload blobs go through the
|
|
95
|
-
* plain methods. Ledger state must live in the same durable store as
|
|
96
|
-
* the blobs.
|
|
97
88
|
*/
|
|
98
89
|
export function memoryCasTable(): StorageCasTable {
|
|
99
90
|
const kv = createMemoryKv();
|
package/src/kv.ts
CHANGED
|
@@ -2,9 +2,8 @@
|
|
|
2
2
|
* The CAS-capable KV **interface** — `@cursor/july/kv`.
|
|
3
3
|
*
|
|
4
4
|
* This module is implementation-free on purpose: it is the contract a
|
|
5
|
-
* bring-your-own KV implements so
|
|
6
|
-
*
|
|
7
|
-
* it. {@link HostKvCasApi} is the conditional-write superset of the plain
|
|
5
|
+
* bring-your-own KV implements so conditional writes can run on it.
|
|
6
|
+
* {@link HostKvCasApi} is the conditional-write superset of the plain
|
|
8
7
|
* {@link HostKvApi}; {@link supportsKvCas} is the runtime probe for
|
|
9
8
|
* stores of unknown provenance — a `defineStorage` backend declares CAS
|
|
10
9
|
* through its `cas` group instead, so it never needs probing.
|
|
@@ -313,9 +313,9 @@ export function cursorHostedStorage(
|
|
|
313
313
|
{ prefix }
|
|
314
314
|
);
|
|
315
315
|
if (result.truncated === true) {
|
|
316
|
-
// Callers
|
|
317
|
-
//
|
|
318
|
-
//
|
|
316
|
+
// Callers treat the result as the complete key set; a silently
|
|
317
|
+
// capped scan hides missing keys. Fail loudly until pagination
|
|
318
|
+
// lands.
|
|
319
319
|
throw new Error(
|
|
320
320
|
`cursorHostedStorage: listKeys("${prefix}") exceeded the server's row cap and was truncated; the result is incomplete`
|
|
321
321
|
);
|
package/src/storage-protocol.ts
CHANGED
|
@@ -4,9 +4,7 @@
|
|
|
4
4
|
* `backend/server/src/agentServe/durableSessionProtocol.ts`) so a dashboard
|
|
5
5
|
* list can read Bugbot KV without importing `@cursor/july`.
|
|
6
6
|
*
|
|
7
|
-
* Additive JSON only — unknown fields are ignored.
|
|
8
|
-
* (`durable-session/`) types here; those are coordination-only and must not
|
|
9
|
-
* appear in the CP session list.
|
|
7
|
+
* Additive JSON only — unknown fields are ignored.
|
|
10
8
|
*/
|
|
11
9
|
|
|
12
10
|
import { createHash } from "node:crypto";
|
|
@@ -19,8 +17,7 @@ export const STORAGE_KEY_ROOT = "agentkit/v1" as const;
|
|
|
19
17
|
|
|
20
18
|
/**
|
|
21
19
|
* Exact session-record keys: `agentkit/v1/{agent}/session/{sessionId}`.
|
|
22
|
-
* Rejects `session-events
|
|
23
|
-
* agent name.
|
|
20
|
+
* Rejects `session-events/` and the CP-owned `_cp` agent name.
|
|
24
21
|
*/
|
|
25
22
|
const DURABLE_SESSION_RECORD_KEY_RE =
|
|
26
23
|
/^agentkit\/v1\/([^/]+)\/session\/([^/]+)$/;
|
|
@@ -109,9 +106,6 @@ export const MAX_KEY_SEGMENT_BYTES = 256;
|
|
|
109
106
|
* digest of the raw value when the encoding exceeds
|
|
110
107
|
* {@link MAX_KEY_SEGMENT_BYTES}. Deterministic, so key construction on the
|
|
111
108
|
* write path and the continuation-lookup path always agree.
|
|
112
|
-
*
|
|
113
|
-
* Exported for the session ledger, which builds its `session/` and
|
|
114
|
-
* `ledger/` keys with the same encoding rules.
|
|
115
109
|
*/
|
|
116
110
|
export function keySegment(raw: string): string {
|
|
117
111
|
const encoded = encodeURIComponent(raw);
|
|
@@ -163,8 +157,8 @@ function stringValue(value: unknown): string | undefined {
|
|
|
163
157
|
}
|
|
164
158
|
|
|
165
159
|
/**
|
|
166
|
-
* Tolerant `SessionRecord` decoder. Extra fields (`auth`, `purpose`,
|
|
167
|
-
*
|
|
160
|
+
* Tolerant `SessionRecord` decoder. Extra fields (`auth`, `purpose`,
|
|
161
|
+
* future keys) are ignored. Returns undefined when required list
|
|
168
162
|
* fields are missing or the wrong type.
|
|
169
163
|
*/
|
|
170
164
|
export function parseDurableSessionRecord(
|
package/src/storage.ts
CHANGED
|
@@ -7,9 +7,8 @@
|
|
|
7
7
|
* only, and eval/A/B history stays in process memory.
|
|
8
8
|
*
|
|
9
9
|
* The sink is a plain key-value store — put/get/delete/list, no schema —
|
|
10
|
-
* plus a conditional-write group ({@link StorageConfig.cas}
|
|
11
|
-
*
|
|
12
|
-
* coordination features). Implementing that small KV core is enough for
|
|
10
|
+
* plus a conditional-write group ({@link StorageConfig.cas}; optional).
|
|
11
|
+
* Implementing that small KV core is enough for
|
|
13
12
|
* FULL functionality: eval-run and A/B history are derived over the core
|
|
14
13
|
* when the optional dedicated tables ({@link StorageConfig.evals} and
|
|
15
14
|
* {@link StorageConfig.abs}) are not authored — those groups exist as
|
|
@@ -178,10 +177,7 @@ export interface StorageABTable {
|
|
|
178
177
|
/**
|
|
179
178
|
* Conditional writes (CAS) — the coordination tier of the storage backend
|
|
180
179
|
* contract. Plain storage works without it, so existing backends keep
|
|
181
|
-
* working across a platform upgrade
|
|
182
|
-
* session ledger — see `@cursor/july/kv` for the backend-agnostic
|
|
183
|
-
* interface) requires it and fails at startup, with an actionable
|
|
184
|
-
* message, when the backend lacks it. The built-in backends all include
|
|
180
|
+
* working across a platform upgrade. The built-in backends all include
|
|
185
181
|
* it, so implementing this group yourself only comes up for BYO storage.
|
|
186
182
|
*
|
|
187
183
|
* Grouped under {@link StorageConfig.cas} rather than as loose methods —
|
|
@@ -243,10 +239,8 @@ export interface StorageConfig {
|
|
|
243
239
|
abs?: StorageABTable;
|
|
244
240
|
/**
|
|
245
241
|
* Conditional writes (CAS). Optional for plain storage — existing
|
|
246
|
-
* backends keep working without it
|
|
247
|
-
*
|
|
248
|
-
* The built-in backends (`fileKv`, `cursorHostedStorage`) both include
|
|
249
|
-
* it. See {@link StorageCasTable}.
|
|
242
|
+
* backends keep working without it. The built-in backends (`fileKv`,
|
|
243
|
+
* `cursorHostedStorage`) both include it. See {@link StorageCasTable}.
|
|
250
244
|
*/
|
|
251
245
|
cas?: StorageCasTable;
|
|
252
246
|
/**
|
|
@@ -321,7 +315,7 @@ export function defineStorage(config: StorageConfig): StorageDefinition {
|
|
|
321
315
|
] as const;
|
|
322
316
|
const cas = config.cas;
|
|
323
317
|
// A partial group is a bug, not a legacy config: half-implemented CAS
|
|
324
|
-
// would pass the capability probe
|
|
318
|
+
// would pass the capability probe.
|
|
325
319
|
const missingCasHooks = casHooks.filter(
|
|
326
320
|
(hook) => typeof cas[hook] !== "function"
|
|
327
321
|
);
|