@cursor/july 0.1.1 → 0.1.2
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 +24 -2
- package/README.md +25 -15
- package/dist/bin/agent-serve.js +101 -12
- package/dist/channels/github/api.d.ts.map +1 -1
- package/dist/channels/github/api.js +31 -14
- package/dist/channels/github/cursor-account.d.ts +43 -0
- package/dist/channels/github/cursor-account.d.ts.map +1 -0
- package/dist/channels/github/cursor-account.js +95 -0
- package/dist/channels/github/github-channel.d.ts.map +1 -1
- package/dist/channels/github/github-channel.js +46 -9
- package/dist/channels/github/index.d.ts +2 -2
- package/dist/channels/github/index.js +2 -2
- package/dist/channels/github/types.d.ts +17 -0
- package/dist/channels/github/types.d.ts.map +1 -1
- package/dist/channels/slack/slack-channel.d.ts +8 -2
- package/dist/channels/slack/slack-channel.d.ts.map +1 -1
- package/dist/channels/slack/slack-channel.js +8 -0
- package/dist/channels/slack/types.d.ts +24 -3
- package/dist/channels/slack/types.d.ts.map +1 -1
- package/dist/channels/slack/types.js +15 -1
- package/dist/docs/404.html +2 -2
- package/dist/docs/ab.html +8 -8
- package/dist/docs/assets/{ab.md.COdXkces.js → ab.md.BMCZ6Hd7.js} +3 -3
- package/dist/docs/assets/{ab.md.COdXkces.lean.js → ab.md.BMCZ6Hd7.lean.js} +1 -1
- package/dist/docs/assets/{app.DqfFEmJd.js → app.Oje4vhlk.js} +1 -1
- package/dist/docs/assets/chunks/@localSearchIndexroot.zwQ9RCQ7.js +1 -0
- package/dist/docs/assets/chunks/{VPLocalSearchBox.BaLEdS15.js → VPLocalSearchBox.H5XZ2zCB.js} +1 -1
- package/dist/docs/assets/chunks/{theme.CZRvu_0q.js → theme.CTR_TuaE.js} +2 -2
- package/dist/docs/assets/{deployment.md.Dx1TYNk5.js → deployment.md.DTKwE15Z.js} +3 -3
- package/dist/docs/assets/{deployment.md.Dx1TYNk5.lean.js → deployment.md.DTKwE15Z.lean.js} +1 -1
- package/dist/docs/assets/{evals.md.DPZ_MAnI.js → evals.md.DAgEc_hL.js} +3 -3
- package/dist/docs/assets/{guides_agent-to-agent.md.CrtrsySy.js → guides_agent-to-agent.md.Bpzgq2Pq.js} +1 -1
- package/dist/docs/assets/{guides_cloud-runtime.md.CYlNMTNp.js → guides_cloud-runtime.md.gVzabdQL.js} +1 -1
- package/dist/docs/assets/{guides_github.md.DwbKhCeS.js → guides_github.md.DOOCpqsW.js} +11 -4
- package/dist/docs/assets/{guides_github.md.DwbKhCeS.lean.js → guides_github.md.DOOCpqsW.lean.js} +1 -1
- package/dist/docs/assets/{guides_human-in-the-loop.md.Dvuctx7s.js → guides_human-in-the-loop.md.DlUqsp1S.js} +2 -2
- package/dist/docs/assets/{guides_slack.md.bv41fHfW.js → guides_slack.md.CCwqHvSV.js} +4 -4
- package/dist/docs/assets/{guides_slack.md.bv41fHfW.lean.js → guides_slack.md.CCwqHvSV.lean.js} +1 -1
- package/dist/docs/assets/{guides_webhooks.md.hFTik3lf.js → guides_webhooks.md.B1EswtUu.js} +2 -2
- package/dist/docs/assets/index.md.m81y7TY7.js +20 -0
- package/dist/docs/assets/{index.md.BPKcj5AI.lean.js → index.md.m81y7TY7.lean.js} +1 -1
- package/dist/docs/assets/quickstart.md.CfU8_uTC.js +192 -0
- package/dist/docs/assets/quickstart.md.CfU8_uTC.lean.js +1 -0
- package/dist/docs/assets/{reference_agent-config.md.Bpd7HQwf.js → reference_agent-config.md.DrW2JUM8.js} +4 -4
- package/dist/docs/assets/{reference_agent-config.md.Bpd7HQwf.lean.js → reference_agent-config.md.DrW2JUM8.lean.js} +1 -1
- package/dist/docs/assets/{reference_channels.md.D7JTR03W.js → reference_channels.md.DdmiKgqf.js} +4 -4
- package/dist/docs/assets/{reference_channels.md.D7JTR03W.lean.js → reference_channels.md.DdmiKgqf.lean.js} +1 -1
- package/dist/docs/assets/{reference_connections.md.C3vNH_DE.js → reference_connections.md.zaEYCLHT.js} +1 -1
- package/dist/docs/assets/{reference_hooks.md.BCEc3MyM.js → reference_hooks.md.DyLVfE1O.js} +1 -1
- package/dist/docs/assets/{reference_hooks.md.BCEc3MyM.lean.js → reference_hooks.md.DyLVfE1O.lean.js} +1 -1
- package/dist/docs/assets/{reference_http-api.md.DBAahtdz.js → reference_http-api.md.Dx_nmDG6.js} +1 -1
- package/dist/docs/assets/{reference_instructions.md.BC05LEQ8.js → reference_instructions.md.CgoV-YEb.js} +9 -7
- package/dist/docs/assets/{reference_instructions.md.BC05LEQ8.lean.js → reference_instructions.md.CgoV-YEb.lean.js} +1 -1
- package/dist/docs/assets/{reference_schedules.md.D7qijxLk.js → reference_schedules.md.w_F2mXB6.js} +2 -2
- package/dist/docs/assets/{reference_skills.md.VQnlBT3Q.js → reference_skills.md.B_jHN7JL.js} +3 -3
- package/dist/docs/assets/{reference_skills.md.VQnlBT3Q.lean.js → reference_skills.md.B_jHN7JL.lean.js} +1 -1
- package/dist/docs/assets/{reference_subagents.md.CIRAVcPK.js → reference_subagents.md.zWAMNfi1.js} +1 -1
- package/dist/docs/assets/{reference_tools.md.DF5kwlt0.js → reference_tools.md.CqgJroI0.js} +2 -2
- package/dist/docs/assets/scaffolding-agents.md.C3pTrmoE.js +1 -0
- package/dist/docs/assets/scaffolding-agents.md.C3pTrmoE.lean.js +1 -0
- package/dist/docs/assets/storage.md.CVnInNiN.js +17 -0
- package/dist/docs/assets/storage.md.CVnInNiN.lean.js +1 -0
- package/dist/docs/building-with-agents.html +4 -4
- package/dist/docs/concepts.html +4 -4
- package/dist/docs/deployment.html +7 -7
- package/dist/docs/evals.html +7 -7
- package/dist/docs/guides/agent-to-agent.html +6 -6
- package/dist/docs/guides/cloud-runtime.html +5 -5
- package/dist/docs/guides/github.html +14 -7
- package/dist/docs/guides/human-in-the-loop.html +6 -6
- package/dist/docs/guides/slack.html +8 -8
- package/dist/docs/guides/webhooks.html +6 -6
- package/dist/docs/hashmap.json +1 -1
- package/dist/docs/hillclimbing.html +5 -5
- package/dist/docs/index.html +7 -7
- package/dist/docs/quickstart.html +180 -23
- package/dist/docs/reference/agent-config.html +7 -7
- package/dist/docs/reference/channels.html +8 -8
- package/dist/docs/reference/cli.html +4 -4
- package/dist/docs/reference/connections.html +5 -5
- package/dist/docs/reference/hooks.html +5 -5
- package/dist/docs/reference/http-api.html +6 -6
- package/dist/docs/reference/instructions.html +13 -11
- package/dist/docs/reference/playground.html +4 -4
- package/dist/docs/reference/project-layout.html +4 -4
- package/dist/docs/reference/schedules.html +7 -7
- package/dist/docs/reference/sessions.html +4 -4
- package/dist/docs/reference/skills.html +6 -6
- package/dist/docs/reference/subagents.html +6 -6
- package/dist/docs/reference/tools.html +7 -7
- package/dist/docs/scaffolding-agents.html +5 -5
- package/dist/docs/storage.html +41 -0
- package/dist/docs/troubleshooting.html +4 -4
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/internal/cli-deploy.d.ts +52 -0
- package/dist/internal/cli-deploy.d.ts.map +1 -0
- package/dist/internal/cli-deploy.js +731 -0
- package/dist/internal/cursor/backend-client.d.ts +10 -1
- package/dist/internal/cursor/backend-client.d.ts.map +1 -1
- package/dist/internal/cursor/backend-client.js +79 -1
- package/dist/internal/cursor/github-credentials.d.ts +44 -0
- package/dist/internal/cursor/github-credentials.d.ts.map +1 -0
- package/dist/internal/cursor/github-credentials.js +195 -0
- package/dist/internal/deploy-client.d.ts +176 -0
- package/dist/internal/deploy-client.d.ts.map +1 -0
- package/dist/internal/deploy-client.js +375 -0
- package/dist/internal/discovery.d.ts.map +1 -1
- package/dist/internal/discovery.js +73 -5
- package/dist/internal/distribution.d.ts.map +1 -1
- package/dist/internal/distribution.js +1 -0
- package/dist/internal/eval-run-store.d.ts +22 -3
- package/dist/internal/eval-run-store.d.ts.map +1 -1
- package/dist/internal/eval-run-store.js +37 -19
- package/dist/internal/handleAgentServeTrigger.d.ts.map +1 -1
- package/dist/internal/handleAgentServeTrigger.js +10 -12
- package/dist/internal/host-platforms.d.ts +7 -2
- package/dist/internal/host-platforms.d.ts.map +1 -1
- package/dist/internal/host-platforms.js +15 -10
- package/dist/internal/hosting.d.ts +37 -0
- package/dist/internal/hosting.d.ts.map +1 -0
- package/dist/internal/hosting.js +67 -0
- package/dist/internal/reminder-runner.d.ts +7 -0
- package/dist/internal/reminder-runner.d.ts.map +1 -1
- package/dist/internal/reminder-runner.js +50 -6
- package/dist/internal/reminder-store.d.ts +2 -0
- package/dist/internal/reminder-store.d.ts.map +1 -1
- package/dist/internal/reminder-store.js +18 -0
- package/dist/internal/server.d.ts.map +1 -1
- package/dist/internal/server.js +173 -42
- package/dist/internal/session-engine.d.ts +49 -0
- package/dist/internal/session-engine.d.ts.map +1 -1
- package/dist/internal/session-engine.js +246 -17
- package/dist/internal/storage-coordinator.d.ts +139 -0
- package/dist/internal/storage-coordinator.d.ts.map +1 -0
- package/dist/internal/storage-coordinator.js +499 -0
- package/dist/playground/assets/cursor-icons-outline-BxTT_FVJ.woff2 +0 -0
- package/dist/playground/assets/index-B1Qc9h2u.css +1 -0
- package/dist/playground/assets/{index-FlWjhg3x.js → index-CpDYCj8W.js} +42 -42
- package/dist/playground/index.html +2 -2
- package/dist/storage.d.ts +204 -0
- package/dist/storage.d.ts.map +1 -0
- package/dist/storage.js +153 -0
- package/dist/types.d.ts +44 -3
- package/dist/types.d.ts.map +1 -1
- package/docs/README.md +3 -2
- package/docs/guides/github.md +43 -8
- package/docs/guides/slack.md +1 -1
- package/docs/quickstart.md +329 -51
- package/docs/reference/instructions.md +8 -6
- package/docs/scaffolding-agents.md +1 -1
- package/docs/storage.md +98 -0
- package/package.json +8 -1
- package/skills/github/SKILL.md +9 -1
- package/src/bin/agent-serve.ts +139 -0
- package/src/channels/github/api.ts +42 -23
- package/src/channels/github/cursor-account.ts +165 -0
- package/src/channels/github/github-channel.ts +66 -6
- package/src/channels/github/index.ts +2 -2
- package/src/channels/github/types.ts +19 -0
- package/src/channels/slack/slack-channel.ts +17 -3
- package/src/channels/slack/types.ts +44 -3
- package/src/index.ts +13 -0
- package/src/internal/cli-deploy.ts +940 -0
- package/src/internal/cursor/backend-client.ts +103 -1
- package/src/internal/cursor/github-credentials.ts +248 -0
- package/src/internal/deploy-client.ts +591 -0
- package/src/internal/discovery.ts +88 -1
- package/src/internal/distribution.ts +1 -0
- package/src/internal/eval-run-store.ts +48 -19
- package/src/internal/handleAgentServeTrigger.ts +10 -12
- package/src/internal/host-platforms.ts +28 -11
- package/src/internal/hosting.ts +77 -0
- package/src/internal/reminder-runner.ts +50 -6
- package/src/internal/reminder-store.ts +21 -0
- package/src/internal/server.ts +213 -27
- package/src/internal/session-engine.ts +285 -7
- package/src/internal/storage-coordinator.ts +615 -0
- package/src/storage.ts +325 -0
- package/src/types.ts +40 -2
- package/dist/docs/assets/chunks/@localSearchIndexroot.CcVk1uKq.js +0 -1
- package/dist/docs/assets/index.md.BPKcj5AI.js +0 -20
- package/dist/docs/assets/quickstart.md.tVPiGK_L.js +0 -35
- package/dist/docs/assets/quickstart.md.tVPiGK_L.lean.js +0 -1
- package/dist/docs/assets/scaffolding-agents.md.CyYfWGdc.js +0 -1
- package/dist/docs/assets/scaffolding-agents.md.CyYfWGdc.lean.js +0 -1
- package/dist/playground/assets/cursor-icons-outline-oY2V_mvK.woff2 +0 -0
- package/dist/playground/assets/index-1K-hG-7p.css +0 -1
- /package/dist/docs/assets/{evals.md.DPZ_MAnI.lean.js → evals.md.DAgEc_hL.lean.js} +0 -0
- /package/dist/docs/assets/{guides_agent-to-agent.md.CrtrsySy.lean.js → guides_agent-to-agent.md.Bpzgq2Pq.lean.js} +0 -0
- /package/dist/docs/assets/{guides_cloud-runtime.md.CYlNMTNp.lean.js → guides_cloud-runtime.md.gVzabdQL.lean.js} +0 -0
- /package/dist/docs/assets/{guides_human-in-the-loop.md.Dvuctx7s.lean.js → guides_human-in-the-loop.md.DlUqsp1S.lean.js} +0 -0
- /package/dist/docs/assets/{guides_webhooks.md.hFTik3lf.lean.js → guides_webhooks.md.B1EswtUu.lean.js} +0 -0
- /package/dist/docs/assets/{reference_connections.md.C3vNH_DE.lean.js → reference_connections.md.zaEYCLHT.lean.js} +0 -0
- /package/dist/docs/assets/{reference_http-api.md.DBAahtdz.lean.js → reference_http-api.md.Dx_nmDG6.lean.js} +0 -0
- /package/dist/docs/assets/{reference_schedules.md.D7qijxLk.lean.js → reference_schedules.md.w_F2mXB6.lean.js} +0 -0
- /package/dist/docs/assets/{reference_subagents.md.CIRAVcPK.lean.js → reference_subagents.md.zWAMNfi1.lean.js} +0 -0
- /package/dist/docs/assets/{reference_tools.md.DF5kwlt0.lean.js → reference_tools.md.CqgJroI0.lean.js} +0 -0
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
* event handlers and hooks.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import { mkdir, rm } from "node:fs/promises";
|
|
11
|
-
import { join } from "node:path";
|
|
10
|
+
import { mkdir, rm, writeFile } from "node:fs/promises";
|
|
11
|
+
import { dirname, join } from "node:path";
|
|
12
12
|
import type { SDKCustomTool } from "@cursor/sdk";
|
|
13
13
|
import {
|
|
14
14
|
type ABDefinition,
|
|
@@ -68,6 +68,7 @@ import { McpHost } from "./mcp-host.js";
|
|
|
68
68
|
import { assertCloudCanHonorAccountServersFilters } from "./resolved-connections.js";
|
|
69
69
|
import type { AgentRunner } from "./sdk-runner.js";
|
|
70
70
|
import { SessionStore } from "./session-store.js";
|
|
71
|
+
import { StorageCoordinator } from "./storage-coordinator.js";
|
|
71
72
|
import { normalizeToolResult, toolCallErrorMessage } from "./tool-result.js";
|
|
72
73
|
import {
|
|
73
74
|
buildAgentToolsCatalog,
|
|
@@ -157,6 +158,7 @@ export interface SessionEngineOptions {
|
|
|
157
158
|
stateRoot: string;
|
|
158
159
|
runner: AgentRunner;
|
|
159
160
|
logger?: (line: string) => void;
|
|
161
|
+
platforms?: HostPlatforms;
|
|
160
162
|
}
|
|
161
163
|
|
|
162
164
|
/**
|
|
@@ -192,6 +194,21 @@ export class SessionEngine {
|
|
|
192
194
|
readonly project: AgentProject;
|
|
193
195
|
readonly stateRoot: string;
|
|
194
196
|
readonly sessions: SessionStore;
|
|
197
|
+
/**
|
|
198
|
+
* Runtime for `agent/storage.ts` (`defineStorage`), when
|
|
199
|
+
* authored. All durable state changes funnel through it (session
|
|
200
|
+
* records, event chunks, A/B samples/snapshots); the eval run store
|
|
201
|
+
* attaches via {@link StorageCoordinator.asEvalRunPersistence} at
|
|
202
|
+
* serve start.
|
|
203
|
+
*/
|
|
204
|
+
readonly storage: StorageCoordinator | undefined;
|
|
205
|
+
/** In-flight lazy restores, deduped per channel + continuation key. */
|
|
206
|
+
private readonly restoreInFlight = new Map<
|
|
207
|
+
string,
|
|
208
|
+
Promise<SessionRecord | undefined>
|
|
209
|
+
>();
|
|
210
|
+
/** Once-per-process cache for the A/B snapshot backfill (see abSnapshot). */
|
|
211
|
+
private restoredAbSnapshot: Promise<ABSnapshot | undefined> | undefined;
|
|
195
212
|
|
|
196
213
|
private readonly runner: AgentRunner;
|
|
197
214
|
private readonly logger: (line: string) => void;
|
|
@@ -229,15 +246,26 @@ export class SessionEngine {
|
|
|
229
246
|
`[agent-serve] failed to persist events for ${sessionId}: ${describeError(error)}`
|
|
230
247
|
)
|
|
231
248
|
);
|
|
249
|
+
this.storage =
|
|
250
|
+
options.project.storage === undefined
|
|
251
|
+
? undefined
|
|
252
|
+
: new StorageCoordinator({
|
|
253
|
+
definition: options.project.storage,
|
|
254
|
+
agentName: options.project.name,
|
|
255
|
+
projectRoot: options.project.rootDir,
|
|
256
|
+
logger: this.logger,
|
|
257
|
+
});
|
|
258
|
+
// Domain-specific hooks win; the defineStorage sink is the fallback.
|
|
259
|
+
const persistSamples =
|
|
260
|
+
options.project.abConfig?.persistSamples ??
|
|
261
|
+
this.storage?.asABSamplePersistence();
|
|
232
262
|
this.abCollector = new ABCollector(
|
|
233
263
|
options.project.abs,
|
|
234
264
|
this.logger,
|
|
235
265
|
async (sessionId) => (await this.logs.get(sessionId)).snapshot(0),
|
|
236
266
|
{
|
|
237
267
|
projectRoot: options.project.rootDir,
|
|
238
|
-
|
|
239
|
-
? {}
|
|
240
|
-
: { persistSamples: options.project.abConfig.persistSamples }),
|
|
268
|
+
persistSamples,
|
|
241
269
|
}
|
|
242
270
|
);
|
|
243
271
|
// Peer and Cursor-account transports are symbolic until the serve host
|
|
@@ -248,7 +276,7 @@ export class SessionEngine {
|
|
|
248
276
|
(connection) => !isSymbolicConnectionTransport(connection.transport)
|
|
249
277
|
)
|
|
250
278
|
);
|
|
251
|
-
this.platforms = createHostPlatforms();
|
|
279
|
+
this.platforms = options.platforms ?? createHostPlatforms();
|
|
252
280
|
for (const channel of options.project.channels) {
|
|
253
281
|
this.channelsById.set(channel.id, channel.definition);
|
|
254
282
|
}
|
|
@@ -305,13 +333,21 @@ export class SessionEngine {
|
|
|
305
333
|
options: EngineSendOptions = {}
|
|
306
334
|
): Promise<ChannelSession> {
|
|
307
335
|
const auth = options.auth ?? null;
|
|
308
|
-
|
|
336
|
+
let existing =
|
|
309
337
|
options.continuationToken === undefined
|
|
310
338
|
? undefined
|
|
311
339
|
: await this.sessions.findByContinuation(
|
|
312
340
|
channelId,
|
|
313
341
|
options.continuationToken
|
|
314
342
|
);
|
|
343
|
+
if (existing === undefined && options.continuationToken !== undefined) {
|
|
344
|
+
// Local miss: the session may live only in the storage sink (it
|
|
345
|
+
// fell outside the startup restore window, or restore is lazy-only).
|
|
346
|
+
existing = await this.restoreSessionByContinuation(
|
|
347
|
+
channelId,
|
|
348
|
+
options.continuationToken
|
|
349
|
+
);
|
|
350
|
+
}
|
|
315
351
|
|
|
316
352
|
if (existing !== undefined) {
|
|
317
353
|
if (!samePrincipal(existing.auth, auth)) {
|
|
@@ -361,6 +397,7 @@ export class SessionEngine {
|
|
|
361
397
|
updatedAt: now,
|
|
362
398
|
};
|
|
363
399
|
await this.sessions.save(record);
|
|
400
|
+
this.storage?.sessionRecord(record);
|
|
364
401
|
await this.appendEvent(record.sessionId, {
|
|
365
402
|
type: "session.started",
|
|
366
403
|
data: { channelId },
|
|
@@ -1337,6 +1374,9 @@ export class SessionEngine {
|
|
|
1337
1374
|
}
|
|
1338
1375
|
|
|
1339
1376
|
private queueDispatch(sessionId: string, event: SessionEvent): void {
|
|
1377
|
+
// Single funnel for both append paths (appendEvent + the held-log emit),
|
|
1378
|
+
// so the storage sink sees every durable event exactly once.
|
|
1379
|
+
this.storage?.event(event);
|
|
1340
1380
|
const chain = this.dispatchChains.get(sessionId) ?? Promise.resolve();
|
|
1341
1381
|
const next = chain
|
|
1342
1382
|
.then(() => this.dispatchEvent(sessionId, event))
|
|
@@ -1344,6 +1384,12 @@ export class SessionEngine {
|
|
|
1344
1384
|
this.logger(
|
|
1345
1385
|
`[agent-serve] event dispatch failed for ${sessionId}: ${describeError(error)}`
|
|
1346
1386
|
);
|
|
1387
|
+
})
|
|
1388
|
+
.then(() => {
|
|
1389
|
+
// Turn-end flush happens here — after the boundary event's handlers
|
|
1390
|
+
// and their record updates settled — so the persisted record carries
|
|
1391
|
+
// the turn's final channel state and continuation token.
|
|
1392
|
+
this.storage?.eventDispatched(event);
|
|
1347
1393
|
});
|
|
1348
1394
|
this.dispatchChains.set(sessionId, next);
|
|
1349
1395
|
}
|
|
@@ -1508,6 +1554,25 @@ export class SessionEngine {
|
|
|
1508
1554
|
abConfig?.maxPlaygroundSessions
|
|
1509
1555
|
);
|
|
1510
1556
|
const foldedSessions = sessions.slice(0, maxPlaygroundSessions);
|
|
1557
|
+
// Backfill for a replacement host with nothing local to fold (e.g.
|
|
1558
|
+
// lazy-only restore before any session resumed): serve the persisted
|
|
1559
|
+
// aggregate instead of an empty surface. Gated on the host having no
|
|
1560
|
+
// sessions AT ALL — the persisted snapshot spans every principal, so a
|
|
1561
|
+
// caller who merely owns no sessions must not see it. Loaded once per
|
|
1562
|
+
// process (playground polls this route ~every 4s); skip the persist
|
|
1563
|
+
// side effect below — re-saving a restored snapshot would echo stale
|
|
1564
|
+
// data back into the store.
|
|
1565
|
+
if (
|
|
1566
|
+
foldedSessions.length === 0 &&
|
|
1567
|
+
this.storage !== undefined &&
|
|
1568
|
+
(await this.sessions.list()).length === 0
|
|
1569
|
+
) {
|
|
1570
|
+
this.restoredAbSnapshot ??= this.storage.getLatestAbSnapshot();
|
|
1571
|
+
const restoredSnapshot = await this.restoredAbSnapshot;
|
|
1572
|
+
if (restoredSnapshot !== undefined) {
|
|
1573
|
+
return restoredSnapshot;
|
|
1574
|
+
}
|
|
1575
|
+
}
|
|
1511
1576
|
const snapshot = await buildABSnapshot({
|
|
1512
1577
|
experiments: this.project.abs,
|
|
1513
1578
|
agentName: this.project.name,
|
|
@@ -1535,6 +1600,9 @@ export class SessionEngine {
|
|
|
1535
1600
|
`[agent-serve] ab persistSnapshots.save threw: ${describeError(error)}`
|
|
1536
1601
|
);
|
|
1537
1602
|
}
|
|
1603
|
+
} else {
|
|
1604
|
+
// defineStorage fallback (throttled inside the coordinator).
|
|
1605
|
+
this.storage?.abSnapshot(snapshot);
|
|
1538
1606
|
}
|
|
1539
1607
|
return snapshot;
|
|
1540
1608
|
}
|
|
@@ -1700,6 +1768,7 @@ export class SessionEngine {
|
|
|
1700
1768
|
updatedAt: new Date().toISOString(),
|
|
1701
1769
|
};
|
|
1702
1770
|
await this.sessions.save(updated);
|
|
1771
|
+
this.storage?.sessionRecord(updated);
|
|
1703
1772
|
return updated;
|
|
1704
1773
|
});
|
|
1705
1774
|
this.updateChains.set(
|
|
@@ -1713,6 +1782,203 @@ export class SessionEngine {
|
|
|
1713
1782
|
// Lifecycle
|
|
1714
1783
|
// ==========================================================================
|
|
1715
1784
|
|
|
1785
|
+
/**
|
|
1786
|
+
* Restore sessions from the authored storage sink. Called once at
|
|
1787
|
+
* serve start, before channels accept traffic: any persisted session
|
|
1788
|
+
* missing from the local `--state-root` is materialized there — events
|
|
1789
|
+
* as `events.ndjson`, record as `session.json` — after which every
|
|
1790
|
+
* existing read path (continuation lookup, event replay/streams, A/B
|
|
1791
|
+
* fold hydration) works unchanged. Local state always wins over the
|
|
1792
|
+
* sink, so restarts on the same host are no-ops.
|
|
1793
|
+
*
|
|
1794
|
+
* Failures are logged and skipped; a broken store (including rows that
|
|
1795
|
+
* violate the SessionRecord shape) never blocks serve start.
|
|
1796
|
+
*/
|
|
1797
|
+
async restoreStoredSessions(): Promise<void> {
|
|
1798
|
+
const storage = this.storage;
|
|
1799
|
+
if (storage === undefined || !storage.canBulkRestore) {
|
|
1800
|
+
return;
|
|
1801
|
+
}
|
|
1802
|
+
const policy = storage.policy.restore;
|
|
1803
|
+
if (policy === "off") {
|
|
1804
|
+
return;
|
|
1805
|
+
}
|
|
1806
|
+
try {
|
|
1807
|
+
await this.runBulkRestore(storage, policy);
|
|
1808
|
+
} catch (error) {
|
|
1809
|
+
this.logger(
|
|
1810
|
+
`[agent-serve] session restore failed: ${describeError(error)}`
|
|
1811
|
+
);
|
|
1812
|
+
}
|
|
1813
|
+
}
|
|
1814
|
+
|
|
1815
|
+
private async runBulkRestore(
|
|
1816
|
+
storage: StorageCoordinator,
|
|
1817
|
+
policy: { maxSessions: number; maxAgeMs: number; maxTotalBytes: number }
|
|
1818
|
+
): Promise<void> {
|
|
1819
|
+
const loaded = await storage.listSessions();
|
|
1820
|
+
// Guardrails on whatever the sink returned: drop invalid rows and
|
|
1821
|
+
// sessions older than the age window, then restore newest-first under
|
|
1822
|
+
// the count cap. Anything filtered out here stays reachable through
|
|
1823
|
+
// the lazy continuation-lookup path.
|
|
1824
|
+
const cutoff = Date.now() - policy.maxAgeMs;
|
|
1825
|
+
const candidates = loaded
|
|
1826
|
+
.map((record) => ({
|
|
1827
|
+
record,
|
|
1828
|
+
// Numeric timestamp for both the age filter and the sort; a
|
|
1829
|
+
// non-string updatedAt (e.g. a raw DB Date from an untyped sink)
|
|
1830
|
+
// parses to NaN and is dropped instead of crashing serve start.
|
|
1831
|
+
updatedAt:
|
|
1832
|
+
typeof record?.updatedAt === "string"
|
|
1833
|
+
? Date.parse(record.updatedAt)
|
|
1834
|
+
: Number.NaN,
|
|
1835
|
+
}))
|
|
1836
|
+
.filter(
|
|
1837
|
+
({ record, updatedAt }) =>
|
|
1838
|
+
typeof record?.sessionId === "string" &&
|
|
1839
|
+
record.sessionId !== "" &&
|
|
1840
|
+
updatedAt >= cutoff
|
|
1841
|
+
)
|
|
1842
|
+
.sort((a, b) => b.updatedAt - a.updatedAt)
|
|
1843
|
+
.slice(0, policy.maxSessions)
|
|
1844
|
+
.map(({ record }) => record);
|
|
1845
|
+
|
|
1846
|
+
let restored = 0;
|
|
1847
|
+
let budget = policy.maxTotalBytes;
|
|
1848
|
+
// Sink round trips dominate restore latency, so event streams are
|
|
1849
|
+
// fetched with bounded parallelism; writes stay sequential, newest
|
|
1850
|
+
// first, so the byte budget is enforced deterministically.
|
|
1851
|
+
outer: for (
|
|
1852
|
+
let i = 0;
|
|
1853
|
+
i < candidates.length;
|
|
1854
|
+
i += RESTORE_FETCH_CONCURRENCY
|
|
1855
|
+
) {
|
|
1856
|
+
const chunk = candidates.slice(i, i + RESTORE_FETCH_CONCURRENCY);
|
|
1857
|
+
const fetched = await Promise.all(
|
|
1858
|
+
chunk.map(async (record) => {
|
|
1859
|
+
try {
|
|
1860
|
+
// Local state always wins over the sink.
|
|
1861
|
+
if ((await this.sessions.get(record.sessionId)) !== undefined) {
|
|
1862
|
+
return undefined;
|
|
1863
|
+
}
|
|
1864
|
+
const events = await storage.listSessionEvents(record.sessionId);
|
|
1865
|
+
return { record, events };
|
|
1866
|
+
} catch (error) {
|
|
1867
|
+
this.logger(
|
|
1868
|
+
`[agent-serve] failed to restore session ${record.sessionId}: ${describeError(error)}`
|
|
1869
|
+
);
|
|
1870
|
+
return undefined;
|
|
1871
|
+
}
|
|
1872
|
+
})
|
|
1873
|
+
);
|
|
1874
|
+
for (const item of fetched) {
|
|
1875
|
+
if (item === undefined) {
|
|
1876
|
+
continue;
|
|
1877
|
+
}
|
|
1878
|
+
const ndjson = eventsNdjson(item.events);
|
|
1879
|
+
// Budget is checked BEFORE anything is written: one oversized
|
|
1880
|
+
// session cannot blow past maxTotalBytes on disk. Skipped sessions
|
|
1881
|
+
// stay reachable through the lazy path.
|
|
1882
|
+
const bytes =
|
|
1883
|
+
Buffer.byteLength(JSON.stringify(item.record), "utf8") +
|
|
1884
|
+
(ndjson === undefined ? 0 : Buffer.byteLength(ndjson, "utf8"));
|
|
1885
|
+
if (bytes > budget) {
|
|
1886
|
+
this.logger(
|
|
1887
|
+
`[agent-serve] session restore stopped at the ${policy.maxTotalBytes}-byte budget after ${restored} session(s); remaining sessions restore lazily on resume`
|
|
1888
|
+
);
|
|
1889
|
+
break outer;
|
|
1890
|
+
}
|
|
1891
|
+
try {
|
|
1892
|
+
await this.materializeSession(item.record, ndjson);
|
|
1893
|
+
budget -= bytes;
|
|
1894
|
+
restored += 1;
|
|
1895
|
+
} catch (error) {
|
|
1896
|
+
this.logger(
|
|
1897
|
+
`[agent-serve] failed to restore session ${item.record.sessionId}: ${describeError(error)}`
|
|
1898
|
+
);
|
|
1899
|
+
}
|
|
1900
|
+
}
|
|
1901
|
+
}
|
|
1902
|
+
if (restored > 0) {
|
|
1903
|
+
this.logger(`[agent-serve] restored ${restored} session(s) from storage`);
|
|
1904
|
+
}
|
|
1905
|
+
}
|
|
1906
|
+
|
|
1907
|
+
/**
|
|
1908
|
+
* Lazy restore: a continuation lookup missed locally, so ask the sink
|
|
1909
|
+
* for that one session and materialize it before resuming. Disk grows
|
|
1910
|
+
* with sessions that are actually resumed, not with history.
|
|
1911
|
+
*
|
|
1912
|
+
* Concurrent misses for the same key (webhook redeliveries, retries)
|
|
1913
|
+
* share one in-flight restore — a second materialization racing the
|
|
1914
|
+
* first resumed turn could truncate events the turn already appended.
|
|
1915
|
+
* Sink errors propagate and fail the send (see
|
|
1916
|
+
* {@link StorageCoordinator.getSessionByContinuation}).
|
|
1917
|
+
*/
|
|
1918
|
+
private restoreSessionByContinuation(
|
|
1919
|
+
channelId: string,
|
|
1920
|
+
continuationKey: string
|
|
1921
|
+
): Promise<SessionRecord | undefined> {
|
|
1922
|
+
const storage = this.storage;
|
|
1923
|
+
if (storage === undefined) {
|
|
1924
|
+
return Promise.resolve(undefined);
|
|
1925
|
+
}
|
|
1926
|
+
const key = `${channelId}\u0000${continuationKey}`;
|
|
1927
|
+
let pending = this.restoreInFlight.get(key);
|
|
1928
|
+
if (pending === undefined) {
|
|
1929
|
+
pending = this.loadAndMaterializeByContinuation(
|
|
1930
|
+
storage,
|
|
1931
|
+
channelId,
|
|
1932
|
+
continuationKey
|
|
1933
|
+
).finally(() => this.restoreInFlight.delete(key));
|
|
1934
|
+
this.restoreInFlight.set(key, pending);
|
|
1935
|
+
}
|
|
1936
|
+
return pending;
|
|
1937
|
+
}
|
|
1938
|
+
|
|
1939
|
+
private async loadAndMaterializeByContinuation(
|
|
1940
|
+
storage: StorageCoordinator,
|
|
1941
|
+
channelId: string,
|
|
1942
|
+
continuationKey: string
|
|
1943
|
+
): Promise<SessionRecord | undefined> {
|
|
1944
|
+
const record = await storage.getSessionByContinuation(
|
|
1945
|
+
channelId,
|
|
1946
|
+
continuationKey
|
|
1947
|
+
);
|
|
1948
|
+
if (record === undefined) {
|
|
1949
|
+
// Definitive miss: the caller starts a fresh session under this token.
|
|
1950
|
+
return undefined;
|
|
1951
|
+
}
|
|
1952
|
+
const local = await this.sessions.get(record.sessionId);
|
|
1953
|
+
if (local !== undefined) {
|
|
1954
|
+
return local;
|
|
1955
|
+
}
|
|
1956
|
+
const events = await storage.listSessionEvents(record.sessionId);
|
|
1957
|
+
await this.materializeSession(record, eventsNdjson(events));
|
|
1958
|
+
this.logger(
|
|
1959
|
+
`[agent-serve] lazily restored session ${record.sessionId} from storage`
|
|
1960
|
+
);
|
|
1961
|
+
return record;
|
|
1962
|
+
}
|
|
1963
|
+
|
|
1964
|
+
/**
|
|
1965
|
+
* Write one restored session to the local state root. Events land on
|
|
1966
|
+
* disk before the record: a session becomes visible (and addressable via
|
|
1967
|
+
* its continuation key) only with its full stream in place, and the
|
|
1968
|
+
* lazily opened event log hydrates from this file.
|
|
1969
|
+
*/
|
|
1970
|
+
private async materializeSession(
|
|
1971
|
+
record: SessionRecord,
|
|
1972
|
+
eventsNdjson: string | undefined
|
|
1973
|
+
): Promise<void> {
|
|
1974
|
+
if (eventsNdjson !== undefined) {
|
|
1975
|
+
const filePath = this.sessions.eventFilePath(record.sessionId);
|
|
1976
|
+
await mkdir(dirname(filePath), { recursive: true });
|
|
1977
|
+
await writeFile(filePath, eventsNdjson, "utf8");
|
|
1978
|
+
}
|
|
1979
|
+
await this.sessions.save(record);
|
|
1980
|
+
}
|
|
1981
|
+
|
|
1716
1982
|
trackBackground(promise: Promise<unknown>): void {
|
|
1717
1983
|
this.backgroundWork.add(promise);
|
|
1718
1984
|
void promise
|
|
@@ -1730,11 +1996,23 @@ export class SessionEngine {
|
|
|
1730
1996
|
await Promise.allSettled([...this.dispatchChains.values()]);
|
|
1731
1997
|
await Promise.allSettled([...this.updateChains.values()]);
|
|
1732
1998
|
await this.logs.flushAll();
|
|
1999
|
+
await this.storage?.close();
|
|
1733
2000
|
await this.mcpHost.close();
|
|
1734
2001
|
await this.runner.dispose?.();
|
|
1735
2002
|
}
|
|
1736
2003
|
}
|
|
1737
2004
|
|
|
2005
|
+
/** Parallel event-stream loads during the startup bulk restore. */
|
|
2006
|
+
const RESTORE_FETCH_CONCURRENCY = 8;
|
|
2007
|
+
|
|
2008
|
+
/** NDJSON payload for a restored event stream; undefined when empty. */
|
|
2009
|
+
function eventsNdjson(events: SessionEvent[]): string | undefined {
|
|
2010
|
+
if (events.length === 0) {
|
|
2011
|
+
return undefined;
|
|
2012
|
+
}
|
|
2013
|
+
return `${events.map((event) => JSON.stringify(event)).join("\n")}\n`;
|
|
2014
|
+
}
|
|
2015
|
+
|
|
1738
2016
|
function subagentPrompt(subagent: ResolvedAgent): string {
|
|
1739
2017
|
if (subagent.instructions !== undefined && subagent.instructions !== "") {
|
|
1740
2018
|
return subagent.instructions;
|