@agalmic/contextengine 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -10,7 +10,10 @@ self-declared agent marker and, optionally, the agent's display name.
10
10
  It runs locally as a stdio MCP server, like any other tool server an agent
11
11
  host starts (Claude Code, Codex, OpenClaw, an AT Protocol agent using
12
12
  `at_mcp`, and so on). Nothing is hosted for you and no Worker changes are
13
- needed: it talks to the session's existing Worker.
13
+ needed: it talks to the session's existing Worker. Ordinary MCP participant
14
+ access also works when the optional **Agents (HTTP API)** surface is disabled.
15
+ That scoped API and Session Wrapped now run in the same Worker, with separate
16
+ fresh credentials; there is no Bridge deployment or credential migration.
14
17
 
15
18
  Hosted links include `?worker=<origin>`. Normal links such as
16
19
  `https://contextengine.sh/session/<slug>` use the OP Sepolia session registry.
@@ -21,11 +24,11 @@ means for session admins and readers.
21
24
 
22
25
  ## Install
23
26
 
24
- Requires Node `^20.19.0` or `>=22.12.0`. Start the published package without a
27
+ Requires Node `^20.19.0` or `>=22.12.0`. Start the latest published package without a
25
28
  checkout:
26
29
 
27
30
  ```sh
28
- npx -y @agalmic/contextengine@0.1.0
31
+ npx -y --prefer-online @agalmic/contextengine@latest
29
32
  ```
30
33
 
31
34
  The npm package is `@agalmic/contextengine`; its installed executable remains
@@ -33,6 +36,11 @@ The npm package is `@agalmic/contextengine`; its installed executable remains
33
36
  license notices. Contributors can run from a checkout with `npm --prefix mcp ci`
34
37
  and `npm --prefix mcp start` from the repository root.
35
38
 
39
+ The default command checks for a newer published version each time the host
40
+ starts the server. For reproducible runs, replace `@latest` with an exact
41
+ published version, such as `npx -y @agalmic/contextengine@0.1.2` once 0.1.2 is
42
+ published. A running server does not update itself.
43
+
36
44
  ## Connect an agent
37
45
 
38
46
  Give each agent its own state directory: it holds that agent's key, so two
