@covia/covia-sdk 1.8.0 → 1.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -43,6 +43,15 @@ var GridError = class extends CoviaError {
43
43
  this.responseBody = responseBody;
44
44
  }
45
45
  };
46
+ var UnsupportedVenueFeatureError = class extends CoviaError {
47
+ constructor(feature) {
48
+ super(
49
+ `The connected venue cannot serve ${feature} without creating a job. Invoke the corresponding operation explicitly if creating a persisted job is intended.`
50
+ );
51
+ this.feature = feature;
52
+ this.name = "UnsupportedVenueFeatureError";
53
+ }
54
+ };
46
55
  var CoviaConnectionError = class extends CoviaError {
47
56
  constructor(message, options = {}) {
48
57
  super(message);
@@ -112,6 +121,14 @@ var JobNotFoundError = class extends NotFoundError {
112
121
  this.jobId = jobId;
113
122
  }
114
123
  };
124
+ var AgentSessionNotFoundError = class extends CoviaError {
125
+ constructor(agentId, sessionId) {
126
+ super(`Agent session not found: ${agentId}/${sessionId}`, 404);
127
+ this.agentId = agentId;
128
+ this.sessionId = sessionId;
129
+ this.name = "AgentSessionNotFoundError";
130
+ }
131
+ };
115
132
 
116
133
  // node_modules/.pnpm/@noble+ed25519@3.1.0/node_modules/@noble/ed25519/index.js
117
134
  var ed25519_CURVE = Object.freeze({
@@ -1031,9 +1048,13 @@ function privateKeyToHex(key) {
1031
1048
  return Array.from(key).map((b) => b.toString(16).padStart(2, "0")).join("");
1032
1049
  }
1033
1050
  function hexToPrivateKey(hex) {
1034
- const bytes = new Uint8Array(hex.length / 2);
1051
+ const cleaned = hex.startsWith("0x") || hex.startsWith("0X") ? hex.slice(2) : hex;
1052
+ if (!/^[0-9a-fA-F]{64}$/.test(cleaned)) {
1053
+ throw new Error("Invalid Ed25519 private key: expected 64 hex characters (32 bytes)");
1054
+ }
1055
+ const bytes = new Uint8Array(32);
1035
1056
  for (let i = 0; i < bytes.length; i++) {
1036
- bytes[i] = parseInt(hex.substring(i * 2, i * 2 + 2), 16);
1057
+ bytes[i] = parseInt(cleaned.substring(i * 2, i * 2 + 2), 16);
1037
1058
  }
1038
1059
  return bytes;
1039
1060
  }
@@ -1147,6 +1168,14 @@ function createEdDSAJWT(privateKey, lifetimeSeconds = 300, audience) {
1147
1168
  }
1148
1169
 
1149
1170
  // src/Credentials.ts
1171
+ function base64Encode(text) {
1172
+ const bytes = new TextEncoder().encode(text);
1173
+ let binary = "";
1174
+ for (let i = 0; i < bytes.length; i++) {
1175
+ binary += String.fromCharCode(bytes[i]);
1176
+ }
1177
+ return btoa(binary);
1178
+ }
1150
1179
  var Auth = class {
1151
1180
  };
1152
1181
  var NoAuth = class extends Auth {
@@ -1169,7 +1198,7 @@ var BasicAuth = class extends Auth {
1169
1198
  this._password = password;
1170
1199
  }
1171
1200
  apply(headers, _audience) {
1172
- const credentials = Buffer.from(`${this._username}:${this._password}`, "utf-8").toString("base64");
1201
+ const credentials = base64Encode(`${this._username}:${this._password}`);
1173
1202
  headers["Authorization"] = `Basic ${credentials}`;
1174
1203
  }
1175
1204
  };
@@ -1186,8 +1215,30 @@ var Ed25519Auth = class _Ed25519Auth extends Auth {
1186
1215
  this._lifetime = tokenLifetimeSeconds;
1187
1216
  }
1188
1217
  apply(headers, audience) {
1189
- const jwt = createEdDSAJWT(this._privateKey, this._lifetime, this._audience ?? audience);
1190
- headers["Authorization"] = `Bearer ${jwt}`;
1218
+ headers["Authorization"] = `Bearer ${this.identityToken(audience)}`;
1219
+ }
1220
+ /**
1221
+ * Mint a bearer-usable identity JWT for this key — the same token
1222
+ * {@link apply} attaches to every request, exposed for callers that need
1223
+ * to present this identity elsewhere (CLI tools, curl, another client:
1224
+ * `Authorization: Bearer <token>`).
1225
+ *
1226
+ * An explicitly-pinned {@link audience} wins; otherwise the JWT `aud` is
1227
+ * bound to the venue DID given here. A token with no `aud` is replayable
1228
+ * at any venue that accepts the caller's DID, so minting one is refused
1229
+ * rather than silently weakened.
1230
+ *
1231
+ * @param audience - The venue DID the token is bound to (`aud` claim).
1232
+ * @param lifetimeSeconds - Token lifetime; defaults to this instance's.
1233
+ */
1234
+ identityToken(audience, lifetimeSeconds) {
1235
+ const aud = this._audience ?? (audience || void 0);
1236
+ if (!aud) {
1237
+ throw new CoviaError(
1238
+ "Ed25519Auth requires a venue audience to bind the token to: connect via Grid.connect()/Venue.connect() so the venue DID is known, or pin auth.audience explicitly."
1239
+ );
1240
+ }
1241
+ return createEdDSAJWT(this._privateKey, lifetimeSeconds ?? this._lifetime, aud);
1191
1242
  }
1192
1243
  /** The caller's DID derived from the public key. */
1193
1244
  getDID() {
@@ -1300,17 +1351,24 @@ async function parseErrorBody(response) {
1300
1351
  let body = null;
1301
1352
  let message = `Request failed with status ${response.status}`;
1302
1353
  try {
1303
- body = await response.json();
1354
+ if (typeof response.text === "function") {
1355
+ const text = await response.text();
1356
+ if (text) {
1357
+ try {
1358
+ body = JSON.parse(text);
1359
+ } catch {
1360
+ body = text;
1361
+ message = text;
1362
+ }
1363
+ }
1364
+ } else {
1365
+ body = await response.json();
1366
+ }
1304
1367
  if (body && typeof body === "object" && "error" in body) {
1305
1368
  const err2 = body.error;
1306
1369
  if (typeof err2 === "string") message = err2;
1307
1370
  }
1308
1371
  } catch {
1309
- try {
1310
- const text = await response.text();
1311
- if (text) message = text;
1312
- } catch {
1313
- }
1314
1372
  }
1315
1373
  return { message, body };
1316
1374
  }
@@ -1604,13 +1662,82 @@ var AdapterManager = class {
1604
1662
  }
1605
1663
  };
1606
1664
 
1607
- // src/AgentManager.ts
1665
+ // src/VenueTransport.ts
1666
+ function requestOptions(venue, options) {
1667
+ const {
1668
+ contentType,
1669
+ headers: suppliedHeaders = {},
1670
+ ...request
1671
+ } = options;
1672
+ const headers = { ...suppliedHeaders };
1673
+ const effectiveContentType = contentType !== void 0 ? contentType : request.body != null ? "application/json" : null;
1674
+ if (effectiveContentType && headers["Content-Type"] === void 0) {
1675
+ headers["Content-Type"] = effectiveContentType;
1676
+ }
1677
+ venue.auth.apply(headers, venue.venueId);
1678
+ return { ...request, headers };
1679
+ }
1680
+ function venueJson(venue, path, options = {}) {
1681
+ return fetchWithError(
1682
+ `${venue.baseUrl}${path}`,
1683
+ requestOptions(venue, options)
1684
+ );
1685
+ }
1686
+ function venueStream(venue, path, options = {}) {
1687
+ return fetchStreamWithError(
1688
+ `${venue.baseUrl}${path}`,
1689
+ requestOptions(venue, options)
1690
+ );
1691
+ }
1692
+
1693
+ // src/venue-features.ts
1608
1694
  function versionAtLeast(version, major, minor) {
1609
1695
  const m = version?.match(/^(\d+)\.(\d+)/);
1610
1696
  if (!m) return true;
1611
1697
  const [maj, min] = [Number(m[1]), Number(m[2])];
1612
1698
  return maj > major || maj === major && min >= minor;
1613
1699
  }
1700
+ var ROUTE_MISSING_404 = /\bEndpoint (GET|POST|PUT|DELETE|PATCH|HEAD) /;
1701
+
1702
+ // src/values-util.ts
1703
+ function record(value) {
1704
+ return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
1705
+ }
1706
+ var SLICE_PAGE_SIZE = 200;
1707
+ async function sliceAll(workspace, path, pageSize = SLICE_PAGE_SIZE) {
1708
+ const items = [];
1709
+ let offset = 0;
1710
+ for (; ; ) {
1711
+ const page = await workspace.slice(path, offset, pageSize);
1712
+ if (!page.exists) {
1713
+ if (items.length === 0) return items;
1714
+ throw new CoviaError(`Sliceable value at ${path} disappeared mid-read`);
1715
+ }
1716
+ const values = page.values ?? [];
1717
+ items.push(...values);
1718
+ const nextOffset = (page.offset ?? offset) + values.length;
1719
+ const done = page.count !== void 0 ? nextOffset >= page.count : values.length < pageSize;
1720
+ if (values.length === 0 || done) break;
1721
+ if (nextOffset <= offset) {
1722
+ throw new CoviaError(`Venue returned a slice of ${path} that did not advance`);
1723
+ }
1724
+ offset = nextOffset;
1725
+ }
1726
+ return items;
1727
+ }
1728
+
1729
+ // src/AgentManager.ts
1730
+ function sessionRecord(sessionId, value) {
1731
+ const session = record(value) ?? {};
1732
+ const meta = record(session.meta);
1733
+ return {
1734
+ id: sessionId,
1735
+ metadata: meta ?? {},
1736
+ pending: Array.isArray(session.pending) ? session.pending : [],
1737
+ frames: Array.isArray(session.frames) ? session.frames : [],
1738
+ wakeTime: typeof session.wakeTime === "number" ? session.wakeTime : void 0
1739
+ };
1740
+ }
1614
1741
  var AgentManager = class {
1615
1742
  constructor(venue) {
1616
1743
  this.venue = venue;
@@ -1618,11 +1745,6 @@ var AgentManager = class {
1618
1745
  // pre-0.4 venues pay the probe once, not one failed GET per read (covia#180).
1619
1746
  this.agentsGetSupported = true;
1620
1747
  }
1621
- _headers() {
1622
- const headers = { "Content-Type": "application/json" };
1623
- this.venue.auth.apply(headers, this.venue.venueId);
1624
- return headers;
1625
- }
1626
1748
  /** Whether the venue serves `GET /api/v1/agents`: no if a probe already
1627
1749
  * 404'd, or if the venue's last known status identifies it as pre-0.4. */
1628
1750
  supportsAgentsGet() {
@@ -1633,25 +1755,25 @@ var AgentManager = class {
1633
1755
  }
1634
1756
  return this.agentsGetSupported;
1635
1757
  }
1636
- /** A job-free agents GET, falling back to the invoke path on pre-0.4 venues. */
1637
- async _agentsOr(path, params, fallback) {
1758
+ /** A job-free agents GET that rejects when the endpoint is unavailable. */
1759
+ async agentsGet(path, params) {
1638
1760
  if (this.supportsAgentsGet()) {
1639
1761
  try {
1640
1762
  const qs = new URLSearchParams();
1641
1763
  for (const [k, v] of Object.entries(params)) if (v !== void 0) qs.set(k, String(v));
1642
1764
  const q = qs.toString();
1643
- return await fetchWithError(
1644
- `${this.venue.baseUrl}/api/v1/agents${path}${q ? `?${q}` : ""}`,
1645
- { headers: this._headers() }
1765
+ return await venueJson(
1766
+ this.venue,
1767
+ `/api/v1/agents${path}${q ? `?${q}` : ""}`
1646
1768
  );
1647
1769
  } catch (e) {
1648
1770
  if (!(e instanceof NotFoundError)) throw e;
1649
- const routeMissing = path === "" || /\bEndpoint (GET|POST|PUT|DELETE|PATCH|HEAD) /.test(e.message);
1771
+ const routeMissing = path === "" || ROUTE_MISSING_404.test(e.message);
1650
1772
  if (!routeMissing) throw e;
1651
1773
  this.agentsGetSupported = false;
1652
1774
  }
1653
1775
  }
1654
- return fallback();
1776
+ throw new UnsupportedVenueFeatureError("agent reads");
1655
1777
  }
1656
1778
  async create(input) {
1657
1779
  return this.venue.operations.run("v/ops/agent/create", input);
@@ -1696,10 +1818,10 @@ var AgentManager = class {
1696
1818
  /**
1697
1819
  * List the caller's agents. **Job-free** on covia ≥ 0.4: goes through
1698
1820
  * `GET /api/v1/agents` (covia#180) — synchronous, no Job persisted. Older
1699
- * venues transparently fall back to the invoke path (one probe, remembered).
1821
+ * venues reject rather than silently creating a persisted Job.
1700
1822
  */
1701
1823
  async list(includeTerminated) {
1702
- const result = await this._agentsOr("", { includeTerminated }, () => this.venue.operations.run("v/ops/agent/list", { includeTerminated }));
1824
+ const result = await this.agentsGet("", { includeTerminated });
1703
1825
  const raw = result?.agents ?? [];
1704
1826
  const agents = raw.map((a) => typeof a === "string" ? { agentId: a } : a);
1705
1827
  return { agents };
@@ -1720,9 +1842,61 @@ var AgentManager = class {
1720
1842
  return this.venue.operations.run("v/ops/agent/cancel-task", { agentId, taskId });
1721
1843
  }
1722
1844
  /** Agent info. **Job-free** on covia ≥ 0.4 (`GET /api/v1/agents/{id}`,
1723
- * covia#180); older venues fall back to the invoke path. */
1845
+ * covia#180); older venues require an explicit compatibility opt-in. */
1724
1846
  async info(agentId) {
1725
- return this._agentsOr(`/${encodeURIComponent(agentId)}`, {}, () => this.venue.operations.run("v/ops/agent/info", { agentId }));
1847
+ return this.agentsGet(`/${encodeURIComponent(agentId)}`, {});
1848
+ }
1849
+ /**
1850
+ * Page an agent's sessions directly from `g/<agentId>/sessions`.
1851
+ * This is a job-free Values read; session adapter-specific fields remain
1852
+ * available on each record's `value` while common runtime fields are typed.
1853
+ */
1854
+ async listSessions(agentId, options = {}) {
1855
+ const result = await this.venue.workspace.slice(
1856
+ `g/${agentId}/sessions`,
1857
+ options.offset,
1858
+ options.limit
1859
+ );
1860
+ const sessions = (result.values ?? []).flatMap((entry) => {
1861
+ const pair = record(entry);
1862
+ if (!pair || typeof pair.key !== "string") return [];
1863
+ return [sessionRecord(pair.key, pair.value)];
1864
+ });
1865
+ return {
1866
+ items: sessions,
1867
+ total: result.count ?? sessions.length,
1868
+ offset: result.offset ?? options.offset ?? 0,
1869
+ limit: options.limit ?? sessions.length
1870
+ };
1871
+ }
1872
+ /** Read one agent session directly from the workspace Values surface. */
1873
+ async getSession(agentId, sessionId) {
1874
+ const path = `g/${agentId}/sessions/${sessionId}`;
1875
+ const result = await this.venue.workspace.read(path);
1876
+ if (!result.exists) {
1877
+ throw new AgentSessionNotFoundError(agentId, sessionId);
1878
+ }
1879
+ if (result.truncated) {
1880
+ return this._readLargeSession(path, sessionId);
1881
+ }
1882
+ if (result.value === void 0 || result.value === null) {
1883
+ throw new AgentSessionNotFoundError(agentId, sessionId);
1884
+ }
1885
+ return sessionRecord(sessionId, result.value);
1886
+ }
1887
+ async _readLargeSession(path, sessionId) {
1888
+ const [meta, wakeTime, pending, frames] = await Promise.all([
1889
+ this.venue.workspace.read(`${path}/meta`),
1890
+ this.venue.workspace.read(`${path}/wakeTime`),
1891
+ sliceAll(this.venue.workspace, `${path}/pending`),
1892
+ sliceAll(this.venue.workspace, `${path}/frames`)
1893
+ ]);
1894
+ return sessionRecord(sessionId, {
1895
+ meta: meta.exists ? meta.value : void 0,
1896
+ wakeTime: wakeTime.exists ? wakeTime.value : void 0,
1897
+ pending,
1898
+ frames
1899
+ });
1726
1900
  }
1727
1901
  async fork(input) {
1728
1902
  return this.venue.operations.run("v/ops/agent/fork", input);
@@ -1884,23 +2058,50 @@ var Job = class {
1884
2058
  };
1885
2059
 
1886
2060
  // src/JobManager.ts
2061
+ function jobPage(value) {
2062
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
2063
+ throw new CoviaError("Venue returned an invalid jobs listing");
2064
+ }
2065
+ const page = value;
2066
+ if (!Array.isArray(page.items) || !page.items.every((item) => typeof item === "string") || !Number.isInteger(page.total) || (page.total ?? -1) < 0 || !Number.isInteger(page.offset) || (page.offset ?? -1) < 0 || !Number.isInteger(page.limit) || (page.limit ?? -1) < 0) {
2067
+ throw new CoviaError("Venue returned an invalid jobs page");
2068
+ }
2069
+ return page;
2070
+ }
1887
2071
  var JobManager = class {
1888
2072
  constructor(venue) {
1889
2073
  this.venue = venue;
1890
2074
  }
1891
2075
  async list() {
1892
- const body = await fetchWithError(`${this.venue.baseUrl}/api/v1/jobs`, {
1893
- headers: this._buildHeaders()
1894
- });
1895
- if (Array.isArray(body)) return body;
1896
- const items = body?.items;
1897
- return Array.isArray(items) ? items : [];
2076
+ const items = [];
2077
+ let offset = 0;
2078
+ let targetTotal;
2079
+ while (targetTotal === void 0 || items.length < targetTotal) {
2080
+ const body = await venueJson(
2081
+ this.venue,
2082
+ `/api/v1/jobs?offset=${offset}&limit=1000`
2083
+ );
2084
+ if (Array.isArray(body)) {
2085
+ if (!body.every((item) => typeof item === "string")) {
2086
+ throw new CoviaError("Venue returned an invalid jobs listing");
2087
+ }
2088
+ return body;
2089
+ }
2090
+ const page = jobPage(body);
2091
+ targetTotal ?? (targetTotal = page.total);
2092
+ items.push(...page.items);
2093
+ if (page.items.length === 0) break;
2094
+ const nextOffset = page.offset + page.items.length;
2095
+ if (nextOffset <= offset) {
2096
+ throw new CoviaError("Venue returned a jobs page that did not advance");
2097
+ }
2098
+ offset = nextOffset;
2099
+ }
2100
+ return items;
1898
2101
  }
1899
2102
  async get(jobId) {
1900
2103
  try {
1901
- const data = await fetchWithError(`${this.venue.baseUrl}/api/v1/jobs/${jobId}`, {
1902
- headers: this._buildHeaders()
1903
- });
2104
+ const data = await venueJson(this.venue, `/api/v1/jobs/${jobId}`);
1904
2105
  return new Job(jobId, this.venue, data);
1905
2106
  } catch (error) {
1906
2107
  if (error instanceof NotFoundError) {
@@ -1911,9 +2112,8 @@ var JobManager = class {
1911
2112
  }
1912
2113
  async cancel(jobId) {
1913
2114
  try {
1914
- return await fetchWithError(`${this.venue.baseUrl}/api/v1/jobs/${jobId}/cancel`, {
1915
- method: "PUT",
1916
- headers: this._buildHeaders()
2115
+ return await venueJson(this.venue, `/api/v1/jobs/${jobId}/cancel`, {
2116
+ method: "PUT"
1917
2117
  });
1918
2118
  } catch (error) {
1919
2119
  if (error instanceof NotFoundError) {
@@ -1924,9 +2124,8 @@ var JobManager = class {
1924
2124
  }
1925
2125
  async delete(jobId) {
1926
2126
  try {
1927
- await fetchStreamWithError(`${this.venue.baseUrl}/api/v1/jobs/${jobId}/delete`, {
1928
- method: "PUT",
1929
- headers: this._buildHeaders()
2127
+ await venueStream(this.venue, `/api/v1/jobs/${jobId}/delete`, {
2128
+ method: "PUT"
1930
2129
  });
1931
2130
  } catch (error) {
1932
2131
  if (error instanceof NotFoundError) {
@@ -1937,9 +2136,8 @@ var JobManager = class {
1937
2136
  }
1938
2137
  async pause(jobId) {
1939
2138
  try {
1940
- return await fetchWithError(`${this.venue.baseUrl}/api/v1/jobs/${jobId}/pause`, {
1941
- method: "PUT",
1942
- headers: this._buildHeaders()
2139
+ return await venueJson(this.venue, `/api/v1/jobs/${jobId}/pause`, {
2140
+ method: "PUT"
1943
2141
  });
1944
2142
  } catch (error) {
1945
2143
  if (error instanceof NotFoundError) {
@@ -1950,9 +2148,8 @@ var JobManager = class {
1950
2148
  }
1951
2149
  async resume(jobId) {
1952
2150
  try {
1953
- return await fetchWithError(`${this.venue.baseUrl}/api/v1/jobs/${jobId}/resume`, {
1954
- method: "PUT",
1955
- headers: this._buildHeaders()
2151
+ return await venueJson(this.venue, `/api/v1/jobs/${jobId}/resume`, {
2152
+ method: "PUT"
1956
2153
  });
1957
2154
  } catch (error) {
1958
2155
  if (error instanceof NotFoundError) {
@@ -1963,9 +2160,8 @@ var JobManager = class {
1963
2160
  }
1964
2161
  async sendMessage(jobId, message) {
1965
2162
  try {
1966
- return await fetchWithError(`${this.venue.baseUrl}/api/v1/jobs/${jobId}`, {
2163
+ return await venueJson(this.venue, `/api/v1/jobs/${jobId}`, {
1967
2164
  method: "POST",
1968
- headers: this._buildHeaders(),
1969
2165
  body: JSON.stringify(message)
1970
2166
  });
1971
2167
  } catch (error) {
@@ -1976,16 +2172,11 @@ var JobManager = class {
1976
2172
  }
1977
2173
  }
1978
2174
  async *stream(jobId) {
1979
- const response = await fetchStreamWithError(`${this.venue.baseUrl}/api/v1/jobs/${jobId}/sse`, {
1980
- headers: { ...this._buildHeaders(), "Accept": "text/event-stream" }
2175
+ const response = await venueStream(this.venue, `/api/v1/jobs/${jobId}/sse`, {
2176
+ headers: { "Accept": "text/event-stream" }
1981
2177
  });
1982
2178
  yield* parseSSEStream(response);
1983
2179
  }
1984
- _buildHeaders() {
1985
- const headers = { "Content-Type": "application/json" };
1986
- this.venue.auth.apply(headers, this.venue.venueId);
1987
- return headers;
1988
- }
1989
2180
  };
1990
2181
 
1991
2182
  // src/Asset.ts
@@ -2065,10 +2256,12 @@ var DataAsset = class extends Asset {
2065
2256
  };
2066
2257
 
2067
2258
  // src/AssetManager.ts
2068
- var cache = /* @__PURE__ */ new Map();
2069
2259
  var AssetManager = class {
2070
2260
  constructor(venue) {
2071
2261
  this.venue = venue;
2262
+ // Memory cache is manager/venue-local. Content-addressed metadata may still
2263
+ // be shared deliberately through the configured persistent metadata store.
2264
+ this.cache = /* @__PURE__ */ new Map();
2072
2265
  }
2073
2266
  /**
2074
2267
  * Get an asset by lattice address.
@@ -2081,23 +2274,25 @@ var AssetManager = class {
2081
2274
  * @returns Returns either an Operation or DataAsset based on the asset's metadata
2082
2275
  */
2083
2276
  async get(assetId) {
2084
- if (cache.has(assetId)) {
2085
- return this._wrap(assetId, cache.get(assetId));
2277
+ if (this.cache.has(assetId)) {
2278
+ return this._wrap(assetId, this.cache.get(assetId));
2086
2279
  }
2087
2280
  const hash = assetHash(assetId);
2088
2281
  if (hash) {
2089
2282
  const persisted = getAssetMetadataStore()?.get(normaliseHash(hash));
2090
2283
  if (persisted) {
2091
- cache.set(assetId, persisted);
2284
+ this.cache.set(assetId, persisted);
2092
2285
  return this._wrap(assetId, persisted);
2093
2286
  }
2094
2287
  }
2095
2288
  try {
2096
- const data = await fetchWithError(`${this.venue.baseUrl}/api/v1/assets/${assetId}`, {
2097
- headers: this._buildHeaders(null)
2098
- });
2289
+ const data = await venueJson(
2290
+ this.venue,
2291
+ `/api/v1/assets/${assetId}`,
2292
+ { contentType: null }
2293
+ );
2099
2294
  if (hash) {
2100
- cache.set(assetId, data);
2295
+ this.cache.set(assetId, data);
2101
2296
  getAssetMetadataStore()?.put(normaliseHash(hash), data);
2102
2297
  }
2103
2298
  return this._wrap(assetId, data);
@@ -2124,18 +2319,19 @@ var AssetManager = class {
2124
2319
  if (options.limit !== void 0) {
2125
2320
  params.set("limit", String(options.limit));
2126
2321
  }
2127
- return fetchWithError(`${this.venue.baseUrl}/api/v1/assets?${params.toString()}`, {
2128
- headers: this._buildHeaders(null)
2129
- });
2322
+ return venueJson(
2323
+ this.venue,
2324
+ `/api/v1/assets?${params.toString()}`,
2325
+ { contentType: null }
2326
+ );
2130
2327
  }
2131
2328
  /**
2132
2329
  * Register a new asset
2133
2330
  * @param assetData - Asset configuration
2134
2331
  */
2135
2332
  async register(assetData) {
2136
- return fetchWithError(`${this.venue.baseUrl}/api/v1/assets`, {
2333
+ return venueJson(this.venue, "/api/v1/assets", {
2137
2334
  method: "POST",
2138
- headers: this._buildHeaders(),
2139
2335
  body: JSON.stringify(assetData)
2140
2336
  }).then((response) => this.get(response));
2141
2337
  }
@@ -2145,9 +2341,11 @@ var AssetManager = class {
2145
2341
  */
2146
2342
  async getMetadata(assetId) {
2147
2343
  try {
2148
- return await fetchWithError(`${this.venue.baseUrl}/api/v1/assets/${assetId}`, {
2149
- headers: this._buildHeaders(null)
2150
- });
2344
+ return await venueJson(
2345
+ this.venue,
2346
+ `/api/v1/assets/${assetId}`,
2347
+ { contentType: null }
2348
+ );
2151
2349
  } catch (error) {
2152
2350
  if (error instanceof NotFoundError) {
2153
2351
  throw new AssetNotFoundError(assetId);
@@ -2163,9 +2361,9 @@ var AssetManager = class {
2163
2361
  */
2164
2362
  async putContent(assetId, content) {
2165
2363
  try {
2166
- return await fetchWithError(`${this.venue.baseUrl}/api/v1/assets/${assetId}/content`, {
2364
+ return await venueJson(this.venue, `/api/v1/assets/${assetId}/content`, {
2167
2365
  method: "PUT",
2168
- headers: this._buildHeaders(null),
2366
+ contentType: null,
2169
2367
  body: content
2170
2368
  });
2171
2369
  } catch (error) {
@@ -2181,9 +2379,11 @@ var AssetManager = class {
2181
2379
  */
2182
2380
  async getContent(assetId) {
2183
2381
  try {
2184
- const response = await fetchStreamWithError(`${this.venue.baseUrl}/api/v1/assets/${assetId}/content`, {
2185
- headers: this._buildHeaders(null)
2186
- });
2382
+ const response = await venueStream(
2383
+ this.venue,
2384
+ `/api/v1/assets/${assetId}/content`,
2385
+ { contentType: null }
2386
+ );
2187
2387
  return response.body;
2188
2388
  } catch (error) {
2189
2389
  if (error instanceof NotFoundError) {
@@ -2204,17 +2404,11 @@ var AssetManager = class {
2204
2404
  * Clear the asset cache.
2205
2405
  */
2206
2406
  clearCache() {
2207
- cache.clear();
2208
- getAssetMetadataStore()?.clear();
2407
+ this.cache.clear();
2209
2408
  }
2210
- // `contentType` defaults to JSON for the metadata/register endpoints. Pass
2211
- // `undefined` for binary content upload so fetch infers the type from the
2212
- // body — forcing application/json would mislabel a Blob/ArrayBuffer/stream.
2213
- _buildHeaders(contentType = "application/json") {
2214
- const headers = {};
2215
- if (contentType) headers["Content-Type"] = contentType;
2216
- this.venue.auth.apply(headers, this.venue.venueId);
2217
- return headers;
2409
+ /** Clear the explicitly configured cross-session metadata store. */
2410
+ clearPersistentCache() {
2411
+ getAssetMetadataStore()?.clear();
2218
2412
  }
2219
2413
  };
2220
2414
 
@@ -2227,23 +2421,23 @@ var OperationManager = class {
2227
2421
  * List all named operations available on this venue
2228
2422
  */
2229
2423
  async list() {
2230
- return fetchWithError(`${this.venue.baseUrl}/api/v1/operations`);
2424
+ return venueJson(this.venue, "/api/v1/operations");
2231
2425
  }
2232
2426
  /**
2233
2427
  * Get details of a named operation
2234
2428
  * @param name - Operation name (e.g., "v/ops/schema/infer")
2235
2429
  */
2236
2430
  async get(name) {
2237
- return fetchWithError(`${this.venue.baseUrl}/api/v1/operations/${name}`);
2431
+ return venueJson(this.venue, `/api/v1/operations/${name}`);
2238
2432
  }
2239
2433
  /**
2240
2434
  * Execute an operation and wait for the result
2241
2435
  * @param assetId - Operation asset ID or named operation
2242
2436
  * @param input - Operation input parameters
2243
- * @param options - Invoke options (e.g., ucans)
2437
+ * @param options - Execution options (e.g., ucans or private execution)
2244
2438
  */
2245
2439
  async run(assetId, input, options) {
2246
- if (this.venue.privateJobs) {
2440
+ if (options?.private ?? this.venue.privateJobs) {
2247
2441
  return this._runPrivate(assetId, input, options);
2248
2442
  }
2249
2443
  const job = await this.invoke(assetId, input, options);
@@ -2265,9 +2459,8 @@ var OperationManager = class {
2265
2459
  wait: true
2266
2460
  };
2267
2461
  if (options?.ucans) payload.ucans = options.ucans;
2268
- const rec = await fetchWithError(`${this.venue.baseUrl}/api/v1/invoke`, {
2462
+ const rec = await venueJson(this.venue, "/api/v1/invoke", {
2269
2463
  method: "POST",
2270
- headers: this._buildHeaders(),
2271
2464
  body: JSON.stringify(payload)
2272
2465
  });
2273
2466
  const status = rec.status;
@@ -2279,6 +2472,7 @@ var OperationManager = class {
2279
2472
  const job = new Job(rec.id ?? "", this.venue, rec);
2280
2473
  return await job.result();
2281
2474
  } catch (e) {
2475
+ if (!(e instanceof NotFoundError)) throw e;
2282
2476
  throw new CoviaError(
2283
2477
  `Private job completed while unobserved \u2014 its record is already forgotten, so the result could not be collected. Private jobs that outlive the venue wait window cannot be reliably collected; consider a non-private run for long operations. (${e.message})`
2284
2478
  );
@@ -2291,7 +2485,7 @@ var OperationManager = class {
2291
2485
  * @param options - Invoke options (e.g., ucans)
2292
2486
  */
2293
2487
  async invoke(assetId, input, options) {
2294
- if (this.venue.privateJobs) {
2488
+ if (options?.private ?? this.venue.privateJobs) {
2295
2489
  throw new CoviaError(
2296
2490
  "Private-jobs mode requires run(): a completed private job is immediately forgotten by the venue, so a poll-style Job cannot collect its result."
2297
2491
  );
@@ -2303,27 +2497,15 @@ var OperationManager = class {
2303
2497
  if (options?.ucans) {
2304
2498
  payload.ucans = options.ucans;
2305
2499
  }
2306
- const response = await fetchWithError(`${this.venue.baseUrl}/api/v1/invoke`, {
2500
+ const response = await venueJson(this.venue, "/api/v1/invoke", {
2307
2501
  method: "POST",
2308
- headers: this._buildHeaders(),
2309
2502
  body: JSON.stringify(payload)
2310
2503
  });
2311
2504
  return new Job(response.id ?? "", this.venue, response);
2312
2505
  }
2313
- _buildHeaders() {
2314
- const headers = { "Content-Type": "application/json" };
2315
- this.venue.auth.apply(headers, this.venue.venueId);
2316
- return headers;
2317
- }
2318
2506
  };
2319
2507
 
2320
2508
  // src/WorkspaceManager.ts
2321
- function versionAtLeast2(version, major, minor) {
2322
- const m = version?.match(/^(\d+)\.(\d+)/);
2323
- if (!m) return true;
2324
- const [maj, min] = [Number(m[1]), Number(m[2])];
2325
- return maj > major || maj === major && min >= minor;
2326
- }
2327
2509
  var WorkspaceManager = class {
2328
2510
  constructor(venue) {
2329
2511
  this.venue = venue;
@@ -2331,16 +2513,11 @@ var WorkspaceManager = class {
2331
2513
  // so pre-0.3 venues pay the probe once, not one failed GET per read.
2332
2514
  this.valuesSupported = true;
2333
2515
  }
2334
- _headers() {
2335
- const headers = { "Content-Type": "application/json" };
2336
- this.venue.auth.apply(headers, this.venue.venueId);
2337
- return headers;
2338
- }
2339
2516
  /** GET a job-free `/api/v1/values/{op}` read; omit undefined params. */
2340
2517
  _values(op, params) {
2341
2518
  const qs = new URLSearchParams();
2342
2519
  for (const [k, v] of Object.entries(params)) if (v !== void 0) qs.set(k, String(v));
2343
- return fetchWithError(`${this.venue.baseUrl}/api/v1/values/${op}?${qs.toString()}`, { headers: this._headers() });
2520
+ return venueJson(this.venue, `/api/v1/values/${op}?${qs.toString()}`);
2344
2521
  }
2345
2522
  /**
2346
2523
  * Whether the venue serves `GET /api/v1/values/*`: no if a probe already
@@ -2351,40 +2528,46 @@ var WorkspaceManager = class {
2351
2528
  supportsValues() {
2352
2529
  if (!this.valuesSupported) return false;
2353
2530
  const status = this.venue.lastKnownStatus;
2354
- if (status && (!status.version || !versionAtLeast2(status.version, 0, 3))) {
2531
+ if (status && (!status.version || !versionAtLeast(status.version, 0, 3))) {
2355
2532
  this.valuesSupported = false;
2356
2533
  }
2357
2534
  return this.valuesSupported;
2358
2535
  }
2359
- /** A job-free values read, falling back to the invoke path on pre-0.3 venues. */
2360
- async _valuesOr(op, params, fallback) {
2536
+ unsupported(feature) {
2537
+ throw new UnsupportedVenueFeatureError(feature);
2538
+ }
2539
+ /** A job-free Values read that rejects when the endpoint is unavailable. */
2540
+ async valuesRead(op, params) {
2361
2541
  if (this.supportsValues()) {
2362
2542
  try {
2363
2543
  return await this._values(op, params);
2364
2544
  } catch (e) {
2365
2545
  if (!(e instanceof NotFoundError)) throw e;
2546
+ if (!ROUTE_MISSING_404.test(e.message)) throw e;
2366
2547
  this.valuesSupported = false;
2367
2548
  }
2368
2549
  }
2369
- return fallback();
2550
+ return this.unsupported(`workspace ${op} reads`);
2370
2551
  }
2371
2552
  // ── job-free reads (#177) ───────────────────────────────────────────────────
2372
2553
  async read(path, maxSize, ucans) {
2373
- if (ucans?.length) return this.venue.operations.run("v/ops/covia/read", { path, maxSize }, { ucans });
2374
- return this._valuesOr("read", { path, maxSize }, () => this.venue.operations.run("v/ops/covia/read", { path, maxSize }));
2554
+ if (ucans?.length) return this.unsupported("UCAN-authorised workspace reads");
2555
+ return this.valuesRead("read", { path, maxSize });
2375
2556
  }
2376
2557
  async list(path, limit, offset, ucans) {
2377
2558
  path = path || "/";
2378
- if (ucans?.length) return this.venue.operations.run("v/ops/covia/list", { path, limit, offset }, { ucans });
2379
- return this._valuesOr("list", { path, limit, offset }, () => this.venue.operations.run("v/ops/covia/list", { path, limit, offset }));
2559
+ if (ucans?.length) return this.unsupported("UCAN-authorised workspace listings");
2560
+ return this.valuesRead("list", { path, limit, offset });
2380
2561
  }
2381
2562
  async slice(path, offset, limit, ucans) {
2382
- if (ucans?.length) return this.venue.operations.run("v/ops/covia/slice", { path, offset, limit }, { ucans });
2383
- return this._valuesOr("slice", { path, offset, limit }, () => this.venue.operations.run("v/ops/covia/slice", { path, offset, limit }));
2563
+ if (ucans?.length) return this.unsupported("UCAN-authorised workspace slices");
2564
+ return this.valuesRead("slice", { path, offset, limit });
2384
2565
  }
2385
2566
  async inspect(paths, budget, compact, ucans) {
2386
- if (ucans?.length || Array.isArray(paths)) return this.venue.operations.run("v/ops/covia/inspect", { paths, budget, compact }, { ucans });
2387
- return this._valuesOr("inspect", { path: paths, budget, compact }, () => this.venue.operations.run("v/ops/covia/inspect", { paths, budget, compact }));
2567
+ if (ucans?.length || Array.isArray(paths)) return this.unsupported(
2568
+ Array.isArray(paths) ? "multi-path workspace inspection" : "UCAN-authorised workspace inspection"
2569
+ );
2570
+ return this.valuesRead("inspect", { path: paths, budget, compact });
2388
2571
  }
2389
2572
  /**
2390
2573
  * Count entries at a depth below `path` — a job-free server-side tally, so the
@@ -2396,8 +2579,8 @@ var WorkspaceManager = class {
2396
2579
  */
2397
2580
  async count(path, opts = {}) {
2398
2581
  const { depth, ucans } = opts;
2399
- if (ucans?.length) return this.venue.operations.run("v/ops/covia/aggregate", { path, depth }, { ucans });
2400
- return this._valuesOr("count", { path, depth }, () => this.venue.operations.run("v/ops/covia/aggregate", { path, depth }));
2582
+ if (ucans?.length) return this.unsupported("UCAN-authorised workspace counts");
2583
+ return this.valuesRead("count", { path, depth });
2401
2584
  }
2402
2585
  /**
2403
2586
  * Count entries at a depth below `path`, optionally partitioned by a field —
@@ -2409,8 +2592,8 @@ var WorkspaceManager = class {
2409
2592
  */
2410
2593
  async aggregate(path, opts = {}) {
2411
2594
  const { depth, groupBy, ucans } = opts;
2412
- if (ucans?.length) return this.venue.operations.run("v/ops/covia/aggregate", { path, depth, groupBy }, { ucans });
2413
- return this._valuesOr("aggregate", { path, depth, groupBy }, () => this.venue.operations.run("v/ops/covia/aggregate", { path, depth, groupBy }));
2595
+ if (ucans?.length) return this.unsupported("UCAN-authorised workspace aggregation");
2596
+ return this.valuesRead("aggregate", { path, depth, groupBy });
2414
2597
  }
2415
2598
  // ── writes stay on the job path (they should leave an audit record) ─────────
2416
2599
  async write(path, value, ucans) {
@@ -2471,6 +2654,121 @@ var SecretManager = class {
2471
2654
  }
2472
2655
  };
2473
2656
 
2657
+ // src/SkillManager.ts
2658
+ var PAGE_SIZE = 100;
2659
+ var SkillManager = class {
2660
+ constructor(venue) {
2661
+ this.venue = venue;
2662
+ }
2663
+ /** Return every skill asset directly contained at `path`. */
2664
+ async list(path) {
2665
+ const location = path.replace(/\/+$/, "");
2666
+ const skills = [];
2667
+ let offset = 0;
2668
+ for (; ; ) {
2669
+ const page = await this.venue.workspace.list(location, PAGE_SIZE, offset);
2670
+ if (!page.exists) {
2671
+ if (skills.length === 0) return [];
2672
+ throw new CoviaError(`Skill listing at ${location} disappeared mid-listing`);
2673
+ }
2674
+ const keys = page.keys ?? [];
2675
+ const assets = await Promise.all(
2676
+ keys.map((key) => this.get(`${location}/${key}`))
2677
+ );
2678
+ skills.push(...assets);
2679
+ const nextOffset = (page.offset ?? offset) + keys.length;
2680
+ if (keys.length === 0 || nextOffset >= (page.count ?? nextOffset)) break;
2681
+ if (nextOffset <= offset) {
2682
+ throw new CoviaError("Venue returned a skills page that did not advance");
2683
+ }
2684
+ offset = nextOffset;
2685
+ }
2686
+ return skills;
2687
+ }
2688
+ /** Return the ordinary asset at exactly `path`, with typed skill metadata. */
2689
+ async get(path) {
2690
+ return this.venue.assets.get(path);
2691
+ }
2692
+ };
2693
+
2694
+ // src/MemoryManager.ts
2695
+ var DEFAULT_MEMORY_PATH = "w/memory";
2696
+ function memoryChange(value) {
2697
+ const result = record(value);
2698
+ if (typeof result?.n !== "number" || typeof result.count !== "number") {
2699
+ throw new CoviaError("Venue returned an invalid memory mutation result");
2700
+ }
2701
+ return { number: result.n, count: result.count };
2702
+ }
2703
+ var MemoryManager = class {
2704
+ constructor(venue) {
2705
+ this.venue = venue;
2706
+ }
2707
+ /**
2708
+ * Read memory directly from the workspace Values surface. This is job-free;
2709
+ * unlike `v/ops/memory {command:'recall'}`, it returns structured entries
2710
+ * rather than an LLM-oriented numbered text block.
2711
+ */
2712
+ async list(path = DEFAULT_MEMORY_PATH) {
2713
+ const result = await this.venue.workspace.read(path);
2714
+ if (!result.exists) {
2715
+ return [];
2716
+ }
2717
+ let raw;
2718
+ if (result.truncated) {
2719
+ raw = await sliceAll(this.venue.workspace, path);
2720
+ } else if (result.value === void 0 || result.value === null) {
2721
+ return [];
2722
+ } else if (Array.isArray(result.value)) {
2723
+ raw = result.value;
2724
+ } else {
2725
+ throw new CoviaError(`Memory at ${path} is not a flat list`);
2726
+ }
2727
+ const entries = raw.map((value, index) => {
2728
+ const item = record(value);
2729
+ const text = typeof value === "string" ? value : typeof item?.text === "string" ? item.text : void 0;
2730
+ if (text === void 0) {
2731
+ throw new CoviaError(`Memory entry ${index + 1} at ${path} has no text`);
2732
+ }
2733
+ return {
2734
+ number: index + 1,
2735
+ text,
2736
+ createdAt: typeof item?.ts === "number" ? item.ts : void 0,
2737
+ updatedAt: typeof item?.updated === "number" ? item.updated : void 0
2738
+ };
2739
+ });
2740
+ return entries;
2741
+ }
2742
+ /** Append a memory entry. Mutations intentionally remain audited Jobs. */
2743
+ async remember(text, path = DEFAULT_MEMORY_PATH) {
2744
+ const result = await this.venue.operations.run("v/ops/memory", {
2745
+ command: "remember",
2746
+ text,
2747
+ path
2748
+ });
2749
+ return memoryChange(result);
2750
+ }
2751
+ /** Replace a 1-based memory entry. Mutations intentionally remain audited Jobs. */
2752
+ async update(number, text, path = DEFAULT_MEMORY_PATH) {
2753
+ const result = await this.venue.operations.run("v/ops/memory", {
2754
+ command: "update",
2755
+ n: number,
2756
+ text,
2757
+ path
2758
+ });
2759
+ return memoryChange(result);
2760
+ }
2761
+ /** Remove a 1-based memory entry. Mutations intentionally remain audited Jobs. */
2762
+ async forget(number, path = DEFAULT_MEMORY_PATH) {
2763
+ const result = await this.venue.operations.run("v/ops/memory", {
2764
+ command: "forget",
2765
+ n: number,
2766
+ path
2767
+ });
2768
+ return memoryChange(result);
2769
+ }
2770
+ };
2771
+
2474
2772
  // src/Agent.ts
2475
2773
  var Agent = class _Agent {
2476
2774
  constructor(id, venue) {
@@ -2512,6 +2810,14 @@ var Agent = class _Agent {
2512
2810
  async info() {
2513
2811
  return this._agents.info(this.id);
2514
2812
  }
2813
+ /** Page this agent's sessions through the job-free workspace Values API. */
2814
+ async listSessions(options) {
2815
+ return this._agents.listSessions(this.id, options);
2816
+ }
2817
+ /** Read one session through the job-free workspace Values API. */
2818
+ async getSession(sessionId) {
2819
+ return this._agents.getSession(this.id, sessionId);
2820
+ }
2515
2821
  /**
2516
2822
  * Fork this agent into a new agent.
2517
2823
  * @param agentId - ID for the new forked agent
@@ -2596,7 +2902,11 @@ function connectionFailureMessage(venueId, attempts) {
2596
2902
  }
2597
2903
  var Venue = class _Venue {
2598
2904
  constructor(options = {}) {
2599
- /** Connection-level private-jobs mode — see {@link setPrivate}. */
2905
+ this._closed = false;
2906
+ /**
2907
+ * Connection-level private-jobs mode — see {@link setPrivate}.
2908
+ * @deprecated Pass `{ private: true }` to `operations.run()` instead.
2909
+ */
2600
2910
  this.privateJobs = false;
2601
2911
  this.baseUrl = options.baseUrl || "";
2602
2912
  this.venueId = options.venueId || "";
@@ -2607,6 +2917,10 @@ var Venue = class _Venue {
2607
2917
  description: options.description || ""
2608
2918
  };
2609
2919
  }
2920
+ /** Whether this handle has been closed and should not be reused by Grid. */
2921
+ get closed() {
2922
+ return this._closed;
2923
+ }
2610
2924
  /**
2611
2925
  * Puts this connection in **private-jobs mode**: every subsequent
2612
2926
  * `operations.run(...)` executes as a memory-only job (covia #192) — never
@@ -2615,7 +2929,10 @@ var Venue = class _Venue {
2615
2929
  *
2616
2930
  * Because a completed private job is immediately forgotten, results are
2617
2931
  * collected through the invoke `wait` window rather than polling — so
2618
- * private mode works with `run()`; a poll-style `invoke()` throws.
2932
+ * private mode works with `run()`; a poll-style `invoke()` throws.
2933
+ *
2934
+ * @deprecated Pass `{ private: true }` to `operations.run()` instead. The
2935
+ * per-call option avoids mutable connection-wide execution state.
2619
2936
  */
2620
2937
  setPrivate(enabled) {
2621
2938
  this.privateJobs = enabled;
@@ -2644,6 +2961,12 @@ var Venue = class _Venue {
2644
2961
  get secrets() {
2645
2962
  return this._secrets ?? (this._secrets = new SecretManager(this));
2646
2963
  }
2964
+ get skills() {
2965
+ return this._skills ?? (this._skills = new SkillManager(this));
2966
+ }
2967
+ get memory() {
2968
+ return this._memory ?? (this._memory = new MemoryManager(this));
2969
+ }
2647
2970
  /**
2648
2971
  * Connect to a venue, validating it with `GET {base}/api/v1/status`. If the
2649
2972
  * venue is auth-gated (status answers 401/403 — public access disabled), the
@@ -2673,7 +2996,7 @@ var Venue = class _Venue {
2673
2996
  baseUrl: venueId.baseUrl,
2674
2997
  venueId: venueId.venueId,
2675
2998
  name: venueId.metadata.name,
2676
- auth,
2999
+ auth: auth ?? venueId.auth,
2677
3000
  status: venueId.lastKnownStatus
2678
3001
  });
2679
3002
  await connected.status();
@@ -2692,7 +3015,7 @@ var Venue = class _Venue {
2692
3015
  if (typeof endpoint !== "string") {
2693
3016
  throw new CoviaError("No (string) endpoint found for DID");
2694
3017
  }
2695
- candidates = [endpoint.replace(/\/api\/v1/, "")];
3018
+ candidates = [stripTrailingSlash(endpoint).replace(/\/api\/v1$/, "")];
2696
3019
  } else {
2697
3020
  candidates = venueBaseUrlCandidates(venueId);
2698
3021
  }
@@ -2700,7 +3023,11 @@ var Venue = class _Venue {
2700
3023
  for (const baseUrl of candidates) {
2701
3024
  const statusUrl = baseUrl + "/api/v1/status";
2702
3025
  try {
2703
- const data = await fetchWithError(statusUrl);
3026
+ const data = await venueJson({
3027
+ baseUrl,
3028
+ venueId: expectedDid ?? "",
3029
+ auth: new NoAuth()
3030
+ }, "/api/v1/status");
2704
3031
  if (!data.did) {
2705
3032
  throw new CoviaError(`Venue status at ${statusUrl} did not include a DID`);
2706
3033
  }
@@ -2785,9 +3112,10 @@ var Venue = class _Venue {
2785
3112
  * @returns {Promise<string[]>}
2786
3113
  */
2787
3114
  async listSecrets() {
2788
- const result = await fetchWithError(`${this.baseUrl}/api/v1/secrets`, {
2789
- headers: this._buildHeaders()
2790
- });
3115
+ const result = await venueJson(
3116
+ this,
3117
+ "/api/v1/secrets"
3118
+ );
2791
3119
  return Array.isArray(result.items) ? result.items : [];
2792
3120
  }
2793
3121
  /**
@@ -2795,9 +3123,8 @@ var Venue = class _Venue {
2795
3123
  * @param name - Secret name
2796
3124
  */
2797
3125
  async deleteSecret(name) {
2798
- await fetchStreamWithError(`${this.baseUrl}/api/v1/secrets/${encodeURIComponent(name)}`, {
2799
- method: "DELETE",
2800
- headers: this._buildHeaders()
3126
+ await venueStream(this, `/api/v1/secrets/${encodeURIComponent(name)}`, {
3127
+ method: "DELETE"
2801
3128
  });
2802
3129
  }
2803
3130
  /**
@@ -2806,7 +3133,7 @@ var Venue = class _Venue {
2806
3133
  * @throws {VenueIdentityChangedError} If this address reports a different known DID.
2807
3134
  */
2808
3135
  async status() {
2809
- const data = await fetchWithError(`${this.baseUrl}/api/v1/status`);
3136
+ const data = await venueJson(this, "/api/v1/status");
2810
3137
  if (data.did) {
2811
3138
  if (this.venueId) {
2812
3139
  assertVenueIdentity(this.venueId, data.did, this.baseUrl);
@@ -2874,7 +3201,8 @@ var Venue = class _Venue {
2874
3201
  * Clears cached asset data for this venue.
2875
3202
  */
2876
3203
  close() {
2877
- this.assets.clearCache();
3204
+ this._closed = true;
3205
+ this._assets?.clearCache();
2878
3206
  }
2879
3207
  /**
2880
3208
  * Disposable support — allows `using venue = await Grid.connect(...)` in TS 5.2+.
@@ -2882,15 +3210,28 @@ var Venue = class _Venue {
2882
3210
  [Symbol.dispose]() {
2883
3211
  this.close();
2884
3212
  }
2885
- _buildHeaders() {
2886
- const headers = { "Content-Type": "application/json" };
2887
- this.auth.apply(headers, this.venueId);
2888
- return headers;
2889
- }
2890
3213
  };
2891
3214
 
2892
3215
  // src/Grid.ts
2893
- var cache2 = /* @__PURE__ */ new Map();
3216
+ var anonymousConnections = /* @__PURE__ */ new Map();
3217
+ var authenticatedConnections = /* @__PURE__ */ new WeakMap();
3218
+ function connectionsFor(auth) {
3219
+ if (!auth) return anonymousConnections;
3220
+ let connections = authenticatedConnections.get(auth);
3221
+ if (!connections) {
3222
+ connections = /* @__PURE__ */ new Map();
3223
+ authenticatedConnections.set(auth, connections);
3224
+ }
3225
+ return connections;
3226
+ }
3227
+ function normaliseKey(venueId) {
3228
+ return venueId.replace(/\/+$/, "");
3229
+ }
3230
+ function removeEntry(cache, entry) {
3231
+ for (const [key, candidate] of cache) {
3232
+ if (candidate === entry) cache.delete(key);
3233
+ }
3234
+ }
2894
3235
  var Grid = class {
2895
3236
  /**
2896
3237
  * Static method to connect to a venue
@@ -2899,12 +3240,27 @@ var Grid = class {
2899
3240
  * @returns {Promise<Venue>} A new Venue instance configured appropriately
2900
3241
  */
2901
3242
  static async connect(venueId, auth) {
2902
- const cached = cache2.get(venueId);
2903
- if (cached && cached.auth === auth)
2904
- return cached.venue;
2905
- const connectedVenue = await Venue.connect(venueId, auth);
2906
- cache2.set(venueId, { auth, venue: connectedVenue });
2907
- return connectedVenue;
3243
+ const cache = connectionsFor(auth);
3244
+ const key = normaliseKey(venueId);
3245
+ const cached = cache.get(key);
3246
+ if (cached) {
3247
+ if (!cached.venue?.closed) return cached.promise;
3248
+ removeEntry(cache, cached);
3249
+ }
3250
+ const entry = {};
3251
+ entry.promise = Venue.connect(venueId, auth).then((venue) => {
3252
+ entry.venue = venue;
3253
+ cache.set(normaliseKey(venue.baseUrl), entry);
3254
+ if (key.startsWith("did:")) {
3255
+ cache.set(normaliseKey(venue.venueId), entry);
3256
+ }
3257
+ return venue;
3258
+ }).catch((error) => {
3259
+ removeEntry(cache, entry);
3260
+ throw error;
3261
+ });
3262
+ cache.set(key, entry);
3263
+ return entry.promise;
2908
3264
  }
2909
3265
  };
2910
3266
 
@@ -2947,4 +3303,4 @@ function relayDelegation(privateKey, venueDID, lifetimeSeconds, caps = []) {
2947
3303
  (*! noble-ed25519 - MIT License (c) 2019 Paul Miller (paulmillr.com) *)
2948
3304
  */
2949
3305
 
2950
- export { AdapterManager, Agent, AgentManager, AgentStatus, Asset, AssetManager, AssetNotFoundError, Auth, BasicAuth, BearerAuth, ChatSession, CoviaConnectionError, CoviaError, CoviaTimeoutError, CredentialsHTTP, DataAsset, Ed25519Auth, Grid, GridError, Job, JobFailedError, JobManager, JobNotFoundError, JobStatus, KeyPairAuth, Namespace, NoAuth, NotFoundError, Operation, OperationManager, RateLimitError, RunStatus, SecretManager, UCANManager, VENUE_RELAY, Venue, VenueIdentityChangedError, WorkspaceManager, assetHash, createSSEEvent, createUCANJWT, decodePublicKey, didFor, didFromPublicKey, didMethod, didUrl, encodePublicKey, fetchStreamWithError, fetchWithError, generateKeyPair, getAssetIdFromPath, getAssetIdFromVenueId, getAssetMetadataStore, getParsedAssetId, grant, hexToPrivateKey, identityToken, isDid, isJobComplete, isJobFinished, isJobPaused, logger, parseDidUrl, parseRetryAfterMs, parseSSEStream, privateKeyToHex, relayDelegation, retryDelayMs, setAssetMetadataStore, venueBaseUrlCandidates };
3306
+ export { AdapterManager, Agent, AgentManager, AgentSessionNotFoundError, AgentStatus, Asset, AssetManager, AssetNotFoundError, Auth, BasicAuth, BearerAuth, ChatSession, CoviaConnectionError, CoviaError, CoviaTimeoutError, CredentialsHTTP, DataAsset, Ed25519Auth, Grid, GridError, Job, JobFailedError, JobManager, JobNotFoundError, JobStatus, KeyPairAuth, MemoryManager, Namespace, NoAuth, NotFoundError, Operation, OperationManager, RateLimitError, RunStatus, SecretManager, SkillManager, UCANManager, UnsupportedVenueFeatureError, VENUE_RELAY, Venue, VenueIdentityChangedError, WorkspaceManager, assetHash, createSSEEvent, createUCANJWT, decodePublicKey, didFor, didFromPublicKey, didMethod, didUrl, encodePublicKey, fetchStreamWithError, fetchWithError, generateKeyPair, getAssetIdFromPath, getAssetIdFromVenueId, getAssetMetadataStore, getParsedAssetId, grant, hexToPrivateKey, identityToken, isDid, isJobComplete, isJobFinished, isJobPaused, logger, parseDidUrl, parseRetryAfterMs, parseSSEStream, privateKeyToHex, relayDelegation, retryDelayMs, setAssetMetadataStore, venueBaseUrlCandidates };