@mida-context/sdk 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -230,6 +230,11 @@ var MidaError = class extends Error {
230
230
  function isMidaError(value, code) {
231
231
  return value instanceof MidaError && (code === void 0 || value.code === code);
232
232
  }
233
+ var ACCEPTABLE_AGENT_NAME_CHARS = new RegExp(
234
+ "^[\\p{L}\\p{N}][\\p{L}\\p{M}\\p{N} ._\\-\\u200c\\u200d]*$",
235
+ "u"
236
+ );
237
+ var STACKED_MARK_RUN = new RegExp("\\p{M}{5}", "u");
233
238
 
234
239
  // packages/protocol/src/constants.ts
235
240
  var PERMISSION = { READ: 1, CREATE: 2, SUPERSEDE_OWN: 4, SUPERSEDE_ANY: 8 };
@@ -4987,6 +4992,38 @@ var REJECT_NAMES = new Map(Object.entries(BATCH_REJECT).map(([name, code]) => [c
4987
4992
  import { zeroHash as zeroHash6 } from "viem";
4988
4993
  var lower = (value) => value.toLowerCase();
4989
4994
  var RECORDS_PER_MULTICALL = 200;
4995
+ var multicall3Probes = /* @__PURE__ */ new Map();
4996
+ var multicall3AbsentSink;
4997
+ var multicall3AbsentSent = /* @__PURE__ */ new Set();
4998
+ function multicall3ProbeEntry(context) {
4999
+ const key = `${context.deployment.chainId}:${MULTICALL3_ADDRESS}`;
5000
+ const held = multicall3Probes.get(key);
5001
+ if (held !== void 0) return held;
5002
+ const entry = {
5003
+ size: void 0,
5004
+ // Promise.resolve().then(...) so a client without getCode — a custom transport, a test rig —
5005
+ // throws INSIDE the chain and lands on the same eviction as an RPC that refuses the call.
5006
+ probe: Promise.resolve().then(() => context.publicClient.getCode({ address: MULTICALL3_ADDRESS })).then((code) => {
5007
+ const absent = code === void 0 || code === "0x";
5008
+ if (absent && multicall3AbsentSink !== void 0 && !multicall3AbsentSent.has(context.deployment.chainId)) {
5009
+ multicall3AbsentSent.add(context.deployment.chainId);
5010
+ try {
5011
+ multicall3AbsentSink(context.deployment.chainId);
5012
+ } catch {
5013
+ }
5014
+ }
5015
+ return entry.size = absent ? 1 : RECORDS_PER_MULTICALL;
5016
+ })
5017
+ };
5018
+ entry.probe.catch(() => {
5019
+ if (multicall3Probes.get(key) === entry) multicall3Probes.delete(key);
5020
+ });
5021
+ multicall3Probes.set(key, entry);
5022
+ return entry;
5023
+ }
5024
+ function sharedMulticall3Probe(context) {
5025
+ return multicall3ProbeEntry(context).probe;
5026
+ }
4990
5027
  var RegistryReader = class {
4991
5028
  constructor(context) {
4992
5029
  this.context = context;
@@ -4995,22 +5032,24 @@ var RegistryReader = class {
4995
5032
  #recordBatchSize;
4996
5033
  #recordBatchSizeProbe;
4997
5034
  /**
4998
- * The probed batch size once known, undefined while unprobed. Lets a BudgetedReader return a
4999
- * cached answer without charging the request for a chain read that does not happen.
5035
+ * The probed batch size once known, undefined while unprobed. Lets a BudgetedReader answer
5036
+ * without charging the request — but "unprobed" does not mean "no read": on a cold cache
5037
+ * `multicall3ProbeEntry` starts the `getCode` probe right here, the chain answers it in the
5038
+ * background, and the getter reports undefined until that answer lands (in-38 V-3).
5000
5039
  */
5001
5040
  get knownRecordBatchSize() {
5002
- return this.#recordBatchSize;
5041
+ return this.#recordBatchSize ?? multicall3ProbeEntry(this.context).size;
5003
5042
  }
5004
5043
  /**
5005
5044
  * The most contextIds one `getRecords` call answers in a single chain read: RECORDS_PER_MULTICALL
5006
- * when the chain carries Multicall3, 1 where it does not. Probed once per reader with `getCode`
5007
- * and cached; a chain that cannot answer the probe cannot run a multicall either, so a failed
5008
- * probe also resolves to 1 — the per-row path — rather than breaking reads on an RPC that does
5009
- * not serve `eth_getCode`.
5045
+ * when the chain carries Multicall3, 1 where it does not. Probed once per PROCESS with `getCode`
5046
+ * — see `multicall3Probes` — and cached per reader once answered; a chain that cannot answer the
5047
+ * probe cannot run a multicall either, so a failed probe resolves to 1 — the per-row path —
5048
+ * rather than breaking reads on an RPC that does not serve `eth_getCode`.
5010
5049
  */
5011
5050
  recordBatchSize() {
5012
5051
  if (this.#recordBatchSize !== void 0) return Promise.resolve(this.#recordBatchSize);
5013
- this.#recordBatchSizeProbe ??= Promise.resolve().then(() => this.context.publicClient.getCode({ address: MULTICALL3_ADDRESS })).then((code) => this.#recordBatchSize = code === void 0 || code === "0x" ? 1 : RECORDS_PER_MULTICALL).catch(() => this.#recordBatchSize = 1);
5052
+ this.#recordBatchSizeProbe ??= multicall3ProbeEntry(this.context).probe.then((size) => this.#recordBatchSize = size).catch(() => this.#recordBatchSize = 1);
5014
5053
  return this.#recordBatchSizeProbe;
5015
5054
  }
5016
5055
  now() {
@@ -5077,43 +5116,54 @@ var RegistryReader = class {
5077
5116
  }
5078
5117
  }
5079
5118
  /**
5080
- * `getRecord` for a batch of contextIds. With Multicall3 the batch is ONE `eth_call` — hundreds of
5081
- * never-anchored uploads can no longer spend a request's read budget a row at a time. The
5082
- * `ContextNotFound` revert of an orphaned upload comes back as a null entry — "not anchored",
5083
- * never an error — and it is the ONLY failure that does: an out-of-gas, an undecodable result or
5084
- * any other revert throws exactly as `getRecord` throws, so a caller can never read a list that
5085
- * silently dropped a real record. Chains without Multicall3 fall back to one `getRecord` per id,
5086
- * unchanged.
5087
- * Callers pass at most `recordBatchSize()` ids per call, so each call costs exactly one unit of
5088
- * read budget — or `contextIds.length` units on the per-row path, identical to `getRecord`.
5119
+ * `getRecord` for a batch of contextIds. With Multicall3 each `recordBatchSize()`-sized chunk of
5120
+ * the ids is ONE `eth_call` — hundreds of never-anchored uploads can no longer spend a request's
5121
+ * read budget a row at a time. The `ContextNotFound` revert of an orphaned upload comes back as
5122
+ * a null entry — "not anchored", never an error — and it is the ONLY failure that does: an
5123
+ * out-of-gas, an undecodable result or any other revert throws exactly as `getRecord` throws, so
5124
+ * a caller can never read a list that silently dropped a real record. Chains without Multicall3
5125
+ * fall back to one `getRecord` per id, unchanged.
5126
+ * Chunks run in order and a failing chunk fails the whole call. Callers that pass more than
5127
+ * `recordBatchSize()` ids pay one aggregate call per chunk — the budgeted wrapper slices to the
5128
+ * batch size itself, so inside a request each call still costs exactly one unit of read budget
5129
+ * (or `contextIds.length` units on the per-row path, identical to `getRecord`).
5089
5130
  */
5090
5131
  async getRecords(contextIds) {
5091
5132
  if (contextIds.length === 0) return [];
5092
- if (await this.recordBatchSize() === 1) {
5133
+ const batchSize = await this.recordBatchSize();
5134
+ if (batchSize === 1) {
5093
5135
  return Promise.all(contextIds.map((contextId2) => this.getRecord(contextId2)));
5094
5136
  }
5095
- const results = await this.context.publicClient.multicall({
5096
- // batchSize 0 disables viem's calldata chunking (default 1024 bytes): the whole batch is a
5097
- // single aggregate3 eth_call, which is what one unit of read budget pays for.
5098
- batchSize: 0,
5099
- multicallAddress: MULTICALL3_ADDRESS,
5100
- allowFailure: true,
5101
- contracts: contextIds.map((contextId2) => ({
5102
- address: this.context.deployment.contextRegistry,
5103
- abi: contextRegistryAbi,
5104
- functionName: "getRecord",
5105
- args: [contextId2]
5106
- }))
5107
- });
5108
- return results.map((entry) => {
5109
- if (entry.status === "success") {
5110
- const record = entry.result;
5111
- return { ...record, owner: lower(record.owner) };
5137
+ const records = [];
5138
+ for (let offset = 0; offset < contextIds.length; offset += batchSize) {
5139
+ const results = await this.context.publicClient.multicall({
5140
+ // batchSize 0 disables viem's calldata chunking (default 1024 bytes): the whole batch is a
5141
+ // single aggregate3 eth_call, which is what one unit of read budget pays for.
5142
+ batchSize: 0,
5143
+ multicallAddress: MULTICALL3_ADDRESS,
5144
+ allowFailure: true,
5145
+ contracts: contextIds.slice(offset, offset + batchSize).map((contextId2) => ({
5146
+ address: this.context.deployment.contextRegistry,
5147
+ abi: contextRegistryAbi,
5148
+ functionName: "getRecord",
5149
+ args: [contextId2]
5150
+ }))
5151
+ });
5152
+ for (const entry of results) {
5153
+ if (entry.status === "success") {
5154
+ const record = entry.result;
5155
+ records.push({ ...record, owner: lower(record.owner) });
5156
+ continue;
5157
+ }
5158
+ const mapped = toMidaError(entry.error);
5159
+ if (isMidaError(mapped, "NOT_FOUND")) {
5160
+ records.push(null);
5161
+ continue;
5162
+ }
5163
+ throw mapped;
5112
5164
  }
5113
- const mapped = toMidaError(entry.error);
5114
- if (isMidaError(mapped, "NOT_FOUND")) return null;
5115
- throw mapped;
5116
- });
5165
+ }
5166
+ return records;
5117
5167
  }
5118
5168
  async #capability(functionName, args) {
5119
5169
  try {
@@ -5387,6 +5437,79 @@ function requireAnchor(deployment) {
5387
5437
  }
5388
5438
  return batchAnchor;
5389
5439
  }
5440
+ var lowerAddress = (value) => value.toLowerCase();
5441
+ async function prefetchBatchedLookups(input) {
5442
+ const batchAnchor = input.deployment.batchAnchor;
5443
+ if (batchAnchor === void 0) return void 0;
5444
+ const batchSize = await sharedMulticall3Probe({ publicClient: input.client, deployment: input.deployment }).catch(() => 1);
5445
+ if (batchSize === 1) return void 0;
5446
+ const owner = input.owner.toLowerCase();
5447
+ const namespaceId2 = input.namespaceId.toLowerCase();
5448
+ const batchIds = /* @__PURE__ */ new Set();
5449
+ const lineageIds = /* @__PURE__ */ new Set();
5450
+ const signers = /* @__PURE__ */ new Set();
5451
+ for (const item of input.items) {
5452
+ const wire2 = item.save.message;
5453
+ if (wire2.owner.toLowerCase() !== owner || wire2.namespaceId.toLowerCase() !== namespaceId2) continue;
5454
+ if (item.state === "ANCHORED") {
5455
+ if (item.batchId !== void 0) batchIds.add(item.batchId.toLowerCase());
5456
+ if (item.lineageId !== void 0) lineageIds.add(item.lineageId.toLowerCase());
5457
+ } else if (item.state !== "QUEUED" && item.state !== "SUBMITTED") {
5458
+ continue;
5459
+ }
5460
+ try {
5461
+ signers.add(
5462
+ lowerAddress(
5463
+ await recoverTypedDataAddress2({
5464
+ ...batchSaveTypedData({ chainId: input.chainId, batchAnchor, message: wireMessage(wire2) }),
5465
+ signature: item.save.signature
5466
+ })
5467
+ )
5468
+ );
5469
+ } catch {
5470
+ }
5471
+ }
5472
+ const batchKeys = [...batchIds];
5473
+ const headKeys = [...lineageIds];
5474
+ const signerKeys = [...signers];
5475
+ const contracts = [
5476
+ ...batchKeys.map((batchId) => ({ address: batchAnchor, abi: batchAnchorAbi, functionName: "batchOf", args: [batchId] })),
5477
+ ...headKeys.map((lineageId) => ({ address: batchAnchor, abi: batchAnchorAbi, functionName: "headCommitOf", args: [lineageId] })),
5478
+ ...signerKeys.map(
5479
+ (signer) => ({
5480
+ address: input.deployment.capabilityRegistry,
5481
+ abi: capabilityRegistryAbi,
5482
+ functionName: "agentIdOfSigner",
5483
+ args: [signer]
5484
+ })
5485
+ )
5486
+ ];
5487
+ const results = [];
5488
+ for (let offset = 0; offset < contracts.length; offset += RECORDS_PER_MULTICALL) {
5489
+ results.push(
5490
+ ...await input.client.multicall({
5491
+ // batchSize 0 disables viem's calldata chunking: the chunk below is one aggregate3 call.
5492
+ batchSize: 0,
5493
+ multicallAddress: MULTICALL3_ADDRESS,
5494
+ allowFailure: false,
5495
+ contracts: contracts.slice(offset, offset + RECORDS_PER_MULTICALL)
5496
+ })
5497
+ );
5498
+ }
5499
+ const batches = /* @__PURE__ */ new Map();
5500
+ const heads = /* @__PURE__ */ new Map();
5501
+ const agents = /* @__PURE__ */ new Map();
5502
+ results.forEach((result, index) => {
5503
+ if (index < batchKeys.length) {
5504
+ batches.set(batchKeys[index], result);
5505
+ } else if (index < batchKeys.length + headKeys.length) {
5506
+ heads.set(headKeys[index - batchKeys.length], result);
5507
+ } else {
5508
+ agents.set(signerKeys[index - batchKeys.length - headKeys.length], result);
5509
+ }
5510
+ });
5511
+ return { batches, heads, signers: agents };
5512
+ }
5390
5513
  async function checkSignedSave(input) {
5391
5514
  const { item, chainId, batchAnchor, client } = input;
5392
5515
  const message = wireMessage(item.save.message);
@@ -5413,7 +5536,7 @@ async function checkSignedSave(input) {
5413
5536
  } catch {
5414
5537
  return { ok: false, reason: "signature" };
5415
5538
  }
5416
- const agentId2 = await client.readContract({
5539
+ const agentId2 = input.lookups?.signers?.get(lowerAddress(signer)) ?? await client.readContract({
5417
5540
  address: input.capabilityRegistry,
5418
5541
  abi: capabilityRegistryAbi,
5419
5542
  functionName: "agentIdOfSigner",
@@ -5435,12 +5558,12 @@ async function checkSignedSave(input) {
5435
5558
  async function verifyBatchedItem(input) {
5436
5559
  const { item, chainId, deployment, client } = input;
5437
5560
  const batchAnchor = requireAnchor(deployment);
5438
- const checked2 = await checkSignedSave({ item, chainId, batchAnchor, capabilityRegistry: deployment.capabilityRegistry, client });
5561
+ const checked2 = await checkSignedSave({ item, chainId, batchAnchor, capabilityRegistry: deployment.capabilityRegistry, client, lookups: input.lookups });
5439
5562
  if (!checked2.ok) return checked2;
5440
5563
  const { message, agentId: agentId2 } = checked2;
5441
5564
  if (item.state !== "ANCHORED") return { ok: false, reason: "not-anchored" };
5442
5565
  if (item.batchId === void 0) return { ok: false, reason: "unknown-batch" };
5443
- const [root, blockNumber] = await client.readContract({
5566
+ const [root, blockNumber] = input.lookups?.batches?.get(item.batchId.toLowerCase()) ?? await client.readContract({
5444
5567
  address: batchAnchor,
5445
5568
  abi: batchAnchorAbi,
5446
5569
  functionName: "batchOf",
@@ -5459,7 +5582,7 @@ async function verifyBatchedItem(input) {
5459
5582
  });
5460
5583
  if (!verifyMerkleProof(leaf, item.proof, root)) return { ok: false, reason: "proof" };
5461
5584
  if (input.requireLatest) {
5462
- const head = await client.readContract({
5585
+ const head = input.lookups?.heads?.get(item.lineageId.toLowerCase()) ?? await client.readContract({
5463
5586
  address: batchAnchor,
5464
5587
  abi: batchAnchorAbi,
5465
5588
  functionName: "headCommitOf",
@@ -5480,7 +5603,7 @@ async function verifyPendingItem(input) {
5480
5603
  const { item, chainId, deployment, client } = input;
5481
5604
  const batchAnchor = requireAnchor(deployment);
5482
5605
  if (item.state !== "QUEUED" && item.state !== "SUBMITTED") return { ok: false, reason: "not-pending" };
5483
- const checked2 = await checkSignedSave({ item, chainId, batchAnchor, capabilityRegistry: deployment.capabilityRegistry, client });
5606
+ const checked2 = await checkSignedSave({ item, chainId, batchAnchor, capabilityRegistry: deployment.capabilityRegistry, client, lookups: input.lookups });
5484
5607
  if (!checked2.ok) return checked2;
5485
5608
  const { message, agentId: agentId2 } = checked2;
5486
5609
  const hasAuthority = (permission) => client.readContract({
@@ -5662,8 +5785,13 @@ var MidaAgent = class {
5662
5785
  const { deployment } = this.#chain;
5663
5786
  const { objects, partial } = await this.#api.listObjects({ owner: ownerAddress, namespaceId: namespaceId2, capabilityId: capability.capabilityId });
5664
5787
  const epochKeyFor = this.#epochKeyResolver(ownerAddress, namespaceId2, capability.capabilityId);
5788
+ const listed = /* @__PURE__ */ new Map();
5789
+ const batched = await this.#reader.getRecords(objects.map((object) => object.contextId));
5790
+ for (const [index, record] of batched.entries()) {
5791
+ listed.set(objects[index].contextId.toLowerCase(), record);
5792
+ }
5665
5793
  const readObject = async (object) => {
5666
- const record = await this.#verifiedRecord(ownerAddress, namespaceId2, object);
5794
+ const record = await this.#verifiedRecord(ownerAddress, namespaceId2, object, listed);
5667
5795
  const epochPrivateKey = await epochKeyFor(record.readEpoch);
5668
5796
  const payload = openContextObject({
5669
5797
  manifest: object.manifest,
@@ -5672,7 +5800,7 @@ var MidaAgent = class {
5672
5800
  epochPrivateKey,
5673
5801
  binding: { chainId: deployment.chainId, contextRegistry: deployment.contextRegistry, contextId: record.contextId, namespaceId: namespaceId2, readEpoch: record.readEpoch }
5674
5802
  });
5675
- await this.#verifyReferences(ownerAddress, record, payload);
5803
+ await this.#verifyReferences(ownerAddress, record, payload, listed);
5676
5804
  return this.#toObject(record, name, payload, { at: record.createdAt });
5677
5805
  };
5678
5806
  const results = new Array(objects.length);
@@ -5735,13 +5863,21 @@ var MidaAgent = class {
5735
5863
  capabilityId: capability.capabilityId
5736
5864
  });
5737
5865
  const epochKeyFor = this.#epochKeyResolver(ownerAddress, namespaceId2, capability.capabilityId);
5866
+ const lookups = await prefetchBatchedLookups({
5867
+ items,
5868
+ owner: ownerAddress,
5869
+ namespaceId: namespaceId2,
5870
+ chainId: deployment.chainId,
5871
+ deployment,
5872
+ client: this.#chain.publicClient
5873
+ });
5738
5874
  const blockTime = blockTimeCache(this.#chain.publicClient);
5739
5875
  const processItem = async (item) => {
5740
5876
  const message = item.save.message;
5741
5877
  if (message.owner.toLowerCase() !== ownerAddress || message.namespaceId.toLowerCase() !== namespaceId2) {
5742
5878
  return { kind: "skipped", skipped: { contextId: item.contextId, reason: "wrong-scope" } };
5743
5879
  }
5744
- const verdict = item.state === "ANCHORED" ? await verifyBatchedItem({ item, chainId: deployment.chainId, deployment, client: this.#chain.publicClient, requireLatest: true }) : await verifyPendingItem({ item, chainId: deployment.chainId, deployment, client: this.#chain.publicClient });
5880
+ const verdict = item.state === "ANCHORED" ? await verifyBatchedItem({ item, chainId: deployment.chainId, deployment, client: this.#chain.publicClient, requireLatest: true, lookups }) : await verifyPendingItem({ item, chainId: deployment.chainId, deployment, client: this.#chain.publicClient, lookups });
5745
5881
  if (!verdict.ok) {
5746
5882
  return { kind: "skipped", skipped: { contextId: item.contextId, reason: verdict.reason } };
5747
5883
  }
@@ -6269,8 +6405,15 @@ var MidaAgent = class {
6269
6405
  return pending;
6270
6406
  };
6271
6407
  }
6272
- async #verifiedRecord(owner, namespaceId2, object) {
6273
- const record = await this.#reader.getRecord(object.contextId);
6408
+ /**
6409
+ * The record the object claims plus every check that claim implies — the same set either way.
6410
+ * `listed` is the one-batch answer readWithStatus fetched: a key present in it (even a null,
6411
+ * the registry's "no such record") is used as-is; an id the batch somehow did not cover falls
6412
+ * back to a lone getRecord rather than skipping verification.
6413
+ */
6414
+ async #verifiedRecord(owner, namespaceId2, object, listed) {
6415
+ const id = object.contextId.toLowerCase();
6416
+ const record = listed.has(id) ? listed.get(id) : await this.#reader.getRecord(object.contextId);
6274
6417
  if (record === null || object.manifest.contextId !== object.contextId || record.owner !== owner || record.namespaceId !== namespaceId2 || record.manifestHash !== manifestHash(object.manifest) || record.ciphertextCommitment !== object.manifest.ciphertextHash) {
6275
6418
  throw new MidaError("COMMITMENT_MISMATCH", `object ${object.contextId} does not match its Monad commitments`);
6276
6419
  }
@@ -6282,7 +6425,7 @@ var MidaAgent = class {
6282
6425
  * acknowledges an agent proposal, which is itself a CONTEXT record. USER_CONFIRMED then needs at least one
6283
6426
  * `confirmed_from` reference, and IMPORTED/EXTERNAL_ATTESTATION at least one evidence-record target.
6284
6427
  */
6285
- async #verifyReferences(owner, record, payload) {
6428
+ async #verifyReferences(owner, record, payload, listed) {
6286
6429
  const references = payload.provenance.references ?? [];
6287
6430
  const commitment = references.length === 0 ? zeroHash9 : evidenceCommitment(references);
6288
6431
  if (commitment !== record.evidenceCommitment) {
@@ -6290,7 +6433,12 @@ var MidaAgent = class {
6290
6433
  }
6291
6434
  let evidenceTargets = 0;
6292
6435
  let confirmedFrom = 0;
6293
- const targets = await Promise.all(references.map((reference) => this.#reader.getRecord(reference.recordId)));
6436
+ const targets = await Promise.all(
6437
+ references.map((reference) => {
6438
+ const id = reference.recordId.toLowerCase();
6439
+ return listed.has(id) ? listed.get(id) : this.#reader.getRecord(reference.recordId);
6440
+ })
6441
+ );
6294
6442
  for (const [index, reference] of references.entries()) {
6295
6443
  const target = targets[index];
6296
6444
  if (target === null || target.owner !== owner) {
@@ -35,6 +35,22 @@ export interface ContextRecordView {
35
35
  }
36
36
  /** The most contextIds one batched `getRecords` asks about — one Multicall3 `eth_call` per batch. */
37
37
  export declare const RECORDS_PER_MULTICALL = 200;
38
+ /** Registers the process-wide absent-Multicall3 reporter; `undefined` removes it. */
39
+ export declare function onMulticall3Absent(sink: ((chainId: bigint) => void) | undefined): void;
40
+ /**
41
+ * The process-wide Multicall3 probe for readers that are not a RegistryReader — the SDK's
42
+ * batched-lookup prefetch (in-39 nit 3). Resolves to this chain's record batch size
43
+ * (RECORDS_PER_MULTICALL where Multicall3 answers, 1 where it does not), asking the chain's
44
+ * `getCode` at most once per process and chain; a rejected probe is evicted and rejects here
45
+ * too, so the caller falls back to per-row reads and the next call asks the chain again.
46
+ */
47
+ export declare function sharedMulticall3Probe(context: ChainContext): Promise<number>;
48
+ /**
49
+ * Forget one cached probe answer. Production chains never change whether they carry Multicall3;
50
+ * a test rig or a rebuilt dev chain can install the runtime mid-process (anvil_setCode), and
51
+ * then the next reader must ask the chain again rather than trust a stale "not here".
52
+ */
53
+ export declare function evictMulticall3Probe(chainId: bigint): void;
38
54
  /**
39
55
  * Every Monad read the Context API and SDK make. Nothing here is cached: each call reads current chain state, so no
40
56
  * local value can make Monad authorization true (§12, `currentlyAllowedByMonad`).
@@ -44,16 +60,18 @@ export declare class RegistryReader {
44
60
  readonly context: ChainContext;
45
61
  constructor(context: ChainContext);
46
62
  /**
47
- * The probed batch size once known, undefined while unprobed. Lets a BudgetedReader return a
48
- * cached answer without charging the request for a chain read that does not happen.
63
+ * The probed batch size once known, undefined while unprobed. Lets a BudgetedReader answer
64
+ * without charging the request — but "unprobed" does not mean "no read": on a cold cache
65
+ * `multicall3ProbeEntry` starts the `getCode` probe right here, the chain answers it in the
66
+ * background, and the getter reports undefined until that answer lands (in-38 V-3).
49
67
  */
50
68
  get knownRecordBatchSize(): number | undefined;
51
69
  /**
52
70
  * The most contextIds one `getRecords` call answers in a single chain read: RECORDS_PER_MULTICALL
53
- * when the chain carries Multicall3, 1 where it does not. Probed once per reader with `getCode`
54
- * and cached; a chain that cannot answer the probe cannot run a multicall either, so a failed
55
- * probe also resolves to 1 — the per-row path — rather than breaking reads on an RPC that does
56
- * not serve `eth_getCode`.
71
+ * when the chain carries Multicall3, 1 where it does not. Probed once per PROCESS with `getCode`
72
+ * — see `multicall3Probes` — and cached per reader once answered; a chain that cannot answer the
73
+ * probe cannot run a multicall either, so a failed probe resolves to 1 — the per-row path —
74
+ * rather than breaking reads on an RPC that does not serve `eth_getCode`.
57
75
  */
58
76
  recordBatchSize(): Promise<number>;
59
77
  now(): Promise<bigint>;
@@ -73,15 +91,17 @@ export declare class RegistryReader {
73
91
  } | null>;
74
92
  getRecord(contextId: Hex): Promise<ContextRecordView | null>;
75
93
  /**
76
- * `getRecord` for a batch of contextIds. With Multicall3 the batch is ONE `eth_call` — hundreds of
77
- * never-anchored uploads can no longer spend a request's read budget a row at a time. The
78
- * `ContextNotFound` revert of an orphaned upload comes back as a null entry — "not anchored",
79
- * never an error — and it is the ONLY failure that does: an out-of-gas, an undecodable result or
80
- * any other revert throws exactly as `getRecord` throws, so a caller can never read a list that
81
- * silently dropped a real record. Chains without Multicall3 fall back to one `getRecord` per id,
82
- * unchanged.
83
- * Callers pass at most `recordBatchSize()` ids per call, so each call costs exactly one unit of
84
- * read budget — or `contextIds.length` units on the per-row path, identical to `getRecord`.
94
+ * `getRecord` for a batch of contextIds. With Multicall3 each `recordBatchSize()`-sized chunk of
95
+ * the ids is ONE `eth_call` — hundreds of never-anchored uploads can no longer spend a request's
96
+ * read budget a row at a time. The `ContextNotFound` revert of an orphaned upload comes back as
97
+ * a null entry — "not anchored", never an error — and it is the ONLY failure that does: an
98
+ * out-of-gas, an undecodable result or any other revert throws exactly as `getRecord` throws, so
99
+ * a caller can never read a list that silently dropped a real record. Chains without Multicall3
100
+ * fall back to one `getRecord` per id, unchanged.
101
+ * Chunks run in order and a failing chunk fails the whole call. Callers that pass more than
102
+ * `recordBatchSize()` ids pay one aggregate call per chunk — the budgeted wrapper slices to the
103
+ * batch size itself, so inside a request each call still costs exactly one unit of read budget
104
+ * (or `contextIds.length` units on the per-row path, identical to `getRecord`).
85
105
  */
86
106
  getRecords(contextIds: readonly Hex[]): Promise<(ContextRecordView | null)[]>;
87
107
  }
@@ -13,3 +13,74 @@ export declare class MidaError extends Error {
13
13
  constructor(code: MidaErrorCode, detail?: string);
14
14
  }
15
15
  export declare function isMidaError(value: unknown, code?: MidaErrorCode): value is MidaError;
16
+ /**
17
+ * The one sentence the owner page shows when a request carries a refused character in a field it renders — a manifest name, a folder root, a project label.
18
+ * One fixed sentence, with no field name or code, so the refusal itself can never smuggle part
19
+ * of the request onto the page (in-27 R-1). This string is final copy.
20
+ */
21
+ export declare const UNACCEPTABLE_REQUEST_TEXT = "This request contains characters Mida does not accept, so this page will not show or sign it.";
22
+ /**
23
+ * The characters that must never reach a field a page or a terminal renders. The set is wider
24
+ * than C0+DEL (in-30 T-3): every control character, the Unicode line and paragraph separators,
25
+ * the zero-width space (200B), the directional marks (200E-200F), the bidirectional controls
26
+ * (202A-202E, 2066-2069) and the BOM. Each can forge a rendered line or hide inside one.
27
+ * The joiners are NOT refused (in-31 V-3): the zero-width non-joiner (200C) is part of Persian
28
+ * and Indic spelling and the zero-width joiner (200D) holds emoji sequences together — neither
29
+ * can mint or hide a line.
30
+ */
31
+ export declare const UNACCEPTABLE_REQUEST_CHARS: RegExp;
32
+ /**
33
+ * A refused character folded to a single space — for a display-only field that is size-checked
34
+ * but not refused (a manifest's purpose description or scope reason, in-30 T-3). Validation keeps
35
+ * the bytes untouched so the manifest's hash still matches what was registered; whatever later
36
+ * renders the field passes it through here first.
37
+ */
38
+ export declare function displaySafeText(value: string): string;
39
+ /**
40
+ * The multi-line form of displaySafeText (in-40 L-4) — for text whose own line breaks must
41
+ * survive, like a handoff shown by `mida task show`. The Unicode line and paragraph separators
42
+ * become `\n` rather than spaces so a line break can never hide, and `\n` and `\t` stay; every
43
+ * other refused character still folds to one space. No collapsing, no trimming: the writer's
44
+ * line structure is the contract.
45
+ */
46
+ export declare function displaySafeBlock(value: string): string;
47
+ /**
48
+ * The single-line form of displaySafeText (in-41 U-2) — for text Mida did not write that must
49
+ * print as ONE line: a checkpoint's stored task name, a reason the store returned, a code an
50
+ * RPC error carried. Every refused character folds to a space, whitespace runs collapse to one
51
+ * space, the ends are trimmed, and a line still longer than `maxChars` ends in `…` inside the
52
+ * cap — no value from a store or a chain can open a control sequence or forge a second line.
53
+ */
54
+ export declare function displaySafeLine(value: string, maxChars: number): string;
55
+ /**
56
+ * Five or more combining marks in a row is a Zalgo stack, not a name: a browser piles them
57
+ * vertically without a limit and lets the ink paint over the lines above — the advisor
58
+ * verdict and warnings on the same page (in-37, review 3.1). Four stays legal — Tibetan
59
+ * stacks three, and Devanagari, Hebrew, Arabic and Thai all stay under. The owner-link
60
+ * parser applies the same run limit to every rendered field, so a stack cannot be carried
61
+ * in through a label or a root either.
62
+ */
63
+ export declare const STACKED_MARK_RUN: RegExp;
64
+ /**
65
+ * The one agent-name rule, shared by the manifest validator, the owner-link parser and /me
66
+ * (in-32 X-2, in-34; tightened in-37) — an allow-list, not a deny-list. A name renders
67
+ * inside the summary's `Agent "…"` quotes, so it admits only the characters a name needs
68
+ * and refuses everything else: a quote shape could close or imitate the quotes, a colon or
69
+ * bracket could forge a rendered line, a symbol or emoji needs no reason to be in a name at
70
+ * all. in-37 adds the three refusals a class allow-list cannot express: the first character
71
+ * is a letter or number — never a combining mark, which would strike the quote itself —
72
+ * the eight letter-class look-alikes refuse by name, and five or more combining marks in a
73
+ * row refuse as a Zalgo stack. A name that is empty after trimming, or one that opens with
74
+ * a space or a joiner, is refused too — it would render as `Agent ""` or smuggle a space
75
+ * into the first character. The byte-length limit stays where it always lived — in the
76
+ * manifest validator — not here.
77
+ */
78
+ export declare function isAcceptableAgentName(name: string): boolean;
79
+ /**
80
+ * INVALID_WIRE whose message is already the owner-facing sentence. MidaError prefixes its detail
81
+ * with the code — "INVALID_WIRE: This request…" is not the page's text — so this subclass keeps
82
+ * the code for API/log handling and the bare sentence for describeError.
83
+ */
84
+ export declare class UnacceptableCharactersError extends MidaError {
85
+ constructor();
86
+ }
@@ -38,6 +38,38 @@ export declare function signBatchSave(input: {
38
38
  batchAnchor: Address;
39
39
  message: BatchSaveMessage;
40
40
  }): Promise<Hex>;
41
+ /**
42
+ * The chain answers a batched read needs per distinct VALUE rather than per row (in-38 V-4):
43
+ * every batch's `batchOf` ([root, anchor block]), every lineage's `headCommitOf`, and every
44
+ * signer's `agentIdOfSigner`, fetched through Multicall3 before the row checks run. The maps live
45
+ * for ONE `readBatchedWithStatus` call and nothing may outlive it — a lineage head or an
46
+ * authority can move before the next read.
47
+ */
48
+ export interface BatchedReadLookups {
49
+ /** batchId (lowercased) → the `batchOf` answer: [root, blockNumber]. */
50
+ readonly batches?: ReadonlyMap<Hex, readonly [Hex, bigint]>;
51
+ /** lineageId (lowercased) → the `headCommitOf` answer. */
52
+ readonly heads?: ReadonlyMap<Hex, Hex>;
53
+ /** signer (lowercased) → the `agentIdOfSigner` answer. */
54
+ readonly signers?: ReadonlyMap<Address, Hex>;
55
+ }
56
+ /**
57
+ * Fetches the three distinct-value sets one batched read will consult, in aggregate calls of at
58
+ * most RECORDS_PER_MULTICALL contract reads with `allowFailure: false` — a failed lookup fails
59
+ * the read exactly as a failed `readContract` does today. Returns undefined where the chain
60
+ * carries no Multicall3 (no code at the canonical address, or the getCode probe itself fails —
61
+ * the probe is the process-wide one shared with every RegistryReader, in-38 V-3): the verifiers
62
+ * then read per row, unchanged. Only rows that would reach a chain check spend a
63
+ * lookup — wrong-scope rows and states that refuse before the signature check contribute nothing.
64
+ */
65
+ export declare function prefetchBatchedLookups(input: {
66
+ items: readonly BatchedReadItem[];
67
+ owner: Address;
68
+ namespaceId: Hex;
69
+ chainId: bigint;
70
+ deployment: Deployment;
71
+ client: PublicClient;
72
+ }): Promise<BatchedReadLookups | undefined>;
41
73
  /**
42
74
  * The five-check anchored read. Chain state is the only authority: the signer→agent lookup, the
43
75
  * batch root, and the lineage head all come from the contracts through `client`; the store supplies
@@ -50,6 +82,7 @@ export declare function verifyBatchedItem(input: {
50
82
  deployment: Deployment;
51
83
  client: PublicClient;
52
84
  requireLatest: boolean;
85
+ lookups?: BatchedReadLookups;
53
86
  }): Promise<BatchedVerdict>;
54
87
  /**
55
88
  * Amendment B.2 pending read: everything checkable without anchor inclusion — bytes, signature,
@@ -65,4 +98,5 @@ export declare function verifyPendingItem(input: {
65
98
  chainId: bigint;
66
99
  deployment: Deployment;
67
100
  client: PublicClient;
101
+ lookups?: BatchedReadLookups;
68
102
  }): Promise<PendingVerdict>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mida-context/sdk",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Give your agent the memory a user approved: read and write it through the user's Mida.",
5
5
  "type": "module",
6
6
  "license": "MIT",