@agalmic/contextengine 0.1.0 → 0.2.0

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.
@@ -7618,8 +7618,8 @@ var require_bytes32 = __commonJS({
7618
7618
  var constants_1 = require_lib8();
7619
7619
  var bytes_1 = require_lib2();
7620
7620
  var utf8_1 = require_utf8();
7621
- function formatBytes32String(text4) {
7622
- var bytes = (0, utf8_1.toUtf8Bytes)(text4);
7621
+ function formatBytes32String(text5) {
7622
+ var bytes = (0, utf8_1.toUtf8Bytes)(text5);
7623
7623
  if (bytes.length > 31) {
7624
7624
  throw new Error("bytes32 string must be less than 32 bytes");
7625
7625
  }
@@ -8137,8 +8137,8 @@ var require_id = __commonJS({
8137
8137
  exports.id = void 0;
8138
8138
  var keccak256_1 = require_lib5();
8139
8139
  var strings_1 = require_lib9();
8140
- function id(text4) {
8141
- return (0, keccak256_1.keccak256)((0, strings_1.toUtf8Bytes)(text4));
8140
+ function id(text5) {
8141
+ return (0, keccak256_1.keccak256)((0, strings_1.toUtf8Bytes)(text5));
8142
8142
  }
8143
8143
  exports.id = id;
8144
8144
  }
@@ -21923,13 +21923,13 @@ var require_aes_js = __commonJS({
21923
21923
  targetArray.set(sourceArray, targetStart);
21924
21924
  }
21925
21925
  var convertUtf8 = /* @__PURE__ */ (function() {
21926
- function toBytes(text4) {
21926
+ function toBytes(text5) {
21927
21927
  var result = [], i = 0;
21928
- text4 = encodeURI(text4);
21929
- while (i < text4.length) {
21930
- var c = text4.charCodeAt(i++);
21928
+ text5 = encodeURI(text5);
21929
+ while (i < text5.length) {
21930
+ var c = text5.charCodeAt(i++);
21931
21931
  if (c === 37) {
21932
- result.push(parseInt(text4.substr(i, 2), 16));
21932
+ result.push(parseInt(text5.substr(i, 2), 16));
21933
21933
  i += 2;
21934
21934
  } else {
21935
21935
  result.push(c);
@@ -21960,10 +21960,10 @@ var require_aes_js = __commonJS({
21960
21960
  };
21961
21961
  })();
21962
21962
  var convertHex = /* @__PURE__ */ (function() {
21963
- function toBytes(text4) {
21963
+ function toBytes(text5) {
21964
21964
  var result = [];
21965
- for (var i = 0; i < text4.length; i += 2) {
21966
- result.push(parseInt(text4.substr(i, 2), 16));
21965
+ for (var i = 0; i < text5.length; i += 2) {
21966
+ result.push(parseInt(text5.substr(i, 2), 16));
21967
21967
  }
21968
21968
  return result;
21969
21969
  }
@@ -31004,7 +31004,7 @@ var require_websocket = __commonJS({
31004
31004
  var http = __require("http");
31005
31005
  var net = __require("net");
31006
31006
  var tls = __require("tls");
31007
- var { randomBytes, createHash } = __require("crypto");
31007
+ var { randomBytes, createHash: createHash2 } = __require("crypto");
31008
31008
  var { Readable } = __require("stream");
31009
31009
  var { URL: URL2 } = __require("url");
31010
31010
  var PerMessageDeflate = require_permessage_deflate();
@@ -31586,7 +31586,7 @@ var require_websocket = __commonJS({
31586
31586
  abortHandshake(websocket, socket, "Invalid Upgrade header");
31587
31587
  return;
31588
31588
  }
31589
- const digest = createHash("sha1").update(key + GUID).digest("base64");
31589
+ const digest = createHash2("sha1").update(key + GUID).digest("base64");
31590
31590
  if (res.headers["sec-websocket-accept"] !== digest) {
31591
31591
  abortHandshake(websocket, socket, "Invalid Sec-WebSocket-Accept header");
31592
31592
  return;
@@ -31900,7 +31900,7 @@ var require_websocket_server = __commonJS({
31900
31900
  var https = __require("https");
31901
31901
  var net = __require("net");
31902
31902
  var tls = __require("tls");
31903
- var { createHash } = __require("crypto");
31903
+ var { createHash: createHash2 } = __require("crypto");
31904
31904
  var PerMessageDeflate = require_permessage_deflate();
31905
31905
  var WebSocket = require_websocket();
31906
31906
  var { format, parse } = require_extension();
@@ -32127,7 +32127,7 @@ var require_websocket_server = __commonJS({
32127
32127
  );
32128
32128
  }
32129
32129
  if (this._state > RUNNING) return abortHandshake(socket, 503);
32130
- const digest = createHash("sha1").update(key + GUID).digest("base64");
32130
+ const digest = createHash2("sha1").update(key + GUID).digest("base64");
32131
32131
  const headers = [
32132
32132
  "HTTP/1.1 101 Switching Protocols",
32133
32133
  "Upgrade: websocket",
@@ -32137,7 +32137,7 @@ var require_websocket_server = __commonJS({
32137
32137
  const ws = new WebSocket(null);
32138
32138
  let protocol = req.headers["sec-websocket-protocol"];
32139
32139
  if (protocol) {
32140
- protocol = protocol.split(",").map(trim7);
32140
+ protocol = protocol.split(",").map(trim9);
32141
32141
  if (this.options.handleProtocols) {
32142
32142
  protocol = this.options.handleProtocols(protocol, req);
32143
32143
  } else {
@@ -32206,7 +32206,7 @@ var require_websocket_server = __commonJS({
32206
32206
  socket.removeListener("error", socketOnError);
32207
32207
  socket.destroy();
32208
32208
  }
32209
- function trim7(str) {
32209
+ function trim9(str) {
32210
32210
  return str.trim();
32211
32211
  }
32212
32212
  }
@@ -36317,10 +36317,9 @@ var require_rpcDefaults = __commonJS({
36317
36317
  }
36318
36318
  });
36319
36319
 
36320
- // client/src/utilities/crypto/groupPasswordDerivation.cjs
36320
+ // shared/crypto/groupPasswordDerivation.cjs
36321
36321
  var require_groupPasswordDerivation = __commonJS({
36322
- "client/src/utilities/crypto/groupPasswordDerivation.cjs"(exports, module) {
36323
- "use strict";
36322
+ "shared/crypto/groupPasswordDerivation.cjs"(exports, module) {
36324
36323
  var GROUP_PASSWORD_DERIVATION = "sbt-group-password-v4";
36325
36324
  var GROUP_PASSWORD_PBKDF2_ITERATIONS = 6e5;
36326
36325
  var createGroupPasswordDerivation2 = (ethers7) => {
@@ -36407,15 +36406,15 @@ var SETTINGS = Object.freeze({
36407
36406
  var MAX_LABEL_LENGTH = 80;
36408
36407
  var trim = (value) => String(value ?? "").trim();
36409
36408
  var normalizeLabel = (value, field) => {
36410
- const text4 = trim(value).replace(/\s+/g, " ");
36411
- if (text4.length > MAX_LABEL_LENGTH) throw new Error(`${field} must be ${MAX_LABEL_LENGTH} characters or fewer.`);
36412
- return text4;
36409
+ const text5 = trim(value).replace(/\s+/g, " ");
36410
+ if (text5.length > MAX_LABEL_LENGTH) throw new Error(`${field} must be ${MAX_LABEL_LENGTH} characters or fewer.`);
36411
+ return text5;
36413
36412
  };
36414
36413
  var normalizeHttpOrigin = (value, field) => {
36415
- const text4 = trim(value);
36414
+ const text5 = trim(value);
36416
36415
  let parsed;
36417
36416
  try {
36418
- parsed = new URL(text4);
36417
+ parsed = new URL(text5);
36419
36418
  } catch {
36420
36419
  throw new Error(`${field} must be an http(s) origin.`);
36421
36420
  }
@@ -36426,11 +36425,11 @@ var normalizeHttpOrigin = (value, field) => {
36426
36425
  };
36427
36426
  var isLocalUrl = (url) => ["localhost", "127.0.0.1", "[::1]"].includes(url.hostname);
36428
36427
  var normalizeRpcUrl = (value, allowLocal) => {
36429
- const text4 = trim(value);
36430
- if (!text4) return "";
36428
+ const text5 = trim(value);
36429
+ if (!text5) return "";
36431
36430
  let parsed;
36432
36431
  try {
36433
- parsed = new URL(text4);
36432
+ parsed = new URL(text5);
36434
36433
  } catch {
36435
36434
  throw new Error("CE_MCP_RPC_URL must be an https URL.");
36436
36435
  }
@@ -36495,13 +36494,22 @@ var ensureStateDir = async (stateDir) => {
36495
36494
  var writeOwnerOnlyJson = async (filePath, value, { exclusive = false } = {}) => {
36496
36495
  const body = `${JSON.stringify(value, null, 2)}
36497
36496
  `;
36498
- if (exclusive) {
36499
- await fs.writeFile(filePath, body, { mode: OWNER_ONLY_FILE, flag: "wx" });
36500
- return;
36501
- }
36502
36497
  const temporary = `${filePath}.${process.pid}.${crypto.randomBytes(6).toString("hex")}.tmp`;
36503
36498
  await fs.writeFile(temporary, body, { mode: OWNER_ONLY_FILE, flag: "wx" });
36504
- await fs.rename(temporary, filePath);
36499
+ try {
36500
+ if (exclusive) {
36501
+ try {
36502
+ await fs.link(temporary, filePath);
36503
+ } catch (error) {
36504
+ if (!["EPERM", "ENOTSUP", "EXDEV"].includes(error?.code)) throw error;
36505
+ await fs.writeFile(filePath, body, { mode: OWNER_ONLY_FILE, flag: "wx" });
36506
+ }
36507
+ } else {
36508
+ await fs.rename(temporary, filePath);
36509
+ }
36510
+ } finally {
36511
+ await fs.rm(temporary, { force: true });
36512
+ }
36505
36513
  };
36506
36514
  var readKeyFile = async (keyPath) => {
36507
36515
  const info = await fs.lstat(keyPath);
@@ -36521,8 +36529,7 @@ var readKeyFile = async (keyPath) => {
36521
36529
  }
36522
36530
  return wallet;
36523
36531
  };
36524
- var loadOrCreateWallet = async (stateDir) => {
36525
- await ensureStateDir(stateDir);
36532
+ var loadOrCreateKey = async (stateDir) => {
36526
36533
  const keyPath = path2.join(stateDir, KEY_FILE);
36527
36534
  for (let attempt = 0; attempt < 2; attempt += 1) {
36528
36535
  try {
@@ -36530,6 +36537,12 @@ var loadOrCreateWallet = async (stateDir) => {
36530
36537
  } catch (error) {
36531
36538
  if (error?.code !== "ENOENT") throw error;
36532
36539
  }
36540
+ const saved = await readAgentState(stateDir);
36541
+ if (saved.address || Object.keys(saved.sessions).length) {
36542
+ throw new Error(
36543
+ `${keyPath} is missing, but ${STATE_FILE} belongs to ${saved.address ? `agent ${saved.address}` : "an earlier agent"}. Restore the key from a backup, or move ${STATE_FILE} aside to start as a new agent.`
36544
+ );
36545
+ }
36533
36546
  const wallet = import_ethers.ethers.Wallet.createRandom();
36534
36547
  try {
36535
36548
  await writeOwnerOnlyJson(
@@ -36544,7 +36557,21 @@ var loadOrCreateWallet = async (stateDir) => {
36544
36557
  }
36545
36558
  throw new Error(`${keyPath} could not be read or created.`);
36546
36559
  };
36547
- var emptyState = () => ({ version: 1, profile: {}, sessions: {}, current: "" });
36560
+ var loadOrCreateWallet = async (stateDir) => {
36561
+ await ensureStateDir(stateDir);
36562
+ const loaded = await loadOrCreateKey(stateDir);
36563
+ await withStateWriter(stateDir, async () => {
36564
+ const state = await readAgentState(stateDir);
36565
+ if (state.address && state.address.toLowerCase() !== loaded.wallet.address.toLowerCase()) {
36566
+ throw new Error(
36567
+ `${path2.join(stateDir, STATE_FILE)} belongs to agent ${state.address}, but ${KEY_FILE} holds ${loaded.wallet.address}. Restore the matching files, or move ${STATE_FILE} aside to start fresh with this key.`
36568
+ );
36569
+ }
36570
+ if (!state.address) await writeOwnerOnlyJson(path2.join(stateDir, STATE_FILE), { ...state, address: loaded.wallet.address });
36571
+ });
36572
+ return loaded;
36573
+ };
36574
+ var emptyState = () => ({ version: 1, address: "", profile: {}, sessions: {}, current: "" });
36548
36575
  var readAgentState = async (stateDir) => {
36549
36576
  let parsed;
36550
36577
  try {
@@ -36556,6 +36583,8 @@ var readAgentState = async (stateDir) => {
36556
36583
  if (!isRecord(parsed)) return emptyState();
36557
36584
  return {
36558
36585
  version: 1,
36586
+ // The agent this state belongs to; its remembered answers are that agent's.
36587
+ address: typeof parsed.address === "string" ? parsed.address : "",
36559
36588
  profile: isRecord(parsed.profile) ? parsed.profile : {},
36560
36589
  sessions: isRecord(parsed.sessions) ? parsed.sessions : {},
36561
36590
  current: typeof parsed.current === "string" ? parsed.current : ""
@@ -36606,7 +36635,15 @@ var hasId = (message) => Object.prototype.hasOwnProperty.call(message, "id");
36606
36635
  var isValidId = (id) => typeof id === "string" || typeof id === "number" && Number.isFinite(id);
36607
36636
  var errorResponse = (id, code, message) => ({ jsonrpc: "2.0", id, error: { code, message } });
36608
36637
  var resultResponse = (id, result) => ({ jsonrpc: "2.0", id, result });
36609
- var describeError = (error) => String(error?.message || error || "Unknown error.");
36638
+ var MAX_ERROR_LENGTH = 1e3;
36639
+ var DIAGNOSTICS = /\[ See: |\((?=\w+=)|\b(?:requestBody|body|url|transaction|data|error)=/i;
36640
+ var URLS = /\b[a-z][\w+.-]*:\/\/[^\s"'`]+/gi;
36641
+ var hostOnly = (url) => /<\w+>/.test(url) ? url : url.replace(/^([^:]+:\/\/)(?:[^/?#]*@)?([^/?#]*)[\s\S]*$/, "$1$2");
36642
+ var describeError = (error) => {
36643
+ const text5 = String(error?.message || error || "").slice(0, MAX_ERROR_LENGTH);
36644
+ const cut = text5.search(DIAGNOSTICS);
36645
+ return (cut < 0 ? text5 : text5.slice(0, cut)).trimEnd().replace(URLS, hostOnly) || "Unknown error.";
36646
+ };
36610
36647
  var DEFAULT_TOOL_TIMEOUT_MS = 15 * 60 * 1e3;
36611
36648
  var createMcpServer = ({
36612
36649
  serverInfo,
@@ -36628,8 +36665,8 @@ var createMcpServer = ({
36628
36665
  ...annotations ? { annotations } : {}
36629
36666
  }));
36630
36667
  const toolResult = (value, isError = false) => {
36631
- const text4 = typeof value === "string" ? value : JSON.stringify(value, null, 2);
36632
- const result = { content: [{ type: "text", text: text4 }] };
36668
+ const text5 = typeof value === "string" ? value : JSON.stringify(value, null, 2);
36669
+ const result = { content: [{ type: "text", text: text5 }] };
36633
36670
  if (!isError && isRecord2(value) && protocolVersion >= STRUCTURED_CONTENT_SINCE) {
36634
36671
  result.structuredContent = value;
36635
36672
  }
@@ -36724,11 +36761,11 @@ var createMcpServer = ({
36724
36761
  }
36725
36762
  };
36726
36763
  const handleLine = async (line) => {
36727
- const text4 = String(line || "").trim();
36728
- if (!text4) return "";
36764
+ const text5 = String(line || "").trim();
36765
+ if (!text5) return "";
36729
36766
  let decoded;
36730
36767
  try {
36731
- decoded = JSON.parse(text4);
36768
+ decoded = JSON.parse(text5);
36732
36769
  } catch {
36733
36770
  return JSON.stringify(errorResponse(null, PARSE_ERROR, "Parse error."));
36734
36771
  }
@@ -36801,13 +36838,45 @@ var describeTestEthOffer = ({ config, resourcePresence } = {}) => {
36801
36838
  return { ...result, reason: "Session config is unavailable or unsupported." };
36802
36839
  }
36803
36840
  if (!network) return { ...result, reason: "No supported test network is configured." };
36804
- if (hosted && !(Array.isArray(config.workerAuthority?.participantScopes) && config.workerAuthority.participantScopes.includes("faucet"))) {
36841
+ if (hosted && !(Array.isArray(config.workerAuthority?.participantScopes) && config.workerAuthority.participantScopes.some((scope) => scope === "faucet_self" || scope === "faucet"))) {
36805
36842
  return { ...result, reason: "Participants are not offered test ETH." };
36806
36843
  }
36807
36844
  if (resourcePresence?.txGas !== true) return { ...result, reason: "A configured faucet has not been confirmed." };
36808
36845
  return { ...result, offered: true };
36809
36846
  };
36810
36847
 
36848
+ // mcp/lib/chainErrors.mjs
36849
+ var SAFE_CODE = /^[A-Z_]{3,40}$/;
36850
+ var SAFE_REASON = /^[A-Za-z][\w .,'()-]{0,119}$/;
36851
+ var TX_HASH = /^0x[0-9a-f]{64}$/i;
36852
+ var safeErrorCode = (error) => typeof error?.code === "string" && SAFE_CODE.test(error.code) ? error.code : "";
36853
+ var safeErrorReason = (error) => [error?.reason, error?.errorName, error?.error?.reason].map((value) => typeof value === "string" ? value.replace(/^execution reverted:?\s*/i, "").trim() : "").find((value) => SAFE_REASON.test(value)) || "";
36854
+ var transactionHashOf = (error) => [error?.transactionHash, error?.transaction?.hash, error?.receipt?.transactionHash].find((value) => typeof value === "string" && TX_HASH.test(value));
36855
+ var safeChainError = (error, { submission = false } = {}) => {
36856
+ const code = safeErrorCode(error);
36857
+ const detail = [code, safeErrorReason(error)].filter(Boolean).join(": ");
36858
+ const note = detail ? ` (${detail})` : "";
36859
+ if (submission) {
36860
+ const txHash = transactionHashOf(error);
36861
+ return Object.assign(
36862
+ new Error(`${txHash ? `Transaction ${txHash}` : "The transaction"} may have been submitted but its outcome is unknown${note}. Check the chain before retrying.`),
36863
+ { ...code ? { code } : {}, reconciliationRequired: true, ...txHash ? { txHash } : {} }
36864
+ );
36865
+ }
36866
+ const retryable = code !== "CALL_EXCEPTION";
36867
+ return Object.assign(
36868
+ new Error(retryable ? `The chain request failed${note}. Check the RPC connection and retry.` : `The contract rejected the request${note}.`),
36869
+ { ...code ? { code } : {}, retryable }
36870
+ );
36871
+ };
36872
+ var chainRequest = async (request, options) => {
36873
+ try {
36874
+ return await request();
36875
+ } catch (error) {
36876
+ throw safeChainError(error, options);
36877
+ }
36878
+ };
36879
+
36811
36880
  // mcp/lib/testEth.mjs
36812
36881
  var amount = (value) => typeof value === "string" && /^\d+(?:\.\d{1,18})?$/.test(value) ? value : null;
36813
36882
  var pause = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
@@ -36836,15 +36905,16 @@ var createTestEthRequester = ({
36836
36905
  await ctx.client.ensureToken();
36837
36906
  if (ctx.client.tokenClaims?.scopes?.faucet !== true) throw new Error("This session does not offer test ETH to you.");
36838
36907
  const provider = suppliedProvider || createProvider(chainId);
36839
- const reported = Number(await provider.send("eth_chainId", []));
36908
+ const reported = Number(await chainRequest(() => provider.send("eth_chainId", [])));
36840
36909
  if (reported !== chainId) throw new Error(`The RPC for chain ${chainId} reports chain ${reported}; refusing the faucet request.`);
36841
- const before = await provider.getBalance(wallet.address);
36910
+ const before = await chainRequest(() => provider.getBalance(wallet.address));
36842
36911
  checkpoint();
36843
36912
  let paid;
36844
36913
  try {
36845
36914
  paid = await ctx.client.request("/", {
36846
36915
  method: "POST",
36847
36916
  auth: true,
36917
+ signal,
36848
36918
  headers: { "X-Session-Slug": ctx.info.slug },
36849
36919
  body: { action: "request_test_eth", to: wallet.address }
36850
36920
  });
@@ -36872,7 +36942,7 @@ var createTestEthRequester = ({
36872
36942
  let observed;
36873
36943
  try {
36874
36944
  observed = await Promise.race([
36875
- provider.getBalance(wallet.address).catch(() => null),
36945
+ chainRequest(() => provider.getBalance(wallet.address)).catch(() => null),
36876
36946
  new Promise((resolve) => {
36877
36947
  timer = setTimeout(() => resolve(null), Math.max(0, deadline - now()));
36878
36948
  })
@@ -36895,9 +36965,23 @@ var createTestEthRequester = ({
36895
36965
  };
36896
36966
  };
36897
36967
 
36968
+ // mcp/lib/participant.mjs
36969
+ import { createHash, randomUUID as randomUUID2 } from "node:crypto";
36970
+
36898
36971
  // mcp/lib/registrySession.mjs
36899
36972
  var import_ethers5 = __toESM(require_lib32(), 1);
36900
36973
 
36974
+ // shared/responseReadWindow.mjs
36975
+ var responsePairKey = (responder, id) => `${String(responder || "").toLowerCase()}|${String(id || "").toLowerCase()}`;
36976
+ var responseReadWindowOptions = (window2, responder, id) => window2 ? {
36977
+ _responseReadWindow: window2,
36978
+ blockTag: window2.laterPairs && !window2.laterPairs.has(responsePairKey(responder, id)) ? window2.unchangedBlockTag : window2.blockTag
36979
+ } : {};
36980
+ var historicalResponseReadError = (error, blockTag) => Object.assign(
36981
+ new Error(`Response state at block ${String(blockTag)} is unavailable: ${error instanceof Error ? error.message : String(error)}`),
36982
+ { code: "RESPONSE_HISTORY_UNAVAILABLE", retryable: true, blockTag, cause: error }
36983
+ );
36984
+
36901
36985
  // client/src/variables/contracts.json
36902
36986
  var contracts_default = {
36903
36987
  sessionRegistryAddresses: {
@@ -38193,50 +38277,41 @@ var normalizeRatingScale = (question = {}) => {
38193
38277
  )
38194
38278
  };
38195
38279
  };
38280
+ var DEFAULT_RATING_SCALE = Object.freeze({ min: 0, max: 10, minLabel: "0", maxLabel: "10" });
38281
+ var ratingStepFor = (question = {}) => {
38282
+ const source = [question.scale, question.ratingScale].find((record) => isObj(record) && recordHasRatingScaleMetadata(record)) || question;
38283
+ const step = Number(source.step);
38284
+ return Number.isFinite(step) && step > 0 ? step : 1;
38285
+ };
38286
+ var decimalPlaces = (value) => {
38287
+ const [coefficient, exponent = "0"] = String(value).split("e");
38288
+ return Math.max(0, (coefficient.split(".")[1]?.length || 0) - Number(exponent));
38289
+ };
38290
+ var normalizeRatingAnswer = (value, question = {}) => {
38291
+ if (typeof value !== "number" && typeof value !== "string") return null;
38292
+ if (typeof value === "string" && value.trim() === "") return null;
38293
+ const number = Number(value);
38294
+ const { min, max } = normalizeRatingScale(question) || DEFAULT_RATING_SCALE;
38295
+ const step = ratingStepFor(question);
38296
+ const index = (number - min) / step;
38297
+ if (!Number.isFinite(number) || number < min || number > max || !Number.isFinite(index) || Math.abs(index - Math.round(index)) > 1e-9) return null;
38298
+ const precision = Math.min(100, Math.max(decimalPlaces(step), decimalPlaces(min)));
38299
+ return Number((min + Math.round(index) * step).toFixed(precision));
38300
+ };
38196
38301
 
38197
- // mcp/lib/records.mjs
38302
+ // shared/questions/answerValue.mjs
38198
38303
  var QUESTION_TYPES = Object.freeze(["binary", "rating", "multichoice", "quadratic", "freeform"]);
38199
38304
  var BINARY_ANSWERS = Object.freeze(["Agree", "Unsure", "Disagree"]);
38200
- var MAX_PROMPT_LENGTH = 1e3;
38201
- var MAX_OPTION_LENGTH = 200;
38202
- var MAX_OPTIONS = 12;
38203
- var MAX_TAGS = 10;
38204
- var MAX_TAG_LENGTH = 40;
38205
38305
  var MAX_FREEFORM_LENGTH = 8e3;
38206
- var MAX_COMMENT_LENGTH = 4e3;
38207
38306
  var trim3 = (value) => typeof value === "string" ? value.trim() : typeof value === "number" ? String(value) : "";
38208
- var isRecord3 = (value) => !!value && typeof value === "object" && !Array.isArray(value);
38209
38307
  var fail = (error) => ({ ok: false, error });
38210
- var generateQuestionId = (type, prompt, options = [], singleSelect = false, voiceCredits = 99) => {
38211
- let dataToHash = `${type}:${(prompt || "").trim().toLowerCase()}`;
38212
- const validOptions = Array.isArray(options) ? options.filter((option) => typeof option === "string" && option.trim() !== "") : [];
38213
- if (type === "multichoice") {
38214
- if (validOptions.length > 0) dataToHash += `:${validOptions.map((option) => option.trim()).join(",").toLowerCase()}`;
38215
- if (singleSelect) dataToHash += ":single";
38216
- }
38217
- if (type === "quadratic") {
38218
- dataToHash += `:${JSON.stringify(validOptions.map((option) => option.trim().toLowerCase()))}:${voiceCredits}`;
38219
- }
38220
- return import_ethers3.ethers.utils.id(dataToHash);
38221
- };
38222
- var ratingScaleFor = (question = {}) => normalizeRatingScale(question) || { min: 0, max: 10, minLabel: "0", maxLabel: "10" };
38308
+ var ratingScaleFor = (question = {}) => ({ ...normalizeRatingScale(question) || DEFAULT_RATING_SCALE, step: ratingStepFor(question) });
38223
38309
  var optionsOf = (question) => Array.isArray(question.options) ? question.options.filter((option) => typeof option === "string") : [];
38224
38310
  var isSingleSelect = (question) => question.singleSelect === true || question.oneSelectionOnly === true || question.singleChoice === true;
38225
- var isLockedQuestion = (question) => question?.locked === true || !!question?.promptEncrypted || !!question?.encryption && typeof question.encryption === "object" || typeof question?.prompt === "string" && question.prompt.trim() === "[encrypted]";
38226
- var normalizeRecruitmentSource = (value) => {
38227
- const normalized2 = (typeof value === "string" ? value : "").trim().replace(/\s+/g, "-").slice(0, 128);
38228
- return /^[a-zA-Z0-9][a-zA-Z0-9._:-]{0,127}$/.test(normalized2) ? normalized2 : "";
38229
- };
38230
38311
  var matchOption = (options, value) => {
38231
38312
  const wanted = trim3(value).toLowerCase();
38232
38313
  return options.find((option) => option.trim().toLowerCase() === wanted);
38233
38314
  };
38234
- var readScore = (value, field) => {
38235
- if (value === void 0 || value === null || value === "") return { ok: true, value: null };
38236
- const number = Number(value);
38237
- if (!Number.isInteger(number) || number < 0 || number > 10) return fail(`${field} must be a whole number from 0 to 10.`);
38238
- return { ok: true, value: number };
38239
- };
38240
38315
  var normalizeAnswerValue = (question = {}, value) => {
38241
38316
  switch (question.type) {
38242
38317
  case "binary": {
@@ -38245,9 +38320,9 @@ var normalizeAnswerValue = (question = {}, value) => {
38245
38320
  }
38246
38321
  case "rating": {
38247
38322
  const scale = ratingScaleFor(question);
38248
- const number = typeof value === "string" && trim3(value) !== "" ? Number(value) : value;
38249
- if (!Number.isInteger(number) || number < scale.min || number > scale.max) {
38250
- return fail(`Rating answers must be a whole number from ${scale.min} to ${scale.max}.`);
38323
+ const number = normalizeRatingAnswer(value, question);
38324
+ if (number === null) {
38325
+ return fail(`Rating answers must be from ${scale.min} to ${scale.max} in increments of ${scale.step} from ${scale.min}.`);
38251
38326
  }
38252
38327
  return { ok: true, value: number };
38253
38328
  }
@@ -38275,15 +38350,72 @@ var normalizeAnswerValue = (question = {}, value) => {
38275
38350
  return { ok: true, value: votes };
38276
38351
  }
38277
38352
  case "freeform": {
38278
- const text4 = trim3(value);
38279
- if (!text4) return fail("Freeform answers must be non-empty text.");
38280
- if (text4.length > MAX_FREEFORM_LENGTH) return fail(`Freeform answers must be ${MAX_FREEFORM_LENGTH} characters or fewer.`);
38281
- return { ok: true, value: text4 };
38353
+ const text5 = trim3(value);
38354
+ if (!text5) return fail("Freeform answers must be non-empty text.");
38355
+ if (text5.length > MAX_FREEFORM_LENGTH) return fail(`Freeform answers must be ${MAX_FREEFORM_LENGTH} characters or fewer.`);
38356
+ return { ok: true, value: text5 };
38282
38357
  }
38283
38358
  default:
38284
38359
  return fail(`Unsupported question type "${trim3(question.type)}".`);
38285
38360
  }
38286
38361
  };
38362
+
38363
+ // shared/questions/questionId.mjs
38364
+ var validOptions = (options) => Array.isArray(options) ? options.filter((o) => o && o.trim() !== "") : [];
38365
+ var legacyPreimage = (type, prompt, options, singleSelect, voiceCredits) => {
38366
+ let preimage = `${type}:${(prompt || "").trim().toLowerCase()}`;
38367
+ const validOpts = validOptions(options);
38368
+ if (type === "multichoice") {
38369
+ if (validOpts.length > 0)
38370
+ preimage += `:${validOpts.map((o) => o.trim()).join(",").toLowerCase()}`;
38371
+ if (singleSelect) preimage += ":single";
38372
+ }
38373
+ if (type === "quadratic") {
38374
+ preimage += `:${JSON.stringify(validOpts.map((option) => option.trim().toLowerCase()))}:${voiceCredits}`;
38375
+ }
38376
+ return preimage;
38377
+ };
38378
+ var legacyIsAmbiguous = (type, options, singleSelect) => {
38379
+ if (type !== "multichoice") return false;
38380
+ const labels = validOptions(options).map((o) => o.trim().toLowerCase());
38381
+ return labels.length === 0 || labels.some((label) => label.includes(":") || label.includes(",")) || !singleSelect && labels.length === 1 && labels[0] === "single";
38382
+ };
38383
+ var v2Text = (text5) => (text5 || "").trim().toLowerCase().normalize("NFC");
38384
+ var v2Preimage = (type, prompt, options, singleSelect) => JSON.stringify(["ce-question-id", 2, type, v2Text(prompt), validOptions(options).map(v2Text), !!singleSelect]);
38385
+ var createQuestionIdDerivation = (ethers7) => {
38386
+ const { id: hash } = ethers7.utils;
38387
+ const generateQuestionId2 = (type, prompt, options = [], singleSelect = false, voiceCredits = 99) => hash(
38388
+ legacyIsAmbiguous(type, options, singleSelect) ? v2Preimage(type, prompt, options, singleSelect) : legacyPreimage(type, prompt, options, singleSelect, voiceCredits)
38389
+ );
38390
+ const questionIdMatchesContent2 = (id, { type, prompt, options = [], singleSelect = false, voiceCredits = 99 } = {}) => {
38391
+ const wanted = String(id).toLowerCase();
38392
+ return hash(legacyPreimage(type, prompt, options, singleSelect, voiceCredits)) === wanted || type === "multichoice" && hash(v2Preimage(type, prompt, options, singleSelect)) === wanted;
38393
+ };
38394
+ return Object.freeze({ generateQuestionId: generateQuestionId2, questionIdMatchesContent: questionIdMatchesContent2 });
38395
+ };
38396
+
38397
+ // mcp/lib/records.mjs
38398
+ var MAX_PROMPT_LENGTH = 1e3;
38399
+ var MAX_OPTION_LENGTH = 200;
38400
+ var MAX_OPTIONS = 12;
38401
+ var MAX_TAGS = 10;
38402
+ var MAX_TAG_LENGTH = 40;
38403
+ var MAX_COMMENT_LENGTH = 4e3;
38404
+ var trim4 = (value) => typeof value === "string" ? value.trim() : typeof value === "number" ? String(value) : "";
38405
+ var isRecord3 = (value) => !!value && typeof value === "object" && !Array.isArray(value);
38406
+ var fail2 = (error) => ({ ok: false, error });
38407
+ var { generateQuestionId, questionIdMatchesContent } = createQuestionIdDerivation(import_ethers3.ethers);
38408
+ var isLockedQuestion = (question) => question?.locked === true || !!question?.promptEncrypted || !!question?.encryption && typeof question.encryption === "object" || typeof question?.prompt === "string" && question.prompt.trim() === "[encrypted]";
38409
+ var normalizeRecruitmentSource = (value) => {
38410
+ const normalized2 = (typeof value === "string" ? value : "").trim().replace(/\s+/g, "-").slice(0, 128);
38411
+ return /^[a-zA-Z0-9][a-zA-Z0-9._:-]{0,127}$/.test(normalized2) ? normalized2 : "";
38412
+ };
38413
+ var readScore = (value, field) => {
38414
+ if (value === void 0 || value === null || value === "") return { ok: true, value: null };
38415
+ const number = Number(value);
38416
+ if (!Number.isInteger(number) || number < 0 || number > 10) return fail2(`${field} must be a whole number from 0 to 10.`);
38417
+ return { ok: true, value: number };
38418
+ };
38287
38419
  var plaintextField = (value, hash) => ({
38288
38420
  value,
38289
38421
  encrypted: false,
@@ -38296,8 +38428,8 @@ var plaintextField = (value, hash) => ({
38296
38428
  var textHash = (value) => typeof value === "string" && value ? import_ethers3.ethers.utils.id(value) : "";
38297
38429
  var buildParticipantMarker = ({ modelId = "", platform = "" } = {}) => ({
38298
38430
  kind: "agent",
38299
- ...trim3(modelId) ? { modelId: trim3(modelId) } : {},
38300
- ...trim3(platform) ? { platform: trim3(platform) } : {},
38431
+ ...trim4(modelId) ? { modelId: trim4(modelId) } : {},
38432
+ ...trim4(platform) ? { platform: trim4(platform) } : {},
38301
38433
  verification: "self_reported"
38302
38434
  });
38303
38435
  var buildResponseRecord = ({
@@ -38311,21 +38443,21 @@ var buildResponseRecord = ({
38311
38443
  profile = {},
38312
38444
  recruitmentSource = ""
38313
38445
  }) => {
38314
- if (!isRecord3(question) || !trim3(question.id)) return fail("Unknown question.");
38315
- if (isLockedQuestion(question)) return fail("This question is locked to certain members; agents cannot answer locked questions yet.");
38446
+ if (!isRecord3(question) || !trim4(question.id)) return fail2("Unknown question.");
38447
+ if (isLockedQuestion(question)) return fail2("This question is locked to certain members; agents cannot answer locked questions yet.");
38316
38448
  const answer = normalizeAnswerValue(question, value);
38317
38449
  if (!answer.ok) return answer;
38318
- const note = trim3(comment);
38319
- if (note.length > MAX_COMMENT_LENGTH) return fail(`Comments must be ${MAX_COMMENT_LENGTH} characters or fewer.`);
38450
+ const note = trim4(comment);
38451
+ if (note.length > MAX_COMMENT_LENGTH) return fail2(`Comments must be ${MAX_COMMENT_LENGTH} characters or fewer.`);
38320
38452
  const convictionScore = readScore(conviction, "Conviction");
38321
38453
  if (!convictionScore.ok) return convictionScore;
38322
38454
  const importanceScore = readScore(importance, "Importance");
38323
38455
  if (!importanceScore.ok) return importanceScore;
38324
- const displayName = trim3(profile.displayName);
38456
+ const displayName = trim4(profile.displayName);
38325
38457
  return {
38326
38458
  ok: true,
38327
38459
  record: {
38328
- questionID: trim3(question.id).toLowerCase(),
38460
+ questionID: trim4(question.id).toLowerCase(),
38329
38461
  responder: import_ethers3.ethers.utils.getAddress(responder),
38330
38462
  type: question.type,
38331
38463
  prompt: typeof question.prompt === "string" ? question.prompt : "",
@@ -38343,19 +38475,19 @@ var buildResponseRecord = ({
38343
38475
  };
38344
38476
  };
38345
38477
  var normalizeOptionList = (options) => {
38346
- const list = (Array.isArray(options) ? options : []).map(trim3).filter(Boolean);
38347
- if (list.length < 2) return fail("Provide at least two options.");
38348
- if (list.length > MAX_OPTIONS) return fail(`Provide at most ${MAX_OPTIONS} options.`);
38349
- if (list.some((option) => option.length > MAX_OPTION_LENGTH)) return fail(`Options must be ${MAX_OPTION_LENGTH} characters or fewer.`);
38350
- if (new Set(list.map((option) => option.toLowerCase())).size !== list.length) return fail("Options must be unique.");
38478
+ const list = (Array.isArray(options) ? options : []).map(trim4).filter(Boolean);
38479
+ if (list.length < 2) return fail2("Provide at least two options.");
38480
+ if (list.length > MAX_OPTIONS) return fail2(`Provide at most ${MAX_OPTIONS} options.`);
38481
+ if (list.some((option) => option.length > MAX_OPTION_LENGTH)) return fail2(`Options must be ${MAX_OPTION_LENGTH} characters or fewer.`);
38482
+ if (new Set(list.map((option) => option.toLowerCase())).size !== list.length) return fail2("Options must be unique.");
38351
38483
  return { ok: true, value: list };
38352
38484
  };
38353
38485
  var buildQuestionRecord = ({ type = "binary", prompt, options, singleSelect = false, voiceCredits, tags = [], creator, session, profile = {} }) => {
38354
- if (!QUESTION_TYPES.includes(type)) return fail(`Question type must be one of: ${QUESTION_TYPES.join(", ")}.`);
38355
- const text4 = trim3(prompt);
38356
- if (!text4) return fail("The statement text is empty.");
38357
- if (text4.length > MAX_PROMPT_LENGTH) return fail(`Statements must be ${MAX_PROMPT_LENGTH} characters or fewer.`);
38358
- const record = { id: "", type, prompt: text4 };
38486
+ if (!QUESTION_TYPES.includes(type)) return fail2(`Question type must be one of: ${QUESTION_TYPES.join(", ")}.`);
38487
+ const text5 = trim4(prompt);
38488
+ if (!text5) return fail2("The statement text is empty.");
38489
+ if (text5.length > MAX_PROMPT_LENGTH) return fail2(`Statements must be ${MAX_PROMPT_LENGTH} characters or fewer.`);
38490
+ const record = { id: "", type, prompt: text5 };
38359
38491
  let optionList = [];
38360
38492
  if (type === "multichoice" || type === "quadratic") {
38361
38493
  const normalized2 = normalizeOptionList(options);
@@ -38369,20 +38501,20 @@ var buildQuestionRecord = ({ type = "binary", prompt, options, singleSelect = fa
38369
38501
  credits = voiceCredits === void 0 || voiceCredits === null ? 99 : Number(voiceCredits);
38370
38502
  record.voiceCredits = credits;
38371
38503
  const error = validateQuadraticQuestion(record);
38372
- if (error) return fail(error);
38504
+ if (error) return fail2(error);
38373
38505
  }
38374
- const tagList = (Array.isArray(tags) ? tags : []).map(trim3).filter(Boolean);
38506
+ const tagList = (Array.isArray(tags) ? tags : []).map(trim4).filter(Boolean);
38375
38507
  if (tagList.length > MAX_TAGS || tagList.some((tag) => tag.length > MAX_TAG_LENGTH)) {
38376
- return fail(`Use at most ${MAX_TAGS} tags of ${MAX_TAG_LENGTH} characters or fewer.`);
38508
+ return fail2(`Use at most ${MAX_TAGS} tags of ${MAX_TAG_LENGTH} characters or fewer.`);
38377
38509
  }
38378
38510
  record.tags = tagList;
38379
38511
  record.creator = import_ethers3.ethers.utils.getAddress(creator);
38380
38512
  record.associatedSurveyId = import_ethers3.ethers.constants.HashZero;
38381
38513
  record.participant = buildParticipantMarker(profile);
38382
- record.sessionName = trim3(session.sessionName) || session.sessionSlug;
38514
+ record.sessionName = trim4(session.sessionName) || session.sessionSlug;
38383
38515
  record.sessionSlug = session.sessionSlug;
38384
38516
  record.sessionId = session.sessionId;
38385
- record.id = generateQuestionId(type, text4, optionList, record.singleSelect === true, credits);
38517
+ record.id = generateQuestionId(type, text5, optionList, record.singleSelect === true, credits);
38386
38518
  return { ok: true, id: record.id, record };
38387
38519
  };
38388
38520
 
@@ -38390,9 +38522,9 @@ var buildQuestionRecord = ({ type = "binary", prompt, options, singleSelect = fa
38390
38522
  var SLUG_RE = /^[a-z0-9_-]{1,128}$/;
38391
38523
  var ADDRESS_RE = /^0x[0-9a-fA-F]{40}$/;
38392
38524
  var GROUP_ID_RE = /^[A-Za-z0-9._:-]{1,128}$/;
38393
- var trim4 = (value) => String(value ?? "").trim();
38525
+ var trim5 = (value) => String(value ?? "").trim();
38394
38526
  var normalizeSessionSlug = (value) => {
38395
- const slug = trim4(value).toLowerCase();
38527
+ const slug = trim5(value).toLowerCase();
38396
38528
  if (!SLUG_RE.test(slug)) throw new Error("Session slug must be 1-128 lowercase letters, digits, dashes, or underscores.");
38397
38529
  return slug;
38398
38530
  };
@@ -38415,9 +38547,9 @@ var readClaimPairs = (params) => {
38415
38547
  const pairs = [];
38416
38548
  const seen = /* @__PURE__ */ new Set();
38417
38549
  const add = (sbt, gp, inv, auto) => {
38418
- const address = trim4(sbt);
38550
+ const address = trim5(sbt);
38419
38551
  if (!ADDRESS_RE.test(address)) return;
38420
- const pair = { sbt: address, groupPassword: trim4(gp), inviteCode: trim4(inv), auto };
38552
+ const pair = { sbt: address, groupPassword: trim5(gp), inviteCode: trim5(inv), auto };
38421
38553
  const key = `${address.toLowerCase()}|${pair.groupPassword}|${pair.inviteCode}`;
38422
38554
  if (seen.has(key)) return;
38423
38555
  seen.add(key);
@@ -38435,7 +38567,7 @@ var readClaimPairs = (params) => {
38435
38567
  var parseSessionLink = (link, { allowLocalWorkers = false } = {}) => {
38436
38568
  let url;
38437
38569
  try {
38438
- url = new URL(trim4(link));
38570
+ url = new URL(trim5(link));
38439
38571
  } catch {
38440
38572
  throw new Error("Session link must be a full URL, such as https://contextengine.sh/session/<slug>?worker=<origin>.");
38441
38573
  }
@@ -38448,7 +38580,7 @@ var parseSessionLink = (link, { allowLocalWorkers = false } = {}) => {
38448
38580
  if (workers.length > 1) {
38449
38581
  throw new Error("Session link must name exactly one hosted Worker when using ?worker=.");
38450
38582
  }
38451
- const joinGroup = trim4(params.get("joinGroup"));
38583
+ const joinGroup = trim5(params.get("joinGroup"));
38452
38584
  if (joinGroup && !GROUP_ID_RE.test(joinGroup)) throw new Error("Session link has an invalid group id.");
38453
38585
  return {
38454
38586
  appOrigin: normalizeHttpOrigin(url.origin, "Session link"),
@@ -38462,6 +38594,51 @@ var parseSessionLink = (link, { allowLocalWorkers = false } = {}) => {
38462
38594
  };
38463
38595
  var buildSessionWebUrl = ({ appOrigin, slug, workerOrigin, kind }) => `${appOrigin}/session/${encodeURIComponent(slug)}${kind === "registry" ? "" : `?worker=${encodeURIComponent(workerOrigin)}`}`;
38464
38596
 
38597
+ // shared/auth/siweMessage.mjs
38598
+ var normalizeSessionIdHex = (value) => {
38599
+ if (typeof value !== "string" || value !== value.trim()) return "";
38600
+ const lower = value.toLowerCase();
38601
+ const hex = lower.startsWith("0x") ? lower.slice(2) : lower;
38602
+ if (/^[0-9a-f]{32}$/.test(hex) && !/^0+$/.test(hex)) return `0x${hex}`;
38603
+ if (/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/.test(lower)) {
38604
+ const uuidHex = lower.replace(/-/g, "");
38605
+ return /^0+$/.test(uuidHex) ? "" : `0x${uuidHex}`;
38606
+ }
38607
+ return "";
38608
+ };
38609
+ var buildSiweMessage = ({
38610
+ address,
38611
+ nonce,
38612
+ chainId,
38613
+ domain,
38614
+ uri,
38615
+ statement,
38616
+ issuedAt,
38617
+ expirationTime,
38618
+ workerUrl,
38619
+ sessionId,
38620
+ sessionSlug
38621
+ }) => {
38622
+ const id = String(sessionId || "").trim().toLowerCase();
38623
+ const slug = sessionSlug === "general" ? "" : String(sessionSlug || "");
38624
+ const sessionResource = id ? `urn:context-engine:session:id:${encodeURIComponent(id)}` : `urn:context-engine:session:slug:${encodeURIComponent(slug)}`;
38625
+ const resources = workerUrl ? `
38626
+ Resources:
38627
+ - ${new URL(String(workerUrl)).origin}
38628
+ - ${sessionResource}` : "";
38629
+ return `${domain} wants you to sign in with your Ethereum account:
38630
+ ${address}
38631
+
38632
+ ${statement}
38633
+
38634
+ URI: ${uri}
38635
+ Version: 1
38636
+ Chain ID: ${chainId}
38637
+ Nonce: ${nonce}
38638
+ Issued At: ${issuedAt}
38639
+ Expiration Time: ${expirationTime}${resources}`;
38640
+ };
38641
+
38465
38642
  // mcp/lib/workerClient.mjs
38466
38643
  var REQUEST_TIMEOUT_MS = 2e4;
38467
38644
  var MAX_RESPONSE_BYTES = 8 * 1024 * 1024;
@@ -38521,17 +38698,6 @@ var readLimitedText = async (response, maxBytes = MAX_RESPONSE_BYTES) => {
38521
38698
  }
38522
38699
  return Buffer.concat(chunks.map((chunk) => Buffer.from(chunk))).toString("utf8");
38523
38700
  };
38524
- var normalizeSessionIdHex = (value) => {
38525
- if (typeof value !== "string" || value !== value.trim()) return "";
38526
- const lower = value.toLowerCase();
38527
- const hex = lower.startsWith("0x") ? lower.slice(2) : lower;
38528
- if (/^[0-9a-f]{32}$/.test(hex) && !/^0+$/.test(hex)) return `0x${hex}`;
38529
- if (/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/.test(lower)) {
38530
- const uuidHex = lower.replace(/-/g, "");
38531
- return /^0+$/.test(uuidHex) ? "" : `0x${uuidHex}`;
38532
- }
38533
- return "";
38534
- };
38535
38701
  var resolveSessionIdHex = (config) => {
38536
38702
  const present = (value) => value !== void 0 && value !== null && value !== "";
38537
38703
  const sessionId = normalizeSessionIdHex(config?.sessionId);
@@ -38541,24 +38707,19 @@ var resolveSessionIdHex = (config) => {
38541
38707
  }
38542
38708
  return sessionId || sessionIdHex;
38543
38709
  };
38544
- var buildSiweMessage = ({ address, nonce, appOrigin, workerOrigin, sessionId, sessionSlug = "", issuedAt, expirationTime, chainId = 1 }) => {
38545
- const { host } = new URL(appOrigin);
38546
- const resource = sessionId ? `urn:context-engine:session:id:${encodeURIComponent(sessionId)}` : `urn:context-engine:session:slug:${encodeURIComponent(sessionSlug === "general" ? "" : sessionSlug)}`;
38547
- return `${host} wants you to sign in with your Ethereum account:
38548
- ${address}
38549
-
38550
- Sign in to Context Engine.
38551
-
38552
- URI: ${appOrigin}
38553
- Version: 1
38554
- Chain ID: ${chainId}
38555
- Nonce: ${nonce}
38556
- Issued At: ${issuedAt}
38557
- Expiration Time: ${expirationTime}
38558
- Resources:
38559
- - ${workerOrigin}
38560
- - ${resource}`;
38561
- };
38710
+ var buildSiweMessage2 = ({ address, nonce, appOrigin, workerOrigin, sessionId, sessionSlug = "", issuedAt, expirationTime, chainId = 1 }) => buildSiweMessage({
38711
+ address,
38712
+ nonce,
38713
+ domain: new URL(appOrigin).host,
38714
+ uri: appOrigin,
38715
+ workerUrl: workerOrigin,
38716
+ sessionId,
38717
+ sessionSlug,
38718
+ issuedAt,
38719
+ expirationTime,
38720
+ chainId,
38721
+ statement: "Sign in to Context Engine."
38722
+ });
38562
38723
  var decodeTokenClaims = (token) => {
38563
38724
  try {
38564
38725
  const [payload] = text(token).split(".");
@@ -38595,8 +38756,13 @@ var createWorkerClient = ({ workerOrigin, slug, trustedOrigins, wallet, fetchImp
38595
38756
  let pendingLogin = null;
38596
38757
  let presence = null;
38597
38758
  let presenceLoadedAt = -Infinity;
38598
- const request = async (path3, { method = "GET", headers = {}, body, auth = false, retried = false, maxBytes = MAX_RESPONSE_BYTES } = {}) => {
38759
+ const request = async (path3, { method = "GET", headers = {}, body, auth = false, retried = false, maxBytes = MAX_RESPONSE_BYTES, signal } = {}) => {
38760
+ const checkpoint = () => {
38761
+ if (signal?.aborted) throw new Error("Stopped before the Worker request was sent.");
38762
+ };
38763
+ checkpoint();
38599
38764
  if (auth) await ensureToken();
38765
+ checkpoint();
38600
38766
  let response;
38601
38767
  try {
38602
38768
  response = await fetchImpl(`${workerOrigin}${path3}`, {
@@ -38626,7 +38792,7 @@ var createWorkerClient = ({ workerOrigin, slug, trustedOrigins, wallet, fetchImp
38626
38792
  const error = isRecord4(data) ? text(data.error) || text(data.message) : "";
38627
38793
  if (response.status === 401 && auth && !retried && (!error || TOKEN_ERROR_RE.test(error))) {
38628
38794
  token = "";
38629
- return request(path3, { method, headers, body, auth, retried: true, maxBytes });
38795
+ return request(path3, { method, headers, body, auth, retried: true, maxBytes, signal });
38630
38796
  }
38631
38797
  if (response.status === 401 && error && !TOKEN_ERROR_RE.test(error)) {
38632
38798
  throw new WorkerRequestError(error.slice(0, MAX_ERROR_DETAIL), { status: 401, code: error });
@@ -38679,7 +38845,7 @@ var createWorkerClient = ({ workerOrigin, slug, trustedOrigins, wallet, fetchImp
38679
38845
  const nonce = text(nonceData?.nonce);
38680
38846
  if (!/^[A-Za-z0-9]{8,200}$/.test(nonce)) throw new WorkerRequestError("The Worker did not issue a usable sign-in nonce.");
38681
38847
  const issued = new Date(now());
38682
- const message = buildSiweMessage({
38848
+ const message = buildSiweMessage2({
38683
38849
  address,
38684
38850
  nonce,
38685
38851
  appOrigin: loginOrigin,
@@ -38744,15 +38910,16 @@ var createWorkerClient = ({ workerOrigin, slug, trustedOrigins, wallet, fetchImp
38744
38910
  }
38745
38911
  return data;
38746
38912
  };
38747
- const upload = async ({ resource, data, tags = [] }) => {
38913
+ const upload = async ({ resource, data, tags = [], requestId = "", signal }) => {
38748
38914
  const result = await request("/storage/upload", {
38915
+ signal,
38749
38916
  method: "POST",
38750
38917
  auth: true,
38751
- body: { backend: "cloudflare", resource, contentType: "application/json", payloadEncrypted: false, tags, data }
38918
+ body: { backend: "cloudflare", resource, contentType: "application/json", payloadEncrypted: false, tags, data, ...requestId ? { requestId } : {} }
38752
38919
  });
38753
38920
  return isRecord4(result?.storageRef) ? result.storageRef : { id: text(result?.id) };
38754
38921
  };
38755
- const joinGroup = (groupId) => request("/groups/join", { method: "POST", auth: true, body: { sessionId, groupId } });
38922
+ const joinGroup = (groupId, { signal } = {}) => request("/groups/join", { method: "POST", auth: true, signal, body: { sessionId, groupId } });
38756
38923
  const describeTestEth = async () => {
38757
38924
  await refreshConfig();
38758
38925
  if (now() - presenceLoadedAt >= CONFIG_MAX_AGE_MS) {
@@ -38793,7 +38960,7 @@ var createWorkerClient = ({ workerOrigin, slug, trustedOrigins, wallet, fetchImp
38793
38960
  // mcp/lib/registryChain.mjs
38794
38961
  var compareChainOrder = (a, b) => Number(a.blockNumber || 0) - Number(b.blockNumber || 0) || Number(a.transactionIndex || 0) - Number(b.transactionIndex || 0) || Number(a.logIndex || 0) - Number(b.logIndex || 0);
38795
38962
  var checkRegistryChain = async (provider, chainId) => {
38796
- const actual = Number(await provider.send("eth_chainId", []));
38963
+ const actual = Number(await chainRequest(() => provider.send("eth_chainId", [])));
38797
38964
  if (actual !== chainId) throw new Error(`The RPC is on a different chain (${actual}); expected ${chainId}.`);
38798
38965
  };
38799
38966
  var findStartBlock = async (provider, createdAt, head) => {
@@ -38801,7 +38968,7 @@ var findStartBlock = async (provider, createdAt, head) => {
38801
38968
  let high = head;
38802
38969
  while (low < high) {
38803
38970
  const mid = Math.floor((low + high) / 2);
38804
- const block = await provider.getBlock(mid);
38971
+ const block = await chainRequest(() => provider.getBlock(mid));
38805
38972
  if (!block || !Number.isFinite(Number(block.timestamp))) throw new Error("Could not locate the session creation block.");
38806
38973
  if (Number(block.timestamp) < createdAt) low = mid + 1;
38807
38974
  else high = mid;
@@ -38839,7 +39006,7 @@ var scanRegistryLogs = async ({
38839
39006
  range = Math.max(1, Math.floor((end - begin + 1) / 2));
38840
39007
  continue;
38841
39008
  }
38842
- throw error;
39009
+ throw safeChainError(error);
38843
39010
  }
38844
39011
  batch = batch.sort((a, b) => descending ? compareChainOrder(b, a) : compareChainOrder(a, b));
38845
39012
  let skipped = 0;
@@ -38971,13 +39138,14 @@ var stopped = (signal) => {
38971
39138
  if (signal?.aborted) throw new Error("Stopped before the chain transaction was sent.");
38972
39139
  };
38973
39140
  var createRegistryWriter = ({ provider, chainId, wallet, surveys, slug, remember }) => {
39141
+ const checkChain = () => checkRegistryChain(provider, chainId);
38974
39142
  const prepare = async ({ client, gasLimit, fund, signal }) => {
38975
39143
  stopped(signal);
38976
- await checkRegistryChain(provider, chainId);
38977
- const gasPrice = await provider.getGasPrice();
39144
+ await checkChain();
39145
+ const gasPrice = await chainRequest(() => provider.getGasPrice());
38978
39146
  const cost = gasPrice.mul(gasLimit);
38979
39147
  if (cost.gt(MAX_COST)) throw new Error("This transaction would exceed the 0.005 ETH gas cap; retry when gas is cheaper.");
38980
- let balance = await provider.getBalance(wallet.address);
39148
+ let balance = await chainRequest(() => provider.getBalance(wallet.address));
38981
39149
  const shortfall = `Fund ${wallet.address} with test ETH on chain ${chainId}; this transaction needs up to ${import_ethers4.ethers.utils.formatEther(cost)} ETH.`;
38982
39150
  let fundingNote = "";
38983
39151
  if (balance.lt(cost) && fund) {
@@ -38985,13 +39153,14 @@ var createRegistryWriter = ({ provider, chainId, wallet, surveys, slug, remember
38985
39153
  if (offer.offered && offer.tokenHasFaucetScope !== false) {
38986
39154
  let funding;
38987
39155
  try {
39156
+ stopped(signal);
38988
39157
  funding = await fund();
38989
39158
  } catch (error) {
38990
39159
  throw new Error(`${shortfall} ${String(error?.message || "The faucet request failed.").slice(0, 250)}`);
38991
39160
  }
38992
39161
  if (funding?.sent) fundingNote = ` Faucet transaction ${funding.txHash} was sent; check it before requesting another payout.`;
38993
39162
  else if (funding?.reason === "balance_high") fundingNote = " The faucet refused because the balance is above its threshold.";
38994
- balance = await provider.getBalance(wallet.address).catch(() => null);
39163
+ balance = await chainRequest(() => provider.getBalance(wallet.address)).catch(() => null);
38995
39164
  if (!balance) throw new Error(`${shortfall}${fundingNote} The current balance could not be checked.`);
38996
39165
  }
38997
39166
  }
@@ -39001,7 +39170,7 @@ var createRegistryWriter = ({ provider, chainId, wallet, surveys, slug, remember
39001
39170
  };
39002
39171
  const upload = async (client, resource, id, record, signal) => {
39003
39172
  stopped(signal);
39004
- const result = await client.request("/arweave/upload", { method: "POST", auth: true, body: {
39173
+ const result = await client.request("/arweave/upload", { method: "POST", auth: true, signal, body: {
39005
39174
  data: JSON.stringify(record),
39006
39175
  contentType: "application/json",
39007
39176
  requestId: randomUUID(),
@@ -39021,7 +39190,7 @@ var createRegistryWriter = ({ provider, chainId, wallet, surveys, slug, remember
39021
39190
  })
39022
39191
  ]);
39023
39192
  } catch {
39024
- throw new Error(`Transaction ${tx.hash} was submitted but could not be confirmed. Check it before retrying.`);
39193
+ throw Object.assign(new Error(`Transaction ${tx.hash} was submitted but could not be confirmed. Check it before retrying.`), { txHash: tx.hash, reconciliationRequired: true });
39025
39194
  } finally {
39026
39195
  clearTimeout(timer);
39027
39196
  }
@@ -39046,22 +39215,24 @@ var createRegistryWriter = ({ provider, chainId, wallet, surveys, slug, remember
39046
39215
  const pointers = [];
39047
39216
  for (const record of records) pointers.push(await upload(client, "responses", record.questionID, record, signal));
39048
39217
  stopped(signal);
39049
- await checkRegistryChain(provider, chainId);
39050
- const tx = await surveys.connect(wallet.connect(provider)).submitResponses(ids, pointers, import_ethers4.ethers.constants.HashZero, import_ethers4.ethers.constants.HashZero, fees);
39218
+ await checkChain();
39219
+ stopped(signal);
39220
+ const tx = await chainRequest(() => surveys.connect(wallet.connect(provider)).submitResponses(ids, pointers, import_ethers4.ethers.constants.HashZero, import_ethers4.ethers.constants.HashZero, fees), { submission: true });
39051
39221
  const receipt = await confirm(tx, "ResponsesSubmitted", ids);
39052
39222
  records.forEach((record, index) => remember("responses", record.questionID, record, pointers[index], receipt));
39053
39223
  return { txHash: tx.hash, receipt };
39054
39224
  };
39055
39225
  const addStatement = async ({ client, record, fund, signal }) => {
39056
- await checkRegistryChain(provider, chainId);
39057
- if (await surveys.questionHashes(record.id) !== import_ethers4.ethers.constants.HashZero) {
39226
+ await checkChain();
39227
+ if (await chainRequest(() => surveys.questionHashes(record.id)) !== import_ethers4.ethers.constants.HashZero) {
39058
39228
  throw new Error("This statement already exists on chain, possibly in another session; reword it to add a new statement.");
39059
39229
  }
39060
39230
  const fees = await prepare({ client, gasLimit: 4e5, fund, signal });
39061
39231
  const pointer = await upload(client, "questions", record.id, record, signal);
39062
39232
  stopped(signal);
39063
- await checkRegistryChain(provider, chainId);
39064
- const tx = await surveys.connect(wallet.connect(provider)).addQuestions([record.id], [pointer], [import_ethers4.ethers.constants.HashZero], fees);
39233
+ await checkChain();
39234
+ stopped(signal);
39235
+ const tx = await chainRequest(() => surveys.connect(wallet.connect(provider)).addQuestions([record.id], [pointer], [import_ethers4.ethers.constants.HashZero], fees), { submission: true });
39065
39236
  const receipt = await confirm(tx, "QuestionsAdded", [record.id]);
39066
39237
  remember("questions", record.id, record, pointer, receipt);
39067
39238
  return { txHash: tx.hash };
@@ -39124,6 +39295,11 @@ var createRegistrySession = async ({ info, config, wallet, fetchImpl = globalThi
39124
39295
  let workerOrigin;
39125
39296
  let surveys;
39126
39297
  let startBlock;
39298
+ const creationBlocks = /* @__PURE__ */ new Map();
39299
+ let endBlock = null;
39300
+ let readEnd;
39301
+ let windowKey;
39302
+ let questionMark = null;
39127
39303
  let questionCursor;
39128
39304
  const pending = /* @__PURE__ */ new Map();
39129
39305
  const questions = /* @__PURE__ */ new Map();
@@ -39131,16 +39307,30 @@ var createRegistrySession = async ({ info, config, wallet, fetchImpl = globalThi
39131
39307
  const localAnswers = /* @__PURE__ */ new Map();
39132
39308
  const answerScans = /* @__PURE__ */ new Map();
39133
39309
  let loadingQuestions;
39310
+ const containsBlock = (block) => Number.isSafeInteger(block) && block >= startBlock && (endBlock === null || block <= readEnd);
39311
+ const readWindowEnd = async () => {
39312
+ const head = await chainRequest(() => provider.getBlockNumber());
39313
+ readEnd = endBlock === null ? head : Math.min(endBlock, head);
39314
+ return readEnd;
39315
+ };
39316
+ const configuredBlock = (value, name) => {
39317
+ if (value == null) return null;
39318
+ const block = Number(value);
39319
+ if (typeof value !== "number" && typeof value !== "string" || String(value).trim() === "" || !Number.isSafeInteger(block) || block < 0) {
39320
+ throw new Error(`Invalid session block limit: ${name} must be a nonnegative integer.`);
39321
+ }
39322
+ return block;
39323
+ };
39134
39324
  const loadConfig = async () => {
39135
39325
  await checkRegistryChain(provider, chainId);
39136
- const slug = info.slug === "general" ? "" : info.slug;
39137
- const row = await registry.getSessionBySlug(slug);
39326
+ const slug = info.slug || "general";
39327
+ const row = await chainRequest(() => registry.getSessionBySlug(slug));
39138
39328
  if (normalized(row[0]) !== slug) throw new Error("The registry returned a different session.");
39139
39329
  if (text(row[3])) throw new Error("Encrypted registry metadata is unsupported; use an unencrypted session.");
39140
39330
  if (Number(row[1]) !== chainId) throw new Error("The session uses a different chain from this registry.");
39141
39331
  const metadata = await readRegistryMetadata(row[2], { fetchImpl });
39142
39332
  checkMetadata(metadata);
39143
- const fields = await registry.getSessionFields(slug, ["corsWorkerUrl"]);
39333
+ const fields = await chainRequest(() => registry.getSessionFields(slug, ["corsWorkerUrl"]));
39144
39334
  const workerUrl = text(fields[0]) || text(metadata.corsWorkerUrl);
39145
39335
  if (/^\s*(?:\{|enc:|lit:)/i.test(workerUrl)) throw new Error("Encrypted Worker URLs are unsupported; use an unencrypted registry session.");
39146
39336
  const nextOrigin = normalizeWorkerOrigin(workerUrl, { allowLocalWorkers: config.allowLocalWorkers });
@@ -39161,11 +39351,32 @@ var createRegistrySession = async ({ info, config, wallet, fetchImpl = globalThi
39161
39351
  corsWorkerUrl: workerOrigin,
39162
39352
  contracts: { ...metadata.contracts, surveys: { address, chainId } }
39163
39353
  };
39164
- if (startBlock === void 0) {
39165
- const configured = Number(metadata.blockLimits?.start);
39166
- const head = await provider.getBlockNumber();
39167
- startBlock = metadata.blockLimits?.start !== void 0 && Number.isSafeInteger(configured) && configured >= 0 ? configured : await findStartBlock(provider, Number(row[5]), head);
39354
+ const head = await chainRequest(() => provider.getBlockNumber());
39355
+ let nextStart = configuredBlock(metadata.blockLimits?.start, "start");
39356
+ if (nextStart === null) {
39357
+ const createdAt = Number(row[5]);
39358
+ if (!creationBlocks.has(createdAt)) {
39359
+ const lookup = findStartBlock(provider, createdAt, head).catch((error) => {
39360
+ creationBlocks.delete(createdAt);
39361
+ throw error;
39362
+ });
39363
+ creationBlocks.set(createdAt, lookup);
39364
+ }
39365
+ nextStart = await creationBlocks.get(createdAt);
39366
+ }
39367
+ const nextEnd = configuredBlock(metadata.blockLimits?.end, "end");
39368
+ if (nextEnd !== null && nextEnd < nextStart) throw new Error("Invalid session block window: end must not precede start.");
39369
+ const nextKey = `${nextStart}:${nextEnd ?? "head"}`;
39370
+ startBlock = nextStart;
39371
+ endBlock = nextEnd;
39372
+ readEnd = endBlock === null ? head : Math.min(endBlock, head);
39373
+ if (windowKey !== nextKey) {
39374
+ windowKey = nextKey;
39168
39375
  questionCursor = { block: startBlock, offset: 0 };
39376
+ questionMark = null;
39377
+ pending.clear();
39378
+ for (const [id, entry] of questions) if (!entry.local || !containsBlock(entry.blockNumber)) questions.delete(id);
39379
+ answerScans.clear();
39169
39380
  }
39170
39381
  return sessionConfig;
39171
39382
  };
@@ -39181,28 +39392,66 @@ var createRegistrySession = async ({ info, config, wallet, fetchImpl = globalThi
39181
39392
  return payload;
39182
39393
  };
39183
39394
  const filter = (eventName) => ({ address: surveys.address, topics: [surveys.interface.getEventTopic(eventName)] });
39395
+ const readBlock = (number, blocks) => {
39396
+ if (!blocks.has(number)) blocks.set(number, chainRequest(() => provider.getBlock(number)).catch(() => null));
39397
+ return blocks.get(number);
39398
+ };
39399
+ const readHeadMark = async (number, blocks) => {
39400
+ const block = await readBlock(number, blocks);
39401
+ return block?.hash ? { number, hash: block.hash } : null;
39402
+ };
39403
+ const historyReplaced = async (mark, blocks) => {
39404
+ if (!mark?.hash) return false;
39405
+ const block = await readBlock(mark.number, blocks);
39406
+ return block?.hash ? block.hash !== mark.hash : null;
39407
+ };
39408
+ const replacedWrite = (entry, blocks = /* @__PURE__ */ new Map()) => historyReplaced(entry?.blockHash ? { number: entry.blockNumber, hash: entry.blockHash } : null, blocks);
39409
+ const visibleQuestions = () => {
39410
+ const blocked = new Set((Array.isArray(sessionConfig.BLOCKED_QUESTION_IDS) ? sessionConfig.BLOCKED_QUESTION_IDS : []).map(normalized));
39411
+ return [...questions.values()].filter((entry) => !blocked.has(entry.id) && containsBlock(entry.blockNumber));
39412
+ };
39184
39413
  const readQuestions = async ({ signal } = {}) => {
39185
39414
  const deadline = now() + 9e4;
39415
+ const blocks = /* @__PURE__ */ new Map();
39416
+ const end = await readWindowEnd();
39186
39417
  let complete = true;
39418
+ const replaced = await historyReplaced(questionMark, blocks);
39419
+ if (replaced === null) return { items: visibleQuestions(), complete: false };
39420
+ if (replaced) {
39421
+ questions.clear();
39422
+ pending.clear();
39423
+ questionCursor = { block: startBlock, offset: 0 };
39424
+ questionMark = null;
39425
+ }
39426
+ for (const [id, entry] of questions) {
39427
+ if (!entry.local) continue;
39428
+ const replaced2 = await replacedWrite(entry, blocks);
39429
+ if (replaced2 === null) complete = false;
39430
+ if (replaced2) questions.delete(id);
39431
+ }
39187
39432
  if (pending.size < MAX_CACHE && questions.size < MAX_QUESTIONS) {
39188
- const scanned = await scanRegistryLogs({
39189
- provider,
39190
- filter: filter("QuestionsAdded"),
39191
- cursor: questionCursor,
39192
- toBlock: await provider.getBlockNumber(),
39193
- maxLogs: 1e3,
39194
- deadline,
39195
- now,
39196
- signal
39197
- });
39198
- questionCursor = scanned.cursor;
39199
- complete = scanned.complete;
39200
- for (const log2 of scanned.logs) {
39201
- const event = surveys.interface.parseLog(log2);
39202
- event.args.questionIds.forEach((id, index) => {
39203
- if (event.args.surveyIds[index] === import_ethers5.ethers.constants.HashZero && !questions.has(normalized(id))) pending.set(normalized(id), log2);
39433
+ const head = await readHeadMark(end, blocks);
39434
+ if (head) {
39435
+ const scanned = await scanRegistryLogs({
39436
+ provider,
39437
+ filter: filter("QuestionsAdded"),
39438
+ cursor: questionCursor,
39439
+ toBlock: head.number,
39440
+ maxLogs: 1e3,
39441
+ deadline,
39442
+ now,
39443
+ signal
39204
39444
  });
39205
- }
39445
+ questionCursor = scanned.cursor;
39446
+ complete = complete && scanned.complete;
39447
+ if (replaced !== null && (!questionMark || head.number >= questionMark.number)) questionMark = head;
39448
+ for (const log2 of scanned.logs) {
39449
+ const event = surveys.interface.parseLog(log2);
39450
+ event.args.questionIds.forEach((id, index) => {
39451
+ if (event.args.surveyIds[index] === import_ethers5.ethers.constants.HashZero && !questions.has(normalized(id))) pending.set(normalized(id), log2);
39452
+ });
39453
+ }
39454
+ } else complete = false;
39206
39455
  } else complete = false;
39207
39456
  const work = [...pending.entries()];
39208
39457
  let next = 0;
@@ -39210,7 +39459,7 @@ var createRegistrySession = async ({ info, config, wallet, fetchImpl = globalThi
39210
39459
  while (next < work.length && now() < deadline && !signal?.aborted && questions.size < MAX_QUESTIONS) {
39211
39460
  const [id, log2] = work[next++];
39212
39461
  try {
39213
- const pointer = await surveys.getQuestionHash(id);
39462
+ const pointer = await chainRequest(() => surveys.getQuestionHash(id));
39214
39463
  const payload = await readPayload(pointer);
39215
39464
  const matches = registryQuestionMatchesSession(payload, sessionConfig);
39216
39465
  if (matches) questions.set(id, { id, payload: { ...payload, id }, createdAt: String(log2.blockNumber).padStart(16, "0"), ...log2 });
@@ -39220,8 +39469,7 @@ var createRegistrySession = async ({ info, config, wallet, fetchImpl = globalThi
39220
39469
  }
39221
39470
  }
39222
39471
  }));
39223
- const blocked = new Set((Array.isArray(sessionConfig.BLOCKED_QUESTION_IDS) ? sessionConfig.BLOCKED_QUESTION_IDS : []).map(normalized));
39224
- return { items: [...questions.values()].filter((entry) => !blocked.has(entry.id)), complete: complete && !pending.size && questions.size < MAX_QUESTIONS };
39472
+ return { items: visibleQuestions(), complete: complete && !pending.size && questions.size < MAX_QUESTIONS };
39225
39473
  };
39226
39474
  const loadQuestions = (args) => {
39227
39475
  if (!loadingQuestions) loadingQuestions = readQuestions(args).finally(() => {
@@ -39232,17 +39480,42 @@ var createRegistrySession = async ({ info, config, wallet, fetchImpl = globalThi
39232
39480
  const readAnswers = async ({ questionIds, maxRecords = 1e4, mine = false, signal } = {}) => {
39233
39481
  const known = new Set(questionIds);
39234
39482
  const deadline = now() + 9e4;
39483
+ const blocks = /* @__PURE__ */ new Map();
39484
+ let verified = true;
39235
39485
  const responseFilter = filter("ResponsesSubmitted");
39236
39486
  if (mine) responseFilter.topics.push(import_ethers5.ethers.utils.hexZeroPad(wallet.address, 32));
39237
- const head = await provider.getBlockNumber();
39487
+ const head = await readWindowEnd();
39238
39488
  const scope = [...known].sort().join(",");
39239
39489
  let state = answerScans.get(mine);
39240
- if (!state || state.scope !== scope || head < state.head) {
39241
- state = { scope, head: startBlock - 1, ranges: [], latest: /* @__PURE__ */ new Map(), capped: false };
39490
+ const replaced = !!state && state.scope === scope && head >= state.head && await historyReplaced(state.mark, blocks);
39491
+ if (replaced === null) {
39492
+ const held = new Map((state.result?.entries || []).map((entry) => [`${entry.questionId}:${normalized(entry.responder)}`, entry]));
39493
+ const chainOrders2 = new Map(state.result?.chainOrders || []);
39494
+ for (const entry of localAnswers.values()) {
39495
+ if (!known.has(entry.questionId) || !containsBlock(entry.blockNumber)) continue;
39496
+ const key = `${entry.questionId}:${normalized(entry.responder)}`;
39497
+ if ([state.latest.get(key), held.get(key)].some((previous) => previous && compareChainOrder(entry, previous) < 0)) continue;
39498
+ chainOrders2.set(entry.questionId, entry);
39499
+ held.delete(key);
39500
+ if (registryResponseMatchesSession(entry.payload, sessionConfig) && entry.payload.answer?.encrypted !== true && normalized(entry.payload.questionID || entry.payload.questionId) === entry.questionId) held.set(key, entry);
39501
+ }
39502
+ return {
39503
+ entries: [...held.values()].sort((a, b) => compareChainOrder(b, a)).slice(0, maxRecords),
39504
+ complete: false,
39505
+ chainOrders: chainOrders2,
39506
+ unreadAnswers: state.result?.unreadAnswers || 0
39507
+ };
39508
+ }
39509
+ if (!state || state.scope !== scope || head < state.head || replaced) {
39510
+ state = { scope, head: startBlock - 1, mark: null, ranges: [], latest: /* @__PURE__ */ new Map(), capped: false };
39242
39511
  } else state = { ...state, ranges: state.ranges.map((range) => ({ ...range })), latest: new Map(state.latest) };
39243
39512
  if (head > state.head) {
39244
- state.ranges.unshift({ from: Math.max(startBlock, state.head + 1), cursor: { block: head, offset: 0 } });
39245
- state.head = head;
39513
+ const mark = await readHeadMark(head, blocks);
39514
+ if (mark) {
39515
+ state.ranges.unshift({ from: Math.max(startBlock, state.head + 1), cursor: { block: head, offset: 0 } });
39516
+ state.head = head;
39517
+ if (replaced !== null) state.mark = mark;
39518
+ } else verified = false;
39246
39519
  }
39247
39520
  if (state.ranges.length > 64) {
39248
39521
  const oldest = state.ranges.pop();
@@ -39295,8 +39568,14 @@ var createRegistrySession = async ({ info, config, wallet, fetchImpl = globalThi
39295
39568
  answerScans.set(mine, state);
39296
39569
  const scannedAll = !state.ranges.length && !state.capped;
39297
39570
  const latest = new Map(state.latest);
39298
- for (const entry of localAnswers.values()) {
39299
- if (!known.has(entry.questionId)) continue;
39571
+ for (const [id, entry] of localAnswers) {
39572
+ if (!known.has(entry.questionId) || !containsBlock(entry.blockNumber)) continue;
39573
+ const replaced2 = await replacedWrite(entry, blocks);
39574
+ if (replaced2 === null) verified = false;
39575
+ if (replaced2) {
39576
+ localAnswers.delete(id);
39577
+ continue;
39578
+ }
39300
39579
  if (!scannedAll && entry.blockNumber <= head) continue;
39301
39580
  const key = `${entry.questionId}:${normalized(entry.responder)}`;
39302
39581
  if (!latest.has(key) || compareChainOrder(entry, latest.get(key)) > 0) latest.set(key, { ...entry, local: true });
@@ -39304,8 +39583,8 @@ var createRegistrySession = async ({ info, config, wallet, fetchImpl = globalThi
39304
39583
  const candidates = [...latest.values()].sort((a, b) => compareChainOrder(b, a));
39305
39584
  const chainOrders = new Map(candidates.filter((entry) => normalized(entry.responder) === normalized(wallet.address)).map((entry) => [entry.questionId, entry]));
39306
39585
  const entries = [];
39307
- const blockTimes = /* @__PURE__ */ new Map();
39308
- let complete = scannedAll && candidates.length <= maxRecords;
39586
+ let complete = verified && scannedAll && candidates.length <= maxRecords;
39587
+ let unreadAnswers = 0;
39309
39588
  let next = 0;
39310
39589
  const selected = candidates.slice(0, maxRecords);
39311
39590
  await Promise.all(Array.from({ length: Math.min(6, selected.length) }, async () => {
@@ -39316,20 +39595,32 @@ var createRegistrySession = async ({ info, config, wallet, fetchImpl = globalThi
39316
39595
  }
39317
39596
  const entry = selected[next++];
39318
39597
  try {
39319
- const pointer = entry.local ? entry.storageId : await surveys.getResponse(entry.responder, entry.questionId);
39598
+ const { blockTag } = responseReadWindowOptions(endBlock === null ? null : { blockTag: head }, entry.responder, entry.questionId);
39599
+ let pointer;
39600
+ if (entry.local) pointer = entry.storageId;
39601
+ else {
39602
+ try {
39603
+ pointer = await chainRequest(() => blockTag == null ? surveys.getResponse(entry.responder, entry.questionId) : surveys.getResponse(entry.responder, entry.questionId, { blockTag }));
39604
+ } catch (error) {
39605
+ throw blockTag == null ? error : historicalResponseReadError(error, blockTag);
39606
+ }
39607
+ }
39320
39608
  const payload = await readPayload(pointer);
39321
39609
  if (!registryResponseMatchesSession(payload, sessionConfig) || payload.answer?.encrypted === true) continue;
39322
39610
  if (normalized(payload.questionID || payload.questionId) !== entry.questionId) continue;
39323
- if (!blockTimes.has(entry.blockNumber)) blockTimes.set(entry.blockNumber, provider.getBlock(entry.blockNumber));
39324
- const block = await blockTimes.get(entry.blockNumber);
39325
- const createdAt = block?.timestamp ? new Date(Number(block.timestamp) * 1e3).toISOString() : "";
39611
+ const block = await readBlock(entry.blockNumber, blocks);
39612
+ if (!block) complete = false;
39613
+ const createdAt = block?.timestamp ? new Date(Number(block.timestamp) * 1e3).toISOString() : entry.createdAt || "";
39326
39614
  entries.push({ ...entry, storageId: pointer, createdAt, payload });
39327
39615
  } catch {
39328
39616
  complete = false;
39617
+ unreadAnswers++;
39329
39618
  }
39330
39619
  }
39331
39620
  }));
39332
- return { entries, complete, chainOrders };
39621
+ unreadAnswers += selected.length - next;
39622
+ state.result = { entries, complete, chainOrders, unreadAnswers };
39623
+ return state.result;
39333
39624
  };
39334
39625
  const writer = createRegistryWriter({ provider, chainId, wallet, surveys, slug: info.slug, remember: (resource, id, payload, pointer, receipt) => {
39335
39626
  cache(pointer, payload);
@@ -39340,20 +39631,75 @@ var createRegistrySession = async ({ info, config, wallet, fetchImpl = globalThi
39340
39631
  storageId: pointer,
39341
39632
  responder: wallet.address,
39342
39633
  blockNumber: receipt.blockNumber,
39634
+ blockHash: receipt.blockHash,
39343
39635
  transactionIndex: receipt.transactionIndex,
39344
39636
  logIndex: receipt.logs.at(-1)?.logIndex || 0,
39345
39637
  createdAt: new Date(now()).toISOString()
39346
39638
  };
39347
- if (resource === "questions") questions.set(id, entry);
39639
+ if (resource === "questions") questions.set(id, { ...entry, local: true });
39348
39640
  else localAnswers.set(id, entry);
39349
39641
  } });
39350
- return { loadConfig, loadQuestions, readAnswers, ...writer, get config() {
39642
+ return { loadConfig, loadQuestions, readAnswers, replacedWrite, containsBlock, get windowKey() {
39643
+ return windowKey;
39644
+ }, ...writer, get config() {
39351
39645
  return sessionConfig;
39352
39646
  }, workerOrigin };
39353
39647
  };
39354
39648
 
39355
- // mcp/lib/results.mjs
39649
+ // shared/storage/canonicalStorageVersions.mjs
39650
+ var trim6 = (value) => (typeof value === "string" ? value : value == null ? "" : String(value)).trim();
39651
+ var compareStorageIds = (left, right) => left < right ? -1 : left > right ? 1 : 0;
39652
+ var compareVersionAge = (left, right) => (Number(left.createdAtMs) || 0) - (Number(right.createdAtMs) || 0) || compareStorageIds(trim6(left.storageRefId), trim6(right.storageRefId));
39653
+ var isAdminStorageVersion = (candidate) => candidate.uploadAuthority === "admin";
39654
+ var isPreferredStorageVersion = (candidate, current) => {
39655
+ if (!current) return true;
39656
+ const candidateAdmin = isAdminStorageVersion(candidate);
39657
+ const currentAdmin = isAdminStorageVersion(current);
39658
+ if (candidateAdmin !== currentAdmin) return candidateAdmin;
39659
+ const age = compareVersionAge(candidate, current);
39660
+ return candidateAdmin ? age > 0 : age < 0;
39661
+ };
39662
+
39663
+ // shared/sessionContext.mjs
39664
+ var SESSION_CONTEXT_LIMITS = Object.freeze({
39665
+ titleChars: 80,
39666
+ paragraphs: 6,
39667
+ paragraphChars: 2e3,
39668
+ links: 8,
39669
+ labelChars: 120,
39670
+ urlChars: 2e3,
39671
+ htmlChars: 2e4
39672
+ });
39673
+ var L = SESSION_CONTEXT_LIMITS;
39674
+ var isRecord5 = (value) => !!value && typeof value === "object" && !Array.isArray(value);
39356
39675
  var text2 = (value) => typeof value === "string" ? value.trim() : "";
39676
+ var NAMED_ENTITIES = { amp: "&", lt: "<", gt: ">", quot: '"', apos: "'", nbsp: " " };
39677
+ var decodeEntity = (match, entity) => {
39678
+ const code = /^#x/i.test(entity) ? parseInt(entity.slice(2), 16) : /^#/.test(entity) ? parseInt(entity.slice(1), 10) : NaN;
39679
+ if (Number.isInteger(code)) return code > 0 && code <= 1114111 ? String.fromCodePoint(code) : " ";
39680
+ return NAMED_ENTITIES[entity.toLowerCase()] ?? match;
39681
+ };
39682
+ var htmlToPlainText = (html) => String(html ?? "").replace(/<(script|style|template)\b[\s\S]*?<\/\1\s*>/gi, " ").replace(/<!--[\s\S]*?-->/g, " ").replace(/<br\s*\/?>|<\/(?:p|div|li|h[1-6]|tr|summary|details|blockquote|pre|table|ul|ol|dt|dd)>/gi, "\n").replace(/<[^>]*>/g, " ").replace(/&(#x[0-9a-f]+|#\d+|[a-z]+);/gi, decodeEntity).split("\n").map((line) => line.replace(/\s+/g, " ").trim()).filter(Boolean).join("\n");
39683
+ var isHttpsUrl = (value) => {
39684
+ if (typeof value !== "string" || !value || value.length > L.urlChars) return false;
39685
+ try {
39686
+ return new URL(value).protocol === "https:";
39687
+ } catch {
39688
+ return false;
39689
+ }
39690
+ };
39691
+ var normalizeSessionContext = (value) => {
39692
+ const context = isRecord5(value) ? value : {};
39693
+ const title = text2(context.title).slice(0, L.titleChars);
39694
+ const html = text2(context.html).slice(0, L.htmlChars);
39695
+ if (html) return { title, html, paragraphs: [], links: [] };
39696
+ const paragraphs = (Array.isArray(context.paragraphs) ? context.paragraphs : [context.body]).map(text2).filter(Boolean).slice(0, L.paragraphs).map((paragraph) => paragraph.slice(0, L.paragraphChars));
39697
+ const links = (Array.isArray(context.links) ? context.links : []).filter(isRecord5).map((link) => ({ label: text2(link.label).slice(0, L.labelChars), url: text2(link.url) })).filter((link) => isHttpsUrl(link.url)).slice(0, L.links).map((link) => ({ label: link.label || link.url, url: link.url }));
39698
+ return { title, paragraphs, links };
39699
+ };
39700
+
39701
+ // mcp/lib/results.mjs
39702
+ var text3 = (value) => typeof value === "string" ? value.trim() : "";
39357
39703
  var MAX_FREEFORM_SAMPLES = 5;
39358
39704
  var MAX_SAMPLE_LENGTH = 280;
39359
39705
  var isNewer = (candidate, current) => !current || candidate.createdAt > current.createdAt || candidate.createdAt === current.createdAt && candidate.storageId > current.storageId;
@@ -39362,11 +39708,11 @@ var latestAnswersByQuestion = (entries, { sessionSlug, sessionId, registryConfig
39362
39708
  for (const entry of entries) {
39363
39709
  const payload = entry?.payload;
39364
39710
  if (!payload || typeof payload !== "object" || Array.isArray(payload)) continue;
39365
- const questionId = (text2(payload.questionID) || text2(payload.questionId)).toLowerCase();
39711
+ const questionId = (text3(payload.questionID) || text3(payload.questionId)).toLowerCase();
39366
39712
  if (registryConfig) {
39367
39713
  if (!questionIds?.has(questionId) || !registryResponseMatchesSession(payload, registryConfig) || payload.answer?.encrypted === true) continue;
39368
- } else if (text2(payload.sessionSlug) !== sessionSlug || normalizeSessionIdHex(text2(payload.sessionId)) !== sessionId) continue;
39369
- const responder = text2(entry.responder).toLowerCase();
39714
+ } else if (text3(payload.sessionSlug) !== sessionSlug || normalizeSessionIdHex(text3(payload.sessionId)) !== sessionId) continue;
39715
+ const responder = text3(entry.responder).toLowerCase();
39370
39716
  if (!questionId || !responder) continue;
39371
39717
  const answers = byQuestion.get(questionId) || /* @__PURE__ */ new Map();
39372
39718
  const current = answers.get(responder);
@@ -39398,25 +39744,21 @@ var BINARY_VOTES = Object.freeze({
39398
39744
  0: "Unsure"
39399
39745
  });
39400
39746
  var validValue = (question, entry) => {
39401
- const payloadType = text2(entry?.payload?.type);
39747
+ const payloadType = text3(entry?.payload?.type);
39402
39748
  if (payloadType && payloadType !== question.type) return void 0;
39403
39749
  const value = rawValue(entry);
39404
39750
  if (value === void 0 || value === null || value === "*") return void 0;
39405
39751
  switch (question.type) {
39406
39752
  case "binary": {
39407
39753
  const scalar = Array.isArray(value) && value.every((item) => ["string", "number"].includes(typeof item)) ? value.join(",") : value;
39408
- const key = typeof scalar === "boolean" || typeof scalar === "number" ? String(scalar) : text2(scalar).toLowerCase();
39754
+ const key = typeof scalar === "boolean" || typeof scalar === "number" ? String(scalar) : text3(scalar).toLowerCase();
39409
39755
  return Object.hasOwn(BINARY_VOTES, key) ? BINARY_VOTES[key] : void 0;
39410
39756
  }
39411
- case "rating": {
39412
- if (typeof value !== "number" && !(typeof value === "string" && value.trim())) return void 0;
39413
- const number = Number(value);
39414
- const scale = ratingScaleFor(question);
39415
- return Number.isFinite(number) && number >= scale.min && number <= scale.max ? number : void 0;
39416
- }
39757
+ case "rating":
39758
+ return normalizeRatingAnswer(value, question) ?? void 0;
39417
39759
  case "multichoice": {
39418
- const options = (Array.isArray(question.options) ? question.options : []).map(text2).filter(Boolean);
39419
- const picks = (Array.isArray(value) ? value : [value]).map(text2);
39760
+ const options = (Array.isArray(question.options) ? question.options : []).map(text3).filter(Boolean);
39761
+ const picks = (Array.isArray(value) ? value : [value]).map(text3);
39420
39762
  if (!picks.length || picks.some((pick) => !pick)) return void 0;
39421
39763
  const canonical = options.length ? picks.map((pick) => options.find((option) => option.toLowerCase() === pick.toLowerCase()) || "") : picks;
39422
39764
  if (canonical.some((pick) => !pick)) return void 0;
@@ -39467,7 +39809,7 @@ var summarizeQuestion = (question, answersByResponder = /* @__PURE__ */ new Map(
39467
39809
  return { ...summary, scale: { min: scale.min, max: scale.max }, mean, histogram };
39468
39810
  }
39469
39811
  case "multichoice": {
39470
- const counts = new Map((Array.isArray(question.options) ? question.options : []).map(text2).filter(Boolean).map((option) => [option, 0]));
39812
+ const counts = new Map((Array.isArray(question.options) ? question.options : []).map(text3).filter(Boolean).map((option) => [option, 0]));
39471
39813
  answered.forEach((entry) => validValue(question, entry).forEach((option) => counts.set(option, (counts.get(option) || 0) + 1)));
39472
39814
  return { ...summary, counts: Object.fromEntries(counts) };
39473
39815
  }
@@ -39517,12 +39859,12 @@ var REVEAL_WAIT_MS = 9e4;
39517
39859
  var POLL_MS = 2e3;
39518
39860
  var GAS_HEADROOM_PERCENT = 130;
39519
39861
  var PASSWORD_CLAIM_GAS_BUDGET = 4e5;
39520
- var trim5 = (value) => typeof value === "string" ? value.trim() : typeof value === "number" ? String(value) : "";
39521
- var isRecord5 = (value) => !!value && typeof value === "object" && !Array.isArray(value);
39862
+ var trim7 = (value) => typeof value === "string" ? value.trim() : typeof value === "number" ? String(value) : "";
39863
+ var isRecord6 = (value) => !!value && typeof value === "object" && !Array.isArray(value);
39522
39864
  var defaultSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
39523
39865
  var { deriveGroupPasswordWallet } = createGroupPasswordDerivation(import_ethers6.ethers);
39524
39866
  var normalizeGroupPasswordInput = (raw) => {
39525
- const compact = trim5(raw).replace(/\s+/g, "");
39867
+ const compact = trim7(raw).replace(/\s+/g, "");
39526
39868
  if (!compact) return "";
39527
39869
  if (import_ethers6.ethers.utils.isHexString(compact)) {
39528
39870
  try {
@@ -39534,14 +39876,14 @@ var normalizeGroupPasswordInput = (raw) => {
39534
39876
  return compact;
39535
39877
  };
39536
39878
  var decodeInviteCode = (raw) => {
39537
- let code = trim5(raw);
39879
+ let code = trim7(raw);
39538
39880
  const lower = code.toLowerCase();
39539
39881
  if (lower.startsWith("inv:")) code = code.slice(4).trim();
39540
39882
  else if (lower.startsWith("invite:")) code = code.slice(7).trim();
39541
39883
  if (!code || code.length > 4096) return null;
39542
39884
  try {
39543
39885
  const parsed = JSON.parse(Buffer.from(code.replace(/-/g, "+").replace(/_/g, "/"), "base64").toString("utf8"));
39544
- if (!isRecord5(parsed) || Object.keys(parsed).sort().join(",") !== "a,c,n,s") return null;
39886
+ if (!isRecord6(parsed) || Object.keys(parsed).sort().join(",") !== "a,c,n,s") return null;
39545
39887
  const chainId = import_ethers6.ethers.BigNumber.from(String(parsed.c));
39546
39888
  const slot = import_ethers6.ethers.BigNumber.from(String(parsed.n));
39547
39889
  const sbtAddress = import_ethers6.ethers.utils.getAddress(String(parsed.a));
@@ -39562,13 +39904,13 @@ var accessConditionsOf = (config) => [
39562
39904
  config?.sessionModeProfile?.encryption?.accessConditions,
39563
39905
  config?.storageProfile?.payloadAccessControl?.accessConditions
39564
39906
  ].flatMap((document) => Array.isArray(document?.conditions) ? document.conditions : []);
39565
- var conditionAddresses = (condition) => [condition?.contract, condition?.address, ...Array.isArray(condition?.sbtAddresses) ? condition.sbtAddresses : []].map((value) => trim5(value).toLowerCase()).filter(Boolean);
39907
+ var conditionAddresses = (condition) => [condition?.contract, condition?.address, ...Array.isArray(condition?.sbtAddresses) ? condition.sbtAddresses : []].map((value) => trim7(value).toLowerCase()).filter(Boolean);
39566
39908
  var positiveChainId = (value) => {
39567
39909
  const number = typeof value === "number" ? value : typeof value === "string" && value.trim() ? Number(value) : NaN;
39568
39910
  return Number.isSafeInteger(number) && number > 0 ? number : 0;
39569
39911
  };
39570
39912
  var resolveSbtChainId = (config, sbtAddress) => {
39571
- const target = trim5(sbtAddress).toLowerCase();
39913
+ const target = trim7(sbtAddress).toLowerCase();
39572
39914
  const condition = accessConditionsOf(config).find(
39573
39915
  (entry) => entry?.kind === "sbt_onchain" && conditionAddresses(entry).includes(target) && positiveChainId(entry.chainId)
39574
39916
  );
@@ -39586,19 +39928,13 @@ var listSessionSbts = (config) => {
39586
39928
  return [...seen.values()];
39587
39929
  };
39588
39930
  var defaultCreateProvider = (rpcOverride) => (chainId) => {
39589
- const url = trim5(rpcOverride) || import_rpcDefaults3.default.getPublicRpcUrls(chainId)[0] || import_rpcDefaults3.default.getPathRpcUrl(chainId);
39931
+ const url = trim7(rpcOverride) || import_rpcDefaults3.default.getPublicRpcUrls(chainId)[0] || import_rpcDefaults3.default.getPathRpcUrl(chainId);
39590
39932
  if (!url) throw new Error(`No public RPC is known for chain ${chainId}; set CE_MCP_RPC_URL.`);
39591
39933
  return new import_ethers6.ethers.providers.StaticJsonRpcProvider(url, chainId);
39592
39934
  };
39593
39935
  var ClaimError = class extends Error {
39594
39936
  };
39595
- var SAFE_REASON_RE = /^[A-Za-z][\w .,'()-]{0,119}$/;
39596
- var safeMessage = (error, fallback) => {
39597
- if (error instanceof ClaimError) return error.message;
39598
- const reason = [error?.reason, error?.errorName, error?.error?.reason].map((value) => typeof value === "string" ? value.replace(/^execution reverted:?\s*/i, "").trim() : "").find((value) => SAFE_REASON_RE.test(value));
39599
- const code = typeof error?.code === "string" && /^[A-Z_]{3,40}$/.test(error.code) ? error.code : "";
39600
- return [fallback, reason || code].filter(Boolean).join(": ");
39601
- };
39937
+ var safeMessage = (error, fallback) => error instanceof ClaimError ? error.message : [fallback, safeErrorReason(error) || safeErrorCode(error)].filter(Boolean).join(": ");
39602
39938
  var createSbtClaimer = ({
39603
39939
  config = {},
39604
39940
  wallet,
@@ -39711,7 +40047,7 @@ var createSbtClaimer = ({
39711
40047
  const claimWithGroupPassword = async ({ ctx, provider, chainId, contract, sbtAddress, password }) => {
39712
40048
  const normalized2 = normalizeGroupPasswordInput(password);
39713
40049
  if (!normalized2) throw new ClaimError("This SBT needs a group password.");
39714
- const expectedHash = trim5(await contract.groupPasswordHash()).toLowerCase();
40050
+ const expectedHash = trim7(await contract.groupPasswordHash()).toLowerCase();
39715
40051
  if (!expectedHash || expectedHash === import_ethers6.ethers.constants.HashZero) throw new ClaimError("This SBT has no group password set.");
39716
40052
  const groupWallet = await deriveGroupPasswordWallet({ password: normalized2, sbtAddress });
39717
40053
  if (import_ethers6.ethers.utils.solidityKeccak256(["address"], [groupWallet.address]) !== expectedHash) {
@@ -39745,7 +40081,7 @@ var createSbtClaimer = ({
39745
40081
  signal = callSignal;
39746
40082
  let address;
39747
40083
  try {
39748
- address = import_ethers6.ethers.utils.getAddress(trim5(sbtAddress));
40084
+ address = import_ethers6.ethers.utils.getAddress(trim7(sbtAddress));
39749
40085
  } catch {
39750
40086
  throw new ClaimError("Pass the SBT contract address (0x\u2026).");
39751
40087
  }
@@ -39778,7 +40114,7 @@ var createSbtClaimer = ({
39778
40114
  const args = { ctx, provider, chainId, contract, sbtAddress: address };
39779
40115
  let transactions;
39780
40116
  if (mode === "public") transactions = [(await send({ ...args, method: "claim", args: [] })).transactionHash];
39781
- else if (mode === "password") transactions = await claimWithCommitReveal({ ...args, password: trim5(claimPassword) || trim5(groupPassword) });
40117
+ else if (mode === "password") transactions = await claimWithCommitReveal({ ...args, password: trim7(claimPassword) || trim7(groupPassword) });
39782
40118
  else if (mode === "group_password") transactions = await claimWithGroupPassword({ ...args, password: groupPassword });
39783
40119
  else transactions = await claimWithInviteCode({ ...args, code: inviteCode || groupPassword });
39784
40120
  return { ...base, mode, status: "claimed", transactions };
@@ -39804,11 +40140,9 @@ var MAX_CACHED_RECORDS = 2e4;
39804
40140
  var MISSING_RECORD_RETRY_MS = 6e4;
39805
40141
  var MAX_OWN_ANSWER_ITEMS = 5e3;
39806
40142
  var MAX_TITLE_LENGTH = 120;
39807
- var MAX_CONTEXT_PARAGRAPHS = 4;
39808
- var MAX_CONTEXT_LENGTH = 2e3;
39809
- var MAX_CONTEXT_LINKS = 4;
39810
- var trim6 = (value) => typeof value === "string" ? value.trim() : typeof value === "number" ? String(value) : "";
39811
- var isRecord6 = (value) => !!value && typeof value === "object" && !Array.isArray(value);
40143
+ var MAX_DESCRIPTION_LENGTH = 2e3;
40144
+ var trim8 = (value) => typeof value === "string" ? value.trim() : typeof value === "number" ? String(value) : "";
40145
+ var isRecord7 = (value) => !!value && typeof value === "object" && !Array.isArray(value);
39812
40146
  var sessionKey = (info) => info.kind === "registry" ? `registry ${info.chainId || 11155420} ${info.slug}` : `${info.workerOrigin} ${info.slug}`;
39813
40147
  var mapLimit = async (items, limit, task) => {
39814
40148
  const results = new Array(items.length);
@@ -39824,10 +40158,10 @@ var mapLimit = async (items, limit, task) => {
39824
40158
  return results;
39825
40159
  };
39826
40160
  var contextOf = (config) => {
39827
- const context = isRecord6(config?.sessionContext) ? config.sessionContext : {};
39828
- const paragraphs = (Array.isArray(context.paragraphs) ? context.paragraphs.map(text) : [text(context.body)]).filter(Boolean).slice(0, MAX_CONTEXT_PARAGRAPHS).map((paragraph) => paragraph.slice(0, MAX_CONTEXT_LENGTH));
39829
- const links = (Array.isArray(context.links) ? context.links : []).filter((link) => isRecord6(link) && /^https?:\/\//i.test(text(link.url))).slice(0, MAX_CONTEXT_LINKS).map((link) => ({ label: (text(link.label) || text(link.url)).slice(0, 200), url: text(link.url).slice(0, 2e3) }));
39830
- return { title: text(context.title).slice(0, 200), paragraphs, links };
40161
+ const { html, ...context } = normalizeSessionContext(config?.sessionContext);
40162
+ if (!html) return context;
40163
+ const words = htmlToPlainText(html);
40164
+ return { ...context, paragraphs: words ? [words] : [] };
39831
40165
  };
39832
40166
  var idList = (value) => new Set((Array.isArray(value) ? value : []).map((id) => text(id).toLowerCase()).filter(Boolean));
39833
40167
  var stringOptions = (payload) => Array.isArray(payload.options) ? payload.options.filter((option) => typeof option === "string") : [];
@@ -39836,9 +40170,9 @@ var finiteNumber = (value) => {
39836
40170
  if (typeof value === "string" && value.trim() && Number.isFinite(Number(value))) return Number(value);
39837
40171
  return void 0;
39838
40172
  };
39839
- var SCALE_KEYS = ["min", "minimum", "max", "maximum", "minLabel", "lowLabel", "maxLabel", "highLabel"];
40173
+ var SCALE_KEYS = ["step", "min", "minimum", "max", "maximum", "minLabel", "lowLabel", "maxLabel", "highLabel"];
39840
40174
  var cleanScale = (record) => {
39841
- if (!isRecord6(record)) return {};
40175
+ if (!isRecord7(record)) return {};
39842
40176
  const out = {};
39843
40177
  SCALE_KEYS.forEach((key) => {
39844
40178
  if (typeof record[key] === "string" || typeof record[key] === "number" && Number.isFinite(record[key])) out[key] = record[key];
@@ -39868,18 +40202,10 @@ var sanitizeQuestion = (payload, id) => {
39868
40202
  ratingScaleFor(question);
39869
40203
  return question;
39870
40204
  };
39871
- var contentId = (question) => generateQuestionId(question.type, question.prompt, stringOptions(question), isSingleSelect(question), question.voiceCredits ?? 99);
39872
- var chooseQuestionVersion = (versions) => {
39873
- const admin = versions.filter((version) => version.uploaderIsAdmin);
39874
- if (admin.length) return admin.sort((left, right) => left.createdAt < right.createdAt ? 1 : -1)[0];
39875
- const ranked = [...versions].sort((left, right) => {
39876
- const leftMatches = left.contentMatches === true ? 0 : 1;
39877
- const rightMatches = right.contentMatches === true ? 0 : 1;
39878
- if (leftMatches !== rightMatches) return leftMatches - rightMatches;
39879
- return left.createdAt < right.createdAt ? -1 : left.createdAt > right.createdAt ? 1 : 0;
39880
- });
39881
- return ranked[0];
39882
- };
40205
+ var chooseQuestionVersion = (versions) => versions.reduce(
40206
+ (chosen, candidate) => isPreferredStorageVersion(candidate, chosen) ? candidate : chosen,
40207
+ null
40208
+ );
39883
40209
  var describeQuestion = (question, myAnswer) => {
39884
40210
  const options = stringOptions(question);
39885
40211
  const entry = {
@@ -39915,7 +40241,9 @@ var sameAuthoringInputs = (stored, record) => {
39915
40241
  var progressNotes = (questions, mine) => ({
39916
40242
  ...questions.locked ? { lockedQuestions: questions.locked } : {},
39917
40243
  ...questions.complete === false ? { questionsComplete: false } : {},
39918
- ...mine.complete === false ? { myAnswersComplete: false, note: "This session is large: answers made outside this agent may not all be listed." } : {}
40244
+ ...questions.unread ? { unreadQuestions: questions.unread } : {},
40245
+ ...mine.unread ? { unreadMyAnswers: mine.unread } : {},
40246
+ ...mine.complete === false ? { myAnswersComplete: false, note: "Some answers could not be read or the scan is unfinished; the answers shown may be incomplete." } : {}
39919
40247
  });
39920
40248
  var createParticipant = ({ config, wallet, fetchImpl = globalThis.fetch, now = () => Date.now(), sbt = null, createProvider, sleep, registry = {} }) => {
39921
40249
  const requestTestEth = createTestEthRequester({ config, wallet, createProvider, now, sleep });
@@ -39941,9 +40269,9 @@ var createParticipant = ({ config, wallet, fetchImpl = globalThis.fetch, now = (
39941
40269
  state = await updateAgentState(config.stateDir, mutate);
39942
40270
  };
39943
40271
  const profile = () => ({
39944
- displayName: trim6(state?.profile?.displayName ?? config.profile.displayName),
39945
- modelId: trim6(state?.profile?.modelId ?? config.profile.modelId),
39946
- platform: trim6(state?.profile?.platform ?? config.profile.platform)
40272
+ displayName: trim8(state?.profile?.displayName ?? config.profile.displayName),
40273
+ modelId: trim8(state?.profile?.modelId ?? config.profile.modelId),
40274
+ platform: trim8(state?.profile?.platform ?? config.profile.platform)
39947
40275
  });
39948
40276
  const trustedOriginsFor = (preferred) => config.trustedAppOrigins.includes(preferred) ? [preferred, ...config.trustedAppOrigins.filter((origin) => origin !== preferred)] : config.trustedAppOrigins;
39949
40277
  const openSession = async (info) => {
@@ -39971,7 +40299,7 @@ var createParticipant = ({ config, wallet, fetchImpl = globalThis.fetch, now = (
39971
40299
  };
39972
40300
  const remember = (ctx, title, recruitmentSource) => saveState((fresh) => {
39973
40301
  const key = sessionKey(ctx.info);
39974
- const stored = isRecord6(fresh.sessions[key]) ? fresh.sessions[key] : {};
40302
+ const stored = isRecord7(fresh.sessions[key]) ? fresh.sessions[key] : {};
39975
40303
  const previous = stored.sessionId && stored.sessionId !== ctx.client.sessionId ? {} : stored;
39976
40304
  fresh.sessions[key] = {
39977
40305
  ...previous,
@@ -39988,24 +40316,25 @@ var createParticipant = ({ config, wallet, fetchImpl = globalThis.fetch, now = (
39988
40316
  });
39989
40317
  const sessionState = (ctx) => {
39990
40318
  const entry = state?.sessions?.[sessionKey(ctx.info)];
39991
- return isRecord6(entry) ? entry : {};
40319
+ return isRecord7(entry) ? entry : {};
39992
40320
  };
39993
40321
  const rememberAnswers = (ctx, answered) => saveState((fresh) => {
39994
40322
  const entry = fresh.sessions[sessionKey(ctx.info)];
39995
- if (!isRecord6(entry) || entry.sessionId !== ctx.client.sessionId) return;
39996
- entry.answers = isRecord6(entry.answers) ? entry.answers : {};
39997
- answered.forEach(({ questionId, record, answeredAt, chainOrder }) => {
40323
+ if (!isRecord7(entry) || entry.sessionId !== ctx.client.sessionId) return;
40324
+ entry.answers = isRecord7(entry.answers) ? entry.answers : {};
40325
+ answered.forEach(({ questionId, record, answeredAt, chainOrder, requestId }) => {
39998
40326
  entry.answers[questionId] = {
39999
40327
  value: record.answer.value,
40000
40328
  comment: record.additional.value,
40001
40329
  answeredAt,
40002
40330
  ...ctx.registry && chainOrder ? chainOrder : {}
40003
40331
  };
40332
+ if (requestId && entry.pendingUploads?.[questionId]?.requestId === requestId) delete entry.pendingUploads[questionId];
40004
40333
  });
40005
40334
  });
40006
40335
  const resolveSession = async (session) => {
40007
40336
  await loadState();
40008
- const ref = trim6(session);
40337
+ const ref = trim8(session);
40009
40338
  if (/^https?:\/\//i.test(ref)) {
40010
40339
  const parsed = parseSessionLink(ref, { allowLocalWorkers: config.allowLocalWorkers });
40011
40340
  const key2 = sessionKey(parsed);
@@ -40021,7 +40350,7 @@ var createParticipant = ({ config, wallet, fetchImpl = globalThis.fetch, now = (
40021
40350
  if (!key) throw new Error(`No joined session named "${slug}". Join it first with join_session.`);
40022
40351
  }
40023
40352
  const entry = state.sessions[key];
40024
- if (!key || !isRecord6(entry)) throw new Error("No session joined yet. Call join_session with a session link.");
40353
+ if (!key || !isRecord7(entry)) throw new Error("No session joined yet. Call join_session with a session link.");
40025
40354
  return openSession({
40026
40355
  workerOrigin: normalizeWorkerOrigin(entry.workerOrigin, { allowLocalWorkers: config.allowLocalWorkers }),
40027
40356
  slug: normalizeSessionSlug(entry.slug),
@@ -40041,33 +40370,27 @@ var createParticipant = ({ config, wallet, fetchImpl = globalThis.fetch, now = (
40041
40370
  };
40042
40371
  const readCached = async (ctx, storageId) => {
40043
40372
  if (ctx.records.has(storageId)) return ctx.records.get(storageId);
40044
- if (now() - (ctx.missing.get(storageId) ?? -Infinity) < MISSING_RECORD_RETRY_MS) return null;
40045
- let payload = null;
40373
+ if (now() - (ctx.missing.get(storageId) ?? -Infinity) < MISSING_RECORD_RETRY_MS) return void 0;
40046
40374
  try {
40047
- payload = await ctx.client.read(storageId);
40375
+ const payload = await ctx.client.read(storageId);
40376
+ ctx.missing.delete(storageId);
40377
+ cacheRecord(ctx, storageId, payload);
40378
+ return payload;
40048
40379
  } catch (error) {
40049
40380
  if (!(error instanceof WorkerRequestError) || error.status !== 404) throw error;
40050
- }
40051
- if (payload === null) {
40052
40381
  ctx.missing.set(storageId, now());
40053
40382
  if (ctx.missing.size > MAX_CACHED_RECORDS) ctx.missing.delete(ctx.missing.keys().next().value);
40054
- } else {
40055
- ctx.missing.delete(storageId);
40056
- cacheRecord(ctx, storageId, payload);
40383
+ return void 0;
40057
40384
  }
40058
- return payload;
40059
40385
  };
40060
40386
  const readMany = async (ctx, storageIds) => {
40061
40387
  const deadline = now() + READ_DEADLINE_MS;
40062
- let complete = true;
40063
40388
  const payloads = await mapLimit(storageIds, READ_CONCURRENCY, async (storageId) => {
40064
- if (!ctx.records.has(storageId) && (now() >= deadline || signal?.aborted)) {
40065
- complete = false;
40066
- return void 0;
40067
- }
40068
- return readCached(ctx, storageId);
40389
+ if (!ctx.records.has(storageId) && (now() >= deadline || signal?.aborted)) return void 0;
40390
+ return readCached(ctx, storageId).catch(() => void 0);
40069
40391
  });
40070
- return { payloads, complete };
40392
+ const unread = payloads.filter((payload) => payload === void 0).length;
40393
+ return { payloads, complete: unread === 0, unread };
40071
40394
  };
40072
40395
  const loadQuestions = async (ctx) => {
40073
40396
  const sessionConfig = await ensureReady(ctx);
@@ -40077,7 +40400,7 @@ var createParticipant = ({ config, wallet, fetchImpl = globalThis.fetch, now = (
40077
40400
  const all2 = [];
40078
40401
  for (const row of result.items) {
40079
40402
  try {
40080
- if (!isRecord6(row.payload) || typeof row.payload.type !== "string") continue;
40403
+ if (!isRecord7(row.payload) || typeof row.payload.type !== "string") continue;
40081
40404
  const payload = isLockedQuestion(row.payload) ? { ...row.payload, prompt: "[encrypted]", locked: true } : row.payload;
40082
40405
  if (typeof payload.prompt !== "string") continue;
40083
40406
  all2.push({
@@ -40098,15 +40421,15 @@ var createParticipant = ({ config, wallet, fetchImpl = globalThis.fetch, now = (
40098
40421
  }
40099
40422
  const { items, complete } = await ctx.client.listAll("questions", { maxItems: MAX_QUESTIONS2, signal });
40100
40423
  const hidden = new Set(items.filter((item) => item.hidden === true).map((item) => text(item.metadata?.resourceId).toLowerCase()));
40101
- const rows = items.filter((item) => item.hidden !== true && text(item.storageRef?.id));
40102
- const { payloads, complete: readComplete } = await readMany(ctx, rows.map((item) => text(item.storageRef.id)));
40103
40424
  const blocked = idList(sessionConfig.BLOCKED_QUESTION_IDS);
40425
+ const rows = items.filter((item) => item.hidden !== true && text(item.storageRef?.id) && !hidden.has(text(item.metadata?.resourceId).toLowerCase()) && !blocked.has(text(item.metadata?.resourceId).toLowerCase()));
40426
+ const { payloads, complete: readComplete, unread } = await readMany(ctx, rows.map((item) => text(item.storageRef.id)));
40104
40427
  const highlighted = idList(sessionConfig.HIGHLIGHTED_QUESTION_IDS);
40105
40428
  const versions = /* @__PURE__ */ new Map();
40106
40429
  rows.forEach((item, index) => {
40107
40430
  try {
40108
40431
  const payload = payloads[index];
40109
- if (!isRecord6(payload) || typeof payload.prompt !== "string" || typeof payload.type !== "string") return;
40432
+ if (!isRecord7(payload) || typeof payload.prompt !== "string" || typeof payload.type !== "string") return;
40110
40433
  if (text(payload.sessionSlug) !== ctx.info.slug) return;
40111
40434
  if (text(payload.sessionId) && normalizeSessionIdHex(text(payload.sessionId)) !== ctx.client.sessionId) return;
40112
40435
  const id = (text(payload.id) || text(item.metadata?.resourceId)).toLowerCase();
@@ -40116,8 +40439,9 @@ var createParticipant = ({ config, wallet, fetchImpl = globalThis.fetch, now = (
40116
40439
  list.push({
40117
40440
  payload: question,
40118
40441
  createdAt: text(item.metadata?.createdAt),
40119
- uploaderIsAdmin: item.metadata?.uploaderIsAdmin === true,
40120
- contentMatches: question.locked ? null : contentId(question) === id
40442
+ createdAtMs: Date.parse(text(item.metadata?.createdAt)) || 0,
40443
+ storageRefId: text(item.storageRef.id),
40444
+ uploadAuthority: item.metadata?.uploadAuthority
40121
40445
  });
40122
40446
  versions.set(id, list);
40123
40447
  } catch {
@@ -40142,11 +40466,12 @@ var createParticipant = ({ config, wallet, fetchImpl = globalThis.fetch, now = (
40142
40466
  questions.locked = all.length - questions.length;
40143
40467
  questions.lockedIds = new Set(all.filter((question) => isLockedQuestion(question)).map((question) => question.id));
40144
40468
  questions.complete = complete && readComplete;
40469
+ questions.unread = unread;
40145
40470
  return questions;
40146
40471
  };
40147
40472
  const toEntries = async (ctx, items) => {
40148
- const rows = items.filter((item) => text(item.storageRef?.id));
40149
- const { payloads, complete } = await readMany(ctx, rows.map((item) => text(item.storageRef.id)));
40473
+ const rows = items.filter((item) => item.hidden !== true && text(item.storageRef?.id));
40474
+ const { payloads, complete, unread } = await readMany(ctx, rows.map((item) => text(item.storageRef.id)));
40150
40475
  const entries = rows.map((item, index) => ({
40151
40476
  storageId: text(item.storageRef.id),
40152
40477
  createdAt: text(item.metadata?.createdAt) || text(item.storageRef?.createdAt),
@@ -40154,22 +40479,23 @@ var createParticipant = ({ config, wallet, fetchImpl = globalThis.fetch, now = (
40154
40479
  payload: payloads[index]
40155
40480
  }));
40156
40481
  entries.complete = complete;
40482
+ entries.unread = unread;
40157
40483
  return entries;
40158
40484
  };
40159
40485
  const scanMyAnswers = async (ctx) => {
40160
- if (ctx.ownScan && (!ctx.registry || ctx.ownScan.complete) && now() - ctx.ownScan.at < OWN_ANSWER_SCAN_MAX_AGE_MS && ctx.ownScan.sessionId === ctx.client.sessionId) {
40486
+ if (ctx.ownScan && ctx.ownScan.complete && now() - ctx.ownScan.at < OWN_ANSWER_SCAN_MAX_AGE_MS && ctx.ownScan.sessionId === ctx.client.sessionId && ctx.ownScan.windowKey === ctx.registry?.windowKey) {
40161
40487
  return ctx.ownScan;
40162
40488
  }
40163
40489
  if (ctx.registry) {
40164
40490
  const questions = await loadQuestions(ctx);
40165
40491
  const questionIds = new Set(questions.map((question) => question.id));
40166
- const { entries: entries2, complete: complete2, chainOrders } = await ctx.registry.readAnswers({ questionIds, maxRecords: MAX_OWN_ANSWER_ITEMS, mine: true, signal });
40492
+ const { entries: entries2, complete: complete2, chainOrders, unreadAnswers } = await ctx.registry.readAnswers({ questionIds, maxRecords: MAX_OWN_ANSWER_ITEMS, mine: true, signal });
40167
40493
  const latest2 = latestAnswersByQuestion(entries2, { registryConfig: ctx.client.config, questionIds });
40168
40494
  const answers2 = /* @__PURE__ */ new Map();
40169
40495
  latest2.forEach((byResponder, id) => {
40170
40496
  if (byResponder.has(wallet.address.toLowerCase())) answers2.set(id, byResponder.get(wallet.address.toLowerCase()));
40171
40497
  });
40172
- ctx.ownScan = { at: now(), sessionId: ctx.client.sessionId, answers: answers2, complete: complete2, chainOrders };
40498
+ ctx.ownScan = { at: now(), sessionId: ctx.client.sessionId, windowKey: ctx.registry.windowKey, answers: answers2, complete: complete2, chainOrders, unread: unreadAnswers };
40173
40499
  return ctx.ownScan;
40174
40500
  }
40175
40501
  const { items, complete: listed } = await ctx.client.listAll("responses", {
@@ -40188,36 +40514,41 @@ var createParticipant = ({ config, wallet, fetchImpl = globalThis.fetch, now = (
40188
40514
  const entry = byResponder.get(self2);
40189
40515
  if (entry) answers.set(questionId, entry);
40190
40516
  });
40191
- ctx.ownScan = { at: now(), sessionId: ctx.client.sessionId, answers, complete };
40517
+ ctx.ownScan = { at: now(), sessionId: ctx.client.sessionId, answers, complete, unread: entries.unread };
40192
40518
  return ctx.ownScan;
40193
40519
  };
40194
40520
  const loadMyAnswers = async (ctx) => {
40195
40521
  await ensureReady(ctx);
40196
40522
  const scan = await scanMyAnswers(ctx);
40197
40523
  const mine = new Map(scan.answers);
40198
- const complete = scan.complete;
40524
+ let complete = scan.complete;
40525
+ const blocks = /* @__PURE__ */ new Map();
40199
40526
  await loadState();
40200
- const local = sessionState(ctx).sessionId === ctx.client.sessionId && isRecord6(sessionState(ctx).answers) ? sessionState(ctx).answers : {};
40201
- Object.entries(local).forEach(([questionId, answer]) => {
40202
- if (!isRecord6(answer)) return;
40203
- if (ctx.registry && !complete) return;
40527
+ const local = sessionState(ctx).sessionId === ctx.client.sessionId && isRecord7(sessionState(ctx).answers) ? sessionState(ctx).answers : {};
40528
+ for (const [questionId, answer] of Object.entries(local)) {
40529
+ if (!isRecord7(answer)) continue;
40530
+ if (ctx.registry && (!scan.complete || !ctx.registry.containsBlock(answer.blockNumber))) continue;
40204
40531
  const known = mine.get(questionId);
40205
40532
  const chainOrder = scan.chainOrders?.get(questionId);
40206
- if (ctx.registry && chainOrder && compareChainOrder(answer, chainOrder) < 0) return;
40533
+ if (ctx.registry && chainOrder && compareChainOrder(answer, chainOrder) < 0) continue;
40207
40534
  const newer = ctx.registry ? !known || compareChainOrder(answer, known) > 0 : !known || text(answer.answeredAt) > known.createdAt;
40208
- if (newer) {
40535
+ if (!newer) continue;
40536
+ const replaced = ctx.registry ? await ctx.registry.replacedWrite(answer, blocks) : false;
40537
+ if (replaced === null) complete = false;
40538
+ if (!replaced) {
40209
40539
  mine.set(questionId, {
40210
40540
  createdAt: text(answer.answeredAt),
40211
40541
  ...ctx.registry ? { blockNumber: answer.blockNumber, transactionIndex: answer.transactionIndex, logIndex: answer.logIndex } : {},
40212
40542
  payload: { answer: { value: answer.value }, additional: { value: answer.comment } }
40213
40543
  });
40214
40544
  }
40215
- });
40545
+ }
40216
40546
  [...mine.entries()].forEach(([questionId, entry]) => {
40217
40547
  const value = entry?.payload?.answer?.value;
40218
40548
  if (value === "" || value === null || value === void 0 || Array.isArray(value) && !value.length) mine.delete(questionId);
40219
40549
  });
40220
40550
  mine.complete = complete;
40551
+ mine.unread = scan.unread;
40221
40552
  return mine;
40222
40553
  };
40223
40554
  const sessionSummary = (ctx, sessionConfig) => ({
@@ -40233,7 +40564,7 @@ var createParticipant = ({ config, wallet, fetchImpl = globalThis.fetch, now = (
40233
40564
  address: wallet.address,
40234
40565
  profile: profile(),
40235
40566
  currentSession: state.sessions[state.current]?.slug || null,
40236
- sessions: await mapLimit(Object.values(state.sessions).filter(isRecord6), READ_CONCURRENCY, async (entry) => ({
40567
+ sessions: await mapLimit(Object.values(state.sessions).filter(isRecord7), READ_CONCURRENCY, async (entry) => ({
40237
40568
  session: entry.slug,
40238
40569
  title: entry.title || entry.slug,
40239
40570
  webUrl: buildSessionWebUrl(entry),
@@ -40254,9 +40585,9 @@ var createParticipant = ({ config, wallet, fetchImpl = globalThis.fetch, now = (
40254
40585
  },
40255
40586
  async joinSession({ link, worker, session, inviteCode, groupPassword, claimPassword } = {}) {
40256
40587
  let parsed;
40257
- if (trim6(link)) {
40588
+ if (trim8(link)) {
40258
40589
  parsed = parseSessionLink(link, { allowLocalWorkers: config.allowLocalWorkers });
40259
- } else if (trim6(worker) && trim6(session)) {
40590
+ } else if (trim8(worker) && trim8(session)) {
40260
40591
  parsed = {
40261
40592
  appOrigin: config.appOrigin,
40262
40593
  slug: normalizeSessionSlug(session),
@@ -40277,9 +40608,9 @@ var createParticipant = ({ config, wallet, fetchImpl = globalThis.fetch, now = (
40277
40608
  const offered = parsed.sbtClaims.filter((claim) => !claim.auto).map((claim) => claim.sbt);
40278
40609
  if (offered.length) outcome.sbtsToClaimWithClaimSbt = offered;
40279
40610
  const separateCodes = {
40280
- inviteCode: trim6(inviteCode),
40281
- groupPassword: trim6(groupPassword),
40282
- claimPassword: trim6(claimPassword)
40611
+ inviteCode: trim8(inviteCode),
40612
+ groupPassword: trim8(groupPassword),
40613
+ claimPassword: trim8(claimPassword)
40283
40614
  };
40284
40615
  const hasSeparateCodes = Object.values(separateCodes).some(Boolean);
40285
40616
  if (hasSeparateCodes && claims.length !== 1) {
@@ -40309,7 +40640,7 @@ var createParticipant = ({ config, wallet, fetchImpl = globalThis.fetch, now = (
40309
40640
  })
40310
40641
  );
40311
40642
  } catch (error) {
40312
- outcome.sbtClaims.push({ sbt: claim.sbt, status: "failed", error: error.message });
40643
+ outcome.sbtClaims.push({ sbt: claim.sbt, status: "failed", error: describeError(error) });
40313
40644
  }
40314
40645
  }
40315
40646
  tokenClaims = await ctx.client.login().then((claims2) => {
@@ -40320,22 +40651,22 @@ var createParticipant = ({ config, wallet, fetchImpl = globalThis.fetch, now = (
40320
40651
  outcome.testEth = await ctx.client.describeTestEth();
40321
40652
  outcome.signedIn = !loginError;
40322
40653
  if (loginError) {
40323
- outcome.readOnlyReason = loginError.message;
40654
+ outcome.readOnlyReason = describeError(loginError);
40324
40655
  outcome.note = "Joined read-only: public questions and results are available, but uploads, answers, statements, and faucet requests need Worker access.";
40325
40656
  }
40326
- const scopes = isRecord6(tokenClaims?.scopes) ? tokenClaims.scopes : {};
40657
+ const scopes = isRecord7(tokenClaims?.scopes) ? tokenClaims.scopes : {};
40327
40658
  outcome.scopes = Object.keys(scopes).filter((scope) => scopes[scope] === true);
40328
40659
  if (parsed.joinGroupId) {
40329
40660
  checkpoint();
40330
40661
  try {
40331
40662
  if (ctx.registry) throw new Error("Worker groups are not available in on-chain sessions.");
40332
- await ctx.client.joinGroup(parsed.joinGroupId);
40663
+ await ctx.client.joinGroup(parsed.joinGroupId, { signal });
40333
40664
  outcome.joinedGroup = parsed.joinGroupId;
40334
40665
  } catch (error) {
40335
- outcome.groupJoinError = error.message;
40666
+ outcome.groupJoinError = describeError(error);
40336
40667
  }
40337
40668
  }
40338
- outcome.description = text(sessionConfig.sessionInfo).slice(0, MAX_CONTEXT_LENGTH);
40669
+ outcome.description = text(sessionConfig.sessionInfo).slice(0, MAX_DESCRIPTION_LENGTH);
40339
40670
  outcome.context = contextOf(sessionConfig);
40340
40671
  const sessionSbts = listSessionSbts(sessionConfig);
40341
40672
  if (sessionSbts.length) outcome.sessionSbts = sessionSbts;
@@ -40355,7 +40686,7 @@ var createParticipant = ({ config, wallet, fetchImpl = globalThis.fetch, now = (
40355
40686
  return {
40356
40687
  ...sessionSummary(ctx, sessionConfig),
40357
40688
  testEth: await ctx.client.describeTestEth(),
40358
- description: text(sessionConfig.sessionInfo).slice(0, MAX_CONTEXT_LENGTH),
40689
+ description: text(sessionConfig.sessionInfo).slice(0, MAX_DESCRIPTION_LENGTH),
40359
40690
  context: contextOf(sessionConfig),
40360
40691
  ...endsAt ? { endsAt } : {},
40361
40692
  ...sessionSbts.length ? { sessionSbts } : {},
@@ -40393,10 +40724,12 @@ var createParticipant = ({ config, wallet, fetchImpl = globalThis.fetch, now = (
40393
40724
  const results = [];
40394
40725
  const answered = [];
40395
40726
  const pendingAnswers = [];
40727
+ const operationId = randomUUID2();
40396
40728
  let txHash;
40729
+ let reconciliationRequired = false;
40397
40730
  const seen = /* @__PURE__ */ new Set();
40398
40731
  for (const item of answers) {
40399
- const questionId = trim6(item?.questionId).toLowerCase();
40732
+ const questionId = trim8(item?.questionId).toLowerCase();
40400
40733
  if (seen.has(questionId)) {
40401
40734
  results.push({ questionId, ok: false, error: "This question appears more than once in the call; send one answer per question." });
40402
40735
  continue;
@@ -40436,12 +40769,24 @@ var createParticipant = ({ config, wallet, fetchImpl = globalThis.fetch, now = (
40436
40769
  continue;
40437
40770
  }
40438
40771
  try {
40439
- const storageRef = await ctx.client.upload({ resource: "responses", data: JSON.stringify(built.record) });
40772
+ const data = JSON.stringify(built.record);
40773
+ const digest = createHash("sha256").update(data).digest("hex");
40774
+ let requestId;
40775
+ await saveState((fresh) => {
40776
+ const entry = fresh.sessions[sessionKey(ctx.info)];
40777
+ if (!isRecord7(entry) || entry.sessionId !== ctx.client.sessionId) throw new Error("Session identity changed; join again before answering.");
40778
+ entry.pendingUploads = isRecord7(entry.pendingUploads) ? entry.pendingUploads : {};
40779
+ const previous = entry.pendingUploads[questionId];
40780
+ requestId = previous?.payloadDigest === digest && trim8(previous.requestId) ? previous.requestId : `answer:v1:${operationId}:${questionId}`;
40781
+ entry.pendingUploads[questionId] = { payloadDigest: digest, requestId };
40782
+ });
40783
+ checkpoint();
40784
+ const storageRef = await ctx.client.upload({ resource: "responses", data, requestId, signal });
40440
40785
  if (text(storageRef?.id)) cacheRecord(ctx, text(storageRef.id), built.record);
40441
- answered.push({ questionId, record: built.record, answeredAt: text(storageRef?.createdAt) || new Date(now()).toISOString() });
40786
+ answered.push({ questionId, requestId, record: built.record, answeredAt: text(storageRef?.createdAt) || new Date(now()).toISOString() });
40442
40787
  results.push({ questionId, ok: true, answer: built.record.answer.value });
40443
40788
  } catch (error) {
40444
- results.push({ questionId, ok: false, error: error.message });
40789
+ results.push({ questionId, ok: false, error: describeError(error) });
40445
40790
  }
40446
40791
  }
40447
40792
  if (pendingAnswers.length) {
@@ -40456,19 +40801,32 @@ var createParticipant = ({ config, wallet, fetchImpl = globalThis.fetch, now = (
40456
40801
  pendingAnswers.forEach(({ questionId, record }) => {
40457
40802
  answered.push({ questionId, record, answeredAt: new Date(now()).toISOString(), chainOrder: {
40458
40803
  blockNumber: submitted.receipt.blockNumber,
40804
+ blockHash: submitted.receipt.blockHash,
40459
40805
  transactionIndex: submitted.receipt.transactionIndex,
40460
40806
  logIndex: submitted.receipt.logs.at(-1)?.logIndex || 0
40461
40807
  } });
40462
40808
  results.push({ questionId, ok: true, answer: record.answer.value });
40463
40809
  });
40464
40810
  } catch (error) {
40465
- pendingAnswers.forEach(({ questionId }) => results.push({ questionId, ok: false, error: error.message }));
40811
+ if (typeof error.txHash === "string" && /^0x[0-9a-f]{64}$/i.test(error.txHash)) txHash = error.txHash;
40812
+ reconciliationRequired = error.reconciliationRequired === true;
40813
+ pendingAnswers.forEach(({ questionId }) => results.push({ questionId, ok: false, error: describeError(error) }));
40814
+ }
40815
+ }
40816
+ let warning;
40817
+ if (answered.length) {
40818
+ try {
40819
+ await rememberAnswers(ctx, answered);
40820
+ } catch {
40821
+ ctx.ownScan = null;
40822
+ warning = "Answers were submitted, but the local answer history could not be saved. Do not resubmit them to fix this warning.";
40466
40823
  }
40467
40824
  }
40468
- if (answered.length) await rememberAnswers(ctx, answered);
40469
40825
  return {
40470
40826
  session: ctx.info.slug,
40471
40827
  ...txHash ? { txHash } : {},
40828
+ ...reconciliationRequired ? { reconciliationRequired: true } : {},
40829
+ ...warning ? { warning } : {},
40472
40830
  submitted: results.filter((result) => result.ok).length,
40473
40831
  failed: results.filter((result) => !result.ok).length,
40474
40832
  results
@@ -40510,7 +40868,7 @@ var createParticipant = ({ config, wallet, fetchImpl = globalThis.fetch, now = (
40510
40868
  ];
40511
40869
  checkpoint();
40512
40870
  try {
40513
- const storageRef = await ctx.client.upload({ resource: "questions", data: built.record, tags: tagList });
40871
+ const storageRef = await ctx.client.upload({ resource: "questions", data: built.record, tags: tagList, signal });
40514
40872
  if (text(storageRef?.id)) cacheRecord(ctx, text(storageRef.id), built.record);
40515
40873
  return { session: ctx.info.slug, questionId: built.id, created: true, prompt: built.record.prompt };
40516
40874
  } catch (error) {
@@ -40554,18 +40912,21 @@ var createParticipant = ({ config, wallet, fetchImpl = globalThis.fetch, now = (
40554
40912
  let entries;
40555
40913
  let complete;
40556
40914
  let listedCount;
40915
+ let unreadAnswers;
40557
40916
  const knownIds = new Set(questions.map((question) => question.id));
40558
40917
  if (ctx.registry) {
40559
40918
  const result = await ctx.registry.readAnswers({ questionIds: knownIds, maxRecords: cap, signal });
40560
40919
  entries = result.entries;
40561
40920
  complete = result.complete;
40562
40921
  listedCount = entries.length;
40922
+ unreadAnswers = result.unreadAnswers;
40563
40923
  } else {
40564
40924
  const result = await ctx.client.listAll("responses", { maxItems: MAX_RESULT_RECORDS, signal });
40565
40925
  const listed = result.items.filter((item) => text(item.storageRef?.id)).sort((left, right) => text(left.metadata?.createdAt) < text(right.metadata?.createdAt) ? 1 : -1);
40566
40926
  entries = await toEntries(ctx, listed.slice(0, cap));
40567
40927
  complete = result.complete;
40568
40928
  listedCount = listed.length;
40929
+ unreadAnswers = entries.unread;
40569
40930
  }
40570
40931
  const latest = latestAnswersByQuestion(entries, {
40571
40932
  sessionSlug: ctx.info.slug,
@@ -40578,6 +40939,7 @@ var createParticipant = ({ config, wallet, fetchImpl = globalThis.fetch, now = (
40578
40939
  resultsPublic: true,
40579
40940
  webUrl: buildSessionWebUrl(ctx.info),
40580
40941
  participants: participants.size,
40942
+ unreadAnswers,
40581
40943
  partial: !complete || listedCount > cap || questions.complete === false || entries.complete === false,
40582
40944
  questions: questions.filter((question) => !wanted || wanted.has(question.id)).map((question) => summarizeQuestion(question, latest.get(question.id)))
40583
40945
  };
@@ -40586,10 +40948,10 @@ var createParticipant = ({ config, wallet, fetchImpl = globalThis.fetch, now = (
40586
40948
  const ctx = await resolveSession(session);
40587
40949
  if (ctx.registry) throw new Error("Worker groups are not available in on-chain sessions.");
40588
40950
  await ensureReady(ctx);
40589
- const id = trim6(groupId);
40951
+ const id = trim8(groupId);
40590
40952
  if (!/^[A-Za-z0-9._:-]{1,128}$/.test(id)) throw new Error("Pass a valid group id.");
40591
40953
  checkpoint();
40592
- await ctx.client.joinGroup(id);
40954
+ await ctx.client.joinGroup(id, { signal });
40593
40955
  return { session: ctx.info.slug, joinedGroup: id };
40594
40956
  },
40595
40957
  async claimSbt({ session, sbt: sbtAddress, inviteCode, groupPassword, claimPassword } = {}) {
@@ -40601,10 +40963,10 @@ var createParticipant = ({ config, wallet, fetchImpl = globalThis.fetch, now = (
40601
40963
  });
40602
40964
  const result = await sbt.claim({
40603
40965
  ctx,
40604
- sbtAddress: trim6(sbtAddress),
40605
- inviteCode: trim6(inviteCode),
40606
- groupPassword: trim6(groupPassword),
40607
- claimPassword: trim6(claimPassword),
40966
+ sbtAddress: trim8(sbtAddress),
40967
+ inviteCode: trim8(inviteCode),
40968
+ groupPassword: trim8(groupPassword),
40969
+ claimPassword: trim8(claimPassword),
40608
40970
  signal
40609
40971
  });
40610
40972
  await ctx.client.login().catch(() => {
@@ -40628,7 +40990,7 @@ var sessionParam = {
40628
40990
  type: "string",
40629
40991
  description: "Session slug or link. Defaults to the session joined most recently."
40630
40992
  };
40631
- var text3 = (description, extra = {}) => ({ type: "string", description, ...extra });
40993
+ var text4 = (description, extra = {}) => ({ type: "string", description, ...extra });
40632
40994
  var READ = { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true };
40633
40995
  var WRITE = { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true };
40634
40996
  var SPENDS = { ...WRITE, destructiveHint: true };
@@ -40648,9 +41010,9 @@ var buildTools = (participant) => [
40648
41010
  inputSchema: {
40649
41011
  type: "object",
40650
41012
  properties: {
40651
- displayName: text3("Name shown in reports; up to 80 characters. Empty string clears it."),
40652
- modelId: text3("Model identifier, for example the model you run on."),
40653
- platform: text3("Agent platform or harness.")
41013
+ displayName: text4("Name shown in reports; up to 80 characters. Empty string clears it."),
41014
+ modelId: text4("Model identifier, for example the model you run on."),
41015
+ platform: text4("Agent platform or harness.")
40654
41016
  },
40655
41017
  additionalProperties: false
40656
41018
  },
@@ -40664,12 +41026,12 @@ var buildTools = (participant) => [
40664
41026
  inputSchema: {
40665
41027
  type: "object",
40666
41028
  properties: {
40667
- link: text3("Session link, including any invite parameters."),
40668
- worker: text3("Worker origin, when you have no link."),
40669
- session: text3("Session slug, when you have no link."),
40670
- inviteCode: text3("SBT invite code, if one was given separately from the link."),
40671
- groupPassword: text3("SBT group password, if one was given separately."),
40672
- claimPassword: text3("SBT claim password, if one was given separately.")
41029
+ link: text4("Session link, including any invite parameters."),
41030
+ worker: text4("Worker origin, when you have no link."),
41031
+ session: text4("Session slug, when you have no link."),
41032
+ inviteCode: text4("SBT invite code, if one was given separately from the link."),
41033
+ groupPassword: text4("SBT group password, if one was given separately."),
41034
+ claimPassword: text4("SBT claim password, if one was given separately.")
40673
41035
  },
40674
41036
  additionalProperties: false
40675
41037
  },
@@ -40695,7 +41057,7 @@ var buildTools = (participant) => [
40695
41057
  {
40696
41058
  name: "list_questions",
40697
41059
  title: "List questions",
40698
- description: "List the session\u2019s statements and questions with their type, options or scale, and your latest answer. Unanswered ones come first by default.",
41060
+ description: "List the session\u2019s statements and questions with their type, options or scale, and your latest answer. Unanswered ones come first by default. questionsComplete or myAnswersComplete is false when listing or payload reads are unfinished; unreadQuestions and unreadMyAnswers count listed payloads that could not be read.",
40699
41061
  inputSchema: {
40700
41062
  type: "object",
40701
41063
  properties: {
@@ -40711,7 +41073,7 @@ var buildTools = (participant) => [
40711
41073
  {
40712
41074
  name: "answer",
40713
41075
  title: "Answer questions",
40714
- description: 'Submit answers. Values by type: binary "Agree" | "Unsure" | "Disagree"; rating a whole number on the question\u2019s scale; multichoice the option text (an array when several are allowed); quadratic one signed whole-number vote per option whose squares sum within the credit budget; freeform text. Answering again replaces your previous answer.',
41076
+ description: 'Submit answers. Values by type: binary "Agree" | "Unsure" | "Disagree"; rating a number on the question\u2019s scale and step grid (default step 1); multichoice the option text (an array when several are allowed); quadratic one signed whole-number vote per option whose squares sum within the credit budget; freeform text. Answering again replaces your previous answer.',
40715
41077
  inputSchema: {
40716
41078
  type: "object",
40717
41079
  properties: {
@@ -40723,9 +41085,9 @@ var buildTools = (participant) => [
40723
41085
  items: {
40724
41086
  type: "object",
40725
41087
  properties: {
40726
- questionId: text3("Question id from list_questions."),
41088
+ questionId: text4("Question id from list_questions."),
40727
41089
  value: { description: "The answer value; see the tool description for each type." },
40728
- comment: text3("Optional reasoning shown with your answer (up to 4000 characters)."),
41090
+ comment: text4("Optional reasoning shown with your answer (up to 4000 characters)."),
40729
41091
  importance: { type: "integer", minimum: 0, maximum: 10, description: "How much this question matters to you." },
40730
41092
  conviction: { type: "integer", minimum: 0, maximum: 10, description: "How sure you are of your answer." }
40731
41093
  },
@@ -40748,7 +41110,7 @@ var buildTools = (participant) => [
40748
41110
  type: "object",
40749
41111
  properties: {
40750
41112
  session: sessionParam,
40751
- prompt: text3("The statement or question text."),
41113
+ prompt: text4("The statement or question text."),
40752
41114
  type: { type: "string", enum: ["binary", "rating", "multichoice", "quadratic", "freeform"] },
40753
41115
  options: { type: "array", items: { type: "string" }, description: "Options for multichoice or quadratic." },
40754
41116
  singleSelect: { type: "boolean", description: "Multichoice: allow only one option." },
@@ -40764,7 +41126,7 @@ var buildTools = (participant) => [
40764
41126
  {
40765
41127
  name: "get_results",
40766
41128
  title: "Get results",
40767
- description: "Summarize everyone\u2019s latest answers per question when the session publishes results: agree/unsure/disagree counts, rating means, option counts, and how many answers came from self-declared agents.",
41129
+ description: "Summarize everyone\u2019s latest answers per question when the session publishes results: agree/unsure/disagree counts, rating means, option counts, and how many answers came from self-declared agents. partial is true when a scan is unfinished, capped, or any listed question or answer payload could not be read. unreadAnswers counts the selected listed answer records that could not be read; readable older answers may still appear until those records recover.",
40768
41130
  inputSchema: {
40769
41131
  type: "object",
40770
41132
  properties: {
@@ -40783,7 +41145,7 @@ var buildTools = (participant) => [
40783
41145
  description: "Join an open group in the session by its id (from a ?joinGroup= link).",
40784
41146
  inputSchema: {
40785
41147
  type: "object",
40786
- properties: { session: sessionParam, groupId: text3("Group id.") },
41148
+ properties: { session: sessionParam, groupId: text4("Group id.") },
40787
41149
  required: ["groupId"],
40788
41150
  additionalProperties: false
40789
41151
  },
@@ -40798,10 +41160,10 @@ var buildTools = (participant) => [
40798
41160
  type: "object",
40799
41161
  properties: {
40800
41162
  session: sessionParam,
40801
- sbt: text3("SBT contract address."),
40802
- inviteCode: text3("Invite code, when the SBT uses invites."),
40803
- groupPassword: text3("Group password, when the SBT uses one."),
40804
- claimPassword: text3("Claim password, when the SBT uses per-claim passwords.")
41163
+ sbt: text4("SBT contract address."),
41164
+ inviteCode: text4("Invite code, when the SBT uses invites."),
41165
+ groupPassword: text4("Group password, when the SBT uses one."),
41166
+ claimPassword: text4("Claim password, when the SBT uses per-claim passwords.")
40805
41167
  },
40806
41168
  required: ["sbt"],
40807
41169
  additionalProperties: false