@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
|
@@ -0,0 +1,499 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runtime for `defineStorage` (`agent/storage.ts`).
|
|
3
|
+
*
|
|
4
|
+
* The coordinator is the single funnel between the engine's hot paths and
|
|
5
|
+
* the author's key-value sink. The sink is four functions (`put` / `get` /
|
|
6
|
+
* `delete` / `list`); the coordinator owns everything else: it mints every
|
|
7
|
+
* key from the versioned scheme ({@link storageKeys}), coalesces
|
|
8
|
+
* session records and batches event chunks per turn (or debounce window),
|
|
9
|
+
* serializes delivery on one bounded queue per agent, and isolates
|
|
10
|
+
* failures — a throwing sink is logged and its write dropped; storage
|
|
11
|
+
* must never stall or fail a turn.
|
|
12
|
+
*
|
|
13
|
+
* Scaling shape: sessions, reminders, evals, and A/Bs all reduce to keyed
|
|
14
|
+
* puts on the same queue, so one author-owned sink covers every domain,
|
|
15
|
+
* and new domains are new key prefixes — not new config surface.
|
|
16
|
+
*/
|
|
17
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
18
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
19
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
20
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
21
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
22
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
23
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
24
|
+
});
|
|
25
|
+
};
|
|
26
|
+
import { resolveStoragePolicy, storageKeys, } from "../storage.js";
|
|
27
|
+
import { describeError } from "./describe-error.js";
|
|
28
|
+
import { isReminderRecord } from "./reminder-store.js";
|
|
29
|
+
/** Event types that close a unit of work — flush point for turn-end batching. */
|
|
30
|
+
const TURN_BOUNDARY_EVENTS = new Set([
|
|
31
|
+
"turn.completed",
|
|
32
|
+
"turn.failed",
|
|
33
|
+
"session.waiting",
|
|
34
|
+
"session.completed",
|
|
35
|
+
"session.failed",
|
|
36
|
+
]);
|
|
37
|
+
/**
|
|
38
|
+
* Max writes waiting on the delivery queue before new ones are dropped.
|
|
39
|
+
* At-most-once delivery already tolerates loss; bounding the queue keeps a
|
|
40
|
+
* slow sink from growing memory without limit under sustained traffic.
|
|
41
|
+
*/
|
|
42
|
+
const MAX_PENDING_OPS = 1000;
|
|
43
|
+
/**
|
|
44
|
+
* How long {@link StorageCoordinator.close} waits for the sink to
|
|
45
|
+
* drain before giving up. Keeps a hung sink from stalling shutdown past
|
|
46
|
+
* the pod grace period (where a SIGKILL would lose the flush anyway).
|
|
47
|
+
*/
|
|
48
|
+
const CLOSE_DRAIN_TIMEOUT_MS = 10000;
|
|
49
|
+
/** Debounced event batching flushes early past this many buffered events. */
|
|
50
|
+
const DEBOUNCE_MAX_BATCH = 200;
|
|
51
|
+
/**
|
|
52
|
+
* Aggregate A/B snapshots are refreshed at most this often — the
|
|
53
|
+
* playground polls `GET /v1/abs` every few seconds, and the snapshot is a
|
|
54
|
+
* convenience backfill, not a source of truth.
|
|
55
|
+
*/
|
|
56
|
+
const AB_SNAPSHOT_MIN_INTERVAL_MS = 60000;
|
|
57
|
+
export class StorageCoordinator {
|
|
58
|
+
constructor(options) {
|
|
59
|
+
/** Per-session pending state (events + coalesced record). */
|
|
60
|
+
this.buffers = new Map();
|
|
61
|
+
/**
|
|
62
|
+
* Last continuation key written to the sink per session, so a token
|
|
63
|
+
* change emits a delete for the stale index entry alongside the new put.
|
|
64
|
+
*/
|
|
65
|
+
this.continuationIndex = new Map();
|
|
66
|
+
/** Single delivery chain: writes reach the sink serialized, in order. */
|
|
67
|
+
this.queue = Promise.resolve();
|
|
68
|
+
/** Ops on the queue not yet delivered (bounded by MAX_PENDING_OPS). */
|
|
69
|
+
this.pending = 0;
|
|
70
|
+
/** Ops dropped because the queue was full. */
|
|
71
|
+
this.dropped = 0;
|
|
72
|
+
this.lastAbSnapshotAt = 0;
|
|
73
|
+
this.closed = false;
|
|
74
|
+
this.definition = options.definition;
|
|
75
|
+
this.policy = resolveStoragePolicy(options.definition.policy);
|
|
76
|
+
this.agentName = options.agentName;
|
|
77
|
+
this.projectRoot = options.projectRoot;
|
|
78
|
+
this.logger = options.logger;
|
|
79
|
+
}
|
|
80
|
+
// ==========================================================================
|
|
81
|
+
// Sessions (writes)
|
|
82
|
+
// ==========================================================================
|
|
83
|
+
/** Durable session-record update (engine calls after every store save). */
|
|
84
|
+
sessionRecord(record) {
|
|
85
|
+
if (this.closed) {
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
// Coalesced: only the newest record at flush time reaches the sink.
|
|
89
|
+
this.buffer(record.sessionId).record = record;
|
|
90
|
+
}
|
|
91
|
+
/** One appended session event (engine calls from its dispatch funnel). */
|
|
92
|
+
event(event) {
|
|
93
|
+
if (this.closed) {
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
const buffer = this.buffer(event.sessionId);
|
|
97
|
+
buffer.events.push(event);
|
|
98
|
+
const events = this.policy.events;
|
|
99
|
+
if (events !== "turnEnd") {
|
|
100
|
+
// Debounce spans turn boundaries by design ("flush when the session
|
|
101
|
+
// goes quiet", not once per turn); the coalesced record rides the
|
|
102
|
+
// same debounced flush.
|
|
103
|
+
if (buffer.events.length >= DEBOUNCE_MAX_BATCH) {
|
|
104
|
+
this.flushSession(event.sessionId);
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
this.armDebounce(event.sessionId, buffer, events.debounceMs);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
// Turn boundaries do NOT flush here: the boundary event's own handlers
|
|
111
|
+
// (and their record updates) haven't run yet. The engine calls
|
|
112
|
+
// eventDispatched() once dispatch settles — that's the flush point.
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* A boundary event's dispatch settled: channel/hook handlers ran and
|
|
116
|
+
* their record updates (channel state, continuation tokens) are
|
|
117
|
+
* buffered. This — not the append — is the turn-end flush point, so the
|
|
118
|
+
* persisted session record includes the boundary event's own handler
|
|
119
|
+
* mutations. Debounced batching intentionally ignores boundaries.
|
|
120
|
+
*/
|
|
121
|
+
eventDispatched(event) {
|
|
122
|
+
if (this.closed ||
|
|
123
|
+
this.policy.events !== "turnEnd" ||
|
|
124
|
+
!TURN_BOUNDARY_EVENTS.has(event.type)) {
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
this.flushSession(event.sessionId);
|
|
128
|
+
}
|
|
129
|
+
/** Flush everything buffered for one session (ordered: events, record). */
|
|
130
|
+
flushSession(sessionId, reason = "policy") {
|
|
131
|
+
const buffer = this.buffers.get(sessionId);
|
|
132
|
+
if (buffer === undefined) {
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
this.buffers.delete(sessionId);
|
|
136
|
+
if (buffer.debounceTimer !== undefined) {
|
|
137
|
+
clearTimeout(buffer.debounceTimer);
|
|
138
|
+
}
|
|
139
|
+
const ops = [];
|
|
140
|
+
const first = buffer.events[0];
|
|
141
|
+
if (first !== undefined) {
|
|
142
|
+
ops.push({
|
|
143
|
+
op: "put",
|
|
144
|
+
key: storageKeys.sessionEvents(this.agentName, sessionId, first.index),
|
|
145
|
+
value: buffer.events,
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
if (buffer.record !== undefined) {
|
|
149
|
+
ops.push({
|
|
150
|
+
op: "put",
|
|
151
|
+
key: storageKeys.session(this.agentName, sessionId),
|
|
152
|
+
value: buffer.record,
|
|
153
|
+
});
|
|
154
|
+
ops.push(...this.continuationOps(buffer.record));
|
|
155
|
+
}
|
|
156
|
+
if (ops.length > 0) {
|
|
157
|
+
this.enqueue(ops, reason);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Index maintenance for `continuation/{channelId}/{token}` → sessionId:
|
|
162
|
+
* put the current token, delete the previous one when it changed.
|
|
163
|
+
*/
|
|
164
|
+
continuationOps(record) {
|
|
165
|
+
const previous = this.continuationIndex.get(record.sessionId);
|
|
166
|
+
const next = record.continuationKey;
|
|
167
|
+
if (previous === next) {
|
|
168
|
+
return [];
|
|
169
|
+
}
|
|
170
|
+
this.continuationIndex.set(record.sessionId, next);
|
|
171
|
+
const ops = [];
|
|
172
|
+
if (previous != null) {
|
|
173
|
+
ops.push({
|
|
174
|
+
op: "delete",
|
|
175
|
+
key: storageKeys.continuation(this.agentName, record.channelId, previous),
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
if (next != null) {
|
|
179
|
+
ops.push({
|
|
180
|
+
op: "put",
|
|
181
|
+
key: storageKeys.continuation(this.agentName, record.channelId, next),
|
|
182
|
+
value: { sessionId: record.sessionId },
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
return ops;
|
|
186
|
+
}
|
|
187
|
+
// ==========================================================================
|
|
188
|
+
// Sessions (reads / restore)
|
|
189
|
+
// ==========================================================================
|
|
190
|
+
/** Whether the sink can serve the startup bulk restore (`list`). */
|
|
191
|
+
get canBulkRestore() {
|
|
192
|
+
return this.definition.list !== undefined;
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Saved session records for this agent (startup restore). Errors are
|
|
196
|
+
* logged and read as "nothing saved" — a broken store must never block
|
|
197
|
+
* serve start.
|
|
198
|
+
*/
|
|
199
|
+
listSessions() {
|
|
200
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
201
|
+
const entries = yield this.tryList(storageKeys.sessionPrefix(this.agentName));
|
|
202
|
+
return entries
|
|
203
|
+
.map((entry) => entry.value)
|
|
204
|
+
.filter((record) => record != null);
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Saved event stream for one session: chunk values concatenated in key
|
|
209
|
+
* order (chunks are keyed by their first event's index, zero-padded, so
|
|
210
|
+
* ascending key order is append order). Errors read as an empty stream.
|
|
211
|
+
*/
|
|
212
|
+
listSessionEvents(sessionId) {
|
|
213
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
214
|
+
const entries = yield this.tryList(storageKeys.sessionEventsPrefix(this.agentName, sessionId));
|
|
215
|
+
const events = [];
|
|
216
|
+
for (const entry of entries) {
|
|
217
|
+
if (Array.isArray(entry.value)) {
|
|
218
|
+
events.push(...entry.value);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
return events;
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* One saved session by channel continuation key (lazy restore). Unlike
|
|
226
|
+
* the startup reads, sink errors **propagate**: a failing store must
|
|
227
|
+
* fail the follow-up (which the caller can retry) rather than read as
|
|
228
|
+
* "unknown token" — that would mint a new session under the same
|
|
229
|
+
* continuation key and permanently shadow the real one on this host.
|
|
230
|
+
* A definitive miss (no index entry, or a write-only sink without
|
|
231
|
+
* `get`) resolves undefined.
|
|
232
|
+
*/
|
|
233
|
+
getSessionByContinuation(channelId, continuationKey) {
|
|
234
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
235
|
+
const get = this.definition.get;
|
|
236
|
+
if (get === undefined) {
|
|
237
|
+
return undefined;
|
|
238
|
+
}
|
|
239
|
+
const ctx = this.context("restore");
|
|
240
|
+
const pointer = yield get(storageKeys.continuation(this.agentName, channelId, continuationKey), ctx);
|
|
241
|
+
if (pointer == null) {
|
|
242
|
+
return undefined;
|
|
243
|
+
}
|
|
244
|
+
const sessionId = typeof pointer === "object" && !Array.isArray(pointer)
|
|
245
|
+
? pointer.sessionId
|
|
246
|
+
: undefined;
|
|
247
|
+
if (typeof sessionId !== "string" || sessionId === "") {
|
|
248
|
+
throw new Error(`storage returned a malformed continuation entry for channel ${channelId}`);
|
|
249
|
+
}
|
|
250
|
+
const record = (yield get(storageKeys.session(this.agentName, sessionId), ctx));
|
|
251
|
+
if (record == null) {
|
|
252
|
+
// Index entry without its record (e.g. the record write was shed
|
|
253
|
+
// under at-most-once). The session is unrecoverable; treat as a miss
|
|
254
|
+
// so the conversation can continue rather than failing forever.
|
|
255
|
+
this.logger(`[agent-serve] storage continuation entry points at missing session ${sessionId}; treating as a miss`);
|
|
256
|
+
return undefined;
|
|
257
|
+
}
|
|
258
|
+
// The sink must answer for the exact key it was asked about — a
|
|
259
|
+
// mismatched row is a broken store, not a miss; treating it as a miss
|
|
260
|
+
// would fork the continuation token onto a new session.
|
|
261
|
+
if (record.sessionId !== sessionId ||
|
|
262
|
+
record.channelId !== channelId ||
|
|
263
|
+
record.continuationKey !== continuationKey) {
|
|
264
|
+
throw new Error(`storage returned a session that does not match the requested continuation (channel ${channelId})`);
|
|
265
|
+
}
|
|
266
|
+
return record;
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
// ==========================================================================
|
|
270
|
+
// Reminders
|
|
271
|
+
// ==========================================================================
|
|
272
|
+
/**
|
|
273
|
+
* Mirror one reminder record (create / fire / cancel / disarm). Local
|
|
274
|
+
* disk under `--state-root/reminders` remains the live source of truth;
|
|
275
|
+
* this is the durable copy for host replacement.
|
|
276
|
+
*/
|
|
277
|
+
reminder(record) {
|
|
278
|
+
if (this.closed) {
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
this.enqueue([
|
|
282
|
+
{
|
|
283
|
+
op: "put",
|
|
284
|
+
key: storageKeys.reminder(this.agentName, record.id),
|
|
285
|
+
value: record,
|
|
286
|
+
},
|
|
287
|
+
], "policy");
|
|
288
|
+
}
|
|
289
|
+
/**
|
|
290
|
+
* Saved reminder records for this agent (startup hydrate into local
|
|
291
|
+
* disk). Errors read as empty — same posture as session bulk restore.
|
|
292
|
+
*/
|
|
293
|
+
listReminders() {
|
|
294
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
295
|
+
const entries = yield this.tryList(storageKeys.reminderPrefix(this.agentName));
|
|
296
|
+
return entries.flatMap((entry) => isReminderRecord(entry.value) ? [entry.value] : []);
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
// ==========================================================================
|
|
300
|
+
// Evals
|
|
301
|
+
// ==========================================================================
|
|
302
|
+
/**
|
|
303
|
+
* Adapter for {@link EvalRunPersistence} so the playground eval store
|
|
304
|
+
* can fall back to this sink when `evals.config.ts` sets no
|
|
305
|
+
* `persistRuns`.
|
|
306
|
+
*/
|
|
307
|
+
asEvalRunPersistence() {
|
|
308
|
+
return {
|
|
309
|
+
load: () => __awaiter(this, void 0, void 0, function* () {
|
|
310
|
+
const entries = yield this.tryList(storageKeys.evalRunPrefix(this.agentName));
|
|
311
|
+
return entries
|
|
312
|
+
.map((entry) => entry.value)
|
|
313
|
+
.filter((run) => run != null);
|
|
314
|
+
}),
|
|
315
|
+
save: (run) => {
|
|
316
|
+
this.enqueue([
|
|
317
|
+
{
|
|
318
|
+
op: "put",
|
|
319
|
+
key: storageKeys.evalRun(this.agentName, run.runId),
|
|
320
|
+
value: run,
|
|
321
|
+
},
|
|
322
|
+
], "policy");
|
|
323
|
+
},
|
|
324
|
+
delete: (runId) => {
|
|
325
|
+
this.enqueue([
|
|
326
|
+
{
|
|
327
|
+
op: "delete",
|
|
328
|
+
key: storageKeys.evalRun(this.agentName, runId),
|
|
329
|
+
},
|
|
330
|
+
], "policy");
|
|
331
|
+
},
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
// ==========================================================================
|
|
335
|
+
// A/Bs
|
|
336
|
+
// ==========================================================================
|
|
337
|
+
/**
|
|
338
|
+
* Adapter for {@link ABSamplePersistence} so the AB collector can fall
|
|
339
|
+
* back to this sink when `ab.config.ts` sets no `persistSamples`.
|
|
340
|
+
*/
|
|
341
|
+
asABSamplePersistence() {
|
|
342
|
+
return {
|
|
343
|
+
save: (sample) => {
|
|
344
|
+
this.enqueue([
|
|
345
|
+
{
|
|
346
|
+
op: "put",
|
|
347
|
+
key: storageKeys.abSample(this.agentName, sample.sessionId, sample.at),
|
|
348
|
+
value: sample,
|
|
349
|
+
},
|
|
350
|
+
], "policy");
|
|
351
|
+
},
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
/**
|
|
355
|
+
* Refresh the persisted aggregate A/B snapshot, throttled to once per
|
|
356
|
+
* {@link AB_SNAPSHOT_MIN_INTERVAL_MS} (the playground recomputes the
|
|
357
|
+
* fold on every `GET /v1/abs` poll).
|
|
358
|
+
*/
|
|
359
|
+
abSnapshot(snapshot) {
|
|
360
|
+
const now = Date.now();
|
|
361
|
+
if (this.closed ||
|
|
362
|
+
now - this.lastAbSnapshotAt < AB_SNAPSHOT_MIN_INTERVAL_MS) {
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
this.lastAbSnapshotAt = now;
|
|
366
|
+
this.enqueue([
|
|
367
|
+
{
|
|
368
|
+
op: "put",
|
|
369
|
+
key: storageKeys.abSnapshot(this.agentName),
|
|
370
|
+
value: snapshot,
|
|
371
|
+
},
|
|
372
|
+
], "policy");
|
|
373
|
+
}
|
|
374
|
+
/** Latest persisted aggregate A/B snapshot; errors read as absent. */
|
|
375
|
+
getLatestAbSnapshot() {
|
|
376
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
377
|
+
const get = this.definition.get;
|
|
378
|
+
if (get === undefined) {
|
|
379
|
+
return undefined;
|
|
380
|
+
}
|
|
381
|
+
try {
|
|
382
|
+
const value = yield get(storageKeys.abSnapshot(this.agentName), this.context("restore"));
|
|
383
|
+
return value == null ? undefined : value;
|
|
384
|
+
}
|
|
385
|
+
catch (error) {
|
|
386
|
+
this.logger(`[agent-serve] storage get(ab-snapshot) failed: ${describeError(error)}`);
|
|
387
|
+
return undefined;
|
|
388
|
+
}
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
// ==========================================================================
|
|
392
|
+
// Delivery
|
|
393
|
+
// ==========================================================================
|
|
394
|
+
/**
|
|
395
|
+
* Flush all buffers and drain the queue, giving the sink at most
|
|
396
|
+
* {@link CLOSE_DRAIN_TIMEOUT_MS} — a hung sink must not stall shutdown
|
|
397
|
+
* past the pod grace period. Called from engine close.
|
|
398
|
+
*/
|
|
399
|
+
close() {
|
|
400
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
401
|
+
if (!this.closed) {
|
|
402
|
+
for (const sessionId of [...this.buffers.keys()]) {
|
|
403
|
+
this.flushSession(sessionId, "shutdown");
|
|
404
|
+
}
|
|
405
|
+
this.closed = true;
|
|
406
|
+
}
|
|
407
|
+
let timer;
|
|
408
|
+
const drained = yield Promise.race([
|
|
409
|
+
this.queue.then(() => true),
|
|
410
|
+
new Promise((resolve) => {
|
|
411
|
+
var _a;
|
|
412
|
+
timer = setTimeout(() => resolve(false), CLOSE_DRAIN_TIMEOUT_MS);
|
|
413
|
+
(_a = timer.unref) === null || _a === void 0 ? void 0 : _a.call(timer);
|
|
414
|
+
}),
|
|
415
|
+
]);
|
|
416
|
+
clearTimeout(timer);
|
|
417
|
+
if (!drained) {
|
|
418
|
+
this.logger(`[agent-serve] storage shutdown drain timed out after ${CLOSE_DRAIN_TIMEOUT_MS}ms with ${this.pending} write(s) undelivered`);
|
|
419
|
+
}
|
|
420
|
+
});
|
|
421
|
+
}
|
|
422
|
+
/** Pending deliveries (exposed for tests and drain instrumentation). */
|
|
423
|
+
whenIdle() {
|
|
424
|
+
return this.queue;
|
|
425
|
+
}
|
|
426
|
+
buffer(sessionId) {
|
|
427
|
+
let buffer = this.buffers.get(sessionId);
|
|
428
|
+
if (buffer === undefined) {
|
|
429
|
+
buffer = { events: [] };
|
|
430
|
+
this.buffers.set(sessionId, buffer);
|
|
431
|
+
}
|
|
432
|
+
return buffer;
|
|
433
|
+
}
|
|
434
|
+
armDebounce(sessionId, buffer, debounceMs) {
|
|
435
|
+
var _a, _b;
|
|
436
|
+
if (buffer.debounceTimer !== undefined) {
|
|
437
|
+
clearTimeout(buffer.debounceTimer);
|
|
438
|
+
}
|
|
439
|
+
buffer.debounceTimer = setTimeout(() => {
|
|
440
|
+
buffer.debounceTimer = undefined;
|
|
441
|
+
this.flushSession(sessionId);
|
|
442
|
+
}, debounceMs);
|
|
443
|
+
// Never keep the process alive just for a pending storage flush.
|
|
444
|
+
(_b = (_a = buffer.debounceTimer).unref) === null || _b === void 0 ? void 0 : _b.call(_a);
|
|
445
|
+
}
|
|
446
|
+
/** `list` wrapper for the startup reads: missing hook or throw ⇒ empty. */
|
|
447
|
+
tryList(prefix) {
|
|
448
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
449
|
+
const list = this.definition.list;
|
|
450
|
+
if (list === undefined) {
|
|
451
|
+
return [];
|
|
452
|
+
}
|
|
453
|
+
try {
|
|
454
|
+
const entries = yield list(prefix, this.context("restore"));
|
|
455
|
+
return Array.isArray(entries) ? entries : [];
|
|
456
|
+
}
|
|
457
|
+
catch (error) {
|
|
458
|
+
this.logger(`[agent-serve] storage list(${prefix}) failed: ${describeError(error)}`);
|
|
459
|
+
return [];
|
|
460
|
+
}
|
|
461
|
+
});
|
|
462
|
+
}
|
|
463
|
+
enqueue(ops, reason) {
|
|
464
|
+
if (this.closed && reason !== "shutdown") {
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
if (this.pending + ops.length > MAX_PENDING_OPS) {
|
|
468
|
+
this.dropped += ops.length;
|
|
469
|
+
if (this.dropped <= ops.length || this.dropped % 1000 < ops.length) {
|
|
470
|
+
this.logger(`[agent-serve] storage queue full (${MAX_PENDING_OPS} pending writes); dropped ${this.dropped} write(s) so far — the sink is too slow for the traffic`);
|
|
471
|
+
}
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
474
|
+
this.pending += ops.length;
|
|
475
|
+
const ctx = this.context(reason);
|
|
476
|
+
this.queue = this.queue.then(() => __awaiter(this, void 0, void 0, function* () {
|
|
477
|
+
var _a, _b;
|
|
478
|
+
for (const op of ops) {
|
|
479
|
+
try {
|
|
480
|
+
if (op.op === "put") {
|
|
481
|
+
yield this.definition.put(op.key, op.value, ctx);
|
|
482
|
+
}
|
|
483
|
+
else {
|
|
484
|
+
yield (_b = (_a = this.definition).delete) === null || _b === void 0 ? void 0 : _b.call(_a, op.key, ctx);
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
catch (error) {
|
|
488
|
+
this.logger(`[agent-serve] storage ${op.op}(${op.key}) failed: ${describeError(error)}`);
|
|
489
|
+
}
|
|
490
|
+
finally {
|
|
491
|
+
this.pending -= 1;
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
}));
|
|
495
|
+
}
|
|
496
|
+
context(reason) {
|
|
497
|
+
return { agentName: this.agentName, projectRoot: this.projectRoot, reason };
|
|
498
|
+
}
|
|
499
|
+
}
|
|
Binary file
|