lightnode-sdk 0.7.6 → 0.7.8

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
@@ -226,6 +226,15 @@ forever and **blocks deregistration** - and no official tool clears it. The
226
226
  JobRegistry's `claimTimeout` is permissionless, so the operator can self-clear
227
227
  it. `unstickAndDeregister()` is the one-call rescue.
228
228
 
229
+ The second thing it gets right is **gas-correct writes**. The worker daemon
230
+ under-sets the gas limit on its WorkerRegistry writes, so `addSupportedModel` and
231
+ `deregisterWorker` run out of gas and revert on-chain - the daemon reports a
232
+ failure (or, for deregister, some indexers still flip the worker to
233
+ "deregistered" while the stake never moves). Every write here estimates the gas
234
+ first and sends with a margin, so the transaction lands. `addModel()` is the
235
+ gas-correct version of the model-add the daemon botches; `deregister()` is the
236
+ gas-correct exit.
237
+
229
238
  ```ts
230
239
  import { WorkerOperator } from "lightnode-sdk";
231
240
  import { createPublicClient, createWalletClient, http } from "viem";
@@ -275,8 +284,9 @@ Full method reference (`jobIds` are the worker's IDs from
275
284
  | `withdraw()` | yes | pull the earned balance into the worker wallet |
276
285
  | `topUpStake(lcai)` | yes | add stake |
277
286
  | `withdrawStake(lcai)` | yes | remove stake above the floor |
287
+ | `addModel(tagOrId)` | yes | add a supported model to a registered worker on-chain, gas-correct (no-op if already served) |
278
288
  | `reinstate()` | yes | reactivate a suspended worker |
279
- | `deregister()` | yes | exit and release stake (reverts if in-flight jobs remain) |
289
+ | `deregister()` | yes | exit and release stake, gas-correct (reverts if in-flight jobs remain) |
280
290
  | `unstickAndDeregister(jobIds)` | yes | clear stuck + release + withdraw + deregister, in one call |
281
291
 
282
292
  > **Mainnet slashing.** `claimTimeout` / `clearStuck` / `unstickAndDeregister`
@@ -443,6 +453,8 @@ controls. Mainnet `clearstuck` and `deregister` realize a slash, so they require
443
453
 
444
454
  ```bash
445
455
  npx lightnode worker status 0x... # registration, stake, claimable, live config
456
+ npx lightnode worker models 0x... # models served, reconciled vs chain (servingNow truth)
457
+ PRIVATE_KEY=0x... npx lightnode worker preflight # one real test inference, print verdict + timings
446
458
  PRIVATE_KEY=0x... npx lightnode worker can-deregister # what blocks the exit, before spending gas
447
459
  PRIVATE_KEY=0x... npx lightnode worker settle # release completed jobs past their window + withdraw
448
460
  PRIVATE_KEY=0x... npx lightnode worker withdraw # pull the earned balance into the worker wallet
