@flop-labs/cli 0.4.2 → 0.4.4

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.
Files changed (3) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/dist/cli.js +145 -63
  3. package/package.json +2 -2
package/CHANGELOG.md CHANGED
@@ -7,6 +7,24 @@ and this package follows [Semantic Versioning](https://semver.org/spec/v2.0.0.ht
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.4.4] - 2026-11-10
11
+
12
+ ### Fixed
13
+
14
+ - Gateway tools probes now reconcile ambiguous cooperative closes against finalized
15
+ channel events without submitting a second close.
16
+ - Model backup keeps download receipts beside `--model-dir` unless `--state-dir`
17
+ selects another location on the same filesystem; metadata-only onboarding now
18
+ passes the registered precision to the suggested backup command.
19
+
20
+ ## [0.4.3] - 2026-11-09
21
+
22
+ ### Fixed
23
+
24
+ - Package with SDK 0.5.2 as the minimum compatible SDK, including canonical session
25
+ pallet aliases. The 30-second challenge and 120-second enrollment deadlines
26
+ shipped in 0.4.2 are unchanged; packed offline regressions cover those paths.
27
+
10
28
  ## [0.4.2] - 2026-11-09
11
29
 
12
30
  ### Added
package/dist/cli.js CHANGED
@@ -6462,15 +6462,15 @@ function ownerOnly(mode) {
6462
6462
  }