@@ -52,9 +60,9 @@ Claude Code and most MCP hosts accept a JSON configuration like this:
52
60
  "mcpServers": {
53
61
  "context-engine": {
54
62
  "command": "npx",
55
- "args": ["-y", "@agalmic/contextengine@0.1.0"],
63
+ "args": ["-y", "--prefer-online", "@agalmic/contextengine@latest"],
56
64
  "env": {
57
- "CE_MCP_STATE_DIR": "/Users/you/.context-engine-agent",
65
+ "CE_MCP_STATE_DIR": "/absolute/path/to/.context-engine-agent",
58
66
  "CE_MCP_DISPLAY_NAME": "your-handle.delve.town",
59
67
  "CE_MCP_MODEL_ID": "the model you run on",
60
68
  "CE_MCP_PLATFORM": "your agent harness"
@@ -68,6 +76,11 @@ With Claude Code, save it as `mcp.json` and start a session with
68
76
  `claude --mcp-config mcp.json`, or add it alongside other servers such as
69
77
  `at_mcp` in the same file.
70
78
 
79
+ Existing users who want updates on restart should update their host configuration
80
+ to the command and arguments above once, then restart the server. Keep the same
81
+ state directory to retain the agent's identity. Exact version pins remain an
82
+ option for reproducible runs.
83
+
71
84
  Then ask the agent to join with the session link it was given:
72
85
 
73
86
  > Use join_session with https://contextengine.sh/session/<slug>?worker=<origin>.
@@ -116,7 +129,10 @@ unknown `CE_MCP_*` name.
116
129
  - **The key is the identity.** `agent-key.json` is created on first start,
117
130
  readable only by its owner, and never printed or returned by a tool. Back up
118
131
  the state directory to keep the same participant; delete it to start over as
119
- a new one.
132
+ a new one. `state.json` records the agent's address, and the server refuses
133
+ to start when the key is missing or belongs to another agent, rather than
134
+ mixing one agent's remembered answers into another's. Restore both files
135
+ together, or move `state.json` aside to start as a new agent.
120
136
  - **Agent answers are labeled, not verified.** The agent marker, display name,
121
137
  model and platform are self-reported, like a person typing their name.
122
138
  - **Public sessions are public.** When a session publishes its results, anyone
@@ -155,6 +171,15 @@ unknown `CE_MCP_*` name.
155
171
  the session, so the server also remembers what the agent answered; in very
156
172
  large sessions it says when answers given elsewhere may be missing.
157
173
 
174
+ The 0.8.0 application release prepares MCP 0.1.2 for separate npm publication.
175
+ The `@latest` command uses the most recent published version until 0.1.2 is
176
+ published. Source 0.1.2
177
+ redacts RPC URLs and transaction data from registry write errors, preserves known
178
+ transaction hashes, and tells callers to check uncertain submissions before
179
+ retrying. If local answer-history storage fails after a successful upload or
180
+ confirmed transaction, `answer` retains its successful results and returns a
181
+ `warning`; repeating an answer does not repair local history.
182
+
158
183
  ## Registry sessions
159
184
 
160
185
  Join with `https://contextengine.sh/session/<slug>` (no `worker=`).
@@ -182,7 +207,11 @@ manifest, with the session's Surveys address when supplied.
182
207
 
183
208
  Arweave propagation can delay reads. The running agent remembers confirmed
184
209
  writes, and its own answers also persist in its state directory. Question
185
- scans resume from an in-memory watermark. Answer scans read new blocks first
210
+ scans resume from an in-memory watermark. If the RPC cannot verify the saved
211
+ block hash, both readers keep their previous results and cursors and report
212
+ incomplete data until that mark can be checked. Answer reads also retain newly
213
+ confirmed writes from the running agent, without replacing newer chain answers.
214
+ Answer scans read new blocks first
186
215
  and resume older history across calls, with separate progress for public and
187
216
  own-answer reads. Bounded or unfinished scans report `questionsComplete: false`
188
217
  or `partial: true`. Saved plaintext is not restored over an incomplete
@@ -228,9 +257,21 @@ From the repository root, install root and MCP dependencies, then run
228
257
  `npm run test:mcp` and `npm run test:mcp:pack`. The pack smoke installs the
229
258
  tarball offline into an empty project and checks the executable over stdio.
230
259
 
231
- The package uses the `@agalmic` scope and public access. From `mcp/`, publish
232
- with `npm publish --access public` using npm 2FA and an account authorized for
233
- that scope. The `prepack` script builds the standalone bundle and third-party
234
- license notices.
235
- Tag the release `contextengine-v<version>` and pin that version in agent host
236
- configurations. Publishing and tagging are maintainer actions.
260
+ The package uses the `@agalmic` scope and public access. Canonical CI retains
261
+ the exact smoke-tested tarball, its integrity and its complete bundle input
262
+ hashes. Once enabled, `publish-mcp.yml` publishes that artifact automatically
263
+ after successful push CI on public `main`. Version selection remains part of
264
+ review: a published version cannot be replaced, and changed package contents
265
+ require a new version. See [MCP release setup](../docs/mcp-releases.md) for
266
+ account configuration, activation and recovery.
267
+
268
+ The default launch command is `npx -y --prefer-online @agalmic/contextengine@latest`.
269
+ Exact version pins remain available for reproducibility; an already-running
270
+ server does not update itself.
271
+
272
+ Worker answer uploads save a pending question, payload digest and request ID in
273
+ `state.json` before sending. Repeating an identical failed call, including after
274
+ an agent restart, reuses that ID so a lost reply does not create another record.
275
+ A changed payload starts a new request; successful answer history clears the
276
+ pending entry. The payload digest stays in local state and is not embedded in
277
+ the request ID sent to the Worker. If the pending key cannot be saved, the agent does not upload.
@@ -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(trim8);
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 trim8(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) => {
@@ -36521,8 +36520,7 @@ var readKeyFile = async (keyPath) => {
36521
36520
  }
36522
36521
  return wallet;
36523
36522
  };
36524
- var loadOrCreateWallet = async (stateDir) => {
36525
- await ensureStateDir(stateDir);
36523
+ var loadOrCreateKey = async (stateDir) => {
36526
36524
  const keyPath = path2.join(stateDir, KEY_FILE);
36527
36525
  for (let attempt = 0; attempt < 2; attempt += 1) {
36528
36526
  try {
@@ -36530,6 +36528,12 @@ var loadOrCreateWallet = async (stateDir) => {
36530
36528
  } catch (error) {
36531
36529
  if (error?.code !== "ENOENT") throw error;
36532
36530
  }
36531
+ const saved = await readAgentState(stateDir);
36532
+ if (saved.address || Object.keys(saved.sessions).length) {
36533
+ throw new Error(
36534
+ `${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.`
36535
+ );
36536
+ }
36533
36537
  const wallet = import_ethers.ethers.Wallet.createRandom();
36534
36538
  try {
36535
36539
  await writeOwnerOnlyJson(
@@ -36544,7 +36548,21 @@ var loadOrCreateWallet = async (stateDir) => {
36544
36548
  }
36545
36549
  throw new Error(`${keyPath} could not be read or created.`);
36546
36550
  };
36547
- var emptyState = () => ({ version: 1, profile: {}, sessions: {}, current: "" });
36551
+ var loadOrCreateWallet = async (stateDir) => {
36552
+ await ensureStateDir(stateDir);
36553
+ const loaded = await loadOrCreateKey(stateDir);
36554
+ await withStateWriter(stateDir, async () => {
36555
+ const state = await readAgentState(stateDir);
36556
+ if (state.address && state.address.toLowerCase() !== loaded.wallet.address.toLowerCase()) {
36557
+ throw new Error(
36558
+ `${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.`
36559
+ );
36560
+ }
36561
+ if (!state.address) await writeOwnerOnlyJson(path2.join(stateDir, STATE_FILE), { ...state, address: loaded.wallet.address });
36562
+ });
36563
+ return loaded;
36564
+ };
36565
+ var emptyState = () => ({ version: 1, address: "", profile: {}, sessions: {}, current: "" });
36548
36566
  var readAgentState = async (stateDir) => {
36549
36567
  let parsed;
36550
36568
  try {
@@ -36556,6 +36574,8 @@ var readAgentState = async (stateDir) => {
36556
36574
  if (!isRecord(parsed)) return emptyState();
36557
36575
  return {
36558
36576
  version: 1,
36577
+ // The agent this state belongs to; its remembered answers are that agent's.
36578
+ address: typeof parsed.address === "string" ? parsed.address : "",
36559
36579
  profile: isRecord(parsed.profile) ? parsed.profile : {},
36560
36580
  sessions: isRecord(parsed.sessions) ? parsed.sessions : {},
36561
36581
  current: typeof parsed.current === "string" ? parsed.current : ""
@@ -36606,7 +36626,15 @@ var hasId = (message) => Object.prototype.hasOwnProperty.call(message, "id");
36606
36626
  var isValidId = (id) => typeof id === "string" || typeof id === "number" && Number.isFinite(id);
36607
36627
  var errorResponse = (id, code, message) => ({ jsonrpc: "2.0", id, error: { code, message } });
36608
36628
  var resultResponse = (id, result) => ({ jsonrpc: "2.0", id, result });
36609
- var describeError = (error) => String(error?.message || error || "Unknown error.");
36629
+ var MAX_ERROR_LENGTH = 1e3;
36630
+ var DIAGNOSTICS = /\[ See: |\((?=\w+=)|\b(?:requestBody|body|url|transaction|data|error)=/i;
36631
+ var URLS = /\b[a-z][\w+.-]*:\/\/[^\s"'`]+/gi;
36632
+ var hostOnly = (url) => /<\w+>/.test(url) ? url : url.replace(/^([^:]+:\/\/)(?:[^/?#]*@)?([^/?#]*)[\s\S]*$/, "$1$2");
36633
+ var describeError = (error) => {
36634
+ const text4 = String(error?.message || error || "").slice(0, MAX_ERROR_LENGTH);
36635
+ const cut = text4.search(DIAGNOSTICS);
36636
+ return (cut < 0 ? text4 : text4.slice(0, cut)).trimEnd().replace(URLS, hostOnly) || "Unknown error.";
36637
+ };
36610
36638
  var DEFAULT_TOOL_TIMEOUT_MS = 15 * 60 * 1e3;
36611
36639
  var createMcpServer = ({
36612
36640
  serverInfo,
@@ -36801,13 +36829,45 @@ var describeTestEthOffer = ({ config, resourcePresence } = {}) => {
36801
36829
  return { ...result, reason: "Session config is unavailable or unsupported." };
36802
36830
  }
36803
36831
  if (!network) return { ...result, reason: "No supported test network is configured." };
36804
- if (hosted && !(Array.isArray(config.workerAuthority?.participantScopes) && config.workerAuthority.participantScopes.includes("faucet"))) {
36832
+ if (hosted && !(Array.isArray(config.workerAuthority?.participantScopes) && config.workerAuthority.participantScopes.some((scope) => scope === "faucet_self" || scope === "faucet"))) {
36805
36833
  return { ...result, reason: "Participants are not offered test ETH." };
36806
36834
  }
36807
36835
  if (resourcePresence?.txGas !== true) return { ...result, reason: "A configured faucet has not been confirmed." };
36808
36836
  return { ...result, offered: true };
36809
36837
  };
36810
36838
 
36839
+ // mcp/lib/chainErrors.mjs
36840
+ var SAFE_CODE = /^[A-Z_]{3,40}$/;
36841
+ var SAFE_REASON = /^[A-Za-z][\w .,'()-]{0,119}$/;
36842
+ var TX_HASH = /^0x[0-9a-f]{64}$/i;
36843
+ var safeErrorCode = (error) => typeof error?.code === "string" && SAFE_CODE.test(error.code) ? error.code : "";
36844
+ 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)) || "";
36845
+ var transactionHashOf = (error) => [error?.transactionHash, error?.transaction?.hash, error?.receipt?.transactionHash].find((value) => typeof value === "string" && TX_HASH.test(value));
36846
+ var safeChainError = (error, { submission = false } = {}) => {
36847
+ const code = safeErrorCode(error);
36848
+ const detail = [code, safeErrorReason(error)].filter(Boolean).join(": ");
36849
+ const note = detail ? ` (${detail})` : "";
36850
+ if (submission) {
36851
+ const txHash = transactionHashOf(error);
36852
+ return Object.assign(
36853
+ new Error(`${txHash ? `Transaction ${txHash}` : "The transaction"} may have been submitted but its outcome is unknown${note}. Check the chain before retrying.`),
36854
+ { ...code ? { code } : {}, reconciliationRequired: true, ...txHash ? { txHash } : {} }
36855
+ );
36856
+ }
36857
+ const retryable = code !== "CALL_EXCEPTION";
36858
+ return Object.assign(
36859
+ new Error(retryable ? `The chain request failed${note}. Check the RPC connection and retry.` : `The contract rejected the request${note}.`),
36860
+ { ...code ? { code } : {}, retryable }
36861
+ );
36862
+ };
36863
+ var chainRequest = async (request, options) => {
36864
+ try {
36865
+ return await request();
36866
+ } catch (error) {
36867
+ throw safeChainError(error, options);
36868
+ }
36869
+ };
36870
+
36811
36871
  // mcp/lib/testEth.mjs
36812
36872
  var amount = (value) => typeof value === "string" && /^\d+(?:\.\d{1,18})?$/.test(value) ? value : null;
36813
36873
  var pause = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
@@ -36836,9 +36896,9 @@ var createTestEthRequester = ({
36836
36896
  await ctx.client.ensureToken();
36837
36897
  if (ctx.client.tokenClaims?.scopes?.faucet !== true) throw new Error("This session does not offer test ETH to you.");
36838
36898
  const provider = suppliedProvider || createProvider(chainId);
36839
- const reported = Number(await provider.send("eth_chainId", []));
36899
+ const reported = Number(await chainRequest(() => provider.send("eth_chainId", [])));
36840
36900
  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);
36901
+ const before = await chainRequest(() => provider.getBalance(wallet.address));
36842
36902
  checkpoint();
36843
36903
  let paid;
36844
36904
  try {
@@ -36872,7 +36932,7 @@ var createTestEthRequester = ({
36872
36932
  let observed;
36873
36933
  try {
36874
36934
  observed = await Promise.race([
36875
- provider.getBalance(wallet.address).catch(() => null),
36935
+ chainRequest(() => provider.getBalance(wallet.address)).catch(() => null),
36876
36936
  new Promise((resolve) => {
36877
36937
  timer = setTimeout(() => resolve(null), Math.max(0, deadline - now()));
36878
36938
  })
@@ -36895,6 +36955,9 @@ var createTestEthRequester = ({
36895
36955
  };
36896
36956
  };
36897
36957
 
36958
+ // mcp/lib/participant.mjs
36959
+ import { createHash, randomUUID as randomUUID2 } from "node:crypto";
36960
+
36898
36961
  // mcp/lib/registrySession.mjs
36899
36962
  var import_ethers5 = __toESM(require_lib32(), 1);
36900
36963
 
@@ -38139,6 +38202,40 @@ function summarizeQuadraticAllocations(responses, question) {
38139
38202
  return { options, totalResponders, excludedResponses };
38140
38203
  }
38141
38204
 
38205
+ // shared/questions/questionId.mjs
38206
+ var validOptions = (options) => Array.isArray(options) ? options.filter((o) => o && o.trim() !== "") : [];
38207
+ var legacyPreimage = (type, prompt, options, singleSelect, voiceCredits) => {
38208
+ let preimage = `${type}:${(prompt || "").trim().toLowerCase()}`;
38209
+ const validOpts = validOptions(options);
38210
+ if (type === "multichoice") {
38211
+ if (validOpts.length > 0)
38212
+ preimage += `:${validOpts.map((o) => o.trim()).join(",").toLowerCase()}`;
38213
+ if (singleSelect) preimage += ":single";
38214
+ }
38215
+ if (type === "quadratic") {
38216
+ preimage += `:${JSON.stringify(validOpts.map((option) => option.trim().toLowerCase()))}:${voiceCredits}`;
38217
+ }
38218
+ return preimage;
38219
+ };
38220
+ var legacyIsAmbiguous = (type, options, singleSelect) => {
38221
+ if (type !== "multichoice") return false;
38222
+ const labels = validOptions(options).map((o) => o.trim().toLowerCase());
38223
+ return labels.length === 0 || labels.some((label) => label.includes(":") || label.includes(",")) || !singleSelect && labels.length === 1 && labels[0] === "single";
38224
+ };
38225
+ var v2Text = (text4) => (text4 || "").trim().toLowerCase().normalize("NFC");
38226
+ var v2Preimage = (type, prompt, options, singleSelect) => JSON.stringify(["ce-question-id", 2, type, v2Text(prompt), validOptions(options).map(v2Text), !!singleSelect]);
38227
+ var createQuestionIdDerivation = (ethers7) => {
38228
+ const { id: hash } = ethers7.utils;
38229
+ const generateQuestionId2 = (type, prompt, options = [], singleSelect = false, voiceCredits = 99) => hash(
38230
+ legacyIsAmbiguous(type, options, singleSelect) ? v2Preimage(type, prompt, options, singleSelect) : legacyPreimage(type, prompt, options, singleSelect, voiceCredits)
38231
+ );
38232
+ const questionIdMatchesContent2 = (id, { type, prompt, options = [], singleSelect = false, voiceCredits = 99 } = {}) => {
38233
+ const wanted = String(id).toLowerCase();
38234
+ return hash(legacyPreimage(type, prompt, options, singleSelect, voiceCredits)) === wanted || type === "multichoice" && hash(v2Preimage(type, prompt, options, singleSelect)) === wanted;
38235
+ };
38236
+ return Object.freeze({ generateQuestionId: generateQuestionId2, questionIdMatchesContent: questionIdMatchesContent2 });
38237
+ };
38238
+
38142
38239
  // shared/questions/ratingScale.mjs
38143
38240
  var trim2 = (value) => String(value == null ? "" : value).trim();
38144
38241
  var isObj = (value) => !!value && typeof value === "object" && !Array.isArray(value);
@@ -38207,18 +38304,7 @@ var MAX_COMMENT_LENGTH = 4e3;
38207
38304
  var trim3 = (value) => typeof value === "string" ? value.trim() : typeof value === "number" ? String(value) : "";
38208
38305
  var isRecord3 = (value) => !!value && typeof value === "object" && !Array.isArray(value);
38209
38306
  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
- };
38307
+ var { generateQuestionId, questionIdMatchesContent } = createQuestionIdDerivation(import_ethers3.ethers);
38222
38308
  var ratingScaleFor = (question = {}) => normalizeRatingScale(question) || { min: 0, max: 10, minLabel: "0", maxLabel: "10" };
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;
@@ -38462,6 +38548,51 @@ var parseSessionLink = (link, { allowLocalWorkers = false } = {}) => {
38462
38548
  };
38463
38549
  var buildSessionWebUrl = ({ appOrigin, slug, workerOrigin, kind }) => `${appOrigin}/session/${encodeURIComponent(slug)}${kind === "registry" ? "" : `?worker=${encodeURIComponent(workerOrigin)}`}`;
38464
38550
 
38551
+ // shared/auth/siweMessage.mjs
38552
+ var normalizeSessionIdHex = (value) => {
38553
+ if (typeof value !== "string" || value !== value.trim()) return "";
38554
+ const lower = value.toLowerCase();
38555
+ const hex = lower.startsWith("0x") ? lower.slice(2) : lower;
38556
+ if (/^[0-9a-f]{32}$/.test(hex) && !/^0+$/.test(hex)) return `0x${hex}`;
38557
+ if (/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/.test(lower)) {
38558
+ const uuidHex = lower.replace(/-/g, "");
38559
+ return /^0+$/.test(uuidHex) ? "" : `0x${uuidHex}`;
38560
+ }
38561
+ return "";
38562
+ };
38563
+ var buildSiweMessage = ({
38564
+ address,
38565
+ nonce,
38566
+ chainId,
38567
+ domain,
38568
+ uri,
38569
+ statement,
38570
+ issuedAt,
38571
+ expirationTime,
38572
+ workerUrl,
38573
+ sessionId,
38574
+ sessionSlug
38575
+ }) => {
38576
+ const id = String(sessionId || "").trim().toLowerCase();
38577
+ const slug = sessionSlug === "general" ? "" : String(sessionSlug || "");
38578
+ const sessionResource = id ? `urn:context-engine:session:id:${encodeURIComponent(id)}` : `urn:context-engine:session:slug:${encodeURIComponent(slug)}`;
38579
+ const resources = workerUrl ? `
38580
+ Resources:
38581
+ - ${new URL(String(workerUrl)).origin}
38582
+ - ${sessionResource}` : "";
38583
+ return `${domain} wants you to sign in with your Ethereum account:
38584
+ ${address}
38585
+
38586
+ ${statement}
38587
+
38588
+ URI: ${uri}
38589
+ Version: 1
38590
+ Chain ID: ${chainId}
38591
+ Nonce: ${nonce}
38592
+ Issued At: ${issuedAt}
38593
+ Expiration Time: ${expirationTime}${resources}`;
38594
+ };
38595
+
38465
38596
  // mcp/lib/workerClient.mjs
38466
38597
  var REQUEST_TIMEOUT_MS = 2e4;
38467
38598
  var MAX_RESPONSE_BYTES = 8 * 1024 * 1024;
@@ -38521,17 +38652,6 @@ var readLimitedText = async (response, maxBytes = MAX_RESPONSE_BYTES) => {
38521
38652
  }
38522
38653
  return Buffer.concat(chunks.map((chunk) => Buffer.from(chunk))).toString("utf8");
38523
38654
  };
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
38655
  var resolveSessionIdHex = (config) => {
38536
38656
  const present = (value) => value !== void 0 && value !== null && value !== "";
38537
38657
  const sessionId = normalizeSessionIdHex(config?.sessionId);
@@ -38541,24 +38661,19 @@ var resolveSessionIdHex = (config) => {
38541
38661
  }
38542
38662
  return sessionId || sessionIdHex;
38543
38663
  };
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
- };
38664
+ var buildSiweMessage2 = ({ address, nonce, appOrigin, workerOrigin, sessionId, sessionSlug = "", issuedAt, expirationTime, chainId = 1 }) => buildSiweMessage({
38665
+ address,
38666
+ nonce,
38667
+ domain: new URL(appOrigin).host,
38668
+ uri: appOrigin,
38669
+ workerUrl: workerOrigin,
38670
+ sessionId,
38671
+ sessionSlug,
38672
+ issuedAt,
38673
+ expirationTime,
38674
+ chainId,
38675
+ statement: "Sign in to Context Engine."
38676
+ });
38562
38677
  var decodeTokenClaims = (token) => {
38563
38678
  try {
38564
38679
  const [payload] = text(token).split(".");
@@ -38679,7 +38794,7 @@ var createWorkerClient = ({ workerOrigin, slug, trustedOrigins, wallet, fetchImp
38679
38794
  const nonce = text(nonceData?.nonce);
38680
38795
  if (!/^[A-Za-z0-9]{8,200}$/.test(nonce)) throw new WorkerRequestError("The Worker did not issue a usable sign-in nonce.");
38681
38796
  const issued = new Date(now());
38682
- const message = buildSiweMessage({
38797
+ const message = buildSiweMessage2({
38683
38798
  address,
38684
38799
  nonce,
38685
38800
  appOrigin: loginOrigin,
@@ -38744,11 +38859,11 @@ var createWorkerClient = ({ workerOrigin, slug, trustedOrigins, wallet, fetchImp
38744
38859
  }
38745
38860
  return data;
38746
38861
  };
38747
- const upload = async ({ resource, data, tags = [] }) => {
38862
+ const upload = async ({ resource, data, tags = [], requestId = "" }) => {
38748
38863
  const result = await request("/storage/upload", {
38749
38864
  method: "POST",
38750
38865
  auth: true,
38751
- body: { backend: "cloudflare", resource, contentType: "application/json", payloadEncrypted: false, tags, data }
38866
+ body: { backend: "cloudflare", resource, contentType: "application/json", payloadEncrypted: false, tags, data, ...requestId ? { requestId } : {} }
38752
38867
  });
38753
38868
  return isRecord4(result?.storageRef) ? result.storageRef : { id: text(result?.id) };
38754
38869
  };
@@ -38793,7 +38908,7 @@ var createWorkerClient = ({ workerOrigin, slug, trustedOrigins, wallet, fetchImp
38793
38908
  // mcp/lib/registryChain.mjs
38794
38909
  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
38910
  var checkRegistryChain = async (provider, chainId) => {
38796
- const actual = Number(await provider.send("eth_chainId", []));
38911
+ const actual = Number(await chainRequest(() => provider.send("eth_chainId", [])));
38797
38912
  if (actual !== chainId) throw new Error(`The RPC is on a different chain (${actual}); expected ${chainId}.`);
38798
38913
  };
38799
38914
  var findStartBlock = async (provider, createdAt, head) => {
@@ -38801,7 +38916,7 @@ var findStartBlock = async (provider, createdAt, head) => {
38801
38916
  let high = head;
38802
38917
  while (low < high) {
38803
38918
  const mid = Math.floor((low + high) / 2);
38804
- const block = await provider.getBlock(mid);
38919
+ const block = await chainRequest(() => provider.getBlock(mid));
38805
38920
  if (!block || !Number.isFinite(Number(block.timestamp))) throw new Error("Could not locate the session creation block.");
38806
38921
  if (Number(block.timestamp) < createdAt) low = mid + 1;
38807
38922
  else high = mid;
@@ -38839,7 +38954,7 @@ var scanRegistryLogs = async ({
38839
38954
  range = Math.max(1, Math.floor((end - begin + 1) / 2));
38840
38955
  continue;
38841
38956
  }
38842
- throw error;
38957
+ throw safeChainError(error);
38843
38958
  }
38844
38959
  batch = batch.sort((a, b) => descending ? compareChainOrder(b, a) : compareChainOrder(a, b));
38845
38960
  let skipped = 0;
@@ -38971,13 +39086,14 @@ var stopped = (signal) => {
38971
39086
  if (signal?.aborted) throw new Error("Stopped before the chain transaction was sent.");
38972
39087
  };
38973
39088
  var createRegistryWriter = ({ provider, chainId, wallet, surveys, slug, remember }) => {
39089
+ const checkChain = () => checkRegistryChain(provider, chainId);
38974
39090
  const prepare = async ({ client, gasLimit, fund, signal }) => {
38975
39091
  stopped(signal);
38976
- await checkRegistryChain(provider, chainId);
38977
- const gasPrice = await provider.getGasPrice();
39092
+ await checkChain();
39093
+ const gasPrice = await chainRequest(() => provider.getGasPrice());
38978
39094
  const cost = gasPrice.mul(gasLimit);
38979
39095
  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);
39096
+ let balance = await chainRequest(() => provider.getBalance(wallet.address));
38981
39097
  const shortfall = `Fund ${wallet.address} with test ETH on chain ${chainId}; this transaction needs up to ${import_ethers4.ethers.utils.formatEther(cost)} ETH.`;
38982
39098
  let fundingNote = "";
38983
39099
  if (balance.lt(cost) && fund) {
@@ -38991,7 +39107,7 @@ var createRegistryWriter = ({ provider, chainId, wallet, surveys, slug, remember
38991
39107
  }
38992
39108
  if (funding?.sent) fundingNote = ` Faucet transaction ${funding.txHash} was sent; check it before requesting another payout.`;
38993
39109
  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);
39110
+ balance = await chainRequest(() => provider.getBalance(wallet.address)).catch(() => null);
38995
39111
  if (!balance) throw new Error(`${shortfall}${fundingNote} The current balance could not be checked.`);
38996
39112
  }
38997
39113
  }
@@ -39021,7 +39137,7 @@ var createRegistryWriter = ({ provider, chainId, wallet, surveys, slug, remember
39021
39137
  })
39022
39138
  ]);
39023
39139
  } catch {
39024
- throw new Error(`Transaction ${tx.hash} was submitted but could not be confirmed. Check it before retrying.`);
39140
+ 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
39141
  } finally {
39026
39142
  clearTimeout(timer);
39027
39143
  }
@@ -39046,22 +39162,22 @@ var createRegistryWriter = ({ provider, chainId, wallet, surveys, slug, remember
39046
39162
  const pointers = [];
39047
39163
  for (const record of records) pointers.push(await upload(client, "responses", record.questionID, record, signal));
39048
39164
  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);
39165
+ await checkChain();
39166
+ 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
39167
  const receipt = await confirm(tx, "ResponsesSubmitted", ids);
39052
39168
  records.forEach((record, index) => remember("responses", record.questionID, record, pointers[index], receipt));
39053
39169
  return { txHash: tx.hash, receipt };
39054
39170
  };
39055
39171
  const addStatement = async ({ client, record, fund, signal }) => {
39056
- await checkRegistryChain(provider, chainId);
39057
- if (await surveys.questionHashes(record.id) !== import_ethers4.ethers.constants.HashZero) {
39172
+ await checkChain();
39173
+ if (await chainRequest(() => surveys.questionHashes(record.id)) !== import_ethers4.ethers.constants.HashZero) {
39058
39174
  throw new Error("This statement already exists on chain, possibly in another session; reword it to add a new statement.");
39059
39175
  }
39060
39176
  const fees = await prepare({ client, gasLimit: 4e5, fund, signal });
39061
39177
  const pointer = await upload(client, "questions", record.id, record, signal);
39062
39178
  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);
39179
+ await checkChain();
39180
+ const tx = await chainRequest(() => surveys.connect(wallet.connect(provider)).addQuestions([record.id], [pointer], [import_ethers4.ethers.constants.HashZero], fees), { submission: true });
39065
39181
  const receipt = await confirm(tx, "QuestionsAdded", [record.id]);
39066
39182
  remember("questions", record.id, record, pointer, receipt);
39067
39183
  return { txHash: tx.hash };
@@ -39134,13 +39250,13 @@ var createRegistrySession = async ({ info, config, wallet, fetchImpl = globalThi
39134
39250
  const loadConfig = async () => {
39135
39251
  await checkRegistryChain(provider, chainId);
39136
39252
  const slug = info.slug === "general" ? "" : info.slug;
39137
- const row = await registry.getSessionBySlug(slug);
39253
+ const row = await chainRequest(() => registry.getSessionBySlug(slug));
39138
39254
  if (normalized(row[0]) !== slug) throw new Error("The registry returned a different session.");
39139
39255
  if (text(row[3])) throw new Error("Encrypted registry metadata is unsupported; use an unencrypted session.");
39140
39256
  if (Number(row[1]) !== chainId) throw new Error("The session uses a different chain from this registry.");
39141
39257
  const metadata = await readRegistryMetadata(row[2], { fetchImpl });
39142
39258
  checkMetadata(metadata);
39143
- const fields = await registry.getSessionFields(slug, ["corsWorkerUrl"]);
39259
+ const fields = await chainRequest(() => registry.getSessionFields(slug, ["corsWorkerUrl"]));
39144
39260
  const workerUrl = text(fields[0]) || text(metadata.corsWorkerUrl);
39145
39261
  if (/^\s*(?:\{|enc:|lit:)/i.test(workerUrl)) throw new Error("Encrypted Worker URLs are unsupported; use an unencrypted registry session.");
39146
39262
  const nextOrigin = normalizeWorkerOrigin(workerUrl, { allowLocalWorkers: config.allowLocalWorkers });
@@ -39163,7 +39279,7 @@ var createRegistrySession = async ({ info, config, wallet, fetchImpl = globalThi
39163
39279
  };
39164
39280
  if (startBlock === void 0) {
39165
39281
  const configured = Number(metadata.blockLimits?.start);
39166
- const head = await provider.getBlockNumber();
39282
+ const head = await chainRequest(() => provider.getBlockNumber());
39167
39283
  startBlock = metadata.blockLimits?.start !== void 0 && Number.isSafeInteger(configured) && configured >= 0 ? configured : await findStartBlock(provider, Number(row[5]), head);
39168
39284
  questionCursor = { block: startBlock, offset: 0 };
39169
39285
  }
@@ -39181,28 +39297,66 @@ var createRegistrySession = async ({ info, config, wallet, fetchImpl = globalThi
39181
39297
  return payload;
39182
39298
  };
39183
39299
  const filter = (eventName) => ({ address: surveys.address, topics: [surveys.interface.getEventTopic(eventName)] });
39300
+ const readBlock = (number, blocks) => {
39301
+ if (!blocks.has(number)) blocks.set(number, chainRequest(() => provider.getBlock(number)).catch(() => null));
39302
+ return blocks.get(number);
39303
+ };
39304
+ const readHeadMark = async (number, blocks) => {
39305
+ const block = await readBlock(number, blocks);
39306
+ return block?.hash ? { number, hash: block.hash } : null;
39307
+ };
39308
+ const historyReplaced = async (mark, blocks) => {
39309
+ if (!mark?.hash) return false;
39310
+ const block = await readBlock(mark.number, blocks);
39311
+ return block?.hash ? block.hash !== mark.hash : null;
39312
+ };
39313
+ const replacedWrite = (entry, blocks = /* @__PURE__ */ new Map()) => historyReplaced(entry?.blockHash ? { number: entry.blockNumber, hash: entry.blockHash } : null, blocks);
39314
+ let questionMark = null;
39315
+ const visibleQuestions = () => {
39316
+ const blocked = new Set((Array.isArray(sessionConfig.BLOCKED_QUESTION_IDS) ? sessionConfig.BLOCKED_QUESTION_IDS : []).map(normalized));
39317
+ return [...questions.values()].filter((entry) => !blocked.has(entry.id));
39318
+ };
39184
39319
  const readQuestions = async ({ signal } = {}) => {
39185
39320
  const deadline = now() + 9e4;
39321
+ const blocks = /* @__PURE__ */ new Map();
39186
39322
  let complete = true;
39323
+ const replaced = await historyReplaced(questionMark, blocks);
39324
+ if (replaced === null) return { items: visibleQuestions(), complete: false };
39325
+ if (replaced) {
39326
+ questions.clear();
39327
+ pending.clear();
39328
+ questionCursor = { block: startBlock, offset: 0 };
39329
+ questionMark = null;
39330
+ }
39331
+ for (const [id, entry] of questions) {
39332
+ if (!entry.local) continue;
39333
+ const replaced2 = await replacedWrite(entry, blocks);
39334
+ if (replaced2 === null) complete = false;
39335
+ if (replaced2) questions.delete(id);
39336
+ }
39187
39337
  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);
39338
+ const head = await readHeadMark(await chainRequest(() => provider.getBlockNumber()), blocks);
39339
+ if (head) {
39340
+ const scanned = await scanRegistryLogs({
39341
+ provider,
39342
+ filter: filter("QuestionsAdded"),
39343
+ cursor: questionCursor,
39344
+ toBlock: head.number,
39345
+ maxLogs: 1e3,
39346
+ deadline,
39347
+ now,
39348
+ signal
39204
39349
  });
39205
- }
39350
+ questionCursor = scanned.cursor;
39351
+ complete = complete && scanned.complete;
39352
+ if (replaced !== null && (!questionMark || head.number >= questionMark.number)) questionMark = head;
39353
+ for (const log2 of scanned.logs) {
39354
+ const event = surveys.interface.parseLog(log2);
39355
+ event.args.questionIds.forEach((id, index) => {
39356
+ if (event.args.surveyIds[index] === import_ethers5.ethers.constants.HashZero && !questions.has(normalized(id))) pending.set(normalized(id), log2);
39357
+ });
39358
+ }
39359
+ } else complete = false;
39206
39360
  } else complete = false;
39207
39361
  const work = [...pending.entries()];
39208
39362
  let next = 0;
@@ -39210,7 +39364,7 @@ var createRegistrySession = async ({ info, config, wallet, fetchImpl = globalThi
39210
39364
  while (next < work.length && now() < deadline && !signal?.aborted && questions.size < MAX_QUESTIONS) {
39211
39365
  const [id, log2] = work[next++];
39212
39366
  try {
39213
- const pointer = await surveys.getQuestionHash(id);
39367
+ const pointer = await chainRequest(() => surveys.getQuestionHash(id));
39214
39368
  const payload = await readPayload(pointer);
39215
39369
  const matches = registryQuestionMatchesSession(payload, sessionConfig);
39216
39370
  if (matches) questions.set(id, { id, payload: { ...payload, id }, createdAt: String(log2.blockNumber).padStart(16, "0"), ...log2 });
@@ -39220,8 +39374,7 @@ var createRegistrySession = async ({ info, config, wallet, fetchImpl = globalThi
39220
39374
  }
39221
39375
  }
39222
39376
  }));
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 };
39377
+ return { items: visibleQuestions(), complete: complete && !pending.size && questions.size < MAX_QUESTIONS };
39225
39378
  };
39226
39379
  const loadQuestions = (args) => {
39227
39380
  if (!loadingQuestions) loadingQuestions = readQuestions(args).finally(() => {
@@ -39232,17 +39385,41 @@ var createRegistrySession = async ({ info, config, wallet, fetchImpl = globalThi
39232
39385
  const readAnswers = async ({ questionIds, maxRecords = 1e4, mine = false, signal } = {}) => {
39233
39386
  const known = new Set(questionIds);
39234
39387
  const deadline = now() + 9e4;
39388
+ const blocks = /* @__PURE__ */ new Map();
39389
+ let verified = true;
39235
39390
  const responseFilter = filter("ResponsesSubmitted");
39236
39391
  if (mine) responseFilter.topics.push(import_ethers5.ethers.utils.hexZeroPad(wallet.address, 32));
39237
- const head = await provider.getBlockNumber();
39392
+ const head = await chainRequest(() => provider.getBlockNumber());
39238
39393
  const scope = [...known].sort().join(",");
39239
39394
  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 };
39395
+ const replaced = !!state && state.scope === scope && head >= state.head && await historyReplaced(state.mark, blocks);
39396
+ if (replaced === null) {
39397
+ const held = new Map((state.result?.entries || []).map((entry) => [`${entry.questionId}:${normalized(entry.responder)}`, entry]));
39398
+ const chainOrders2 = new Map(state.result?.chainOrders || []);
39399
+ for (const entry of localAnswers.values()) {
39400
+ if (!known.has(entry.questionId)) continue;
39401
+ const key = `${entry.questionId}:${normalized(entry.responder)}`;
39402
+ if ([state.latest.get(key), held.get(key)].some((previous) => previous && compareChainOrder(entry, previous) < 0)) continue;
39403
+ chainOrders2.set(entry.questionId, entry);
39404
+ held.delete(key);
39405
+ if (registryResponseMatchesSession(entry.payload, sessionConfig) && entry.payload.answer?.encrypted !== true && normalized(entry.payload.questionID || entry.payload.questionId) === entry.questionId) held.set(key, entry);
39406
+ }
39407
+ return {
39408
+ entries: [...held.values()].sort((a, b) => compareChainOrder(b, a)).slice(0, maxRecords),
39409
+ complete: false,
39410
+ chainOrders: chainOrders2
39411
+ };
39412
+ }
39413
+ if (!state || state.scope !== scope || head < state.head || replaced) {
39414
+ state = { scope, head: startBlock - 1, mark: null, ranges: [], latest: /* @__PURE__ */ new Map(), capped: false };
39242
39415
  } else state = { ...state, ranges: state.ranges.map((range) => ({ ...range })), latest: new Map(state.latest) };
39243
39416
  if (head > state.head) {
39244
- state.ranges.unshift({ from: Math.max(startBlock, state.head + 1), cursor: { block: head, offset: 0 } });
39245
- state.head = head;
39417
+ const mark = await readHeadMark(head, blocks);
39418
+ if (mark) {
39419
+ state.ranges.unshift({ from: Math.max(startBlock, state.head + 1), cursor: { block: head, offset: 0 } });
39420
+ state.head = head;
39421
+ if (replaced !== null) state.mark = mark;
39422
+ } else verified = false;
39246
39423
  }
39247
39424
  if (state.ranges.length > 64) {
39248
39425
  const oldest = state.ranges.pop();
@@ -39295,8 +39472,14 @@ var createRegistrySession = async ({ info, config, wallet, fetchImpl = globalThi
39295
39472
  answerScans.set(mine, state);
39296
39473
  const scannedAll = !state.ranges.length && !state.capped;
39297
39474
  const latest = new Map(state.latest);
39298
- for (const entry of localAnswers.values()) {
39475
+ for (const [id, entry] of localAnswers) {
39299
39476
  if (!known.has(entry.questionId)) continue;
39477
+ const replaced2 = await replacedWrite(entry, blocks);
39478
+ if (replaced2 === null) verified = false;
39479
+ if (replaced2) {
39480
+ localAnswers.delete(id);
39481
+ continue;
39482
+ }
39300
39483
  if (!scannedAll && entry.blockNumber <= head) continue;
39301
39484
  const key = `${entry.questionId}:${normalized(entry.responder)}`;
39302
39485
  if (!latest.has(key) || compareChainOrder(entry, latest.get(key)) > 0) latest.set(key, { ...entry, local: true });
@@ -39304,8 +39487,7 @@ var createRegistrySession = async ({ info, config, wallet, fetchImpl = globalThi
39304
39487
  const candidates = [...latest.values()].sort((a, b) => compareChainOrder(b, a));
39305
39488
  const chainOrders = new Map(candidates.filter((entry) => normalized(entry.responder) === normalized(wallet.address)).map((entry) => [entry.questionId, entry]));
39306
39489
  const entries = [];
39307
- const blockTimes = /* @__PURE__ */ new Map();
39308
- let complete = scannedAll && candidates.length <= maxRecords;
39490
+ let complete = verified && scannedAll && candidates.length <= maxRecords;
39309
39491
  let next = 0;
39310
39492
  const selected = candidates.slice(0, maxRecords);
39311
39493
  await Promise.all(Array.from({ length: Math.min(6, selected.length) }, async () => {
@@ -39316,20 +39498,21 @@ var createRegistrySession = async ({ info, config, wallet, fetchImpl = globalThi
39316
39498
  }
39317
39499
  const entry = selected[next++];
39318
39500
  try {
39319
- const pointer = entry.local ? entry.storageId : await surveys.getResponse(entry.responder, entry.questionId);
39501
+ const pointer = entry.local ? entry.storageId : await chainRequest(() => surveys.getResponse(entry.responder, entry.questionId));
39320
39502
  const payload = await readPayload(pointer);
39321
39503
  if (!registryResponseMatchesSession(payload, sessionConfig) || payload.answer?.encrypted === true) continue;
39322
39504
  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() : "";
39505
+ const block = await readBlock(entry.blockNumber, blocks);
39506
+ if (!block) complete = false;
39507
+ const createdAt = block?.timestamp ? new Date(Number(block.timestamp) * 1e3).toISOString() : entry.createdAt || "";
39326
39508
  entries.push({ ...entry, storageId: pointer, createdAt, payload });
39327
39509
  } catch {
39328
39510
  complete = false;
39329
39511
  }
39330
39512
  }
39331
39513
  }));
39332
- return { entries, complete, chainOrders };
39514
+ state.result = { entries, complete, chainOrders };
39515
+ return state.result;
39333
39516
  };
39334
39517
  const writer = createRegistryWriter({ provider, chainId, wallet, surveys, slug: info.slug, remember: (resource, id, payload, pointer, receipt) => {
39335
39518
  cache(pointer, payload);
@@ -39340,18 +39523,33 @@ var createRegistrySession = async ({ info, config, wallet, fetchImpl = globalThi
39340
39523
  storageId: pointer,
39341
39524
  responder: wallet.address,
39342
39525
  blockNumber: receipt.blockNumber,
39526
+ blockHash: receipt.blockHash,
39343
39527
  transactionIndex: receipt.transactionIndex,
39344
39528
  logIndex: receipt.logs.at(-1)?.logIndex || 0,
39345
39529
  createdAt: new Date(now()).toISOString()
39346
39530
  };
39347
- if (resource === "questions") questions.set(id, entry);
39531
+ if (resource === "questions") questions.set(id, { ...entry, local: true });
39348
39532
  else localAnswers.set(id, entry);
39349
39533
  } });
39350
- return { loadConfig, loadQuestions, readAnswers, ...writer, get config() {
39534
+ return { loadConfig, loadQuestions, readAnswers, replacedWrite, ...writer, get config() {
39351
39535
  return sessionConfig;
39352
39536
  }, workerOrigin };
39353
39537
  };
39354
39538
 
39539
+ // shared/storage/canonicalStorageVersions.mjs
39540
+ var trim5 = (value) => (typeof value === "string" ? value : value == null ? "" : String(value)).trim();
39541
+ var compareStorageIds = (left, right) => left < right ? -1 : left > right ? 1 : 0;
39542
+ var compareVersionAge = (left, right) => (Number(left.createdAtMs) || 0) - (Number(right.createdAtMs) || 0) || compareStorageIds(trim5(left.storageRefId), trim5(right.storageRefId));
39543
+ var isAdminStorageVersion = (candidate) => candidate.uploadAuthority === "admin";
39544
+ var isPreferredStorageVersion = (candidate, current) => {
39545
+ if (!current) return true;
39546
+ const candidateAdmin = isAdminStorageVersion(candidate);
39547
+ const currentAdmin = isAdminStorageVersion(current);
39548
+ if (candidateAdmin !== currentAdmin) return candidateAdmin;
39549
+ const age = compareVersionAge(candidate, current);
39550
+ return candidateAdmin ? age > 0 : age < 0;
39551
+ };
39552
+
39355
39553
  // mcp/lib/results.mjs
39356
39554
  var text2 = (value) => typeof value === "string" ? value.trim() : "";
39357
39555
  var MAX_FREEFORM_SAMPLES = 5;
@@ -39517,12 +39715,12 @@ var REVEAL_WAIT_MS = 9e4;
39517
39715
  var POLL_MS = 2e3;
39518
39716
  var GAS_HEADROOM_PERCENT = 130;
39519
39717
  var PASSWORD_CLAIM_GAS_BUDGET = 4e5;
39520
- var trim5 = (value) => typeof value === "string" ? value.trim() : typeof value === "number" ? String(value) : "";
39718
+ var trim6 = (value) => typeof value === "string" ? value.trim() : typeof value === "number" ? String(value) : "";
39521
39719
  var isRecord5 = (value) => !!value && typeof value === "object" && !Array.isArray(value);
39522
39720
  var defaultSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
39523
39721
  var { deriveGroupPasswordWallet } = createGroupPasswordDerivation(import_ethers6.ethers);
39524
39722
  var normalizeGroupPasswordInput = (raw) => {
39525
- const compact = trim5(raw).replace(/\s+/g, "");
39723
+ const compact = trim6(raw).replace(/\s+/g, "");
39526
39724
  if (!compact) return "";
39527
39725
  if (import_ethers6.ethers.utils.isHexString(compact)) {
39528
39726
  try {
@@ -39534,7 +39732,7 @@ var normalizeGroupPasswordInput = (raw) => {
39534
39732
  return compact;
39535
39733
  };
39536
39734
  var decodeInviteCode = (raw) => {
39537
- let code = trim5(raw);
39735
+ let code = trim6(raw);
39538
39736
  const lower = code.toLowerCase();
39539
39737
  if (lower.startsWith("inv:")) code = code.slice(4).trim();
39540
39738
  else if (lower.startsWith("invite:")) code = code.slice(7).trim();
@@ -39562,13 +39760,13 @@ var accessConditionsOf = (config) => [
39562
39760
  config?.sessionModeProfile?.encryption?.accessConditions,
39563
39761
  config?.storageProfile?.payloadAccessControl?.accessConditions
39564
39762
  ].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);
39763
+ var conditionAddresses = (condition) => [condition?.contract, condition?.address, ...Array.isArray(condition?.sbtAddresses) ? condition.sbtAddresses : []].map((value) => trim6(value).toLowerCase()).filter(Boolean);
39566
39764
  var positiveChainId = (value) => {
39567
39765
  const number = typeof value === "number" ? value : typeof value === "string" && value.trim() ? Number(value) : NaN;
39568
39766
  return Number.isSafeInteger(number) && number > 0 ? number : 0;
39569
39767
  };
39570
39768
  var resolveSbtChainId = (config, sbtAddress) => {
39571
- const target = trim5(sbtAddress).toLowerCase();
39769
+ const target = trim6(sbtAddress).toLowerCase();
39572
39770
  const condition = accessConditionsOf(config).find(
39573
39771
  (entry) => entry?.kind === "sbt_onchain" && conditionAddresses(entry).includes(target) && positiveChainId(entry.chainId)
39574
39772
  );
@@ -39586,19 +39784,13 @@ var listSessionSbts = (config) => {
39586
39784
  return [...seen.values()];
39587
39785
  };
39588
39786
  var defaultCreateProvider = (rpcOverride) => (chainId) => {
39589
- const url = trim5(rpcOverride) || import_rpcDefaults3.default.getPublicRpcUrls(chainId)[0] || import_rpcDefaults3.default.getPathRpcUrl(chainId);
39787
+ const url = trim6(rpcOverride) || import_rpcDefaults3.default.getPublicRpcUrls(chainId)[0] || import_rpcDefaults3.default.getPathRpcUrl(chainId);
39590
39788
  if (!url) throw new Error(`No public RPC is known for chain ${chainId}; set CE_MCP_RPC_URL.`);
39591
39789
  return new import_ethers6.ethers.providers.StaticJsonRpcProvider(url, chainId);
39592
39790
  };
39593
39791
  var ClaimError = class extends Error {
39594
39792
  };
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
- };
39793
+ var safeMessage = (error, fallback) => error instanceof ClaimError ? error.message : [fallback, safeErrorReason(error) || safeErrorCode(error)].filter(Boolean).join(": ");
39602
39794
  var createSbtClaimer = ({
39603
39795
  config = {},
39604
39796
  wallet,
@@ -39711,7 +39903,7 @@ var createSbtClaimer = ({
39711
39903
  const claimWithGroupPassword = async ({ ctx, provider, chainId, contract, sbtAddress, password }) => {
39712
39904
  const normalized2 = normalizeGroupPasswordInput(password);
39713
39905
  if (!normalized2) throw new ClaimError("This SBT needs a group password.");
39714
- const expectedHash = trim5(await contract.groupPasswordHash()).toLowerCase();
39906
+ const expectedHash = trim6(await contract.groupPasswordHash()).toLowerCase();
39715
39907
  if (!expectedHash || expectedHash === import_ethers6.ethers.constants.HashZero) throw new ClaimError("This SBT has no group password set.");
39716
39908
  const groupWallet = await deriveGroupPasswordWallet({ password: normalized2, sbtAddress });
39717
39909
  if (import_ethers6.ethers.utils.solidityKeccak256(["address"], [groupWallet.address]) !== expectedHash) {
@@ -39745,7 +39937,7 @@ var createSbtClaimer = ({
39745
39937
  signal = callSignal;
39746
39938
  let address;
39747
39939
  try {
39748
- address = import_ethers6.ethers.utils.getAddress(trim5(sbtAddress));
39940
+ address = import_ethers6.ethers.utils.getAddress(trim6(sbtAddress));
39749
39941
  } catch {
39750
39942
  throw new ClaimError("Pass the SBT contract address (0x\u2026).");
39751
39943
  }
@@ -39778,7 +39970,7 @@ var createSbtClaimer = ({
39778
39970
  const args = { ctx, provider, chainId, contract, sbtAddress: address };
39779
39971
  let transactions;
39780
39972
  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) });
39973
+ else if (mode === "password") transactions = await claimWithCommitReveal({ ...args, password: trim6(claimPassword) || trim6(groupPassword) });
39782
39974
  else if (mode === "group_password") transactions = await claimWithGroupPassword({ ...args, password: groupPassword });
39783
39975
  else transactions = await claimWithInviteCode({ ...args, code: inviteCode || groupPassword });
39784
39976
  return { ...base, mode, status: "claimed", transactions };
@@ -39807,7 +39999,7 @@ var MAX_TITLE_LENGTH = 120;
39807
39999
  var MAX_CONTEXT_PARAGRAPHS = 4;
39808
40000
  var MAX_CONTEXT_LENGTH = 2e3;
39809
40001
  var MAX_CONTEXT_LINKS = 4;
39810
- var trim6 = (value) => typeof value === "string" ? value.trim() : typeof value === "number" ? String(value) : "";
40002
+ var trim7 = (value) => typeof value === "string" ? value.trim() : typeof value === "number" ? String(value) : "";
39811
40003
  var isRecord6 = (value) => !!value && typeof value === "object" && !Array.isArray(value);
39812
40004
  var sessionKey = (info) => info.kind === "registry" ? `registry ${info.chainId || 11155420} ${info.slug}` : `${info.workerOrigin} ${info.slug}`;
39813
40005
  var mapLimit = async (items, limit, task) => {
@@ -39868,18 +40060,10 @@ var sanitizeQuestion = (payload, id) => {
39868
40060
  ratingScaleFor(question);
39869
40061
  return question;
39870
40062
  };
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
- };
40063
+ var chooseQuestionVersion = (versions) => versions.reduce(
40064
+ (chosen, candidate) => isPreferredStorageVersion(candidate, chosen) ? candidate : chosen,
40065
+ null
40066
+ );
39883
40067
  var describeQuestion = (question, myAnswer) => {
39884
40068
  const options = stringOptions(question);
39885
40069
  const entry = {
@@ -39941,9 +40125,9 @@ var createParticipant = ({ config, wallet, fetchImpl = globalThis.fetch, now = (
39941
40125
  state = await updateAgentState(config.stateDir, mutate);
39942
40126
  };
39943
40127
  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)
40128
+ displayName: trim7(state?.profile?.displayName ?? config.profile.displayName),
40129
+ modelId: trim7(state?.profile?.modelId ?? config.profile.modelId),
40130
+ platform: trim7(state?.profile?.platform ?? config.profile.platform)
39947
40131
  });
39948
40132
  const trustedOriginsFor = (preferred) => config.trustedAppOrigins.includes(preferred) ? [preferred, ...config.trustedAppOrigins.filter((origin) => origin !== preferred)] : config.trustedAppOrigins;
39949
40133
  const openSession = async (info) => {
@@ -39994,18 +40178,19 @@ var createParticipant = ({ config, wallet, fetchImpl = globalThis.fetch, now = (
39994
40178
  const entry = fresh.sessions[sessionKey(ctx.info)];
39995
40179
  if (!isRecord6(entry) || entry.sessionId !== ctx.client.sessionId) return;
39996
40180
  entry.answers = isRecord6(entry.answers) ? entry.answers : {};
39997
- answered.forEach(({ questionId, record, answeredAt, chainOrder }) => {
40181
+ answered.forEach(({ questionId, record, answeredAt, chainOrder, requestId }) => {
39998
40182
  entry.answers[questionId] = {
39999
40183
  value: record.answer.value,
40000
40184
  comment: record.additional.value,
40001
40185
  answeredAt,
40002
40186
  ...ctx.registry && chainOrder ? chainOrder : {}
40003
40187
  };
40188
+ if (requestId && entry.pendingUploads?.[questionId]?.requestId === requestId) delete entry.pendingUploads[questionId];
40004
40189
  });
40005
40190
  });
40006
40191
  const resolveSession = async (session) => {
40007
40192
  await loadState();
40008
- const ref = trim6(session);
40193
+ const ref = trim7(session);
40009
40194
  if (/^https?:\/\//i.test(ref)) {
40010
40195
  const parsed = parseSessionLink(ref, { allowLocalWorkers: config.allowLocalWorkers });
40011
40196
  const key2 = sessionKey(parsed);
@@ -40116,8 +40301,9 @@ var createParticipant = ({ config, wallet, fetchImpl = globalThis.fetch, now = (
40116
40301
  list.push({
40117
40302
  payload: question,
40118
40303
  createdAt: text(item.metadata?.createdAt),
40119
- uploaderIsAdmin: item.metadata?.uploaderIsAdmin === true,
40120
- contentMatches: question.locked ? null : contentId(question) === id
40304
+ createdAtMs: Date.parse(text(item.metadata?.createdAt)) || 0,
40305
+ storageRefId: text(item.storageRef.id),
40306
+ uploadAuthority: item.metadata?.uploadAuthority
40121
40307
  });
40122
40308
  versions.set(id, list);
40123
40309
  } catch {
@@ -40195,24 +40381,28 @@ var createParticipant = ({ config, wallet, fetchImpl = globalThis.fetch, now = (
40195
40381
  await ensureReady(ctx);
40196
40382
  const scan = await scanMyAnswers(ctx);
40197
40383
  const mine = new Map(scan.answers);
40198
- const complete = scan.complete;
40384
+ let complete = scan.complete;
40385
+ const blocks = /* @__PURE__ */ new Map();
40199
40386
  await loadState();
40200
40387
  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;
40388
+ for (const [questionId, answer] of Object.entries(local)) {
40389
+ if (!isRecord6(answer)) continue;
40390
+ if (ctx.registry && !scan.complete) continue;
40204
40391
  const known = mine.get(questionId);
40205
40392
  const chainOrder = scan.chainOrders?.get(questionId);
40206
- if (ctx.registry && chainOrder && compareChainOrder(answer, chainOrder) < 0) return;
40393
+ if (ctx.registry && chainOrder && compareChainOrder(answer, chainOrder) < 0) continue;
40207
40394
  const newer = ctx.registry ? !known || compareChainOrder(answer, known) > 0 : !known || text(answer.answeredAt) > known.createdAt;
40208
- if (newer) {
40395
+ if (!newer) continue;
40396
+ const replaced = ctx.registry ? await ctx.registry.replacedWrite(answer, blocks) : false;
40397
+ if (replaced === null) complete = false;
40398
+ if (!replaced) {
40209
40399
  mine.set(questionId, {
40210
40400
  createdAt: text(answer.answeredAt),
40211
40401
  ...ctx.registry ? { blockNumber: answer.blockNumber, transactionIndex: answer.transactionIndex, logIndex: answer.logIndex } : {},
40212
40402
  payload: { answer: { value: answer.value }, additional: { value: answer.comment } }
40213
40403
  });
40214
40404
  }
40215
- });
40405
+ }
40216
40406
  [...mine.entries()].forEach(([questionId, entry]) => {
40217
40407
  const value = entry?.payload?.answer?.value;
40218
40408
  if (value === "" || value === null || value === void 0 || Array.isArray(value) && !value.length) mine.delete(questionId);
@@ -40254,9 +40444,9 @@ var createParticipant = ({ config, wallet, fetchImpl = globalThis.fetch, now = (
40254
40444
  },
40255
40445
  async joinSession({ link, worker, session, inviteCode, groupPassword, claimPassword } = {}) {
40256
40446
  let parsed;
40257
- if (trim6(link)) {
40447
+ if (trim7(link)) {
40258
40448
  parsed = parseSessionLink(link, { allowLocalWorkers: config.allowLocalWorkers });
40259
- } else if (trim6(worker) && trim6(session)) {
40449
+ } else if (trim7(worker) && trim7(session)) {
40260
40450
  parsed = {
40261
40451
  appOrigin: config.appOrigin,
40262
40452
  slug: normalizeSessionSlug(session),
@@ -40277,9 +40467,9 @@ var createParticipant = ({ config, wallet, fetchImpl = globalThis.fetch, now = (
40277
40467
  const offered = parsed.sbtClaims.filter((claim) => !claim.auto).map((claim) => claim.sbt);
40278
40468
  if (offered.length) outcome.sbtsToClaimWithClaimSbt = offered;
40279
40469
  const separateCodes = {
40280
- inviteCode: trim6(inviteCode),
40281
- groupPassword: trim6(groupPassword),
40282
- claimPassword: trim6(claimPassword)
40470
+ inviteCode: trim7(inviteCode),
40471
+ groupPassword: trim7(groupPassword),
40472
+ claimPassword: trim7(claimPassword)
40283
40473
  };
40284
40474
  const hasSeparateCodes = Object.values(separateCodes).some(Boolean);
40285
40475
  if (hasSeparateCodes && claims.length !== 1) {
@@ -40309,7 +40499,7 @@ var createParticipant = ({ config, wallet, fetchImpl = globalThis.fetch, now = (
40309
40499
  })
40310
40500
  );
40311
40501
  } catch (error) {
40312
- outcome.sbtClaims.push({ sbt: claim.sbt, status: "failed", error: error.message });
40502
+ outcome.sbtClaims.push({ sbt: claim.sbt, status: "failed", error: describeError(error) });
40313
40503
  }
40314
40504
  }
40315
40505
  tokenClaims = await ctx.client.login().then((claims2) => {
@@ -40320,7 +40510,7 @@ var createParticipant = ({ config, wallet, fetchImpl = globalThis.fetch, now = (
40320
40510
  outcome.testEth = await ctx.client.describeTestEth();
40321
40511
  outcome.signedIn = !loginError;
40322
40512
  if (loginError) {
40323
- outcome.readOnlyReason = loginError.message;
40513
+ outcome.readOnlyReason = describeError(loginError);
40324
40514
  outcome.note = "Joined read-only: public questions and results are available, but uploads, answers, statements, and faucet requests need Worker access.";
40325
40515
  }
40326
40516
  const scopes = isRecord6(tokenClaims?.scopes) ? tokenClaims.scopes : {};
@@ -40332,7 +40522,7 @@ var createParticipant = ({ config, wallet, fetchImpl = globalThis.fetch, now = (
40332
40522
  await ctx.client.joinGroup(parsed.joinGroupId);
40333
40523
  outcome.joinedGroup = parsed.joinGroupId;
40334
40524
  } catch (error) {
40335
- outcome.groupJoinError = error.message;
40525
+ outcome.groupJoinError = describeError(error);
40336
40526
  }
40337
40527
  }
40338
40528
  outcome.description = text(sessionConfig.sessionInfo).slice(0, MAX_CONTEXT_LENGTH);
@@ -40393,10 +40583,12 @@ var createParticipant = ({ config, wallet, fetchImpl = globalThis.fetch, now = (
40393
40583
  const results = [];
40394
40584
  const answered = [];
40395
40585
  const pendingAnswers = [];
40586
+ const operationId = randomUUID2();
40396
40587
  let txHash;
40588
+ let reconciliationRequired = false;
40397
40589
  const seen = /* @__PURE__ */ new Set();
40398
40590
  for (const item of answers) {
40399
- const questionId = trim6(item?.questionId).toLowerCase();
40591
+ const questionId = trim7(item?.questionId).toLowerCase();
40400
40592
  if (seen.has(questionId)) {
40401
40593
  results.push({ questionId, ok: false, error: "This question appears more than once in the call; send one answer per question." });
40402
40594
  continue;
@@ -40436,12 +40628,23 @@ var createParticipant = ({ config, wallet, fetchImpl = globalThis.fetch, now = (
40436
40628
  continue;
40437
40629
  }
40438
40630
  try {
40439
- const storageRef = await ctx.client.upload({ resource: "responses", data: JSON.stringify(built.record) });
40631
+ const data = JSON.stringify(built.record);
40632
+ const digest = createHash("sha256").update(data).digest("hex");
40633
+ let requestId;
40634
+ await saveState((fresh) => {
40635
+ const entry = fresh.sessions[sessionKey(ctx.info)];
40636
+ if (!isRecord6(entry) || entry.sessionId !== ctx.client.sessionId) throw new Error("Session identity changed; join again before answering.");
40637
+ entry.pendingUploads = isRecord6(entry.pendingUploads) ? entry.pendingUploads : {};
40638
+ const previous = entry.pendingUploads[questionId];
40639
+ requestId = previous?.payloadDigest === digest && trim7(previous.requestId) ? previous.requestId : `answer:v1:${operationId}:${questionId}`;
40640
+ entry.pendingUploads[questionId] = { payloadDigest: digest, requestId };
40641
+ });
40642
+ const storageRef = await ctx.client.upload({ resource: "responses", data, requestId });
40440
40643
  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() });
40644
+ answered.push({ questionId, requestId, record: built.record, answeredAt: text(storageRef?.createdAt) || new Date(now()).toISOString() });
40442
40645
  results.push({ questionId, ok: true, answer: built.record.answer.value });
40443
40646
  } catch (error) {
40444
- results.push({ questionId, ok: false, error: error.message });
40647
+ results.push({ questionId, ok: false, error: describeError(error) });
40445
40648
  }
40446
40649
  }
40447
40650
  if (pendingAnswers.length) {
@@ -40456,19 +40659,32 @@ var createParticipant = ({ config, wallet, fetchImpl = globalThis.fetch, now = (
40456
40659
  pendingAnswers.forEach(({ questionId, record }) => {
40457
40660
  answered.push({ questionId, record, answeredAt: new Date(now()).toISOString(), chainOrder: {
40458
40661
  blockNumber: submitted.receipt.blockNumber,
40662
+ blockHash: submitted.receipt.blockHash,
40459
40663
  transactionIndex: submitted.receipt.transactionIndex,
40460
40664
  logIndex: submitted.receipt.logs.at(-1)?.logIndex || 0
40461
40665
  } });
40462
40666
  results.push({ questionId, ok: true, answer: record.answer.value });
40463
40667
  });
40464
40668
  } catch (error) {
40465
- pendingAnswers.forEach(({ questionId }) => results.push({ questionId, ok: false, error: error.message }));
40669
+ if (typeof error.txHash === "string" && /^0x[0-9a-f]{64}$/i.test(error.txHash)) txHash = error.txHash;
40670
+ reconciliationRequired = error.reconciliationRequired === true;
40671
+ pendingAnswers.forEach(({ questionId }) => results.push({ questionId, ok: false, error: describeError(error) }));
40672
+ }
40673
+ }
40674
+ let warning;
40675
+ if (answered.length) {
40676
+ try {
40677
+ await rememberAnswers(ctx, answered);
40678
+ } catch {
40679
+ ctx.ownScan = null;
40680
+ warning = "Answers were submitted, but the local answer history could not be saved. Do not resubmit them to fix this warning.";
40466
40681
  }
40467
40682
  }
40468
- if (answered.length) await rememberAnswers(ctx, answered);
40469
40683
  return {
40470
40684
  session: ctx.info.slug,
40471
40685
  ...txHash ? { txHash } : {},
40686
+ ...reconciliationRequired ? { reconciliationRequired: true } : {},
40687
+ ...warning ? { warning } : {},
40472
40688
  submitted: results.filter((result) => result.ok).length,
40473
40689
  failed: results.filter((result) => !result.ok).length,
40474
40690
  results
@@ -40586,7 +40802,7 @@ var createParticipant = ({ config, wallet, fetchImpl = globalThis.fetch, now = (
40586
40802
  const ctx = await resolveSession(session);
40587
40803
  if (ctx.registry) throw new Error("Worker groups are not available in on-chain sessions.");
40588
40804
  await ensureReady(ctx);
40589
- const id = trim6(groupId);
40805
+ const id = trim7(groupId);
40590
40806
  if (!/^[A-Za-z0-9._:-]{1,128}$/.test(id)) throw new Error("Pass a valid group id.");
40591
40807
  checkpoint();
40592
40808
  await ctx.client.joinGroup(id);
@@ -40601,10 +40817,10 @@ var createParticipant = ({ config, wallet, fetchImpl = globalThis.fetch, now = (
40601
40817
  });
40602
40818
  const result = await sbt.claim({
40603
40819
  ctx,
40604
- sbtAddress: trim6(sbtAddress),
40605
- inviteCode: trim6(inviteCode),
40606
- groupPassword: trim6(groupPassword),
40607
- claimPassword: trim6(claimPassword),
40820
+ sbtAddress: trim7(sbtAddress),
40821
+ inviteCode: trim7(inviteCode),
40822
+ groupPassword: trim7(groupPassword),
40823
+ claimPassword: trim7(claimPassword),
40608
40824
  signal
40609
40825
  });
40610
40826
  await ctx.client.login().catch(() => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agalmic/contextengine",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "MCP server that lets an AI agent take part in Context Engine sessions as its own participant",
5
5
  "license": "MPL-2.0",
6
6
  "type": "module",