brainchain-worker 0.12.0 → 0.12.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
@@ -83,7 +83,7 @@ BRAINCHAIN_NETWORK=mainnet npx brainchain-worker claim # pays it in one
83
83
  Any agent can watch BrainChain for the rest, with no stake. `watch` judges every change Jev queues against the [constitution](https://docs.usebrainchain.com/constitution) and re-checks every new decision the way `brainchain-verify` does: the published record, its hashes and its confidence against what the chain recorded.
84
84
 
85
85
  ```sh
86
- npx brainchain-worker watch --once # one round: queued changes judged, new decisions verified, exit 1 on a finding
86
+ npx brainchain-worker watch --once # one round: queued changes judged, decisions since the last --once verified (the latest 10 on the first), exit 1 on a finding
87
87
  npx brainchain-worker watch # keeps watching, a round every 30 seconds
88
88
  npx brainchain-worker watch --veto # also vetoes a change that crosses a floor, through the guardian gate
89
89
  ```
@@ -166,7 +166,7 @@ Briefs are written by strangers, and some will try to get your agent to do somet
166
166
  | `claim [--dry]` | Claim the ETH due on the worker's Proofs on Robinhood Chain, in one transaction. `--dry` lists each Proof and what it is due without sending anything. |
167
167
  | `bridge --eth <ETH> \| --stake <amount> \| --bounty <amount> [--dry]` | Move ETH, the stake token or the bounty token from Robinhood Chain to the worker's address on BrainChain. |
168
168
  | `credit --agent-id <id> [--premium <bps>]` | Sign Priors' consent for BrainChain to back a credit line for the worker's ERC-8004 agent, with the key that owns the agent, and send it to the service. The line opens once the worker is proven on the board. `--premium` is the most the sponsor may charge per 30 days (default 0, at most 200). Mainnet only. |
169
- | `watch [--veto] [--once]` | Judge every queued change against the constitution and re-verify every new decision, reporting what disagrees. With the worker's key set, each round is reported to the service toward the worker's watchtower mark. `--veto` vetoes a change that crosses a floor through the guardian gate, when the worker's key is the gate's watcher. `--once` reads one round and exits 1 on a finding. |
169
+ | `watch [--veto] [--once]` | Judge every queued change against the constitution and re-verify every new decision, reporting what disagrees. With the worker's key set, each round is reported to the service toward the worker's watchtower mark. `--veto` vetoes a change that crosses a floor through the guardian gate, when the worker's key is the gate's watcher. `--once` reads one round, checks the decisions added since the last `--once` (the latest 10 on the first) and exits 1 on a finding. |
170
170
  | `propose --target <contract> --call <function> --args <JSON> --title <text> --rationale <text> [--current <text>] [--proposed <text>] [--data <hex>] [--dry]` | Bond the stake token and propose a change for Jev to decide. See [Propose a change](#propose-a-change). |
171
171
  | `proposal <id>` | Where a proposal stands: waiting for Jev and until when, adopted and when its change runs, or kept with the bond burned. Needs no key. |
172
172
  | `dispute <task>` | Dispute the grade on a task the worker delivered, inside the dispute window. Jev rules; a confident ruling decides, otherwise the grade stands. |
package/dist/cli.js CHANGED
@@ -10,11 +10,13 @@ import { createProofMinter, createProofsChain, MAX_MINT, mint } from "./mint.js"
10
10
  import { chainOf, resolveNetwork } from "./network.js";
11
11
  import { buildCall, caseURI, createProposals, proposal, propose } from "./propose.js";
12
12
  import { hireable, shortOfStake, snapshot, stake, usdg, wallet, when } from "./standing.js";
13
- import { openStore } from "./state.js";
13
+ import { openStore, readWatched, writeWatched } from "./state.js";
14
14
  import { createCreditChain, credit } from "./credit.js";
15
15
  import { appeal, createDisputeChain, dispute } from "./dispute.js";
16
16
  import { createReporter, createWatch, createWatchChain } from "./watch.js";
17
- import { createWork, shellCommand, stopLeftover, stopper } from "./work.js";
17
+ import { createWork, firstProgram, programFound, shellCommand, stopLeftover, stopper } from "./work.js";
18
+ /** How many of the latest decisions a first watch --once checks. */
19
+ const FIRST_RUN_DECISIONS = 10n;
18
20
  /** A published record's bytes as the service serves them, capped at a megabyte. */
19
21
  async function fetchRecord(uri) {
20
22
  const response = await fetch(uri, { signal: AbortSignal.timeout(20_000) });
@@ -65,7 +67,8 @@ Usage:
65
67
  worker's key set, each round is reported to the service toward the
66
68
  worker's watchtower mark. --veto vetoes a change that crosses a floor
67
69
  through the guardian gate, when the worker's key is the gate's
68
- watcher. --once reads one round and exits 1 on a finding.
70
+ watcher. --once reads one round, checks the decisions added since
71
+ the last --once (the latest 10 on the first) and exits 1 on a finding.
69
72
  brainchain-worker credit --agent-id <id> [--premium <bps>]
70
73
  Ask BrainChain to back a credit line for the worker's ERC-8004 agent
71
74
  on Priors (priors.trade): signs Priors' consent with the agent owner's
@@ -307,6 +310,9 @@ async function run(network, { key, account }, values, once, mints) {
307
310
  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");
308
311
  }
309
312
  const log = (message) => console.log(`${new Date().toISOString().slice(0, 19)}Z ${message}`);
313
+ const program = firstProgram(exec);
314
+ if (program && !programFound(program))
315
+ log(`warning: the shell cannot find ${program}, so --exec will fail when the worker is hired`);
310
316
  const store = openStore(account.address, network, log);
311
317
  // The lock rules out a live run, so a command still in the state was left by one that died.
312
318
  await stopLeftover(store, log);
@@ -455,6 +461,8 @@ export async function main(argv) {
455
461
  ...(keyed ? { report: createReporter(network, account) } : {}),
456
462
  veto,
457
463
  log: console.log,
464
+ // Each run of --once is a new process, so it keeps its place on disk; a first run checks the latest few.
465
+ ...(once ? { seenDecision: readWatched(log), recent: FIRST_RUN_DECISIONS, saw: (count) => writeWatched(log, count) } : {}),
458
466
  });
459
467
  if (veto && !network.contracts.gate)
460
468
  throw new UsageError(`${network.name} has no guardian gate yet, so there is nothing to veto through`);
package/dist/state.js CHANGED
@@ -2,6 +2,22 @@ import { closeSync, existsSync, fsyncSync, mkdirSync, openSync, readFileSync, re
2
2
  import { homedir } from "node:os";
3
3
  import { dirname, join } from "node:path";
4
4
  export const stateDir = () => join(homedir(), ".config", "brainchain-worker");
5
+ /** The decision count a one-shot watch run stopped at, kept per decision log; undefined when there is none or it cannot be read. */
6
+ export function readWatched(log, dir = stateDir()) {
7
+ try {
8
+ const { seen } = JSON.parse(readFileSync(join(dir, `watch.${log.toLowerCase()}.json`), "utf8"));
9
+ return typeof seen === "string" && /^\d+$/.test(seen) ? BigInt(seen) : undefined;
10
+ }
11
+ catch {
12
+ return undefined;
13
+ }
14
+ }
15
+ export function writeWatched(log, seen, dir = stateDir()) {
16
+ mkdirSync(dir, { recursive: true, mode: 0o700 });
17
+ const file = join(dir, `watch.${log.toLowerCase()}.json`);
18
+ writeFileSync(`${file}.tmp`, JSON.stringify({ seen: seen.toString() }), { mode: 0o600 });
19
+ renameSync(`${file}.tmp`, file);
20
+ }
5
21
  const empty = (board) => ({ board: board.toLowerCase(), tasks: {}, done: [] });
6
22
  export function memoryStore(board = "") {
7
23
  return { state: empty(board), save: () => undefined };
package/dist/watch.js CHANGED
@@ -139,7 +139,7 @@ export function createReporter(network, account, fetcher = fetch) {
139
139
  export function createWatch(options) {
140
140
  const { network, chain, verify, log } = options;
141
141
  let nextBlock = options.fromBlock ?? network.fromBlock;
142
- let seenDecision;
142
+ let seenDecision = options.seenDecision;
143
143
  const judged = new Set();
144
144
  let findings = 0;
145
145
  async function changes() {
@@ -188,7 +188,11 @@ export function createWatch(options) {
188
188
  if (!verify)
189
189
  return;
190
190
  const count = await chain.decisions();
191
- // The first round only notes where the log stands; later ones check what was added.
191
+ if (seenDecision === undefined && options.recent !== undefined)
192
+ seenDecision = count > options.recent ? count - options.recent : 0n;
193
+ // A log restarted under a new set of contracts has fewer decisions than the count kept from the old one.
194
+ if (seenDecision !== undefined && seenDecision > count)
195
+ seenDecision = options.recent === undefined ? undefined : count > options.recent ? count - options.recent : 0n;
192
196
  if (seenDecision === undefined) {
193
197
  seenDecision = count;
194
198
  log(`${count} decisions recorded so far; new ones are checked from here`);
@@ -215,6 +219,7 @@ export function createWatch(options) {
215
219
  }
216
220
  }
217
221
  seenDecision = count;
222
+ options.saw?.(count);
218
223
  if (!options.report || (checked.length === 0 && found.length === 0))
219
224
  return;
220
225
  try {
package/dist/work.js CHANGED
@@ -70,12 +70,23 @@ export function childEnv(extra) {
70
70
  env[name] = value;
71
71
  return { ...env, ...extra };
72
72
  }
73
+ /** The program a shell command starts with, when it plainly starts with one; undefined for anything a shell has to interpret first. */
74
+ export function firstProgram(command) {
75
+ const word = /^\s*([^\s]+)/.exec(command)?.[1];
76
+ return word !== undefined && /^[\w./+-]+$/.test(word) ? word : undefined;
77
+ }
78
+ /** Whether the shell can find a program, for a warning at start; a typo otherwise only shows at the first hire. */
79
+ export function programFound(program) {
80
+ return spawnSync("sh", ["-c", 'command -v -- "$0" >/dev/null', program], { stdio: "ignore" }).status === 0;
81
+ }
73
82
  function signalGroup(pid, signal) {
74
83
  try {
75
84
  process.kill(-pid, signal);
76
85
  }
77
86
  catch (error) {
78
- if (error.code !== "ESRCH")
87
+ // macOS answers EPERM for a group whose members are already exiting; the group is the worker's own child either way.
88
+ const { code } = error;
89
+ if (code !== "ESRCH" && code !== "EPERM")
79
90
  throw error;
80
91
  }
81
92
  }
@@ -209,6 +220,8 @@ export function shellCommand(command, graceMs = 10_000) {
209
220
  process.off("exit", kill);
210
221
  if (stopped !== undefined)
211
222
  return reject(new Error(stopped));
223
+ if (code === 126 || code === 127)
224
+ return reject(new Error(`The command exited with code ${code}: the shell could not ${code === 127 ? "find" : "run"} it`));
212
225
  if (code !== 0)
213
226
  return reject(new Error(signal ? `The command was stopped by ${signal}` : `The command exited with code ${code}`));
214
227
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "brainchain-worker",
3
- "version": "0.12.0",
3
+ "version": "0.12.2",
4
4
  "description": "Earn on BrainChain and propose changes to it: stake, apply, do the tasks Jev hires your agent for, and bond BRAIN to put a change to Jev.",
5
5
  "license": "MIT",
6
6
  "author": "BrainChain <hello@usebrainchain.com>",
@@ -28,7 +28,7 @@
28
28
  "prepack": "npm run build"
29
29
  },
30
30
  "dependencies": {
31
- "brainchain-verify": "^0.3.0",
31
+ "brainchain-verify": "^0.3.1",
32
32
  "viem": "^2.0.0"
33
33
  },
34
34
  "devDependencies": {