package/dist/index.d.ts CHANGED
@@ -133,7 +133,7 @@ export declare class LightNode {
133
133
  * (especially in registry-proxy environments like StackBlitz where lockfiles
134
134
  * may pin an older minor than the local install command suggests).
135
135
  */
136
- export declare const SDK_VERSION = "0.7.6";
136
+ export declare const SDK_VERSION = "0.7.8";
137
137
  export { NETWORKS, WORKER_REGISTRY, REGISTRY_TOPICS, aggregateModelStats, aggregateWorkerStats, networkAnalytics, modelStatsCsv, workerStatsCsv, workerJobsCsv, fromWei, resolveJobTransactions, fetchWorkerModels, computeModelId as modelId, estimateJobFee, JOB_REGISTRY_CONSUMER_ABI, consumerGatewayUrl, consumerGatewayHost, GatewayClient, GatewayHttpError, prepareSession, submitPrompt, decryptResponse, generateEcdhKeyPair, crypto, runInference, runInferenceWithKey, runInferenceStream, Conversation, chat, runInferenceBatch, Agent, parseAgentOutput, workerPreflight, workerWatch, Bridge, BRIDGE_ROUTE, HYPERLANE_ROUTER_ABI, ERC20_ABI, addressToBytes32, quoteBridgeFee, bridgeableBalance, bridgeAllowance, approveBridge, bridgeTransfer, DAO, DAO_ADDRESSES, ProposalState, PROPOSAL_STATE_LABEL, VoteSupport, GOVERNOR_ABI, VOTES_ABI, OnchainModelRegistry, AIVM_MODEL_REGISTRY_ABI, BENCHMARK_REGISTRY_ABI, ModelStatus, MODEL_STATUS_LABEL, StalledWorkerError, OnChainRevertError, RelayTokenTimeoutError, GatewayAuthError, isStalledWorker, WorkerOperator, WORKER_REGISTRY_ABI, JOB_REGISTRY_OPERATOR_ABI, AI_CONFIG_ABI, JOB_STATE, decodeWorkerError, WorkerOpError, isWorkerOpError, };
138
138
  export type { BearerSource, GatewayClientOptions, SelectSessionResult, PrepareSessionResult, UploadBlobResult, SessionTokenResult } from "./gateway.js";
139
139
  export type { SessionPreparation, RunInferenceArgs, RunInferenceResult, RunInferenceWithKeyArgs, RunInferenceStreamResult } from "./inference.js";
package/dist/index.js CHANGED
@@ -212,7 +212,7 @@ export class LightNode {
212
212
  * (especially in registry-proxy environments like StackBlitz where lockfiles
213
213
  * may pin an older minor than the local install command suggests).
214
214
  */
215
- export const SDK_VERSION = "0.7.6";
215
+ export const SDK_VERSION = "0.7.8";
216
216
  export { NETWORKS, WORKER_REGISTRY, REGISTRY_TOPICS, aggregateModelStats, aggregateWorkerStats, networkAnalytics, modelStatsCsv, workerStatsCsv, workerJobsCsv, fromWei,
217
217
  // v0.7.3 per-job transaction-hash resolver (lifts the upstream
218
218
  // subgraph's "block-only" Job entity to a deep-linkable Job + tx pair).
package/dist/inference.js CHANGED
@@ -75,35 +75,57 @@ export const JOB_REGISTRY_CONSUMER_ABI = [
75
75
  */
76
76
  export async function prepareSession(gateway, modelTag) {
77
77
  const id = modelId(modelTag);
78
- const selected = await gateway.selectSession(id);
79
- const sessionKey = await generateSessionKey();
80
- // Workers' pubkeys arrive as base64; disputer's as hex - decodePublicKey
81
- // accepts either.
82
- const workerPub = await importPublicKey(decodePublicKey(selected.workerEncryptionKey));
83
- const encWorker = await encryptSessionKey(sessionKey, workerPub);
84
- const encDisputer = selected.disputerEncryptionKey
85
- ? await encryptSessionKey(sessionKey, await importPublicKey(decodePublicKey(selected.disputerEncryptionKey)))
86
- : new Uint8Array(0);
87
- // The gateway expects the wrapped keys as BASE64; the same bytes are passed
88
- // as HEX to the on-chain createSession. Sending hex to the gateway makes the
89
- // dispatcher reject the prepare with an opaque error.
90
- const prepared = await gateway.prepareSession({
91
- modelId: id,
92
- encWorkerKey: bytesToBase64(encWorker),
93
- encDisputerKey: bytesToBase64(encDisputer),
94
- });
95
- return {
96
- sessionKey,
97
- nonce: prepared.nonce,
98
- createSessionArgs: {
99
- paramsHash: id,
100
- worker: prepared.worker,
101
- encWorkerKey: bytesToHex(encWorker),
102
- ephemeralPubKey: bytesToHex(encDisputer),
103
- initState: prepared.signature,
104
- expiry: BigInt(prepared.expiry),
105
- },
106
- };
78
+ // The gateway returns 409 selection_mismatch when a NEWER selectSession()
79
+ // for the same wallet supersedes ours between the select and the prepare.
80
+ // The error message is literally "re-run POST /api/sessions/select", so we
81
+ // do exactly that: rebuild from a fresh selection. The cap stops a busy
82
+ // pool of callers from looping forever - 4 attempts at 250ms / 750ms /
83
+ // 1500ms covers every churn pattern we have seen.
84
+ const MAX_ATTEMPTS = 4;
85
+ const BACKOFFS_MS = [0, 250, 750, 1500];
86
+ let lastErr = null;
87
+ for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt++) {
88
+ if (attempt > 0)
89
+ await new Promise((r) => setTimeout(r, BACKOFFS_MS[attempt]));
90
+ const selected = await gateway.selectSession(id);
91
+ const sessionKey = await generateSessionKey();
92
+ // Workers' pubkeys arrive as base64; disputer's as hex - decodePublicKey
93
+ // accepts either.
94
+ const workerPub = await importPublicKey(decodePublicKey(selected.workerEncryptionKey));
95
+ const encWorker = await encryptSessionKey(sessionKey, workerPub);
96
+ const encDisputer = selected.disputerEncryptionKey
97
+ ? await encryptSessionKey(sessionKey, await importPublicKey(decodePublicKey(selected.disputerEncryptionKey)))
98
+ : new Uint8Array(0);
99
+ try {
100
+ const prepared = await gateway.prepareSession({
101
+ modelId: id,
102
+ encWorkerKey: bytesToBase64(encWorker),
103
+ encDisputerKey: bytesToBase64(encDisputer),
104
+ });
105
+ return {
106
+ sessionKey,
107
+ nonce: prepared.nonce,
108
+ createSessionArgs: {
109
+ paramsHash: id,
110
+ worker: prepared.worker,
111
+ encWorkerKey: bytesToHex(encWorker),
112
+ ephemeralPubKey: bytesToHex(encDisputer),
113
+ initState: prepared.signature,
114
+ expiry: BigInt(prepared.expiry),
115
+ },
116
+ };
117
+ }
118
+ catch (e) {
119
+ lastErr = e;
120
+ const msg = e instanceof Error ? e.message : String(e);
121
+ if (!/selection_mismatch|selection was superseded|409/.test(msg))
122
+ throw e;
123
+ // else loop: a newer select stole this session, try again from select.
124
+ }
125
+ }
126
+ throw lastErr instanceof Error
127
+ ? lastErr
128
+ : new Error("prepareSession: gateway selection_mismatch did not clear");
107
129
  }
108
130
  /**
109
131
  * Encrypt a UTF-8 prompt with the session key, upload as a blob, and return
@@ -210,9 +210,10 @@ export declare class WorkerOperator {
210
210
  readonly network: NetworkConfig;
211
211
  private readonly pub;
212
212
  private readonly wallet?;
213
- private readonly addr;
213
+ private readonly maybeAddr;
214
214
  private cfgCache?;
215
215
  constructor(network: NetworkId | NetworkConfig, opts: WorkerOperatorOpts);
216
+ private get addr();
216
217
  private requireWallet;
217
218
  private get jobReg();
218
219
  private get workerReg();
@@ -282,6 +283,17 @@ export declare class WorkerOperator {
282
283
  withdraw(): Promise<`0x${string}`>;
283
284
  /** Deregister - releases stake to the wallet. Reverts (ActiveJobsExist) if any in-flight job remains. */
284
285
  deregister(): Promise<`0x${string}`>;
286
+ /**
287
+ * Add a supported model to THIS (already-registered) worker on-chain. Accepts a
288
+ * model tag (e.g. "gemma4:e2b") or a raw bytes32 modelId.
289
+ *
290
+ * This is the gas-correct version of the step the worker daemon's one-shot
291
+ * register botches: the daemon sends addSupportedModel with an under-set gas
292
+ * limit, so it OutOfGas-reverts (and its rollback deregister can too). viem
293
+ * estimates the gas here, so it lands. Use it to finish a worker that staked but
294
+ * failed to add its model. No-op (returns null) if already serving the model.
295
+ */
296
+ addModel(modelTagOrId: string): Promise<`0x${string}` | null>;
285
297
  /**
286
298
  * The flagship rescue: clear stuck jobs then release any settled completed jobs +
287
299
  * withdraw earnings then deregister. The one flow no official tool provides.
@@ -287,9 +287,13 @@ export class WorkerOperator {
287
287
  const acct = opts.walletClient?.account;
288
288
  const fromWallet = typeof acct === "string" ? acct : acct?.address;
289
289
  const a = (opts.workerAddress ?? fromWallet);
290
- if (!a)
291
- throw new Error("WorkerOperator: provide workerAddress or a walletClient with an account");
292
- this.addr = a.toLowerCase();
290
+ this.maybeAddr = a ? a.toLowerCase() : undefined;
291
+ }
292
+ get addr() {
293
+ if (!this.maybeAddr) {
294
+ throw new Error("WorkerOperator: this method needs the worker address. Pass `workerAddress` (or a `walletClient` with an account) when constructing WorkerOperator.");
295
+ }
296
+ return this.maybeAddr;
293
297
  }
294
298
  requireWallet(op) {
295
299
  if (!this.wallet)
@@ -525,6 +529,25 @@ export class WorkerOperator {
525
529
  async deregister() {
526
530
  return this.send("deregister", this.workerReg, WORKER_REGISTRY_ABI_PARSED, "deregisterWorker", []);
527
531
  }
532
+ /**
533
+ * Add a supported model to THIS (already-registered) worker on-chain. Accepts a
534
+ * model tag (e.g. "gemma4:e2b") or a raw bytes32 modelId.
535
+ *
536
+ * This is the gas-correct version of the step the worker daemon's one-shot
537
+ * register botches: the daemon sends addSupportedModel with an under-set gas
538
+ * limit, so it OutOfGas-reverts (and its rollback deregister can too). viem
539
+ * estimates the gas here, so it lands. Use it to finish a worker that staked but
540
+ * failed to add its model. No-op (returns null) if already serving the model.
541
+ */
542
+ async addModel(modelTagOrId) {
543
+ const id = modelTagOrId.startsWith("0x") && modelTagOrId.length === 66
544
+ ? modelTagOrId.toLowerCase()
545
+ : (await import("./inference.js")).modelId(modelTagOrId);
546
+ const already = (await this.read(this.workerReg, WORKER_REGISTRY_ABI_PARSED, "isEligible", [this.addr, id]));
547
+ if (already)
548
+ return null;
549
+ return this.send("addModel", this.workerReg, WORKER_REGISTRY_ABI_PARSED, "addSupportedModel", [id]);
550
+ }
528
551
  /**
529
552
  * The flagship rescue: clear stuck jobs then release any settled completed jobs +
530
553
  * withdraw earnings then deregister. The one flow no official tool provides.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lightnode-sdk",
3
- "version": "0.7.6",
3
+ "version": "0.7.8",
4
4
  "description": "Read-only TypeScript client for LightChain AI: workers, jobs, models, on-chain registration, and per-model network analytics. Independent, community-built (not an official LightChain package).",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",