6463
6463
  async function privateDirectory(path5) {
6464
6464
  await mkdir5(path5, { recursive: true, mode: 448 });
6465
- const stat = await lstat(path5);
6466
- if (!stat.isDirectory() || !ownerOnly(stat.mode) || process.getuid && stat.uid !== process.getuid()) {
6465
+ const stat2 = await lstat(path5);
6466
+ if (!stat2.isDirectory() || !ownerOnly(stat2.mode) || process.getuid && stat2.uid !== process.getuid()) {
6467
6467
  throw new Error("FLOP credential directory must be owned by this user with mode 0700");
6468
6468
  }
6469
6469
  }
6470
6470
  async function cachedCredential(path5, gateway) {
6471
6471
  try {
6472
- const stat = await lstat(path5);
6473
- if (!stat.isFile() || !ownerOnly(stat.mode) || process.getuid && stat.uid !== process.getuid()) {
6472
+ const stat2 = await lstat(path5);
6473
+ if (!stat2.isFile() || !ownerOnly(stat2.mode) || process.getuid && stat2.uid !== process.getuid()) {
6474
6474
  throw new Error("FLOP credential cache must be an owner-only regular file (mode 0600)");
6475
6475
  }
6476
6476
  const cached = JSON.parse(await readFile3(path5, "utf8"));
@@ -6551,8 +6551,8 @@ async function enrollmentWallet(path5) {
6551
6551
  await cryptoWaitReady4();
6552
6552
  const keyring = new Keyring4({ type: "sr25519", ss58Format: 42 });
6553
6553
  try {
6554
- const stat = await lstat(path5);
6555
- if (!stat.isFile() || !ownerOnly(stat.mode) || process.getuid && stat.uid !== process.getuid()) {
6554
+ const stat2 = await lstat(path5);
6555
+ if (!stat2.isFile() || !ownerOnly(stat2.mode) || process.getuid && stat2.uid !== process.getuid()) {
6556
6556
  throw new Error("FLOP devnet wallet must be an owner-only regular file (mode 0600)");
6557
6557
  }
6558
6558
  const stored = JSON.parse(await readFile3(path5, "utf8"));
@@ -6760,8 +6760,8 @@ async function savePublicProfile(model, key) {
6760
6760
  const path5 = publicHermesProfilePath(model, key);
6761
6761
  for (const directory of [join2(homedir2(), ".flop"), publicHermesProfileRoot(), path5]) {
6762
6762
  await mkdir6(directory, { recursive: true, mode: 448 });
6763
- const stat = await lstat2(directory);
6764
- if (!stat.isDirectory() || process.getuid && stat.uid !== process.getuid() || process.platform !== "win32" && (stat.mode & 63) !== 0) {
6763
+ const stat2 = await lstat2(directory);
6764
+ if (!stat2.isDirectory() || process.getuid && stat2.uid !== process.getuid() || process.platform !== "win32" && (stat2.mode & 63) !== 0) {
6765
6765
  throw new Error("FLOP Hermes profile and parent directories must be owner-only regular directories");
6766
6766
  }
6767
6767
  }
@@ -7707,12 +7707,12 @@ async function writeModelFiles(outputDir, files) {
7707
7707
  }
7708
7708
  async function readLocalModelFiles(path5) {
7709
7709
  const root = resolve5(path5);
7710
- const stat = await lstat3(root);
7711
- if (stat.isSymbolicLink()) throw new Error(`model source must not be a symlink: ${path5}`);
7712
- if (stat.isFile()) {
7710
+ const stat2 = await lstat3(root);
7711
+ if (stat2.isSymbolicLink()) throw new Error(`model source must not be a symlink: ${path5}`);
7712
+ if (stat2.isFile()) {
7713
7713
  return [{ path: root.split(sep).pop() ?? "weights.bin", bytes: await readFile4(root) }];
7714
7714
  }
7715
- if (!stat.isDirectory()) throw new Error(`model source is not a file or directory: ${path5}`);
7715
+ if (!stat2.isDirectory()) throw new Error(`model source is not a file or directory: ${path5}`);
7716
7716
  const files = [];
7717
7717
  async function walk(dir) {
7718
7718
  for (const entry of await readdir(dir, { withFileTypes: true })) {
@@ -7899,8 +7899,8 @@ function onboardingDirectory(binding, override) {
7899
7899
  return override ? resolve6(override) : join3(process.env.XDG_STATE_HOME ?? join3(homedir3(), ".local/state"), "flop/model-onboarding", digest(JSON.stringify(binding)));
7900
7900
  }
7901
7901
  async function hashFile(path5) {
7902
- const stat = await lstat4(path5);
7903
- if (!stat.isFile() || stat.isSymbolicLink()) throw new Error("onboarding artifact must be a regular file");
7902
+ const stat2 = await lstat4(path5);
7903
+ if (!stat2.isFile() || stat2.isSymbolicLink()) throw new Error("onboarding artifact must be a regular file");
7904
7904
  const hash2 = createHash2("sha256");
7905
7905
  for await (const chunk of createReadStream(path5)) hash2.update(chunk);
7906
7906
  return hash2.digest("hex");
@@ -8077,8 +8077,8 @@ var privateJson = (value) => JSON.stringify(
8077
8077
  (_k, v) => typeof v === "bigint" ? v.toString() : v instanceof Uint8Array ? hex(v) : v
8078
8078
  );
8079
8079
  async function readPrivateJson(path5) {
8080
- const stat = await lstat5(path5);
8081
- if (!stat.isFile() || stat.isSymbolicLink() || (stat.mode & 63) !== 0)
8080
+ const stat2 = await lstat5(path5);
8081
+ if (!stat2.isFile() || stat2.isSymbolicLink() || (stat2.mode & 63) !== 0)
8082
8082
  throw new Error("replay files must be private regular files (0600)");
8083
8083
  return JSON.parse(await readFile6(path5, "utf8"));
8084
8084
  }
@@ -8467,6 +8467,9 @@ function activeHybridProfile(profile) {
8467
8467
  const image = optionValue(profile.imageId);
8468
8468
  return image !== null && codecHash(image) !== `0x${"00".repeat(32)}` && profile.epoch.toString() !== "0";
8469
8469
  }
8470
+ function modelBackupHandoff(modelHash, precision, revision) {
8471
+ return `flop model backup ${modelHash} --model-dir <path> --precision ${precision}${revision ? ` --revision ${revision}` : ""}`;
8472
+ }
8470
8473
  function normalizePrecision(raw) {
8471
8474
  const upper = raw.trim().toUpperCase();
8472
8475
  const precision = PRECISIONS.find((candidate) => candidate.toUpperCase() === upper);
@@ -8632,10 +8635,10 @@ async function exists(path5) {
8632
8635
  async function materializeSource(raw, outputDir, token, sourceRevision, resumeState, files) {
8633
8636
  const local = await exists(raw);
8634
8637
  if (local) {
8635
- const stat = await lstat6(raw);
8636
- if (stat.isSymbolicLink()) throw new Error(`model source must not be a symlink: ${raw}`);
8637
- const modelDir = stat.isDirectory() ? resolve7(raw) : outputDir;
8638
- if (!stat.isDirectory()) {
8638
+ const stat2 = await lstat6(raw);
8639
+ if (stat2.isSymbolicLink()) throw new Error(`model source must not be a symlink: ${raw}`);
8640
+ const modelDir = stat2.isDirectory() ? resolve7(raw) : outputDir;
8641
+ if (!stat2.isDirectory()) {
8639
8642
  await writeModelFiles(outputDir, await readLocalModelFiles(raw));
8640
8643
  }
8641
8644
  const name = basename(resolve7(raw));
@@ -9352,7 +9355,7 @@ function modelOnboardCmd() {
9352
9355
  if (metadataOnly && !state.weightsRef) {
9353
9356
  process.stderr.write(
9354
9357
  `next: approval needs the DA backup of ${identity.modelHash} (packed image + file table, ${measured.imageSize} bytes in ${Math.max(1, Math.ceil(measured.imageSize / sdk.DA_CHUNK_SIZE))} chunk(s)). On a box with room for the checkpoint, run
9355
- flop model backup ${identity.modelHash} --model-dir <path>${preflight ? ` --revision ${preflight.revision}` : ""}
9358
+ ${modelBackupHandoff(identity.modelHash, precision, preflight?.revision)}
9356
9359
  then bind the DataRef it prints. Rerunning this command with --download packs the image from disk instead, which needs about twice the checkpoint's size.
9357
9360
  `
9358
9361
  );
@@ -9407,9 +9410,9 @@ function modelUploadCmd() {
9407
9410
  imagePath = join6(tempDir, "weights.img");
9408
9411
  if (opts.path) {
9409
9412
  let modelDir = resolve8(opts.path);
9410
- const stat = await lstat7(modelDir);
9411
- if (stat.isSymbolicLink()) throw new Error(`model source must not be a symlink: ${opts.path}`);
9412
- if (stat.isFile()) {
9413
+ const stat2 = await lstat7(modelDir);
9414
+ if (stat2.isSymbolicLink()) throw new Error(`model source must not be a symlink: ${opts.path}`);
9415
+ if (stat2.isFile()) {
9413
9416
  modelDir = join6(tempDir, "model");
9414
9417
  await mkdir11(modelDir);
9415
9418
  await copyFile(resolve8(opts.path), join6(modelDir, basename2(opts.path)));
@@ -9555,8 +9558,9 @@ function modelPreflightCmd() {
9555
9558
 
9556
9559
  // src/commands/model-backup.ts
9557
9560
  import { Command as Command33, Option as Option3 } from "commander";
9558
- import { mkdir as mkdir12, rm as rm4, writeFile as writeFile10 } from "node:fs/promises";
9559
- import { join as join8, resolve as resolve9 } from "node:path";
9561
+ import { createHash as createHash4 } from "node:crypto";
9562
+ import { mkdir as mkdir12, rm as rm4, stat, writeFile as writeFile10 } from "node:fs/promises";
9563
+ import { dirname as dirname6, join as join8, resolve as resolve9 } from "node:path";
9560
9564
  var PRECISIONS3 = ["FP32", "FP16", "BF16", "INT8", "INT4", "Mixed"];
9561
9565
  async function finalizedLeasedRef2(api, commitment, size) {
9562
9566
  const hash2 = await api.rpc.chain.getFinalizedHead();
@@ -9572,6 +9576,17 @@ async function awaitFinalizedLeasedRef(api, commitment, size) {
9572
9576
  await new Promise((wake) => setTimeout(wake, REGISTRATION_POLL_MS));
9573
9577
  }
9574
9578
  }
9579
+ function downloadStateDir(binding, override) {
9580
+ if (override) return resolve9(override);
9581
+ const key = createHash4("sha256").update(JSON.stringify(binding)).digest("hex");
9582
+ return join8(dirname6(binding.modelDir), `.flop-backup-state-${key.slice(0, 16)}`);
9583
+ }
9584
+ async function assertSameFilesystem(stateDir, modelDir) {
9585
+ if ((await stat(stateDir)).dev === (await stat(modelDir)).dev) return;
9586
+ throw new Error(
9587
+ `download state ${stateDir} is not on the same filesystem as ${modelDir}; pass a --state-dir on that filesystem outside the checkpoint tree (if --model-dir is a mount point, use a subdirectory of it)`
9588
+ );
9589
+ }
9575
9590
  function registeredHubRepo(mirrors) {
9576
9591
  const repos = [...new Set(mirrors.filter((m) => m.kind === "huggingface").map((m) => m.repo))];
9577
9592
  if (repos.length > 1) {
@@ -9584,7 +9599,7 @@ function registeredHubRepo(mirrors) {
9584
9599
  function modelBackupCmd() {
9585
9600
  return new Command33("backup").description(
9586
9601
  "Stream a registered content-v1 checkpoint from Hugging Face into validator DA under this box's DA key, without ever writing the packed image out, and print the DataRefs its publisher then binds"
9587
- ).argument("<model_hash>", "Registered model hash (0x-hex)").requiredOption("--model-dir <path>", "Directory the checkpoint streams into (needs room for it)").option("--source <hf-id>", "Hugging Face id (owner/model or hf:owner/model); the model's registered mirror when omitted").option("--revision <sha>", "Immutable 40-hex commit; main is resolved to one when omitted").addOption(new Option3("--precision <precision>", "Measured-root precision").choices(PRECISIONS3).default("FP16")).option("--da-validator <path>", "da-validator executable", "da-validator").option("--token <token>", "Hugging Face bearer token (prefer HF_TOKEN)").option("--state-dir <path>", "Download resume receipts (outside the checkpoint tree)").option("--dry-run", "Print the plan and the DA deposit it would lock, and stop before any transfer").option("--json", "Output as JSON").action(async (modelHash, opts, command) => {
9602
+ ).argument("<model_hash>", "Registered model hash (0x-hex)").requiredOption("--model-dir <path>", "Directory the checkpoint streams into (needs room for it)").option("--source <hf-id>", "Hugging Face id (owner/model or hf:owner/model); the model's registered mirror when omitted").option("--revision <sha>", "Immutable 40-hex commit; main is resolved to one when omitted").addOption(new Option3("--precision <precision>", "Measured-root precision").choices(PRECISIONS3).default("FP16")).option("--da-validator <path>", "da-validator executable", "da-validator").option("--token <token>", "Hugging Face bearer token (prefer HF_TOKEN)").option("--state-dir <path>", "Download resume receipts, on the checkpoint's filesystem but outside its tree (defaults to beside --model-dir)").option("--dry-run", "Print the plan and the DA deposit it would lock, and stop before any transfer").option("--json", "Output as JSON").action(async (modelHash, opts, command) => {
9588
9603
  let client;
9589
9604
  let workDir;
9590
9605
  try {
@@ -9637,10 +9652,12 @@ function modelBackupCmd() {
9637
9652
  workDir = join8(resolve9(modelDir, ".."), `.flop-backup-${modelHash.replace(/^0x/, "").slice(0, 16)}`);
9638
9653
  await mkdir12(workDir, { recursive: true, mode: 448 });
9639
9654
  await mkdir12(modelDir, { recursive: true });
9640
- const resumeState = opts.stateDir ? join8(resolve9(opts.stateDir), "download.json") : void 0;
9641
- if (resumeState) await mkdir12(resolve9(opts.stateDir), { recursive: true, mode: 448 });
9655
+ const stateDir = downloadStateDir({ modelHash, modelDir, repo, revision: preflight.revision }, opts.stateDir);
9656
+ await mkdir12(stateDir, { recursive: true, mode: 448 });
9657
+ await assertSameFilesystem(stateDir, modelDir);
9658
+ const resumeState = join8(stateDir, "download.json");
9642
9659
  process.stderr.write(
9643
- `backing up ${repo}@${preflight.revision} (${preflight.files.length} files, image ${imageSize} bytes, ${preflight.daChunks} DA chunk(s)) as ${modelHash}
9660
+ `backing up ${repo}@${preflight.revision} (${preflight.files.length} files, image ${imageSize} bytes, ${preflight.daChunks} DA chunk(s)) as ${modelHash}; download receipts in ${stateDir}
9644
9661
  `
9645
9662
  );
9646
9663
  await sdk.fetchHuggingFaceToDir(repo, modelDir, {
@@ -10897,7 +10914,7 @@ import { stdin, stdout } from "node:process";
10897
10914
 
10898
10915
  // src/conversations.ts
10899
10916
  import { mkdir as mkdir13, readFile as readFile8, rename as rename8, writeFile as writeFile11 } from "node:fs/promises";
10900
- import { dirname as dirname6 } from "node:path";
10917
+ import { dirname as dirname7 } from "node:path";
10901
10918
  var MAX_CONVERSATIONS = 100;
10902
10919
  var MAX_CONVERSATION_MESSAGES = 100;
10903
10920
  var MAX_MESSAGE_CHARS = 16384;
@@ -10916,7 +10933,7 @@ var ConversationStore = class {
10916
10933
  }
10917
10934
  }
10918
10935
  async write(data) {
10919
- await mkdir13(dirname6(this.path), { recursive: true, mode: 448 });
10936
+ await mkdir13(dirname7(this.path), { recursive: true, mode: 448 });
10920
10937
  const temp = `${this.path}.${process.pid}.tmp`;
10921
10938
  await writeFile11(temp, `${JSON.stringify(data, null, 2)}
10922
10939
  `, { mode: 384 });
@@ -11940,7 +11957,7 @@ function gatewayAllowlistCmd() {
11940
11957
 
11941
11958
  // src/commands/gateway-renew.ts
11942
11959
  import { open as open3, readFile as readFile11, rename as rename9, rm as rm5, writeFile as writeFile13 } from "node:fs/promises";
11943
- import { createHash as createHash6 } from "node:crypto";
11960
+ import { createHash as createHash7 } from "node:crypto";
11944
11961
  import { request as request2 } from "node:http";
11945
11962
  import { setTimeout as delay } from "node:timers/promises";
11946
11963
  import { Keyring as Keyring5 } from "@polkadot/keyring";
@@ -12037,11 +12054,11 @@ function renewConformance(record, evidence, now = Date.now()) {
12037
12054
  }
12038
12055
 
12039
12056
  // ../gateway-core/src/tools-qualification.ts
12040
- import { createHash as createHash4 } from "node:crypto";
12057
+ import { createHash as createHash5 } from "node:crypto";
12041
12058
  import { isDeepStrictEqual } from "node:util";
12042
12059
  var OWNER_TOOLS_QUALIFICATION = "flop.owner-tools-qualification.v1";
12043
12060
  function qualificationHash(value) {
12044
- return createHash4("sha256").update(JSON.stringify(value)).digest("hex");
12061
+ return createHash5("sha256").update(JSON.stringify(value)).digest("hex");
12045
12062
  }
12046
12063
  function toolsQualificationEnvelope(model, version, scope) {
12047
12064
  return {
@@ -12139,12 +12156,12 @@ function qualificationSignedToolCalls(frames) {
12139
12156
 
12140
12157
  // ../gateway-core/src/miner-admissions.ts
12141
12158
  import { closeSync, fstatSync, openSync, readSync, statSync } from "node:fs";
12142
- import { createHash as createHash5 } from "node:crypto";
12159
+ import { createHash as createHash6 } from "node:crypto";
12143
12160
  var MAX_ADMITTED_MINERS = 256;
12144
12161
  var MAX_FILE_BYTES = 512 * 1024;
12145
12162
  var HASH4 = /^0x[0-9a-f]{64}$/;
12146
12163
  var minerSlot = (miner) => `${miner.minerAddress}:${miner.modelIdentity.requestedSlug}`;
12147
- var staticMinerSha256 = (miner) => createHash5("sha256").update(JSON.stringify(miner)).digest("hex");
12164
+ var staticMinerSha256 = (miner) => createHash6("sha256").update(JSON.stringify(miner)).digest("hex");
12148
12165
  var sameModel = (a, b) => Object.keys(a.modelIdentity).every((key) => a.modelIdentity[key] === b.modelIdentity[key]);
12149
12166
  var MinerRouting = class {
12150
12167
  constructor(statics, path5) {
@@ -12161,8 +12178,8 @@ var MinerRouting = class {
12161
12178
  if (this.path) {
12162
12179
  let stamp;
12163
12180
  try {
12164
- const stat = statSync(this.path);
12165
- stamp = `${stat.dev}:${stat.ino}:${stat.size}:${stat.mtimeMs}`;
12181
+ const stat2 = statSync(this.path);
12182
+ stamp = `${stat2.dev}:${stat2.ino}:${stat2.size}:${stat2.mtimeMs}`;
12166
12183
  } catch {
12167
12184
  stamp = "missing";
12168
12185
  }
@@ -12174,8 +12191,8 @@ var MinerRouting = class {
12174
12191
  let fd;
12175
12192
  try {
12176
12193
  fd = openSync(this.path, "r");
12177
- const stat = fstatSync(fd);
12178
- if (!stat.isFile() || stat.size > MAX_FILE_BYTES) throw new Error();
12194
+ const stat2 = fstatSync(fd);
12195
+ if (!stat2.isFile() || stat2.size > MAX_FILE_BYTES) throw new Error();
12179
12196
  const buffer = Buffer.alloc(MAX_FILE_BYTES + 1);
12180
12197
  const size = readSync(fd, buffer, 0, buffer.length, 0);
12181
12198
  if (size > MAX_FILE_BYTES) throw new Error();
@@ -12237,8 +12254,8 @@ var MinerAdmissions = class {
12237
12254
  read(now = Date.now()) {
12238
12255
  let stamp;
12239
12256
  try {
12240
- const stat = statSync(this.path);
12241
- stamp = `${stat.dev}:${stat.ino}:${stat.size}:${stat.mtimeMs}`;
12257
+ const stat2 = statSync(this.path);
12258
+ stamp = `${stat2.dev}:${stat2.ino}:${stat2.size}:${stat2.mtimeMs}`;
12242
12259
  } catch {
12243
12260
  stamp = "missing";
12244
12261
  }
@@ -12249,8 +12266,8 @@ var MinerAdmissions = class {
12249
12266
  let fd;
12250
12267
  try {
12251
12268
  fd = openSync(this.path, "r");
12252
- const stat = fstatSync(fd);
12253
- if (!stat.isFile() || stat.size > MAX_FILE_BYTES) throw new Error("admissions file too large");
12269
+ const stat2 = fstatSync(fd);
12270
+ if (!stat2.isFile() || stat2.size > MAX_FILE_BYTES) throw new Error("admissions file too large");
12254
12271
  const buffer = Buffer.alloc(MAX_FILE_BYTES + 1);
12255
12272
  const size = readSync(fd, buffer, 0, buffer.length, 0);
12256
12273
  if (size > MAX_FILE_BYTES) throw new Error("admissions file too large");
@@ -13001,6 +13018,73 @@ async function waitForSettlementAllowance(api, channelId, aggregateGn, deadline)
13001
13018
  }
13002
13019
  throw new Error("settlement allowance did not arrive before the probe deadline");
13003
13020
  }
13021
+ async function reconcileProbeClose(api, channelId, aggregateGn, deadline, submit) {
13022
+ const bounded = async (operation) => {
13023
+ const remaining = deadline - Date.now();
13024
+ if (remaining <= 0) throw new Error("probe deadline elapsed");
13025
+ let timer;
13026
+ try {
13027
+ return await Promise.race([operation, new Promise((_, reject) => {
13028
+ timer = setTimeout(() => reject(new Error("probe deadline elapsed")), remaining);
13029
+ })]);
13030
+ } finally {
13031
+ if (timer) clearTimeout(timer);
13032
+ }
13033
+ };
13034
+ let next;
13035
+ try {
13036
+ const head = await bounded(api.rpc.chain.getFinalizedHead());
13037
+ next = BigInt((await bounded(api.rpc.chain.getHeader(head))).number.toString());
13038
+ } catch {
13039
+ return "settlement unknown (finalized start unavailable; do not resubmit)";
13040
+ }
13041
+ if (Date.now() >= deadline) return "settlement not submitted (probe deadline elapsed)";
13042
+ try {
13043
+ await bounded(submit());
13044
+ } catch {
13045
+ }
13046
+ try {
13047
+ while (Date.now() < deadline) {
13048
+ const head = await bounded(api.rpc.chain.getFinalizedHead());
13049
+ const height = BigInt((await bounded(api.rpc.chain.getHeader(head))).number.toString());
13050
+ while (next <= height) {
13051
+ const blockHash = await bounded(api.rpc.chain.getBlockHash(next.toString()));
13052
+ const at = await bounded(api.at(blockHash));
13053
+ const events = [...await bounded(at.query.system.events())];
13054
+ for (const row of events) {
13055
+ const event = row.event;
13056
+ if (event.section !== "computeChannel" || !["Settled", "TimedOutRefunded"].includes(event.method) || event.data[0].toHex() !== hex3(channelId) || !row.phase.isApplyExtrinsic) continue;
13057
+ const index = row.phase.asApplyExtrinsic.toNumber();
13058
+ if (!events.some((other) => other.phase.isApplyExtrinsic && other.phase.asApplyExtrinsic.toNumber() === index && other.event.section === "system" && other.event.method === "ExtrinsicSuccess")) continue;
13059
+ if (event.method === "TimedOutRefunded") return `refunded at finalized ${blockHash.toHex()}`;
13060
+ if (BigInt(event.data[3].toString()) !== aggregateGn || !event.data[4].isTrue) {
13061
+ return `settlement mismatch at finalized ${blockHash.toHex()}`;
13062
+ }
13063
+ const paid = BigInt(event.data[1].toString());
13064
+ if (paid <= 0n) return `settlement has zero miner payment at finalized ${blockHash.toHex()}`;
13065
+ const block = await bounded(api.rpc.chain.getBlock(blockHash));
13066
+ return `settled ${block.block.extrinsics[index].hash.toHex()} minerPaid=${paid} at finalized ${blockHash.toHex()}`;
13067
+ }
13068
+ next++;
13069
+ }
13070
+ await bounded(delay(Math.min(1e3, Math.max(1, deadline - Date.now()))));
13071
+ }
13072
+ } catch {
13073
+ }
13074
+ return "settlement unknown (no finalized outcome before probe deadline; do not resubmit)";
13075
+ }
13076
+ async function settleProbe(api, channelId, aggregateGn, deadline, allowance2, submit, forceClose) {
13077
+ try {
13078
+ await allowance2();
13079
+ } catch (error) {
13080
+ try {
13081
+ await forceClose();
13082
+ } catch {
13083
+ }
13084
+ return `cooperative settlement not submitted; force close attempted (${reason(error)})`;
13085
+ }
13086
+ return reconcileProbeClose(api, channelId, aggregateGn, deadline, submit);
13087
+ }
13004
13088
  async function renewalMinerInfo(api, miner, fetchImpl = fetch) {
13005
13089
  const head = await api.rpc.chain.getFinalizedHead();
13006
13090
  const at = await api.at(head);
@@ -13154,28 +13238,26 @@ async function probeToolsConformance(input) {
13154
13238
  }
13155
13239
  throw error;
13156
13240
  }
13157
- let settlement = "force-settlement posted; finalize it with `flop channel finalize`";
13158
- try {
13159
- await waitForSettlementAllowance(api, session.channelId, receipt.gN, deadline);
13160
- const settled = await session.closeCooperative(async (claim) => {
13241
+ const settlement = await settleProbe(
13242
+ api,
13243
+ session.channelId,
13244
+ receipt.gN,
13245
+ deadline,
13246
+ () => waitForSettlementAllowance(api, session.channelId, receipt.gN, deadline),
13247
+ () => session.closeCooperative(async (claim) => {
13161
13248
  const wire = await computeChannel6.notifyMinerClose(miner.endpoint, claim, headers, AbortSignal.timeout(3e4), requestHeaders, fetchImpl);
13162
- return wire ? {
13249
+ if (!wire) throw new Error("miner did not report a settlement");
13250
+ return {
13163
13251
  channelId: bytes3(wire.channelId),
13164
13252
  minerPaid: BigInt(wire.minerPaid),
13165
13253
  burned: BigInt(wire.burned),
13166
13254
  aggregateGn: BigInt(wire.aggregateGn),
13167
13255
  cooperative: wire.cooperative,
13168
13256
  txHash: wire.txHash
13169
- } : null;
13170
- });
13171
- if ("minerPaid" in settled && settled.cooperative) settlement = `settled ${settled.txHash}`;
13172
- } catch (error) {
13173
- try {
13174
- await session.close();
13175
- } catch {
13176
- }
13177
- settlement = `settlement failed (${reason(error)})`;
13178
- }
13257
+ };
13258
+ }),
13259
+ () => session.close()
13260
+ );
13179
13261
  return `${verdict} in channel ${hex3(session.channelId)}; ${settlement}`;
13180
13262
  }
13181
13263
  function envJson(name) {
@@ -13204,7 +13286,7 @@ function gatewayRenewCmd() {
13204
13286
  throw new Error("receipt-only mode requires the receipt/hash and cannot run direct probes");
13205
13287
  }
13206
13288
  const raw = await readFile11(opts.qualificationReceipt);
13207
- if (raw.length > 8e6 || createHash6("sha256").update(raw).digest("hex") !== opts.qualificationReceiptSha256) throw new Error("qualification receipt bytes differ");
13289
+ if (raw.length > 8e6 || createHash7("sha256").update(raw).digest("hex") !== opts.qualificationReceiptSha256) throw new Error("qualification receipt bytes differ");
13208
13290
  const value = JSON.parse(raw.toString("utf8"));
13209
13291
  const records2 = parseCanonicalCatalog(envJson(opts.catalogEnv), { chainPriced: opts.catalogPricing === "chain" });
13210
13292
  const record = records2.find((item) => item.openrouter_slug === value.model);
@@ -13289,7 +13371,7 @@ function gatewayRenewCmd() {
13289
13371
  // src/release-check.ts
13290
13372
  import { mkdir as mkdir14, readFile as readFile12, rename as rename10, writeFile as writeFile14 } from "node:fs/promises";
13291
13373
  import { homedir as homedir5 } from "node:os";
13292
- import { dirname as dirname7, join as join10 } from "node:path";
13374
+ import { dirname as dirname8, join as join10 } from "node:path";
13293
13375
  var DAY_MS = 24 * 60 * 60 * 1e3;
13294
13376
  var READ_ONLY = /* @__PURE__ */ new Set([
13295
13377
  "health",
@@ -13348,7 +13430,7 @@ async function checkRelease(options) {
13348
13430
  if (!response.ok) throw new Error(`HTTP ${response.status}`);
13349
13431
  releases = parseOperatorReleases(await response.json());
13350
13432
  try {
13351
- await mkdir14(dirname7(cacheFile), { recursive: true });
13433
+ await mkdir14(dirname8(cacheFile), { recursive: true });
13352
13434
  const temporary = `${cacheFile}.${process.pid}.tmp`;
13353
13435
  await writeFile14(temporary, JSON.stringify({ fetchedAt: now, releases }), { mode: 384 });
13354
13436
  await rename10(temporary, cacheFile);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flop-labs/cli",
3
- "version": "0.4.2",
3
+ "version": "0.4.4",
4
4
  "description": "FLOP Network CLI — interact with FLOP from the terminal",
5
5
  "license": "MIT OR Apache-2.0",
6
6
  "repository": {
@@ -23,7 +23,7 @@
23
23
  },
24
24
  "dependencies": {
25
25
  "@flop-labs/client": "^0.2.2",
26
- "@flop-labs/sdk": "^0.5.1",
26
+ "@flop-labs/sdk": "^0.5.2",
27
27
  "@polkadot/api": "^17.0.1",
28
28
  "@polkadot/keyring": "^14.0.3",
29
29
  "@polkadot/util-crypto": "^14.0.3",