brainchain-worker 0.3.0 → 0.3.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/README.md CHANGED
@@ -81,8 +81,8 @@ Briefs are written by strangers, and some will try to get your agent to do somet
81
81
  ## Keep it running
82
82
 
83
83
  - **Stopping.** A `run` with no task in hand stops at once on Ctrl-C or SIGTERM. During a task, the first one stops `run` from taking new tasks and lets the current command finish and deliver; a second one stops the command at once. SIGHUP, which a closed terminal or a dropped SSH session sends, stops the command and `run` at once.
84
- - **Restarts.** `run` saves unfinished work to `~/.config/brainchain-worker/<address>.json`, readable only by you: a result waiting to be uploaded or submitted, a submit already sent, when each failed task is due again, and how far it has read the chain. A restart, or the next `--once` run, picks up from there without running the command again for a result it already has, and waits for a sent submit before sending another. A command left running by a `run` that was killed is stopped when the next one starts. If the file is damaged, `run` moves it aside and starts without saved work.
85
- - **One run per worker.** A second `run` for the same address refuses to start while the first is alive.
84
+ - **Restarts.** `run` saves unfinished work to `~/.config/brainchain-worker/<address>.<network>.json`, readable only by you: a result waiting to be uploaded or submitted, a submit already sent, when each failed task is due again, and how far it has read the chain. A restart, or the next `--once` run, picks up from there without running the command again for a result it already has, and waits for a sent submit before sending another. A command left running by a `run` that was killed is stopped when the next one starts. If the file is damaged, `run` moves it aside and starts without saved work.
85
+ - **One run per worker and network.** State is kept per network, where `<network>` is `testnet` or `mainnet`, so one key can run on both at the same time, and a second `run` for the same address on the same network refuses to start while the first is alive.
86
86
  - **Under a supervisor.** systemd sends SIGTERM to every process of a service by default, the command included, and every supervisor kills what is left after a timeout: 90 seconds for systemd, 10 for Docker and 30 for Kubernetes. For a stop that lets the current task deliver, signal the worker alone and allow the task timeout plus two minutes, 22 minutes with the default `--timeout`:
87
87
 
88
88
  | Supervisor | Settings |
package/dist/index.js CHANGED
@@ -1,13 +1,12 @@
1
1
  #!/usr/bin/env node
2
2
  import { createRequire } from "node:module";
3
- import { join as joinPath } from "node:path";
4
3
  import { formatEther, parseUnits } from "viem";
5
4
  import { generatePrivateKey, privateKeyToAccount } from "viem/accounts";
6
5
  import { createChain, faucetReopens, reason, unhealthy } from "./chain.js";
7
6
  import { claim, createIncome } from "./claim.js";
8
7
  import { resolveNetwork } from "./network.js";
9
8
  import { hireable, shortOfStake, snapshot, stake, usdg, wallet, when } from "./standing.js";
10
- import { fileStore, lock, stateDir } from "./state.js";
9
+ import { openStore } from "./state.js";
11
10
  import { createWork, shellCommand, stopLeftover, stopper } from "./work.js";
12
11
  const { version } = createRequire(import.meta.url)("../package.json");
13
12
  const USAGE = `brainchain-worker ${version}
@@ -39,8 +38,8 @@ Environment:
39
38
  localhost or 127.0.0.1.
40
39
 
41
40
  The command sees the worker's environment without any BRAINCHAIN_ variable, plus BRAINCHAIN_TASK_ID.
42
- Unfinished work is kept in ~/.config/brainchain-worker/<address>.json across restarts, and one
43
- run at a time works for each address.
41
+ Unfinished work is kept in ~/.config/brainchain-worker/<address>.<network>.json across restarts,
42
+ and one run at a time works for each address on each network.
44
43
 
45
44
  Docs: https://docs.usebrainchain.com/run-a-worker`;
46
45
  /** A mistake in the command line. Every other error is printed after the program's name. */
@@ -220,9 +219,7 @@ async function run(network, { key, account }, values, once) {
220
219
  throw new UsageError("--timeout is the most minutes the command may spend on one task: more than 0, at most 1440, and 20 by default");
221
220
  }
222
221
  const log = (message) => console.log(`${new Date().toISOString().slice(0, 19)}Z ${message}`);
223
- const file = joinPath(stateDir(), account.address);
224
- lock(`${file}.lock`);
225
- const store = fileStore(`${file}.json`, network.contracts.taskBoard, (message) => log(`warning: ${message}`));
222
+ const store = openStore(account.address, network, (message) => log(`warning: ${message}`));
226
223
  // The lock rules out a live run, so a command still in the state was left by one that died.
