@cursor/july 0.1.40 → 0.1.42
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 +13 -6
- package/dist/bin/agent-serve.js +28 -9
- package/dist/channels/slack/attachments.d.ts +82 -0
- package/dist/channels/slack/attachments.d.ts.map +1 -0
- package/dist/channels/slack/attachments.js +520 -0
- package/dist/channels/slack/cursor-account.d.ts +7 -1
- package/dist/channels/slack/cursor-account.d.ts.map +1 -1
- package/dist/channels/slack/cursor-account.js +2 -2
- package/dist/channels/slack/dispatch.d.ts +4 -3
- package/dist/channels/slack/dispatch.d.ts.map +1 -1
- package/dist/channels/slack/dispatch.js +84 -21
- package/dist/channels/slack/inbound.d.ts +6 -0
- package/dist/channels/slack/inbound.d.ts.map +1 -1
- package/dist/channels/slack/inbound.js +48 -2
- package/dist/channels/slack/index.d.ts +1 -0
- package/dist/channels/slack/index.d.ts.map +1 -1
- package/dist/channels/slack/index.js +1 -0
- package/dist/channels/slack/manifest.d.ts.map +1 -1
- package/dist/channels/slack/manifest.js +1 -0
- package/dist/channels/slack/slack-channel.d.ts.map +1 -1
- package/dist/channels/slack/slack-channel.js +7 -4
- package/dist/channels/slack/types.d.ts +49 -1
- package/dist/channels/slack/types.d.ts.map +1 -1
- package/dist/docs/404.html +2 -2
- package/dist/docs/ab.html +6 -6
- package/dist/docs/assets/{ab.md.6cLOW7--.js → ab.md.hdxsvX5M.js} +2 -2
- package/dist/docs/assets/{app.DpGLSiym.js → app.C4-Gwi7g.js} +1 -1
- package/dist/docs/assets/{building-with-agents.md.txrcGU2B.js → building-with-agents.md.PeZaZA1P.js} +1 -1
- package/dist/docs/assets/chunks/@localSearchIndexroot.BlU3A8EB.js +1 -0
- package/dist/docs/assets/chunks/{VPLocalSearchBox.B11XEMST.js → VPLocalSearchBox.DDoLm843.js} +1 -1
- package/dist/docs/assets/chunks/{theme.iTidHsAx.js → theme.SbjGIiM0.js} +2 -2
- package/dist/docs/assets/{deployment.md.CuK5SNjN.js → deployment.md.BHVjVqRa.js} +4 -4
- package/dist/docs/assets/{deployment.md.CuK5SNjN.lean.js → deployment.md.BHVjVqRa.lean.js} +1 -1
- package/dist/docs/assets/{evals.md.BQXI3rXy.js → evals.md.qeoGgPw0.js} +3 -3
- package/dist/docs/assets/example-agents_index.md.xmJ87d_3.js +2 -0
- package/dist/docs/assets/{guides_webhooks.md.BERuBSJW.js → guides_webhooks.md.DB-r_er9.js} +47 -4
- package/dist/docs/assets/{guides_webhooks.md.BERuBSJW.lean.js → guides_webhooks.md.DB-r_er9.lean.js} +1 -1
- package/dist/docs/assets/index.md.BKpbkCa6.js +24 -0
- package/dist/docs/assets/{index.md.CmhptOmN.lean.js → index.md.BKpbkCa6.lean.js} +1 -1
- package/dist/docs/assets/{quickstart.md.C_b6ESpD.js → quickstart.md.rIbfahJM.js} +4 -4
- package/dist/docs/assets/{reference_cli.md.Byvrg8eu.js → reference_cli.md.DqEqQpGn.js} +5 -5
- package/dist/docs/assets/{reference_cli.md.Byvrg8eu.lean.js → reference_cli.md.DqEqQpGn.lean.js} +1 -1
- package/dist/docs/assets/reference_hooks.md.B40QAG6m.js +14 -0
- package/dist/docs/assets/{reference_hooks.md.BGDw4VLm.lean.js → reference_hooks.md.B40QAG6m.lean.js} +1 -1
- package/dist/docs/assets/{storage.md.mQDtIULc.js → storage.md.DcGSnIxX.js} +10 -4
- package/dist/docs/assets/{storage.md.mQDtIULc.lean.js → storage.md.DcGSnIxX.lean.js} +1 -1
- package/dist/docs/assets/troubleshooting.md.CkgwmVI0.js +1 -0
- package/dist/docs/assets/{troubleshooting.md.B5RVX_tL.lean.js → troubleshooting.md.CkgwmVI0.lean.js} +1 -1
- package/dist/docs/building-with-agents.html +5 -5
- package/dist/docs/concepts.html +3 -3
- package/dist/docs/deployment.html +6 -6
- package/dist/docs/evals.html +7 -7
- 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 +5 -5
- 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/mcp-oauth.html +3 -3
- package/dist/docs/guides/slack.html +3 -3
- package/dist/docs/guides/webhooks.html +49 -6
- package/dist/docs/hashmap.json +1 -1
- package/dist/docs/hillclimbing.html +3 -3
- package/dist/docs/index.html +7 -7
- package/dist/docs/quickstart.html +8 -8
- 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 +8 -8
- package/dist/docs/reference/connections.html +3 -3
- package/dist/docs/reference/hooks.html +9 -7
- 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 +14 -8
- package/dist/docs/troubleshooting.html +5 -5
- package/dist/hooks.d.ts +3 -1
- package/dist/hooks.d.ts.map +1 -1
- package/dist/hooks.js +3 -1
- package/dist/index.d.ts +4 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/internal/cli-ax.d.ts.map +1 -1
- package/dist/internal/cli-ax.js +22 -2
- package/dist/internal/cli-deploy.d.ts +6 -0
- package/dist/internal/cli-deploy.d.ts.map +1 -1
- package/dist/internal/cli-deploy.js +14 -4
- package/dist/internal/cli-mcp.js +4 -4
- package/dist/internal/deploy-client.js +1 -1
- package/dist/internal/discovery.d.ts.map +1 -1
- package/dist/internal/discovery.js +35 -7
- package/dist/internal/distribution.d.ts.map +1 -1
- package/dist/internal/distribution.js +3 -1
- package/dist/internal/durable-session-store.d.ts +48 -0
- package/dist/internal/durable-session-store.d.ts.map +1 -0
- package/dist/internal/durable-session-store.js +86 -0
- package/dist/internal/hosting.d.ts +1 -1
- package/dist/internal/hosting.js +2 -2
- package/dist/internal/init-project.d.ts +34 -0
- package/dist/internal/init-project.d.ts.map +1 -1
- package/dist/internal/init-project.js +121 -2
- package/dist/internal/ledger-serving.d.ts +101 -0
- package/dist/internal/ledger-serving.d.ts.map +1 -0
- package/dist/internal/ledger-serving.js +174 -0
- package/dist/internal/listen-port.d.ts +22 -0
- package/dist/internal/listen-port.d.ts.map +1 -0
- package/dist/internal/listen-port.js +77 -0
- package/dist/internal/playground/static.d.ts +21 -4
- package/dist/internal/playground/static.d.ts.map +1 -1
- package/dist/internal/playground/static.js +82 -18
- package/dist/internal/sdk-runner.d.ts.map +1 -1
- package/dist/internal/sdk-runner.js +10 -1
- package/dist/internal/server.d.ts.map +1 -1
- package/dist/internal/server.js +61 -47
- package/dist/internal/session-engine.d.ts +57 -1
- package/dist/internal/session-engine.d.ts.map +1 -1
- package/dist/internal/session-engine.js +118 -28
- package/dist/internal/slack-provision-client.js +1 -1
- package/dist/internal/workspace.d.ts +6 -2
- package/dist/internal/workspace.d.ts.map +1 -1
- package/dist/internal/workspace.js +10 -2
- package/dist/kv/memory-kv.d.ts +23 -0
- package/dist/kv/memory-kv.d.ts.map +1 -0
- package/dist/kv/memory-kv.js +103 -0
- package/dist/kv.d.ts +22 -0
- package/dist/kv.d.ts.map +1 -0
- package/dist/kv.js +20 -0
- package/dist/ledger/session-ledger.d.ts +204 -0
- package/dist/ledger/session-ledger.d.ts.map +1 -0
- package/dist/ledger/session-ledger.js +382 -0
- package/dist/ledger.d.ts +11 -0
- package/dist/ledger.d.ts.map +1 -0
- package/dist/ledger.js +9 -0
- package/dist/playground/assets/index-B3HBneZZ.js +85 -0
- package/dist/playground/assets/index-Bqk9nplh.css +1 -0
- package/dist/playground/index.html +2 -2
- package/dist/storage-backends/cursor-hosted.d.ts +25 -3
- package/dist/storage-backends/cursor-hosted.d.ts.map +1 -1
- package/dist/storage-backends/cursor-hosted.js +50 -2
- package/dist/storage-backends/file-kv.d.ts +5 -2
- package/dist/storage-backends/file-kv.d.ts.map +1 -1
- package/dist/storage-backends/file-kv.js +107 -7
- package/dist/storage.d.ts +60 -5
- package/dist/storage.d.ts.map +1 -1
- package/dist/storage.js +28 -4
- package/dist/types.d.ts +132 -6
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +14 -0
- package/docs/README.md +10 -7
- package/docs/ab.md +2 -2
- package/docs/building-with-agents.md +1 -1
- package/docs/deployment.md +34 -11
- package/docs/evals.md +3 -3
- package/docs/example-agents/index.md +1 -1
- package/docs/guides/webhooks.md +87 -0
- package/docs/quickstart.md +7 -8
- package/docs/reference/cli.md +15 -6
- package/docs/reference/hooks.md +12 -3
- package/docs/storage.md +67 -7
- package/docs/troubleshooting.md +10 -2
- package/package.json +20 -7
- package/skills/create-agent/SKILL.md +1 -1
- package/skills/debug/SKILL.md +4 -0
- package/src/bin/agent-serve.ts +27 -11
- package/src/channels/slack/attachments.ts +635 -0
- package/src/channels/slack/cursor-account.ts +8 -0
- package/src/channels/slack/dispatch.ts +121 -6
- package/src/channels/slack/inbound.ts +60 -0
- package/src/channels/slack/index.ts +1 -0
- package/src/channels/slack/manifest.ts +1 -0
- package/src/channels/slack/slack-channel.ts +7 -0
- package/src/channels/slack/types.ts +47 -1
- package/src/hooks.ts +3 -1
- package/src/index.ts +9 -0
- package/src/internal/cli-ax.ts +25 -1
- package/src/internal/cli-deploy.ts +22 -4
- package/src/internal/cli-mcp.ts +4 -4
- package/src/internal/deploy-client.ts +1 -1
- package/src/internal/discovery.ts +42 -5
- package/src/internal/distribution.ts +3 -1
- package/src/internal/durable-session-store.ts +95 -0
- package/src/internal/hosting.ts +2 -2
- package/src/internal/init-project.ts +151 -2
- package/src/internal/ledger-serving.ts +240 -0
- package/src/internal/listen-port.ts +85 -0
- package/src/internal/playground/static.ts +111 -20
- package/src/internal/sdk-runner.ts +11 -1
- package/src/internal/server.ts +68 -61
- package/src/internal/session-engine.ts +193 -16
- package/src/internal/slack-provision-client.ts +1 -1
- package/src/internal/workspace.ts +14 -3
- package/src/kv/memory-kv.ts +99 -0
- package/src/kv.ts +27 -0
- package/src/ledger/session-ledger.ts +535 -0
- package/src/ledger.ts +29 -0
- package/src/storage-backends/cursor-hosted.ts +108 -3
- package/src/storage-backends/file-kv.ts +128 -12
- package/src/storage.ts +90 -6
- package/src/types.ts +139 -7
- package/dist/channels/slack/post-update-delivery.d.ts +0 -85
- package/dist/channels/slack/post-update-delivery.d.ts.map +0 -1
- package/dist/docs/assets/chunks/@localSearchIndexroot.D82_RVtF.js +0 -1
- package/dist/docs/assets/example-agents_index.md.DgGBwckv.js +0 -2
- package/dist/docs/assets/index.md.CmhptOmN.js +0 -24
- package/dist/docs/assets/reference_hooks.md.BGDw4VLm.js +0 -12
- package/dist/docs/assets/troubleshooting.md.B5RVX_tL.js +0 -1
- package/dist/internal/json-dir-store.d.ts +0 -32
- package/dist/internal/json-dir-store.d.ts.map +0 -1
- package/dist/internal/persistence-coordinator.d.ts +0 -127
- package/dist/internal/persistence-coordinator.d.ts.map +0 -1
- package/dist/multi-tenant.d.ts +0 -80
- package/dist/multi-tenant.d.ts.map +0 -1
- package/dist/multi-tenant.js +0 -69
- package/dist/persistence.d.ts +0 -184
- package/dist/persistence.d.ts.map +0 -1
- package/dist/playground/assets/index-BRhBbnd4.css +0 -1
- package/dist/playground/assets/index-Bc1oFgFG.js +0 -85
- package/dist/storage-backends/postgres-kv.d.ts +0 -82
- package/dist/storage-backends/postgres-kv.d.ts.map +0 -1
- package/dist/storage-backends/postgres-kv.js +0 -185
- package/src/storage-backends/postgres-kv.ts +0 -253
- /package/dist/docs/assets/{ab.md.6cLOW7--.lean.js → ab.md.hdxsvX5M.lean.js} +0 -0
- /package/dist/docs/assets/{building-with-agents.md.txrcGU2B.lean.js → building-with-agents.md.PeZaZA1P.lean.js} +0 -0
- /package/dist/docs/assets/{evals.md.BQXI3rXy.lean.js → evals.md.qeoGgPw0.lean.js} +0 -0
- /package/dist/docs/assets/{example-agents_index.md.DgGBwckv.lean.js → example-agents_index.md.xmJ87d_3.lean.js} +0 -0
- /package/dist/docs/assets/{quickstart.md.C_b6ESpD.lean.js → quickstart.md.rIbfahJM.lean.js} +0 -0
|
@@ -28,6 +28,7 @@ import {
|
|
|
28
28
|
type AgentProject,
|
|
29
29
|
type AgentProjectInfo,
|
|
30
30
|
type AgentRuntime,
|
|
31
|
+
type AgentServingMode,
|
|
31
32
|
type ArtifactsDefinition,
|
|
32
33
|
type BuiltinToolsConfig,
|
|
33
34
|
type ChannelDefinition,
|
|
@@ -188,11 +189,20 @@ export async function loadAgentProjects(
|
|
|
188
189
|
);
|
|
189
190
|
}
|
|
190
191
|
const results: Array<{ slug: string; project: AgentProject }> = [];
|
|
192
|
+
// Storage namespaces (session mirror, host kv, durable-session ledger)
|
|
193
|
+
// key on the project NAME, not the mount slug — two co-hosted agents
|
|
194
|
+
// sharing a name would silently share sessions and coordination state.
|
|
195
|
+
const slugByName = new Map<string, string>();
|
|
191
196
|
for (const child of children) {
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
197
|
+
const project = await loadAgentProject(child.dir, options);
|
|
198
|
+
const priorSlug = slugByName.get(project.name);
|
|
199
|
+
if (priorSlug !== undefined) {
|
|
200
|
+
throw new Error(
|
|
201
|
+
`Duplicate agent name "${project.name}" under ${resolved}: agents "${priorSlug}" and "${child.slug}" both resolve to it (package.json "name", or the directory name when unset). Co-hosted agents must have distinct names — their storage and session namespaces key on the name.`
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
slugByName.set(project.name, child.slug);
|
|
205
|
+
results.push({ slug: child.slug, project });
|
|
196
206
|
}
|
|
197
207
|
return results;
|
|
198
208
|
}
|
|
@@ -420,6 +430,7 @@ async function loadAgent(
|
|
|
420
430
|
};
|
|
421
431
|
const hosting = resolveHosting(ctx, agentDir, config?.hosting);
|
|
422
432
|
const concurrency = resolveConcurrency(ctx, agentDir, config?.concurrency);
|
|
433
|
+
const serving = resolveServing(ctx, agentDir, config?.serving);
|
|
423
434
|
|
|
424
435
|
if (runtime === "cloud") {
|
|
425
436
|
warnCloudRuntimeLimitations(ctx, agentDir, {
|
|
@@ -443,6 +454,7 @@ async function loadAgent(
|
|
|
443
454
|
local,
|
|
444
455
|
hosting,
|
|
445
456
|
concurrency,
|
|
457
|
+
serving,
|
|
446
458
|
tools,
|
|
447
459
|
skills,
|
|
448
460
|
connections,
|
|
@@ -451,6 +463,27 @@ async function loadAgent(
|
|
|
451
463
|
};
|
|
452
464
|
}
|
|
453
465
|
|
|
466
|
+
/**
|
|
467
|
+
* Validate the authored serving mode. An unrecognized value is an error
|
|
468
|
+
* diagnostic and falls back to undefined (resident) — a typo must not
|
|
469
|
+
* silently opt an agent into (or out of) ledger coordination.
|
|
470
|
+
*/
|
|
471
|
+
function resolveServing(
|
|
472
|
+
ctx: DiscoveryContext,
|
|
473
|
+
agentDir: string,
|
|
474
|
+
serving: AgentServingMode | undefined
|
|
475
|
+
): AgentServingMode | undefined {
|
|
476
|
+
if (serving === undefined || serving === "resident" || serving === "ledger") {
|
|
477
|
+
return serving;
|
|
478
|
+
}
|
|
479
|
+
ctx.diagnostics.push({
|
|
480
|
+
severity: "error",
|
|
481
|
+
path: displayPath(ctx, join(agentDir, "agent.ts")),
|
|
482
|
+
message: `serving must be "resident" or "ledger"; got ${JSON.stringify(serving)}.`,
|
|
483
|
+
});
|
|
484
|
+
return undefined;
|
|
485
|
+
}
|
|
486
|
+
|
|
454
487
|
/** Absolute harness cwd parent, or undefined when unset / blank. */
|
|
455
488
|
function resolveAgentCwd(
|
|
456
489
|
ctx: DiscoveryContext,
|
|
@@ -504,7 +537,7 @@ function resolveHosting(
|
|
|
504
537
|
warn(`hosting.secretNames: ${nameError}`);
|
|
505
538
|
} else if (isReservedSecretName(name)) {
|
|
506
539
|
warn(
|
|
507
|
-
`hosting.secretNames: ${JSON.stringify(name)} is platform-managed (CURSOR_* /
|
|
540
|
+
`hosting.secretNames: ${JSON.stringify(name)} is platform-managed (CURSOR_* / reserved hosted-storage names); omit it from secretNames.`
|
|
508
541
|
);
|
|
509
542
|
}
|
|
510
543
|
}
|
|
@@ -1863,6 +1896,10 @@ export function projectInfo(project: AgentProject): AgentProjectInfo {
|
|
|
1863
1896
|
name: connection.name,
|
|
1864
1897
|
transport: connectionTransportLabel(connection.transport),
|
|
1865
1898
|
hostOnly: connection.hostOnly,
|
|
1899
|
+
cursorAccountServers:
|
|
1900
|
+
"cursorAccount" in connection.transport
|
|
1901
|
+
? (connection.transport.servers ?? "*")
|
|
1902
|
+
: undefined,
|
|
1866
1903
|
})),
|
|
1867
1904
|
subagents: agent.subagents.map((subagent) => ({
|
|
1868
1905
|
name: subagent.name,
|
|
@@ -108,8 +108,10 @@ export const AUTHORING_ENTRY_FILES: ReadonlyArray<
|
|
|
108
108
|
["evals/loaders", "evals/loaders.js"],
|
|
109
109
|
["ab", "ab.js"],
|
|
110
110
|
["artifacts", "artifacts.js"],
|
|
111
|
+
["kv", "kv.js"],
|
|
112
|
+
["kv/memory", "kv/memory-kv.js"],
|
|
113
|
+
["ledger", "ledger.js"],
|
|
111
114
|
["storage", "storage.js"],
|
|
112
115
|
["storage/file-kv", "storage-backends/file-kv.js"],
|
|
113
|
-
["storage/postgres-kv", "storage-backends/postgres-kv.js"],
|
|
114
116
|
["storage/cursor-hosted", "storage-backends/cursor-hosted.js"],
|
|
115
117
|
];
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Durable-session store resolution.
|
|
3
|
+
*
|
|
4
|
+
* Two APIs, one backend: `defineStorage` is the **storage API** (agent
|
|
5
|
+
* state — session records, event chunks, author KV), and the session
|
|
6
|
+
* ledger is the **durable session API** (session identity, leases,
|
|
7
|
+
* fenced commits). The ledger consumes {@link SessionLedgerKv} — exactly
|
|
8
|
+
* the three CAS methods it uses, nothing more; this module adapts a
|
|
9
|
+
* storage backend's `cas` group into that interface, so a deployment's
|
|
10
|
+
* one authored backend serves both APIs by default.
|
|
11
|
+
*
|
|
12
|
+
* Calls go straight at the config's methods — NOT through the
|
|
13
|
+
* StorageCoordinator: the coordinator's queue is bounded, serialized, and
|
|
14
|
+
* at-most-once (mirror semantics), while ledger CAS calls are the
|
|
15
|
+
* correctness path and must observe their own results synchronously.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import type { SessionLedgerKv } from "../ledger/session-ledger.js";
|
|
19
|
+
import {
|
|
20
|
+
keySegment,
|
|
21
|
+
STORAGE_KEY_ROOT,
|
|
22
|
+
type StorageConfig,
|
|
23
|
+
type StorageContext,
|
|
24
|
+
} from "../storage.js";
|
|
25
|
+
|
|
26
|
+
export interface StorageCasKvArgs {
|
|
27
|
+
agentName: string;
|
|
28
|
+
projectRoot: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Adapt a storage backend's `cas` group into the ledger's KV interface.
|
|
33
|
+
* Throws when the backend does not implement `cas` — the caller turns
|
|
34
|
+
* that into the fail-fast startup error for `serving: "ledger"`. Every
|
|
35
|
+
* call carries a `reason: "coordination"` {@link StorageContext}.
|
|
36
|
+
*/
|
|
37
|
+
export function storageCasKv(
|
|
38
|
+
config: StorageConfig,
|
|
39
|
+
args: StorageCasKvArgs
|
|
40
|
+
): SessionLedgerKv {
|
|
41
|
+
const ctx: StorageContext = {
|
|
42
|
+
agentName: args.agentName,
|
|
43
|
+
projectRoot: args.projectRoot,
|
|
44
|
+
reason: "coordination",
|
|
45
|
+
};
|
|
46
|
+
const { cas } = config;
|
|
47
|
+
if (cas === undefined) {
|
|
48
|
+
throw new Error(
|
|
49
|
+
"storageCasKv: the storage backend does not implement the cas group (conditional writes)"
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
return {
|
|
53
|
+
async getWithVersion(key) {
|
|
54
|
+
return await cas.getWithVersion(key, ctx);
|
|
55
|
+
},
|
|
56
|
+
async putIfAbsent(key, value) {
|
|
57
|
+
return await cas.putIfAbsent(key, value, ctx);
|
|
58
|
+
},
|
|
59
|
+
async putIfVersion(key, value, expectedVersion) {
|
|
60
|
+
return await cas.putIfVersion(key, value, expectedVersion, ctx);
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* The one place the ledger's physical namespace is minted:
|
|
67
|
+
* `agentkit/v1/<agent>/durable-session/` — the {@link STORAGE_KEY_ROOT}
|
|
68
|
+
* conventions with the agent segment encoded like every other
|
|
69
|
+
* caller-controlled segment. The ledger itself never sees this string;
|
|
70
|
+
* it speaks bare `session/…` / `ledger/…` keys against a handle that
|
|
71
|
+
* {@link namespacedKv} has already scoped.
|
|
72
|
+
*/
|
|
73
|
+
export function ledgerKeyPrefix(agentName: string): string {
|
|
74
|
+
return `${STORAGE_KEY_ROOT}/${keySegment(agentName)}/durable-session/`;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Scope a raw store handle under a namespace, prepending it to every
|
|
79
|
+
* key on the way in. Applied exactly once, where the engine constructs
|
|
80
|
+
* the ledger's store handle — the same conceptual boundary where
|
|
81
|
+
* `host.kv` gets its agent scoping. Internal: not part of the public kv
|
|
82
|
+
* interface, and callers (including the explicit `durableSessionStore`
|
|
83
|
+
* override) always supply the raw store — they never write prefixes.
|
|
84
|
+
*/
|
|
85
|
+
export function namespacedKv(
|
|
86
|
+
store: SessionLedgerKv,
|
|
87
|
+
namespace: string
|
|
88
|
+
): SessionLedgerKv {
|
|
89
|
+
return {
|
|
90
|
+
getWithVersion: (key) => store.getWithVersion(namespace + key),
|
|
91
|
+
putIfAbsent: (key, value) => store.putIfAbsent(namespace + key, value),
|
|
92
|
+
putIfVersion: (key, value, expectedVersion) =>
|
|
93
|
+
store.putIfVersion(namespace + key, value, expectedVersion),
|
|
94
|
+
};
|
|
95
|
+
}
|
package/src/internal/hosting.ts
CHANGED
|
@@ -15,7 +15,7 @@ export const MAX_SECRETS = 32;
|
|
|
15
15
|
export const MAX_SECRET_VALUE_BYTES = 4096;
|
|
16
16
|
/** Max secret name length. */
|
|
17
17
|
export const MAX_SECRET_NAME_LENGTH = 64;
|
|
18
|
-
/** Secret names the platform
|
|
18
|
+
/** Secret names the platform manages; user writes are rejected. */
|
|
19
19
|
export const RESERVED_SECRET_NAME_PREFIX = "CURSOR_";
|
|
20
20
|
|
|
21
21
|
/**
|
|
@@ -84,7 +84,7 @@ export function validateSettableSecretName(name: string): string | undefined {
|
|
|
84
84
|
return nameError;
|
|
85
85
|
}
|
|
86
86
|
if (isReservedSecretName(name)) {
|
|
87
|
-
return `Secret name ${JSON.stringify(name)} is platform-managed;
|
|
87
|
+
return `Secret name ${JSON.stringify(name)} is platform-managed; user secrets cannot claim CURSOR_* or reserved hosted-storage names.`;
|
|
88
88
|
}
|
|
89
89
|
return undefined;
|
|
90
90
|
}
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import { spawn } from "node:child_process";
|
|
6
|
+
import { existsSync } from "node:fs";
|
|
6
7
|
import { mkdir, writeFile } from "node:fs/promises";
|
|
7
8
|
import { basename, dirname, join, relative, resolve } from "node:path";
|
|
8
9
|
import type { ModelSetting } from "../types.js";
|
|
@@ -104,6 +105,7 @@ export async function initProject(
|
|
|
104
105
|
description: `agent project: ${name}`,
|
|
105
106
|
scripts: {
|
|
106
107
|
check: "tsc --noEmit",
|
|
108
|
+
dev: "agent-sdk dev",
|
|
107
109
|
},
|
|
108
110
|
dependencies: {
|
|
109
111
|
[PACKAGE_NAME]: `^${packageVersion()}`,
|
|
@@ -256,6 +258,141 @@ export async function installScaffoldDependencies(
|
|
|
256
258
|
};
|
|
257
259
|
}
|
|
258
260
|
|
|
261
|
+
export interface LinkCliOntoPathOptions {
|
|
262
|
+
stdio?: "inherit" | "pipe";
|
|
263
|
+
runner?: (args: {
|
|
264
|
+
command: string;
|
|
265
|
+
args: string[];
|
|
266
|
+
cwd: string;
|
|
267
|
+
stdio: "inherit" | "pipe";
|
|
268
|
+
}) => Promise<{ exitCode: number; stderr: string }>;
|
|
269
|
+
/** Test seam — absolute path of the linked CLI when present. */
|
|
270
|
+
whichImpl?: (
|
|
271
|
+
command: string
|
|
272
|
+
) => string | undefined | Promise<string | undefined>;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
export interface LinkCliOntoPathResult {
|
|
276
|
+
ok: boolean;
|
|
277
|
+
exitCode: number;
|
|
278
|
+
stderr: string;
|
|
279
|
+
/** Absolute path to the linked `agent-sdk` when it resolves on PATH. */
|
|
280
|
+
resolvedPath?: string;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Put `agent-sdk` on the user PATH by installing the already-downloaded
|
|
285
|
+
* project copy into the npm global prefix. `npx @cursor/july init` only
|
|
286
|
+
* runs the package; this is the install that makes bare `agent-sdk …`
|
|
287
|
+
* work afterward.
|
|
288
|
+
*/
|
|
289
|
+
export async function linkCliOntoPath(
|
|
290
|
+
rootDir: string,
|
|
291
|
+
options: LinkCliOntoPathOptions = {}
|
|
292
|
+
): Promise<LinkCliOntoPathResult> {
|
|
293
|
+
const packageDir = join(rootDir, "node_modules", PACKAGE_NAME);
|
|
294
|
+
if (!existsSync(packageDir)) {
|
|
295
|
+
return {
|
|
296
|
+
ok: false,
|
|
297
|
+
exitCode: 1,
|
|
298
|
+
stderr: `missing ${PACKAGE_NAME} under ${packageDir} (run npm install first)`,
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
const stdio = options.stdio ?? "inherit";
|
|
302
|
+
const command = process.platform === "win32" ? "npm.cmd" : "npm";
|
|
303
|
+
const args = ["install", "--global", "--no-fund", "--no-audit", packageDir];
|
|
304
|
+
const runner = options.runner ?? defaultNpmInstallRunner;
|
|
305
|
+
const result = await runner({ command, args, cwd: rootDir, stdio });
|
|
306
|
+
if (result.exitCode !== 0) {
|
|
307
|
+
return {
|
|
308
|
+
ok: false,
|
|
309
|
+
exitCode: result.exitCode,
|
|
310
|
+
stderr: result.stderr,
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
const whichImpl = options.whichImpl ?? resolveLinkedCli;
|
|
314
|
+
const resolvedPath = await whichImpl(CLI_COMMAND_NAME);
|
|
315
|
+
return {
|
|
316
|
+
ok: resolvedPath !== undefined,
|
|
317
|
+
exitCode: 0,
|
|
318
|
+
stderr:
|
|
319
|
+
resolvedPath === undefined
|
|
320
|
+
? `${CLI_COMMAND_NAME} linked into the npm global prefix but is not on PATH; open a new shell or add that bin directory to PATH`
|
|
321
|
+
: "",
|
|
322
|
+
resolvedPath,
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/** Prefer the npm-global bin; ignore an older `agent-sdk` earlier on PATH. */
|
|
327
|
+
async function resolveLinkedCli(
|
|
328
|
+
commandName: string
|
|
329
|
+
): Promise<string | undefined> {
|
|
330
|
+
const prefix = await npmGlobalPrefix();
|
|
331
|
+
if (prefix === undefined) {
|
|
332
|
+
return whichOnPath(commandName);
|
|
333
|
+
}
|
|
334
|
+
const binDir = process.platform === "win32" ? prefix : join(prefix, "bin");
|
|
335
|
+
const exts =
|
|
336
|
+
process.platform === "win32"
|
|
337
|
+
? (process.env.PATHEXT ?? ".EXE;.CMD;.BAT").split(";").filter(Boolean)
|
|
338
|
+
: [""];
|
|
339
|
+
for (const ext of exts) {
|
|
340
|
+
const candidate = join(binDir, `${commandName}${ext}`);
|
|
341
|
+
if (!existsSync(candidate)) {
|
|
342
|
+
continue;
|
|
343
|
+
}
|
|
344
|
+
// Only claim PATH success when this linked bin is the first hit.
|
|
345
|
+
const onPath = whichOnPath(commandName);
|
|
346
|
+
return onPath === candidate ? candidate : undefined;
|
|
347
|
+
}
|
|
348
|
+
return undefined;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
async function npmGlobalPrefix(): Promise<string | undefined> {
|
|
352
|
+
const command = process.platform === "win32" ? "npm.cmd" : "npm";
|
|
353
|
+
return await new Promise((resolvePromise) => {
|
|
354
|
+
const child = spawn(command, ["prefix", "-g"], {
|
|
355
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
356
|
+
env: process.env,
|
|
357
|
+
});
|
|
358
|
+
let out = "";
|
|
359
|
+
child.stdout?.setEncoding("utf8");
|
|
360
|
+
child.stdout?.on("data", (chunk: string) => {
|
|
361
|
+
out += chunk;
|
|
362
|
+
});
|
|
363
|
+
child.on("error", () => resolvePromise(undefined));
|
|
364
|
+
child.on("close", (code) => {
|
|
365
|
+
if (code !== 0) {
|
|
366
|
+
resolvePromise(undefined);
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
const prefix = out.trim();
|
|
370
|
+
resolvePromise(prefix === "" ? undefined : prefix);
|
|
371
|
+
});
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function whichOnPath(commandName: string): string | undefined {
|
|
376
|
+
const pathEnv = process.env.PATH ?? "";
|
|
377
|
+
const sep = process.platform === "win32" ? ";" : ":";
|
|
378
|
+
const exts =
|
|
379
|
+
process.platform === "win32"
|
|
380
|
+
? (process.env.PATHEXT ?? ".EXE;.CMD;.BAT").split(";").filter(Boolean)
|
|
381
|
+
: [""];
|
|
382
|
+
for (const dir of pathEnv.split(sep)) {
|
|
383
|
+
if (dir === "") {
|
|
384
|
+
continue;
|
|
385
|
+
}
|
|
386
|
+
for (const ext of exts) {
|
|
387
|
+
const candidate = join(dir, `${commandName}${ext}`);
|
|
388
|
+
if (existsSync(candidate)) {
|
|
389
|
+
return candidate;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
return undefined;
|
|
394
|
+
}
|
|
395
|
+
|
|
259
396
|
async function defaultNpmInstallRunner(args: {
|
|
260
397
|
command: string;
|
|
261
398
|
args: string[];
|
|
@@ -323,6 +460,11 @@ export interface InitNextStepsOptions {
|
|
|
323
460
|
* `login` (used for `--json` and when an interactive login attempt failed).
|
|
324
461
|
*/
|
|
325
462
|
signedIn: boolean;
|
|
463
|
+
/**
|
|
464
|
+
* True when `agent-sdk` resolves on PATH after init linked the CLI.
|
|
465
|
+
* When false, next steps include a PATH export for `node_modules/.bin`.
|
|
466
|
+
*/
|
|
467
|
+
cliOnPath: boolean;
|
|
326
468
|
/** Override TTY color detection (tests / `--json` callers). */
|
|
327
469
|
color?: boolean;
|
|
328
470
|
}
|
|
@@ -415,6 +557,12 @@ function initNextStepEntries(options: InitNextStepsOptions): Array<{
|
|
|
415
557
|
if (cd !== undefined) {
|
|
416
558
|
steps.push({ command: cd });
|
|
417
559
|
}
|
|
560
|
+
if (!options.cliOnPath) {
|
|
561
|
+
steps.push({
|
|
562
|
+
command: 'export PATH="$PWD/node_modules/.bin:$PATH"',
|
|
563
|
+
note: "use the project-local agent-sdk bin",
|
|
564
|
+
});
|
|
565
|
+
}
|
|
418
566
|
if (!options.signedIn) {
|
|
419
567
|
steps.push({
|
|
420
568
|
command: `${CLI_COMMAND_NAME} login`,
|
|
@@ -436,6 +584,7 @@ export function initNextCommands(options: {
|
|
|
436
584
|
rootDir: string;
|
|
437
585
|
cwd: string;
|
|
438
586
|
signedIn: boolean;
|
|
587
|
+
cliOnPath: boolean;
|
|
439
588
|
}): string[] {
|
|
440
589
|
return initNextStepEntries(options).map((step) => step.command);
|
|
441
590
|
}
|
|
@@ -448,10 +597,10 @@ export function printInitGuide(): void {
|
|
|
448
597
|
process.stdout.write(`${CLI_COMMAND_NAME} — coding agent setup
|
|
449
598
|
|
|
450
599
|
Scaffold a project, then open the playground:
|
|
451
|
-
${
|
|
600
|
+
npx ${PACKAGE_NAME} init ./my-agent # installs deps + puts agent-sdk on PATH
|
|
452
601
|
cd ./my-agent && ${CLI_COMMAND_NAME} dev
|
|
453
602
|
|
|
454
|
-
Recommended loop once the project exists:
|
|
603
|
+
Recommended loop once the project exists (from the project directory):
|
|
455
604
|
1. Edit agent/ + tools under agent/tools/
|
|
456
605
|
2. ${CLI_COMMAND_NAME} info --dir . --json
|
|
457
606
|
3. ${CLI_COMMAND_NAME} run --dir . --message "..."
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ledger-mode serving seam (`serving: "ledger"` on `defineAgent`):
|
|
3
|
+
* coordination only, no inbox. Every engine `send` acquires the session
|
|
4
|
+
* lease before its turn runs and commits (head+1, lease released) after
|
|
5
|
+
* it completes, so cross-pod mutual exclusion, generation fencing, and
|
|
6
|
+
* the committed head sequence are real.
|
|
7
|
+
*
|
|
8
|
+
* **Durability trade-off (deliberate):** there is no durable mailbox.
|
|
9
|
+
* An event that arrives while the lease is held does NOT park in the
|
|
10
|
+
* store — the delivering pod waits *in-process*: local concurrent sends
|
|
11
|
+
* serialize on a per-session promise chain (no claim spinning), and a
|
|
12
|
+
* lease held by another pod is waited out with bounded claim retries. A
|
|
13
|
+
* crash therefore loses the in-flight event exactly as resident serving
|
|
14
|
+
* does. A dead pod's lease simply expires (~30s TTL); the next delivery
|
|
15
|
+
* claims at generation+1 — no janitor is needed because nothing is
|
|
16
|
+
* parked.
|
|
17
|
+
*
|
|
18
|
+
* Re-entrancy caveat (same shape as resident `waitForCompletion`): a
|
|
19
|
+
* send issued from *inside* a running turn against the same session
|
|
20
|
+
* chains behind that turn's serve, so awaiting its resolution from the
|
|
21
|
+
* turn would deadlock. Fire-and-forget such sends.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { randomBytes, randomUUID } from "node:crypto";
|
|
25
|
+
import type { SessionLedger } from "../ledger/session-ledger.js";
|
|
26
|
+
import {
|
|
27
|
+
HEARTBEAT_INTERVAL_MS,
|
|
28
|
+
LEASE_TTL_MS,
|
|
29
|
+
LedgerFencedError,
|
|
30
|
+
} from "../ledger/session-ledger.js";
|
|
31
|
+
import type { ChannelSession, SendMessageOptions } from "../types.js";
|
|
32
|
+
import { newContinuationKey } from "./ids.js";
|
|
33
|
+
|
|
34
|
+
/** Wait between claim retries while another holder has the lease. */
|
|
35
|
+
export const CLAIM_RETRY_DELAY_MS = 250;
|
|
36
|
+
/**
|
|
37
|
+
* Give up waiting for the lease after this long — two full lease TTLs,
|
|
38
|
+
* so a dead holder's expiry (~30s) is always survived with margin.
|
|
39
|
+
*/
|
|
40
|
+
export const CLAIM_WAIT_TIMEOUT_MS: number = 2 * LEASE_TTL_MS;
|
|
41
|
+
|
|
42
|
+
/** The `send` options a ledger delivery carries to the resident turn. */
|
|
43
|
+
export type LedgerDeliverOptions = SendMessageOptions & {
|
|
44
|
+
mode?: "chat" | "task";
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
/** One delivery handed back to the engine's resident path. */
|
|
48
|
+
export interface LedgerTurnRequest {
|
|
49
|
+
channelId: string;
|
|
50
|
+
continuationToken: string;
|
|
51
|
+
message: string;
|
|
52
|
+
options: LedgerDeliverOptions;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface LedgerServingOptions {
|
|
56
|
+
ledger: SessionLedger;
|
|
57
|
+
/**
|
|
58
|
+
* Run one delivery as a resident-path turn (the engine's `send` with
|
|
59
|
+
* the internal ledger-delivery flag). Must resolve once the turn is
|
|
60
|
+
* admitted; the pump awaits `waitForCompletion` itself.
|
|
61
|
+
*/
|
|
62
|
+
deliverTurn: (request: LedgerTurnRequest) => Promise<ChannelSession>;
|
|
63
|
+
logger?: (line: string) => void;
|
|
64
|
+
/** Lease heartbeat cadence while the turn runs (default 10s). */
|
|
65
|
+
heartbeatIntervalMs?: number;
|
|
66
|
+
/** Claim retry cadence while the lease is held (default 250ms). */
|
|
67
|
+
claimRetryDelayMs?: number;
|
|
68
|
+
/** Give-up bound for lease waiting (default 2× lease TTL). */
|
|
69
|
+
claimWaitTimeoutMs?: number;
|
|
70
|
+
clock?: () => number;
|
|
71
|
+
/** Injectable wait (tests use short real delays; keep them tiny). */
|
|
72
|
+
sleep?: (ms: number) => Promise<void>;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export class LedgerServing {
|
|
76
|
+
private readonly ledger: SessionLedger;
|
|
77
|
+
private readonly deliverTurn: LedgerServingOptions["deliverTurn"];
|
|
78
|
+
private readonly logger: (line: string) => void;
|
|
79
|
+
private readonly heartbeatIntervalMs: number;
|
|
80
|
+
private readonly claimRetryDelayMs: number;
|
|
81
|
+
private readonly claimWaitTimeoutMs: number;
|
|
82
|
+
private readonly clock: () => number;
|
|
83
|
+
private readonly sleep: (ms: number) => Promise<void>;
|
|
84
|
+
/**
|
|
85
|
+
* Per-ledger-session in-process serialization: local concurrent sends
|
|
86
|
+
* queue here instead of spinning claim retries against our own lease.
|
|
87
|
+
*/
|
|
88
|
+
private readonly chains = new Map<string, Promise<unknown>>();
|
|
89
|
+
|
|
90
|
+
constructor(options: LedgerServingOptions) {
|
|
91
|
+
this.ledger = options.ledger;
|
|
92
|
+
this.deliverTurn = options.deliverTurn;
|
|
93
|
+
this.logger = options.logger ?? (() => {});
|
|
94
|
+
this.heartbeatIntervalMs =
|
|
95
|
+
options.heartbeatIntervalMs ?? HEARTBEAT_INTERVAL_MS;
|
|
96
|
+
this.claimRetryDelayMs = options.claimRetryDelayMs ?? CLAIM_RETRY_DELAY_MS;
|
|
97
|
+
this.claimWaitTimeoutMs =
|
|
98
|
+
options.claimWaitTimeoutMs ?? CLAIM_WAIT_TIMEOUT_MS;
|
|
99
|
+
this.clock = options.clock ?? Date.now;
|
|
100
|
+
this.sleep =
|
|
101
|
+
options.sleep ??
|
|
102
|
+
((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Settle every queued and in-flight delivery. */
|
|
106
|
+
async close(): Promise<void> {
|
|
107
|
+
await Promise.allSettled([...this.chains.values()]);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Ledger-mode `send`: resolve the session identity, wait for the
|
|
112
|
+
* lease (in-process queue first, then cross-pod claim retries), run
|
|
113
|
+
* the turn, commit. Resolves with the engine session handle after the
|
|
114
|
+
* commit released the lease.
|
|
115
|
+
*/
|
|
116
|
+
async deliver(
|
|
117
|
+
channelId: string,
|
|
118
|
+
message: string,
|
|
119
|
+
options: LedgerDeliverOptions
|
|
120
|
+
): Promise<ChannelSession> {
|
|
121
|
+
const continuationToken = options.continuationToken ?? newContinuationKey();
|
|
122
|
+
const ensured = await this.ledger.ensureSession(
|
|
123
|
+
`${channelId}/${continuationToken}`
|
|
124
|
+
);
|
|
125
|
+
return this.enqueue(ensured.session.sessionId, () =>
|
|
126
|
+
this.serveOne(ensured.session.sessionId, {
|
|
127
|
+
channelId,
|
|
128
|
+
continuationToken,
|
|
129
|
+
message,
|
|
130
|
+
options,
|
|
131
|
+
})
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** Chain a delivery behind the session's in-flight ones. */
|
|
136
|
+
private enqueue(
|
|
137
|
+
ledgerSessionId: string,
|
|
138
|
+
run: () => Promise<ChannelSession>
|
|
139
|
+
): Promise<ChannelSession> {
|
|
140
|
+
const prior = this.chains.get(ledgerSessionId) ?? Promise.resolve();
|
|
141
|
+
const next = prior.then(
|
|
142
|
+
() => run(),
|
|
143
|
+
() => run()
|
|
144
|
+
);
|
|
145
|
+
this.chains.set(ledgerSessionId, next);
|
|
146
|
+
void next
|
|
147
|
+
.catch(() => {})
|
|
148
|
+
.finally(() => {
|
|
149
|
+
if (this.chains.get(ledgerSessionId) === next) {
|
|
150
|
+
this.chains.delete(ledgerSessionId);
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
return next;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** claim (waiting) → resident turn → commit-release. */
|
|
157
|
+
private async serveOne(
|
|
158
|
+
ledgerSessionId: string,
|
|
159
|
+
request: LedgerTurnRequest
|
|
160
|
+
): Promise<ChannelSession> {
|
|
161
|
+
const lease = await this.claimWithWait(ledgerSessionId);
|
|
162
|
+
let fenced = false;
|
|
163
|
+
const heartbeat = setInterval(() => {
|
|
164
|
+
this.ledger.heartbeat(ledgerSessionId, lease.generation).then(
|
|
165
|
+
(result) => {
|
|
166
|
+
if (!result.ok) {
|
|
167
|
+
fenced = true;
|
|
168
|
+
}
|
|
169
|
+
},
|
|
170
|
+
() => {
|
|
171
|
+
// Transient KV failure: the next beat retries within the TTL.
|
|
172
|
+
}
|
|
173
|
+
);
|
|
174
|
+
}, this.heartbeatIntervalMs);
|
|
175
|
+
heartbeat.unref?.();
|
|
176
|
+
try {
|
|
177
|
+
let handle: ChannelSession;
|
|
178
|
+
try {
|
|
179
|
+
handle = await this.deliverTurn(request);
|
|
180
|
+
await handle.waitForCompletion();
|
|
181
|
+
} catch (error) {
|
|
182
|
+
// The turn never committed; free the session for the next
|
|
183
|
+
// delivery instead of making it wait out the TTL. A fenced
|
|
184
|
+
// holder's release is a no-op.
|
|
185
|
+
await this.ledger
|
|
186
|
+
.release(ledgerSessionId, lease.generation)
|
|
187
|
+
.catch(() => {});
|
|
188
|
+
throw error;
|
|
189
|
+
}
|
|
190
|
+
if (fenced) {
|
|
191
|
+
// A new holder took over mid-turn (our lease lapsed). Discard:
|
|
192
|
+
// committing would fence anyway, and warm state is now stale.
|
|
193
|
+
this.logger(
|
|
194
|
+
`[ledger] session ${ledgerSessionId} fenced mid-turn (generation ${lease.generation}); discarding without commit`
|
|
195
|
+
);
|
|
196
|
+
throw new LedgerFencedError(
|
|
197
|
+
`deliver(${ledgerSessionId}): fenced mid-turn at generation ${lease.generation}`
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
await this.ledger.commit(ledgerSessionId, {
|
|
201
|
+
generation: lease.generation,
|
|
202
|
+
expectedHead: lease.head,
|
|
203
|
+
commitId: randomUUID(),
|
|
204
|
+
});
|
|
205
|
+
return handle;
|
|
206
|
+
} finally {
|
|
207
|
+
clearInterval(heartbeat);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Claim the lease, waiting out another holder with bounded retries.
|
|
213
|
+
* The common waits are short: our own pod's deliveries never reach
|
|
214
|
+
* here concurrently (the in-process chain serializes them), so a held
|
|
215
|
+
* lease means another pod is serving — usually for one turn — or died
|
|
216
|
+
* (expiry unblocks us at ~30s).
|
|
217
|
+
*/
|
|
218
|
+
private async claimWithWait(
|
|
219
|
+
ledgerSessionId: string
|
|
220
|
+
): Promise<{ generation: number; head: number }> {
|
|
221
|
+
const start = this.clock();
|
|
222
|
+
for (;;) {
|
|
223
|
+
const claim = await this.ledger.claim(ledgerSessionId);
|
|
224
|
+
if (claim.claimed) {
|
|
225
|
+
return { generation: claim.lease.generation, head: claim.head };
|
|
226
|
+
}
|
|
227
|
+
if (this.clock() - start >= this.claimWaitTimeoutMs) {
|
|
228
|
+
throw new Error(
|
|
229
|
+
`ledger serving: session ${ledgerSessionId} is still leased by ${claim.holder} after ${this.claimWaitTimeoutMs}ms; giving up on this delivery`
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
await this.sleep(this.claimRetryDelayMs);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/** Mint the lease holder id for this process. */
|
|
238
|
+
export function newLedgerPodId(): string {
|
|
239
|
+
return `pod_${randomBytes(6).toString("hex")}`;
|
|
240
|
+
}
|