@cursor/july 0.1.45 → 0.1.47
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/AGENTS.md +4 -2
- package/README.md +3 -1
- package/dist/channels/github/api.d.ts +25 -0
- package/dist/channels/github/api.d.ts.map +1 -1
- package/dist/channels/github/api.js +48 -0
- package/dist/channels/github/cursor-account.d.ts +7 -0
- package/dist/channels/github/cursor-account.d.ts.map +1 -1
- package/dist/channels/github/cursor-account.js +18 -0
- package/dist/channels/github/defaults.d.ts +1 -1
- package/dist/channels/github/defaults.js +1 -1
- package/dist/channels/github/github-channel.d.ts.map +1 -1
- package/dist/channels/github/github-channel.js +2 -2
- package/dist/channels/github/progress.d.ts +6 -5
- package/dist/channels/github/progress.d.ts.map +1 -1
- package/dist/channels/github/progress.js +58 -16
- package/dist/channels/github/state.d.ts +11 -0
- package/dist/channels/github/state.d.ts.map +1 -1
- package/dist/channels/github/state.js +1 -1
- package/dist/channels/github/types.d.ts +3 -3
- package/dist/docs/404.html +2 -2
- package/dist/docs/ab.html +3 -3
- package/dist/docs/assets/{app.fHnNXEJ6.js → app.IEXEim5m.js} +1 -1
- package/dist/docs/assets/chunks/@localSearchIndexroot.D7w61u6Y.js +1 -0
- package/dist/docs/assets/chunks/{VPLocalSearchBox.Cf6ghfFY.js → VPLocalSearchBox.78s6nqsH.js} +1 -1
- package/dist/docs/assets/chunks/{theme.B_5-VLe8.js → theme.DroGdz_Q.js} +2 -2
- package/dist/docs/assets/{guides_github.md.DSg0j9qj.js → guides_github.md.B8QKtIpw.js} +7 -6
- package/dist/docs/assets/{guides_github.md.DSg0j9qj.lean.js → guides_github.md.B8QKtIpw.lean.js} +1 -1
- package/dist/docs/assets/{reference_agent-config.md.XRpxctsA.js → reference_agent-config.md.DHkkvcEZ.js} +4 -4
- package/dist/docs/assets/{reference_agent-config.md.XRpxctsA.lean.js → reference_agent-config.md.DHkkvcEZ.lean.js} +1 -1
- package/dist/docs/assets/{reference_hooks.md.4cbRRIIu.js → reference_hooks.md.DJE5DXcT.js} +1 -1
- package/dist/docs/assets/{storage.md.DcGSnIxX.js → storage.md.CJYd5YmN.js} +2 -2
- package/dist/docs/assets/{storage.md.DcGSnIxX.lean.js → storage.md.CJYd5YmN.lean.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 +10 -9
- 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 +6 -6
- 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 +5 -5
- 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 +5 -5
- package/dist/docs/templates/triage.html +3 -3
- package/dist/docs/troubleshooting.html +3 -3
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/internal/auth.d.ts +4 -0
- package/dist/internal/auth.d.ts.map +1 -1
- package/dist/internal/auth.js +7 -0
- package/dist/internal/durable-session-store.d.ts +35 -23
- package/dist/internal/durable-session-store.d.ts.map +1 -1
- package/dist/internal/durable-session-store.js +71 -26
- package/dist/internal/ledger-serving.d.ts +135 -67
- package/dist/internal/ledger-serving.d.ts.map +1 -1
- package/dist/internal/ledger-serving.js +326 -107
- package/dist/internal/sdk-runner.d.ts +21 -0
- package/dist/internal/sdk-runner.d.ts.map +1 -1
- package/dist/internal/sdk-runner.js +34 -1
- package/dist/internal/server.d.ts.map +1 -1
- package/dist/internal/server.js +9 -4
- package/dist/internal/session-engine.d.ts +18 -17
- package/dist/internal/session-engine.d.ts.map +1 -1
- package/dist/internal/session-engine.js +60 -27
- package/dist/kv/memory-kv.d.ts +8 -0
- package/dist/kv/memory-kv.d.ts.map +1 -1
- package/dist/kv/memory-kv.js +8 -0
- package/dist/ledger/janitor.d.ts +60 -0
- package/dist/ledger/janitor.d.ts.map +1 -0
- package/dist/ledger/janitor.js +137 -0
- package/dist/ledger/session-ledger.d.ts +132 -43
- package/dist/ledger/session-ledger.d.ts.map +1 -1
- package/dist/ledger/session-ledger.js +282 -50
- package/dist/ledger.d.ts +9 -8
- package/dist/ledger.d.ts.map +1 -1
- package/dist/ledger.js +7 -7
- package/dist/playground/assets/{index-Dces8BDp.js → index-BgUHfe3L.js} +32 -32
- package/dist/playground/assets/index-uFaeqVZS.css +1 -0
- package/dist/playground/index.html +2 -2
- package/dist/storage-backends/cursor-hosted.d.ts +5 -0
- package/dist/storage-backends/cursor-hosted.d.ts.map +1 -1
- package/dist/storage-backends/cursor-hosted.js +6 -0
- package/dist/types.d.ts +19 -8
- package/dist/types.d.ts.map +1 -1
- package/docs/guides/github.md +23 -18
- package/docs/reference/agent-config.md +31 -36
- package/docs/reference/hooks.md +1 -1
- package/docs/storage.md +26 -9
- package/package.json +1 -1
- package/src/channels/github/api.ts +122 -0
- package/src/channels/github/cursor-account.ts +23 -0
- package/src/channels/github/defaults.ts +1 -1
- package/src/channels/github/github-channel.ts +5 -1
- package/src/channels/github/progress.ts +84 -19
- package/src/channels/github/state.ts +14 -0
- package/src/channels/github/types.ts +3 -3
- package/src/index.ts +1 -0
- package/src/internal/auth.ts +10 -0
- package/src/internal/durable-session-store.ts +66 -27
- package/src/internal/ledger-serving.ts +439 -148
- package/src/internal/sdk-runner.ts +46 -1
- package/src/internal/server.ts +17 -3
- package/src/internal/session-engine.ts +95 -37
- package/src/kv/memory-kv.ts +8 -0
- package/src/ledger/janitor.ts +193 -0
- package/src/ledger/session-ledger.ts +372 -68
- package/src/ledger.ts +21 -6
- package/src/storage-backends/cursor-hosted.ts +17 -2
- package/src/types.ts +19 -8
- package/dist/docs/assets/chunks/@localSearchIndexroot.BMT0xM4I.js +0 -1
- package/dist/playground/assets/index-CZH9EFvO.css +0 -1
- /package/dist/docs/assets/{reference_hooks.md.4cbRRIIu.lean.js → reference_hooks.md.DJE5DXcT.lean.js} +0 -0
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
type AgentDefinition,
|
|
13
13
|
type AgentOptions,
|
|
14
14
|
type CloudAgentOptions,
|
|
15
|
+
Cursor,
|
|
15
16
|
createAgentPlatform,
|
|
16
17
|
JsonlLocalAgentStore,
|
|
17
18
|
type LocalAgentStore,
|
|
@@ -75,6 +76,36 @@ type MaybePrewarmingPlatform = {
|
|
|
75
76
|
) => Promise<() => Promise<void>>;
|
|
76
77
|
};
|
|
77
78
|
|
|
79
|
+
/**
|
|
80
|
+
* How long a non-`--dev` serve holds workspace scans (rules, skills,
|
|
81
|
+
* `AGENTS.md`, ignore files). The SDK default is 20s for an editor whose
|
|
82
|
+
* files move under it; agent-serve checkouts only change on deploy, so a
|
|
83
|
+
* missed mid-process edit is the right trade. `CURSOR_RIPWALK_CACHE_TTL_MS`
|
|
84
|
+
* still wins when set. `--dev` leaves the 20s default so skill edits show up.
|
|
85
|
+
*/
|
|
86
|
+
export const SERVE_WORKSPACE_SCAN_CACHE_TTL_MS: number = 24 * 60 * 60 * 1000;
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Resolve the SDK workspace-scan TTL for this serve. `undefined` means "do
|
|
90
|
+
* not call `Cursor.configure`" and leave the SDK at its 20s editor default.
|
|
91
|
+
*/
|
|
92
|
+
export function resolveServeWorkspaceScanCacheTtlMs(args: {
|
|
93
|
+
dev: boolean;
|
|
94
|
+
env?: NodeJS.ProcessEnv;
|
|
95
|
+
}): number | undefined {
|
|
96
|
+
const raw = (args.env ?? process.env).CURSOR_RIPWALK_CACHE_TTL_MS?.trim();
|
|
97
|
+
if (raw !== undefined && raw !== "") {
|
|
98
|
+
const parsed = Number(raw);
|
|
99
|
+
if (Number.isFinite(parsed) && parsed > 0) {
|
|
100
|
+
return parsed;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
if (args.dev) {
|
|
104
|
+
return undefined;
|
|
105
|
+
}
|
|
106
|
+
return SERVE_WORKSPACE_SCAN_CACHE_TTL_MS;
|
|
107
|
+
}
|
|
108
|
+
|
|
78
109
|
export interface CursorSdkRunnerOptions {
|
|
79
110
|
/** Cursor API key. Defaults to the `CURSOR_API_KEY` environment variable. */
|
|
80
111
|
apiKey?: string;
|
|
@@ -86,6 +117,11 @@ export interface CursorSdkRunnerOptions {
|
|
|
86
117
|
store?: LocalAgentStore;
|
|
87
118
|
/** Diagnostics for degraded paths (resume-miss fallback). */
|
|
88
119
|
logger?: (line: string) => void;
|
|
120
|
+
/**
|
|
121
|
+
* Hold workspace scans this long (ms). Applied via `Cursor.configure` so
|
|
122
|
+
* each new local executor reuses the walk. Omit to keep the SDK 20s default.
|
|
123
|
+
*/
|
|
124
|
+
workspaceScanCacheTtlMs?: number;
|
|
89
125
|
}
|
|
90
126
|
|
|
91
127
|
/**
|
|
@@ -128,7 +164,16 @@ export class CursorSdkRunner implements AgentRunner {
|
|
|
128
164
|
private readonly stores = new Map<string, Promise<LocalAgentStore>>();
|
|
129
165
|
private readonly prewarmReleases: Array<() => Promise<void>> = [];
|
|
130
166
|
|
|
131
|
-
constructor(private readonly options: CursorSdkRunnerOptions = {}) {
|
|
167
|
+
constructor(private readonly options: CursorSdkRunnerOptions = {}) {
|
|
168
|
+
if (options.workspaceScanCacheTtlMs !== undefined) {
|
|
169
|
+
Cursor.configure({
|
|
170
|
+
local: { workspaceScanCacheTtlMs: options.workspaceScanCacheTtlMs },
|
|
171
|
+
});
|
|
172
|
+
this.log(
|
|
173
|
+
`[agent-sdk] workspace scan cache ttl=${options.workspaceScanCacheTtlMs}ms`
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
132
177
|
|
|
133
178
|
async runTurn(request: RunnerTurnRequest): Promise<TurnOutcome> {
|
|
134
179
|
let sawUpdate = false;
|
package/src/internal/server.ts
CHANGED
|
@@ -17,6 +17,7 @@ import { join } from "node:path";
|
|
|
17
17
|
import {
|
|
18
18
|
type CursorGithubRepoScope,
|
|
19
19
|
createCursorAccountGithubTokenResolver,
|
|
20
|
+
cursorGithubPermissionsForChannel,
|
|
20
21
|
type GithubCursorAccountRuntime,
|
|
21
22
|
getGithubCursorAccountRuntime,
|
|
22
23
|
parseCursorGithubRepoScope,
|
|
@@ -146,7 +147,11 @@ import {
|
|
|
146
147
|
} from "./resolved-connections.js";
|
|
147
148
|
import { Router } from "./router.js";
|
|
148
149
|
import { ScheduleRunner, UnknownScheduleError } from "./schedule-runner.js";
|
|
149
|
-
import {
|
|
150
|
+
import {
|
|
151
|
+
type AgentRunner,
|
|
152
|
+
CursorSdkRunner,
|
|
153
|
+
resolveServeWorkspaceScanCacheTtlMs,
|
|
154
|
+
} from "./sdk-runner.js";
|
|
150
155
|
import {
|
|
151
156
|
SessionAuthError,
|
|
152
157
|
SessionBusyError,
|
|
@@ -363,11 +368,17 @@ export async function startServer(
|
|
|
363
368
|
"[agent-sdk] warning: --allow-anonymous admits every HTTP caller as the same principal; use only on a trusted network (e.g. Tailscale) and prefer --bearer-token for shared hosts"
|
|
364
369
|
);
|
|
365
370
|
}
|
|
371
|
+
const workspaceScanCacheTtlMs = resolveServeWorkspaceScanCacheTtlMs({
|
|
372
|
+
dev,
|
|
373
|
+
});
|
|
366
374
|
const runner =
|
|
367
375
|
options.runner ??
|
|
368
376
|
new CursorSdkRunner({
|
|
369
377
|
...(accountKey === undefined ? {} : { apiKey: accountKey.apiKey }),
|
|
370
378
|
logger,
|
|
379
|
+
...(workspaceScanCacheTtlMs === undefined
|
|
380
|
+
? {}
|
|
381
|
+
: { workspaceScanCacheTtlMs }),
|
|
371
382
|
});
|
|
372
383
|
|
|
373
384
|
const router = new Router();
|
|
@@ -1869,8 +1880,11 @@ function resolveCursorGithubEventConfig(input: {
|
|
|
1869
1880
|
]),
|
|
1870
1881
|
];
|
|
1871
1882
|
const permissions = resolveCursorGithubPermissions(
|
|
1872
|
-
input.targets.map(
|
|
1873
|
-
(
|
|
1883
|
+
input.targets.map((target) =>
|
|
1884
|
+
cursorGithubPermissionsForChannel(
|
|
1885
|
+
target.accountRuntime.options,
|
|
1886
|
+
target.accountRuntime.scope?.permissions
|
|
1887
|
+
)
|
|
1874
1888
|
)
|
|
1875
1889
|
);
|
|
1876
1890
|
const scope = parseCursorGithubRepoScope(repos, permissions);
|
|
@@ -76,7 +76,7 @@ import { foldABStream } from "./ab-fold.js";
|
|
|
76
76
|
import { type ABSnapshot, buildABSnapshot } from "./ab-snapshot.js";
|
|
77
77
|
import { ApprovalGate } from "./approval-gate.js";
|
|
78
78
|
import { ArtifactsStore } from "./artifacts-store.js";
|
|
79
|
-
import { samePrincipal } from "./auth.js";
|
|
79
|
+
import { SessionAuthError, samePrincipal } from "./auth.js";
|
|
80
80
|
import { mergeCloudOptions, resolveSessionRuntime } from "./cloud-merge.js";
|
|
81
81
|
import {
|
|
82
82
|
applyCloudUsageToEvents,
|
|
@@ -149,14 +149,9 @@ export class SessionBusyError extends Error {
|
|
|
149
149
|
}
|
|
150
150
|
}
|
|
151
151
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
`Session ${sessionId} belongs to a different principal; pass the original caller auth on follow-up`
|
|
156
|
-
);
|
|
157
|
-
this.name = "SessionAuthError";
|
|
158
|
-
}
|
|
159
|
-
}
|
|
152
|
+
// Defined in auth.js (the ledger pump needs it without importing this
|
|
153
|
+
// module); re-exported here because this is its historical public home.
|
|
154
|
+
export { SessionAuthError } from "./auth.js";
|
|
160
155
|
|
|
161
156
|
export class UnknownChannelError extends Error {
|
|
162
157
|
constructor(channelId: string) {
|
|
@@ -234,10 +229,11 @@ export interface SessionEngineOptions {
|
|
|
234
229
|
/**
|
|
235
230
|
* Explicit durable-session store for `serving: "ledger"` agents — a KV
|
|
236
231
|
* dedicated to the ledger (session identity, leases, commit heads),
|
|
237
|
-
* separate from agent state. Must implement the
|
|
238
|
-
* (`
|
|
239
|
-
*
|
|
240
|
-
*
|
|
232
|
+
* separate from agent state. Must implement the full CAS-capable KV
|
|
233
|
+
* surface (`HostKvCasApi`: the CAS trio plus `get`/`put`/`delete` for
|
|
234
|
+
* mailbox payload blobs and `listKeys` for the janitor scan);
|
|
235
|
+
* construction fails fast otherwise. Supply the **raw** store: the
|
|
236
|
+
* engine owns key scoping and namespaces every ledger key under
|
|
241
237
|
* `agentkit/v1/<agent>/durable-session/` itself — a self-hoster never
|
|
242
238
|
* writes prefixes. Default when omitted: the authored storage backend
|
|
243
239
|
* (`agent/storage.ts`) when it implements the `cas` group, else — with
|
|
@@ -286,12 +282,13 @@ interface ActiveToolContext {
|
|
|
286
282
|
export interface EngineSendOptions extends SendMessageOptions {
|
|
287
283
|
mode?: "chat" | "task";
|
|
288
284
|
/**
|
|
289
|
-
* Internal (ledger serving):
|
|
290
|
-
*
|
|
291
|
-
*
|
|
292
|
-
*
|
|
285
|
+
* Internal (ledger serving): set only by the ledger pump when it
|
|
286
|
+
* executes an already-parked event under its lease, so the call must
|
|
287
|
+
* run the resident path. Without the flag, every send on a
|
|
288
|
+
* `serving: "ledger"` agent routes through the ledger (append → claim
|
|
289
|
+
* → drain → turn → commit) and the pump would recurse into itself.
|
|
293
290
|
*/
|
|
294
|
-
|
|
291
|
+
fromLedgerPump?: boolean;
|
|
295
292
|
}
|
|
296
293
|
|
|
297
294
|
interface CoalescedQueuedTurn {
|
|
@@ -482,11 +479,12 @@ export class SessionEngine {
|
|
|
482
479
|
this.channelsById.set(channel.id, channel.definition);
|
|
483
480
|
}
|
|
484
481
|
this.ledgerServing = this.buildLedgerServing(options);
|
|
482
|
+
this.ledgerServing?.start();
|
|
485
483
|
}
|
|
486
484
|
|
|
487
485
|
/**
|
|
488
486
|
* Ledger-mode wiring (`serving: "ledger"`): resolve a CAS-capable
|
|
489
|
-
* durable-session store and build the
|
|
487
|
+
* durable-session store and build the event pump. Resident agents
|
|
490
488
|
* (the default) return undefined and never touch ledger keys.
|
|
491
489
|
*/
|
|
492
490
|
private buildLedgerServing(
|
|
@@ -495,15 +493,46 @@ export class SessionEngine {
|
|
|
495
493
|
if (options.project.agent.serving !== "ledger") {
|
|
496
494
|
return undefined;
|
|
497
495
|
}
|
|
496
|
+
// The handle is already agent-scoped (resolveDurableSessionStore):
|
|
497
|
+
// two ledger-mode agents sharing one backend never resolve to each
|
|
498
|
+
// other's sessions or janitor-claim each other's parked envelopes.
|
|
498
499
|
const ledgerKv = this.resolveDurableSessionStore(options);
|
|
499
500
|
return new LedgerServing({
|
|
500
501
|
ledger: new SessionLedger({ kv: ledgerKv, podId: newLedgerPodId() }),
|
|
501
|
-
deliverTurn: (
|
|
502
|
-
this.send(
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
502
|
+
deliverTurn: (envelope) =>
|
|
503
|
+
this.send(envelope.channelId, envelope.message, {
|
|
504
|
+
continuationToken: envelope.continuationToken,
|
|
505
|
+
auth: envelope.auth,
|
|
506
|
+
...(envelope.title === undefined ? {} : { title: envelope.title }),
|
|
507
|
+
...(envelope.state === undefined ? {} : { state: envelope.state }),
|
|
508
|
+
...(envelope.mode === undefined ? {} : { mode: envelope.mode }),
|
|
509
|
+
...(envelope.purpose === undefined
|
|
510
|
+
? {}
|
|
511
|
+
: { purpose: envelope.purpose }),
|
|
512
|
+
// Isolation overrides ride the envelope: without them the
|
|
513
|
+
// drained turn would run local with the default harness cwd
|
|
514
|
+
// instead of the cloud VM / channel worktree the sender bound.
|
|
515
|
+
...(envelope.workspaceDir === undefined
|
|
516
|
+
? {}
|
|
517
|
+
: { workspaceDir: envelope.workspaceDir }),
|
|
518
|
+
...(envelope.cloud === undefined ? {} : { cloud: envelope.cloud }),
|
|
519
|
+
fromLedgerPump: true,
|
|
506
520
|
}),
|
|
521
|
+
findSession: async (channelId, continuationToken) => {
|
|
522
|
+
let record = await this.sessions.findByContinuation(
|
|
523
|
+
channelId,
|
|
524
|
+
continuationToken
|
|
525
|
+
);
|
|
526
|
+
if (record === undefined) {
|
|
527
|
+
record = await this.restoreSessionByContinuation(
|
|
528
|
+
channelId,
|
|
529
|
+
continuationToken
|
|
530
|
+
);
|
|
531
|
+
}
|
|
532
|
+
return record === undefined
|
|
533
|
+
? undefined
|
|
534
|
+
: this.channelSession(record.sessionId, record, false);
|
|
535
|
+
},
|
|
507
536
|
logger: this.logger,
|
|
508
537
|
});
|
|
509
538
|
}
|
|
@@ -521,13 +550,14 @@ export class SessionEngine {
|
|
|
521
550
|
*
|
|
522
551
|
* An authored backend without `cas` fails fast: silently falling back
|
|
523
552
|
* to local files would fork coordination away from the deployment's
|
|
524
|
-
* durable store. The check demands
|
|
525
|
-
*
|
|
553
|
+
* durable store. The check demands the full surface the inbox uses —
|
|
554
|
+
* the CAS trio plus `get`/`put`/`delete` for mailbox payload blobs and
|
|
555
|
+
* `listKeys` for the janitor scan and the orphan sweep.
|
|
526
556
|
*
|
|
527
557
|
* Whatever store wins, the returned handle is scoped here — once —
|
|
528
558
|
* under `agentkit/v1/<agent>/durable-session/`. The ledger speaks bare
|
|
529
|
-
* `session/…` / `ledger/…` keys; namespacing is this
|
|
530
|
-
* exactly like `host.kv`'s agent scoping.
|
|
559
|
+
* `session/…` / `ledger/…` / `mailbox/…` keys; namespacing is this
|
|
560
|
+
* boundary's job, exactly like `host.kv`'s agent scoping.
|
|
531
561
|
*/
|
|
532
562
|
private resolveDurableSessionStore(
|
|
533
563
|
options: SessionEngineOptions
|
|
@@ -537,7 +567,7 @@ export class SessionEngine {
|
|
|
537
567
|
if (explicit !== undefined) {
|
|
538
568
|
if (!supportsSessionLedgerKv(explicit)) {
|
|
539
569
|
throw new Error(
|
|
540
|
-
`Agent "${options.project.name}" declares serving: "ledger", but the configured durableSessionStore is not CAS-capable (getWithVersion / putIfAbsent / putIfVersion). Provide a store implementing
|
|
570
|
+
`Agent "${options.project.name}" declares serving: "ledger", but the configured durableSessionStore is not CAS-capable (getWithVersion / putIfAbsent / putIfVersion / listKeys). Provide a store implementing HostKvCasApi (see @cursor/july/kv).`
|
|
541
571
|
);
|
|
542
572
|
}
|
|
543
573
|
return namespacedKv(explicit, namespace);
|
|
@@ -729,10 +759,10 @@ export class SessionEngine {
|
|
|
729
759
|
message: string,
|
|
730
760
|
options: EngineSendOptions = {}
|
|
731
761
|
): Promise<ChannelSession> {
|
|
732
|
-
// Ledger-mode agents
|
|
733
|
-
//
|
|
734
|
-
// resident path.
|
|
735
|
-
if (this.ledgerServing !== undefined && options.
|
|
762
|
+
// Ledger-mode agents park the event durably and coordinate through
|
|
763
|
+
// the session lease before any turn runs; only the pump's own
|
|
764
|
+
// lease-holding deliveries fall through to the resident path.
|
|
765
|
+
if (this.ledgerServing !== undefined && options.fromLedgerPump !== true) {
|
|
736
766
|
return this.ledgerServing.deliver(channelId, message, options);
|
|
737
767
|
}
|
|
738
768
|
const auth = options.auth ?? null;
|
|
@@ -806,13 +836,20 @@ export class SessionEngine {
|
|
|
806
836
|
),
|
|
807
837
|
}));
|
|
808
838
|
}
|
|
839
|
+
const since = (await this.logs.get(record.sessionId)).count;
|
|
809
840
|
const coalesced = await this.startTurn(record.sessionId, message, {
|
|
810
841
|
admission: options.admission ?? "preempt",
|
|
811
842
|
...(options.coalesceSourceTs === undefined
|
|
812
843
|
? {}
|
|
813
844
|
: { sourceTs: options.coalesceSourceTs }),
|
|
814
845
|
});
|
|
815
|
-
return this.channelSession(
|
|
846
|
+
return this.channelSession(
|
|
847
|
+
record.sessionId,
|
|
848
|
+
record,
|
|
849
|
+
false,
|
|
850
|
+
coalesced,
|
|
851
|
+
since
|
|
852
|
+
);
|
|
816
853
|
}
|
|
817
854
|
|
|
818
855
|
const now = new Date().toISOString();
|
|
@@ -838,18 +875,26 @@ export class SessionEngine {
|
|
|
838
875
|
};
|
|
839
876
|
await this.sessions.save(record);
|
|
840
877
|
this.storage?.sessionRecord(record);
|
|
878
|
+
// Ledger serving: another pod can follow up mid-turn, so the session
|
|
879
|
+
// identity (record + continuation index) must hit the shared store
|
|
880
|
+
// before we return a session id. Default turnEnd flush is too late.
|
|
881
|
+
if (this.ledgerServing !== undefined && this.storage !== undefined) {
|
|
882
|
+
this.storage.flushSession(record.sessionId);
|
|
883
|
+
await this.storage.whenIdle();
|
|
884
|
+
}
|
|
841
885
|
await this.appendEvent(record.sessionId, {
|
|
842
886
|
type: "session.started",
|
|
843
887
|
data: { channelId },
|
|
844
888
|
});
|
|
845
889
|
record = await this.enrollABs(record);
|
|
890
|
+
const since = (await this.logs.get(record.sessionId)).count;
|
|
846
891
|
await this.startTurn(record.sessionId, message, {
|
|
847
892
|
admission: options.admission ?? "preempt",
|
|
848
893
|
...(options.coalesceSourceTs === undefined
|
|
849
894
|
? {}
|
|
850
895
|
: { sourceTs: options.coalesceSourceTs }),
|
|
851
896
|
});
|
|
852
|
-
return this.channelSession(record.sessionId, record, true, false);
|
|
897
|
+
return this.channelSession(record.sessionId, record, true, false, since);
|
|
853
898
|
}
|
|
854
899
|
|
|
855
900
|
/** Whether a continuation token already maps to a session on this channel. */
|
|
@@ -2026,6 +2071,7 @@ export class SessionEngine {
|
|
|
2026
2071
|
}),
|
|
2027
2072
|
getSession: (channelId, sessionId) =>
|
|
2028
2073
|
this.getChannelSession(channelId, sessionId),
|
|
2074
|
+
emit: args.emit,
|
|
2029
2075
|
};
|
|
2030
2076
|
try {
|
|
2031
2077
|
const needsGate = await evaluateNeedsApproval(
|
|
@@ -2314,6 +2360,16 @@ export class SessionEngine {
|
|
|
2314
2360
|
this.logger(`[session] tool ${toolName} start (direct ${callId})`);
|
|
2315
2361
|
let result: unknown;
|
|
2316
2362
|
let isError: boolean;
|
|
2363
|
+
const live = record;
|
|
2364
|
+
const emit =
|
|
2365
|
+
live === undefined
|
|
2366
|
+
? undefined
|
|
2367
|
+
: (payload: SessionEventPayload) => {
|
|
2368
|
+
void this.appendEvent(live.sessionId, {
|
|
2369
|
+
...payload,
|
|
2370
|
+
turnId: callId,
|
|
2371
|
+
}).catch(() => {});
|
|
2372
|
+
};
|
|
2317
2373
|
try {
|
|
2318
2374
|
result = normalizeToolResult(
|
|
2319
2375
|
await execute(validation.value as never, {
|
|
@@ -2340,6 +2396,7 @@ export class SessionEngine {
|
|
|
2340
2396
|
}),
|
|
2341
2397
|
getSession: (channelId, sessionId) =>
|
|
2342
2398
|
this.getChannelSession(channelId, sessionId),
|
|
2399
|
+
emit,
|
|
2343
2400
|
})
|
|
2344
2401
|
);
|
|
2345
2402
|
isError = isErrorEnvelope(result);
|
|
@@ -3039,7 +3096,8 @@ export class SessionEngine {
|
|
|
3039
3096
|
sessionId: string,
|
|
3040
3097
|
record: SessionRecord,
|
|
3041
3098
|
isNew: boolean,
|
|
3042
|
-
coalesced = false
|
|
3099
|
+
coalesced = false,
|
|
3100
|
+
since = 0
|
|
3043
3101
|
): ChannelSession {
|
|
3044
3102
|
return {
|
|
3045
3103
|
id: sessionId,
|
|
@@ -3048,7 +3106,7 @@ export class SessionEngine {
|
|
|
3048
3106
|
...(coalesced ? { coalesced: true } : {}),
|
|
3049
3107
|
info: this.sessionInfo(record),
|
|
3050
3108
|
getEventStream: (options?: { startIndex?: number }) =>
|
|
3051
|
-
this.eventStream(sessionId, options?.startIndex ??
|
|
3109
|
+
this.eventStream(sessionId, options?.startIndex ?? since),
|
|
3052
3110
|
waitForCompletion: () => this.waitForCompletion(sessionId),
|
|
3053
3111
|
setContinuationToken: (token: string | null) =>
|
|
3054
3112
|
this.setContinuationToken(sessionId, token),
|
package/src/kv/memory-kv.ts
CHANGED
|
@@ -86,6 +86,14 @@ export function createMemoryKv(): HostKvCasApi {
|
|
|
86
86
|
* fixtures, an off-hosting no-op sink). Non-durable and process-local by
|
|
87
87
|
* construction; a real deployment backend should implement CAS against
|
|
88
88
|
* its own store instead.
|
|
89
|
+
*
|
|
90
|
+
* **Never pair this with `serving: "ledger"` on a real backend.** The
|
|
91
|
+
* ledger derives its coordination store from the whole storage config,
|
|
92
|
+
* so a durable plain backend with a memory `cas` group silently voids
|
|
93
|
+
* ack-after-durable: leases, heads, and pending indexes live only in
|
|
94
|
+
* this process and vanish on restart while payload blobs go through the
|
|
95
|
+
* plain methods. Ledger state must live in the same durable store as
|
|
96
|
+
* the blobs.
|
|
89
97
|
*/
|
|
90
98
|
export function memoryCasTable(): StorageCasTable {
|
|
91
99
|
const kv = createMemoryKv();
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The ledger janitor: lazy crash recovery has a hole — a session with
|
|
3
|
+
* pending events and no future events would wait forever — so serving runs
|
|
4
|
+
* this loop. It scans `ledger/` by prefix, finds pending-with-expired-lease
|
|
5
|
+
* sessions, claims them, and hands each claim to the execution callback.
|
|
6
|
+
* Self-host runs the same loop; it is part of the library, not the control
|
|
7
|
+
* plane.
|
|
8
|
+
*
|
|
9
|
+
* Nothing starts on construction: call `start()` for the interval loop or
|
|
10
|
+
* `runOnce()` for an explicit pass (what unit tests use).
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type {
|
|
14
|
+
ClaimResult,
|
|
15
|
+
LedgerLease,
|
|
16
|
+
SessionLedger,
|
|
17
|
+
} from "./session-ledger.js";
|
|
18
|
+
|
|
19
|
+
export const DEFAULT_JANITOR_INTERVAL_MS = 15_000;
|
|
20
|
+
|
|
21
|
+
export interface JanitorClaimedSession {
|
|
22
|
+
sessionId: string;
|
|
23
|
+
lease: LedgerLease;
|
|
24
|
+
head: number;
|
|
25
|
+
pending: string[];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface JanitorRunResult {
|
|
29
|
+
/** Ledgers scanned this pass. */
|
|
30
|
+
scanned: number;
|
|
31
|
+
/** Sessions this pass claimed and handed to the callback. */
|
|
32
|
+
claimed: JanitorClaimedSession[];
|
|
33
|
+
/** Orphan blob ids deleted (only when the sweep is enabled). */
|
|
34
|
+
orphansDeleted: string[];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface LedgerJanitorOptions {
|
|
38
|
+
ledger: SessionLedger;
|
|
39
|
+
/**
|
|
40
|
+
* Executes a recovered session: drain, run the turn, commit. Awaited so
|
|
41
|
+
* one pass never claims more sessions than it can serve.
|
|
42
|
+
*/
|
|
43
|
+
onSession: (session: JanitorClaimedSession) => Promise<void> | void;
|
|
44
|
+
/** Scan cadence for `start()` (default {@link DEFAULT_JANITOR_INTERVAL_MS}). */
|
|
45
|
+
intervalMs?: number;
|
|
46
|
+
/**
|
|
47
|
+
* Also sweep unreferenced mailbox blobs each pass. Off by default —
|
|
48
|
+
* enable with `{ olderThanMs }` (see `SessionLedger.sweepOrphans`).
|
|
49
|
+
*/
|
|
50
|
+
orphanSweep?: { olderThanMs?: number };
|
|
51
|
+
/** Called with per-session or sweep failures; the pass continues. */
|
|
52
|
+
onError?: (error: unknown, sessionId?: string) => void;
|
|
53
|
+
/** Time source for lease-expiry checks (default `Date.now`). */
|
|
54
|
+
clock?: () => number;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface LedgerJanitor {
|
|
58
|
+
start(): void;
|
|
59
|
+
/**
|
|
60
|
+
* Clear the interval AND await any in-flight pass: a running pass can
|
|
61
|
+
* claim a session and spawn a turn, so shutdown must not proceed while
|
|
62
|
+
* one is mid-scan.
|
|
63
|
+
*/
|
|
64
|
+
stop(): Promise<void>;
|
|
65
|
+
runOnce(): Promise<JanitorRunResult>;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function createLedgerJanitor(
|
|
69
|
+
options: LedgerJanitorOptions
|
|
70
|
+
): LedgerJanitor {
|
|
71
|
+
const intervalMs = options.intervalMs ?? DEFAULT_JANITOR_INTERVAL_MS;
|
|
72
|
+
const clock = options.clock ?? Date.now;
|
|
73
|
+
const reportError =
|
|
74
|
+
options.onError ??
|
|
75
|
+
((): void => {
|
|
76
|
+
// Recovery is best-effort per pass; an unobserved failure is retried
|
|
77
|
+
// on the next scan.
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
let timer: ReturnType<typeof setInterval> | undefined;
|
|
81
|
+
let running = false;
|
|
82
|
+
/** The settled-shape promise of the pass currently scanning, if any. */
|
|
83
|
+
let inFlight: Promise<void> | undefined;
|
|
84
|
+
|
|
85
|
+
const runPass = async (): Promise<JanitorRunResult> => {
|
|
86
|
+
// An unparseable ledger record is reported and skipped: it degrades
|
|
87
|
+
// that one session's recovery, never the whole agent's pass.
|
|
88
|
+
const ledgers = await options.ledger.listLedgers((key, error) => {
|
|
89
|
+
reportError(
|
|
90
|
+
new Error(
|
|
91
|
+
`janitor scan: skipping unparseable ledger record at ${key}: ${String(
|
|
92
|
+
error instanceof Error ? error.message : error
|
|
93
|
+
)}`
|
|
94
|
+
)
|
|
95
|
+
);
|
|
96
|
+
});
|
|
97
|
+
const claimed: JanitorClaimedSession[] = [];
|
|
98
|
+
for (const snapshot of ledgers) {
|
|
99
|
+
if (snapshot.pending.length === 0) {
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
if (snapshot.lease !== null && snapshot.lease.expiresAt > clock()) {
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
let claim: ClaimResult;
|
|
106
|
+
try {
|
|
107
|
+
claim = await options.ledger.claim(snapshot.sessionId);
|
|
108
|
+
} catch (error) {
|
|
109
|
+
reportError(error, snapshot.sessionId);
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
if (!claim.claimed) {
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
const session: JanitorClaimedSession = {
|
|
116
|
+
sessionId: snapshot.sessionId,
|
|
117
|
+
lease: claim.lease,
|
|
118
|
+
head: claim.head,
|
|
119
|
+
pending: claim.pending,
|
|
120
|
+
};
|
|
121
|
+
claimed.push(session);
|
|
122
|
+
try {
|
|
123
|
+
await options.onSession(session);
|
|
124
|
+
} catch (error) {
|
|
125
|
+
reportError(error, snapshot.sessionId);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
let orphansDeleted: string[] = [];
|
|
130
|
+
if (options.orphanSweep !== undefined) {
|
|
131
|
+
try {
|
|
132
|
+
const swept = await options.ledger.sweepOrphans({
|
|
133
|
+
olderThanMs: options.orphanSweep.olderThanMs,
|
|
134
|
+
});
|
|
135
|
+
orphansDeleted = swept.deleted;
|
|
136
|
+
} catch (error) {
|
|
137
|
+
reportError(error);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return { scanned: ledgers.length, claimed, orphansDeleted };
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
const runOnce = (): Promise<JanitorRunResult> => {
|
|
144
|
+
const pass = runPass();
|
|
145
|
+
const settled = pass.then(
|
|
146
|
+
() => undefined,
|
|
147
|
+
() => undefined
|
|
148
|
+
);
|
|
149
|
+
inFlight = settled;
|
|
150
|
+
void settled.finally(() => {
|
|
151
|
+
if (inFlight === settled) {
|
|
152
|
+
inFlight = undefined;
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
return pass;
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
const tick = async (): Promise<void> => {
|
|
159
|
+
// Skip a tick rather than overlap passes when a scan outlasts the
|
|
160
|
+
// cadence — overlapping claims would just fence each other.
|
|
161
|
+
if (running) {
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
running = true;
|
|
165
|
+
try {
|
|
166
|
+
await runOnce();
|
|
167
|
+
} catch (error) {
|
|
168
|
+
reportError(error);
|
|
169
|
+
} finally {
|
|
170
|
+
running = false;
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
return {
|
|
175
|
+
start(): void {
|
|
176
|
+
if (timer !== undefined) {
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
timer = setInterval(() => {
|
|
180
|
+
void tick();
|
|
181
|
+
}, intervalMs);
|
|
182
|
+
timer.unref?.();
|
|
183
|
+
},
|
|
184
|
+
async stop(): Promise<void> {
|
|
185
|
+
if (timer !== undefined) {
|
|
186
|
+
clearInterval(timer);
|
|
187
|
+
timer = undefined;
|
|
188
|
+
}
|
|
189
|
+
await inFlight;
|
|
190
|
+
},
|
|
191
|
+
runOnce,
|
|
192
|
+
};
|
|
193
|
+
}
|