227
224
  await stopLeftover(store, log);
228
225
  const chain = createChain(network, account, store);
package/dist/network.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { defineChain, zeroAddress } from "viem";
2
2
  /** BrainChain testnet, release 3.1.0. */
3
3
  export const TESTNET = {
4
+ id: "testnet",
4
5
  name: "BrainChain testnet",
5
6
  chainId: 274630,
6
7
  rpc: "https://rpc.testnet.usebrainchain.com",
@@ -28,6 +29,7 @@ export const MAINNET_DECISION_EXECUTOR = "0xA03614B09Dcc3813eD665a1693123f2a4f2C
28
29
  const BRAIN = "0x3367085Eb6C79BfB0fCa4f1433ec77bFA8CB8a37";
29
30
  /** BrainChain mainnet, release 3.3.0. */
30
31
  export const MAINNET = {
32
+ id: "mainnet",
31
33
  name: "BrainChain",
32
34
  chainId: 274663,
33
35
  rpc: "https://rpc.usebrainchain.com",
package/dist/state.js CHANGED
@@ -90,6 +90,8 @@ function alive(pid) {
90
90
  return error.code === "EPERM";
91
91
  }
92
92
  }
93
+ const holder = (file) => (existsSync(file) ? Number(readFileSync(file, "utf8").trim()) : 0);
94
+ const held = (pid, file) => new Error(`Another run (pid ${pid}) is already working for this worker. Stop it before starting another; ${file} holds its lock.`);
93
95
  /** Takes the lock in `file` for this process until it exits; refuses while a live process holds it. */
94
96
  export function lock(file) {
95
97
  mkdirSync(dirname(file), { recursive: true, mode: 0o700 });
@@ -103,9 +105,9 @@ export function lock(file) {
103
105
  throw error;
104
106
  }
105
107
  // The holder may have released it since the write failed; then the next try takes it.
106
- const holder = existsSync(file) ? Number(readFileSync(file, "utf8").trim()) : 0;
107
- if (alive(holder))
108
- throw new Error(`Another run (pid ${holder}) is already working for this worker. Stop it before starting another; ${file} holds its lock.`);
108
+ const pid = holder(file);
109
+ if (alive(pid))
110
+ throw held(pid, file);
109
111
  rmSync(file, { force: true });
110
112
  }
111
113
  const release = () => {
@@ -115,3 +117,42 @@ export function lock(file) {
115
117
  process.once("exit", release);
116
118
  return release;
117
119
  }
120
+ /**
121
+ * Moves the state 0.3.0 and earlier kept per address alone to `file`, once,
122
+ * when it was kept for `board`. State for the other network stays where it is
123
+ * until a run there moves it.
124
+ */
125
+ function migrate(old, file, board) {
126
+ let text;
127
+ try {
128
+ text = readFileSync(`${old}.json`, "utf8");
129
+ }
130
+ catch (error) {
131
+ if (error.code === "ENOENT")
132
+ return;
133
+ throw error;
134
+ }
135
+ if (parse(text)?.board !== board.toLowerCase())
136
+ return;
137
+ // A run from before the move may still be working on this network's tasks from the old file.
138
+ const pid = holder(`${old}.lock`);
139
+ if (alive(pid))
140
+ throw held(pid, `${old}.lock`);
141
+ if (existsSync(`${file}.json`))
142
+ return;
143
+ // Removed rather than moved: this run already holds the new lock.
144
+ rmSync(`${old}.lock`, { force: true });
145
+ renameSync(`${old}.json`, `${file}.json`);
146
+ }
147
+ /**
148
+ * Takes the worker's lock on one network and loads its saved state. Each
149
+ * network has its own `<address>.<network>.json` and `.lock`, so one key can
150
+ * run on testnet and mainnet at once without sharing cursors or tasks.
151
+ */
152
+ export function openStore(address, network, warn, dir = stateDir()) {
153
+ const old = join(dir, address);
154
+ const file = `${old}.${network.id}`;
155
+ lock(`${file}.lock`);
156
+ migrate(old, file, network.contracts.taskBoard);
157
+ return fileStore(`${file}.json`, network.contracts.taskBoard, warn);
158
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "brainchain-worker",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
4
4
  "description": "Earn on BrainChain: stake, apply, and do the tasks Jev hires your agent for, with any command that reads a task and prints the work.",
5
5
  "license": "MIT",
6
6
  "author": "BrainChain <hello@usebrainchain.com>",