@wenrwa/marketplace-cli 0.8.6 → 0.8.7

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.
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,135 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const processCpu_1 = require("../bridge/processCpu");
4
+ const relayClient_1 = require("../bridge/relayClient");
5
+ const sessionManager_1 = require("../bridge/sessionManager");
6
+ /**
7
+ * Activity-based sleep — the bridge tells the backend which sessions are
8
+ * burning CPU with no output (a silent build), so a box is never suspended
9
+ * mid-work; an idle dev server (alive, ~0 CPU) never holds a box awake.
10
+ */
11
+ const stat = (pid, ppid, utime, stime = 0, cutime = 0, cstime = 0, comm = 'node') => `${pid} (${comm}) S ${ppid} ${pid} ${pid} 0 -1 4194560 100 0 0 0 ${utime} ${stime} ${cutime} ${cstime} 20 0 1 0 100 1000 100`;
12
+ describe('parseProcStat', () => {
13
+ it('reads ppid and utime+stime+cutime+cstime', () => {
14
+ expect((0, processCpu_1.parseProcStat)(stat(10, 1, 5, 6, 7, 8))).toEqual({ ppid: 1, ticks: 26 });
15
+ });
16
+ it('survives a command name with spaces and parentheses', () => {
17
+ expect((0, processCpu_1.parseProcStat)(stat(10, 3, 1, 1, 0, 0, 'tmux: (server) x'))).toEqual({ ppid: 3, ticks: 2 });
18
+ });
19
+ it('rejects garbage', () => {
20
+ expect((0, processCpu_1.parseProcStat)('nonsense')).toBeNull();
21
+ expect((0, processCpu_1.parseProcStat)('1 (x) S 0')).toBeNull();
22
+ });
23
+ });
24
+ describe('treeTicks', () => {
25
+ it('sums the root and every descendant, nothing else', () => {
26
+ const t = new Map([
27
+ [100, { ppid: 1, ticks: 10 }], // pty shell
28
+ [101, { ppid: 100, ticks: 20 }], // claude
29
+ [102, { ppid: 101, ticks: 300 }], // npm test
30
+ [200, { ppid: 1, ticks: 999 }], // another session
31
+ ]);
32
+ expect((0, processCpu_1.treeTicks)(t, 100)).toBe(330);
33
+ expect((0, processCpu_1.treeTicks)(t, 200)).toBe(999);
34
+ expect((0, processCpu_1.treeTicks)(t, 404)).toBe(0);
35
+ });
36
+ });
37
+ describe('createCpuBusyTracker', () => {
38
+ const tracker = (tables, times) => {
39
+ let i = 0;
40
+ let j = 0;
41
+ return (0, processCpu_1.createCpuBusyTracker)({ readTable: () => tables[i++], now: () => times[j++], clkTck: 100, thresholdPct: 20 });
42
+ };
43
+ it('first sight only sets a baseline; then ≥20% of a core is busy, an idle tree is not', () => {
44
+ const tr = tracker([
45
+ new Map([[100, { ppid: 1, ticks: 0 }], [200, { ppid: 1, ticks: 0 }]]),
46
+ // 30s later: session A's tree used 15s CPU (50%), B's 1s (3%: an idle dev server)
47
+ new Map([[100, { ppid: 1, ticks: 0 }], [101, { ppid: 100, ticks: 1500 }], [200, { ppid: 1, ticks: 100 }]]),
48
+ ], [0, 30_000]);
49
+ const sessions = [{ id: 'A', pid: 100 }, { id: 'B', pid: 200 }];
50
+ expect(tr.sample(sessions)).toEqual([]);
51
+ expect(tr.sample(sessions)).toEqual(['A']);
52
+ });
53
+ it('a reaped child lowering the total never reads as negative or busy', () => {
54
+ const tr = tracker([new Map([[100, { ppid: 1, ticks: 5000 }]]), new Map([[100, { ppid: 1, ticks: 10 }]])], [0, 30_000]);
55
+ tr.sample([{ id: 'A', pid: 100 }]);
56
+ expect(tr.sample([{ id: 'A', pid: 100 }])).toEqual([]);
57
+ });
58
+ it('a session with no pid, or a pid no longer running, is skipped', () => {
59
+ const tr = tracker([new Map(), new Map()], [0, 30_000]);
60
+ expect(tr.sample([{ id: 'A', pid: undefined }, { id: 'B', pid: 9 }])).toEqual([]);
61
+ });
62
+ it('a respawned PTY (new pid) starts a fresh baseline', () => {
63
+ const tr = tracker([new Map([[100, { ppid: 1, ticks: 0 }]]), new Map([[300, { ppid: 1, ticks: 9000 }]])], [0, 30_000]);
64
+ tr.sample([{ id: 'A', pid: 100 }]);
65
+ expect(tr.sample([{ id: 'A', pid: 300 }])).toEqual([]);
66
+ });
67
+ });
68
+ describe('`sessions` frame carries busy only when a tracker is wired', () => {
69
+ class FakePty {
70
+ pid;
71
+ constructor(pid) {
72
+ this.pid = pid;
73
+ }
74
+ write() { }
75
+ resize() { }
76
+ kill() { }
77
+ onData() { }
78
+ onExit() { }
79
+ }
80
+ let nextPid = 100;
81
+ const factory = { spawn: () => new FakePty(nextPid++) };
82
+ const fakeSocket = () => {
83
+ const handlers = new Map();
84
+ const sock = {
85
+ readyState: 1,
86
+ sent: [],
87
+ send(raw) { sock.sent.push(JSON.parse(raw)); },
88
+ close() { handlers.get('close')?.(1006); },
89
+ terminate() { handlers.get('close')?.(1006); },
90
+ on(event, cb) { handlers.set(event, cb); },
91
+ fire(event, ...a) { handlers.get(event)?.(...a); },
92
+ };
93
+ return sock;
94
+ };
95
+ const start = (cpuBusyTracker) => {
96
+ const socket = fakeSocket();
97
+ const sessions = new sessionManager_1.SessionManager(factory);
98
+ const intervals = [];
99
+ const handle = (0, relayClient_1.startRelayClient)({
100
+ relayUrl: 'ws://relay.test',
101
+ relayToken: 'tok',
102
+ sessions,
103
+ wsFactory: () => socket,
104
+ reportSessions: true,
105
+ cpuBusyTracker,
106
+ setSessionsIntervalFn: (cb) => { intervals.push(cb); return intervals.length; },
107
+ clearSessionsIntervalFn: () => { },
108
+ setTimeoutFn: () => 0,
109
+ clearTimeoutFn: () => { },
110
+ });
111
+ return { socket, sessions, handle, tick: () => intervals.forEach((cb) => cb()) };
112
+ };
113
+ it('with a tracker: the frame lists the busy ids, and the tracker sees each session pid', () => {
114
+ const seen = [];
115
+ const { socket, sessions, handle, tick } = start({ sample: (s) => { seen.push(s); return ['agt-1']; } });
116
+ socket.fire('open');
117
+ sessions.open({ cols: 80, rows: 24, id: 'agt-1' });
118
+ tick();
119
+ expect(socket.sent.filter((f) => f.t === 'sessions').at(-1)).toEqual({ t: 'sessions', ids: ['agt-1'], busy: ['agt-1'] });
120
+ expect(seen.at(-1)).toEqual([{ id: 'agt-1', pid: expect.any(Number) }]);
121
+ handle.close();
122
+ });
123
+ it('a tracker that throws still sends the inventory, without busy', () => {
124
+ const { socket, handle } = start({ sample: () => { throw new Error('proc gone'); } });
125
+ socket.fire('open');
126
+ expect(socket.sent.filter((f) => f.t === 'sessions').at(-1)).toEqual({ t: 'sessions', ids: [] });
127
+ handle.close();
128
+ });
129
+ it('no tracker (local bridge, non-Linux) → byte-identical frame', () => {
130
+ const { socket, handle } = start(null);
131
+ socket.fire('open');
132
+ expect(socket.sent.filter((f) => f.t === 'sessions').at(-1)).toEqual({ t: 'sessions', ids: [] });
133
+ handle.close();
134
+ });
135
+ });
@@ -0,0 +1,29 @@
1
+ export interface ProcEntry {
2
+ ppid: number;
3
+ /** utime + stime + cutime + cstime, in clock ticks. */
4
+ ticks: number;
5
+ }
6
+ /** Parse one /proc/<pid>/stat line. The command name sits in parentheses and
7
+ * may itself contain spaces or parentheses, so split after the LAST ')'. */
8
+ export declare function parseProcStat(line: string): ProcEntry | null;
9
+ export declare function readProcTable(procRoot?: string): Map<number, ProcEntry>;
10
+ /** Total ticks of `root` and every live descendant. */
11
+ export declare function treeTicks(table: Map<number, ProcEntry>, root: number): number;
12
+ export interface CpuBusyTrackerOptions {
13
+ readTable?: () => Map<number, ProcEntry>;
14
+ now?: () => number;
15
+ /** Clock ticks per second (Linux USER_HZ — 100 on every mainstream kernel). */
16
+ clkTck?: number;
17
+ /** Busy at or above this share of one core, averaged since the last sample. */
18
+ thresholdPct?: number;
19
+ }
20
+ export interface CpuBusyTracker {
21
+ /** Session ids whose tree used ≥ threshold CPU since the previous sample.
22
+ * A session seen for the first time only sets its baseline. */
23
+ sample(sessions: Array<{
24
+ id: string;
25
+ pid: number | undefined;
26
+ }>): string[];
27
+ }
28
+ export declare const DEFAULT_BUSY_CPU_PCT = 20;
29
+ export declare function createCpuBusyTracker(opts?: CpuBusyTrackerOptions): CpuBusyTracker | null;
@@ -0,0 +1,160 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.DEFAULT_BUSY_CPU_PCT = void 0;
37
+ exports.parseProcStat = parseProcStat;
38
+ exports.readProcTable = readProcTable;
39
+ exports.treeTicks = treeTicks;
40
+ exports.createCpuBusyTracker = createCpuBusyTracker;
41
+ /**
42
+ * Activity-based sleep — which PTY sessions are doing work nobody can see?
43
+ *
44
+ * The backend suspends an idle box (HT_ACTIVITY_IDLE=1). Output and keystrokes
45
+ * already count as activity; this covers the silent rest: a build, a test run
46
+ * or an install a backgrounded shell runs while the agent sits at its prompt.
47
+ * We measure CPU, not "is a child process alive", on purpose: an idle dev
48
+ * server is alive forever and must not hold a box awake, but one actually
49
+ * serving requests burns CPU and does.
50
+ *
51
+ * Linux only (reads /proc). Elsewhere `createCpuBusyTracker` returns null and
52
+ * the `sessions` frame carries no `busy` field, which the backend reads as
53
+ * unknown — never as busy.
54
+ *
55
+ * A session's cost is its whole process tree: utime+stime of every live
56
+ * process under the PTY, plus cutime+cstime, which already holds the CPU of
57
+ * children that have exited and been reaped (an install's short-lived compiler
58
+ * runs). A reaped intermediate parent takes its children's time with it; the
59
+ * delta is clamped at 0, so that can only under-report.
60
+ */
61
+ const fs = __importStar(require("fs"));
62
+ const path = __importStar(require("path"));
63
+ /** Parse one /proc/<pid>/stat line. The command name sits in parentheses and
64
+ * may itself contain spaces or parentheses, so split after the LAST ')'. */
65
+ function parseProcStat(line) {
66
+ const close = line.lastIndexOf(')');
67
+ if (close < 0)
68
+ return null;
69
+ const f = line.slice(close + 2).trim().split(/\s+/);
70
+ // f[0]=state f[1]=ppid … f[11]=utime f[12]=stime f[13]=cutime f[14]=cstime
71
+ if (f.length < 15)
72
+ return null;
73
+ const nums = [f[1], f[11], f[12], f[13], f[14]].map(Number);
74
+ if (nums.some((n) => !Number.isFinite(n)))
75
+ return null;
76
+ return { ppid: nums[0], ticks: nums[1] + nums[2] + nums[3] + nums[4] };
77
+ }
78
+ function readProcTable(procRoot = '/proc') {
79
+ const table = new Map();
80
+ let names;
81
+ try {
82
+ names = fs.readdirSync(procRoot);
83
+ }
84
+ catch {
85
+ return table;
86
+ }
87
+ for (const name of names) {
88
+ if (!/^\d+$/.test(name))
89
+ continue;
90
+ try {
91
+ const e = parseProcStat(fs.readFileSync(path.join(procRoot, name, 'stat'), 'utf8'));
92
+ if (e)
93
+ table.set(Number(name), e);
94
+ }
95
+ catch { /* exited between readdir and read */ }
96
+ }
97
+ return table;
98
+ }
99
+ /** Total ticks of `root` and every live descendant. */
100
+ function treeTicks(table, root) {
101
+ const children = new Map();
102
+ for (const [pid, e] of table) {
103
+ const list = children.get(e.ppid);
104
+ if (list)
105
+ list.push(pid);
106
+ else
107
+ children.set(e.ppid, [pid]);
108
+ }
109
+ let total = 0;
110
+ const stack = [root];
111
+ const seen = new Set();
112
+ while (stack.length) {
113
+ const pid = stack.pop();
114
+ if (seen.has(pid))
115
+ continue;
116
+ seen.add(pid);
117
+ const e = table.get(pid);
118
+ if (e)
119
+ total += e.ticks;
120
+ for (const c of children.get(pid) ?? [])
121
+ stack.push(c);
122
+ }
123
+ return total;
124
+ }
125
+ exports.DEFAULT_BUSY_CPU_PCT = 20;
126
+ function createCpuBusyTracker(opts = {}) {
127
+ const readTable = opts.readTable ?? (process.platform === 'linux' && fs.existsSync('/proc/self/stat') ? () => readProcTable() : null);
128
+ if (!readTable)
129
+ return null;
130
+ const now = opts.now ?? Date.now;
131
+ const clkTck = opts.clkTck ?? 100;
132
+ const thresholdPct = opts.thresholdPct ?? (Number(process.env.HIVEOP_BUSY_CPU_PCT) || exports.DEFAULT_BUSY_CPU_PCT);
133
+ const last = new Map();
134
+ return {
135
+ sample(sessions) {
136
+ const table = readTable();
137
+ const at = now();
138
+ const busy = [];
139
+ const live = new Set();
140
+ for (const { id, pid } of sessions) {
141
+ if (typeof pid !== 'number' || !table.has(pid))
142
+ continue;
143
+ live.add(id);
144
+ const ticks = treeTicks(table, pid);
145
+ const prev = last.get(id);
146
+ last.set(id, { pid, ticks, at });
147
+ if (!prev || prev.pid !== pid || at <= prev.at)
148
+ continue;
149
+ const cpuSeconds = Math.max(0, ticks - prev.ticks) / clkTck;
150
+ const pct = (cpuSeconds / ((at - prev.at) / 1000)) * 100;
151
+ if (pct >= thresholdPct)
152
+ busy.push(id);
153
+ }
154
+ for (const id of [...last.keys()])
155
+ if (!live.has(id))
156
+ last.delete(id);
157
+ return busy;
158
+ },
159
+ };
160
+ }
@@ -98,6 +98,7 @@ function realPtyFactory() {
98
98
  kill: () => proc.kill(),
99
99
  onData: (cb) => proc.onData(cb),
100
100
  onExit: (cb) => proc.onExit(({ exitCode }) => cb(exitCode)),
101
+ pid: typeof proc.pid === 'number' ? proc.pid : undefined,
101
102
  };
102
103
  },
103
104
  };
@@ -1,6 +1,7 @@
1
1
  import { type OpencodeLaunchInput } from './agentLaunch';
2
2
  import type { PreparedLaunch } from './opencodeBinary';
3
3
  import { SessionManager } from './sessionManager';
4
+ import type { CpuBusyTracker } from './processCpu';
4
5
  import { MemInfo } from './containerMemory';
5
6
  import { BrowserDriver } from './browserDriver';
6
7
  import { BrowserIdleTracker } from './browserIdle';
@@ -404,6 +405,14 @@ export interface RelayClientOpts {
404
405
  * — see cli/src/commands/bridge.ts's relay branch.
405
406
  */
406
407
  reportSessions?: boolean;
408
+ /**
409
+ * Activity-based sleep — add `busy` (ids whose process tree burned CPU
410
+ * since the previous report) to every `sessions` frame, so the backend does
411
+ * not suspend a box mid silent build. Rides the inventory report, so it
412
+ * needs reportSessions too. Absent/null (the default, and any non-Linux
413
+ * host) → the frame is byte-identical to before. See processCpu.ts.
414
+ */
415
+ cpuBusyTracker?: CpuBusyTracker | null;
407
416
  /**
408
417
  * tchp-02 — opt IN to per-session working-tree change detection (`changes`
409
418
  * frame). Mirrors reportSessions exactly: absent (the default) → no
@@ -596,7 +596,17 @@ function startRelayClient(opts) {
596
596
  // machine the platform is not supposed to see.
597
597
  if (opts.onlyRelayOwnedSessions)
598
598
  ids = ids.filter((id) => relayOwned.has(id));
599
- send(ws, { t: 'sessions', ids });
599
+ let busy;
600
+ if (opts.cpuBusyTracker) {
601
+ try {
602
+ const pidOf = opts.sessions.pidOf?.bind(opts.sessions);
603
+ busy = opts.cpuBusyTracker.sample(ids.map((id) => ({ id, pid: pidOf?.(id) })));
604
+ }
605
+ catch {
606
+ busy = undefined; // a failed sample must never drop the inventory
607
+ }
608
+ }
609
+ send(ws, busy ? { t: 'sessions', ids, busy } : { t: 'sessions', ids });
600
610
  }
601
611
  /**
602
612
  * tchp-02 — the ACTIVE sessions worth polling for working-tree changes:
@@ -7,6 +7,9 @@ export interface PtyLike {
7
7
  kill(): void;
8
8
  onData(cb: (data: string) => void): void;
9
9
  onExit(cb: (code: number) => void): void;
10
+ /** OS process id of the PTY's child, when the factory knows it (node-pty
11
+ * does). Read by the CPU-busy report (processCpu.ts). */
12
+ readonly pid?: number;
10
13
  }
11
14
  export interface PtyFactory {
12
15
  /**
@@ -198,6 +201,9 @@ export declare class SessionManager {
198
201
  private recoverSerializeWedge;
199
202
  close(sessionId: string): void;
200
203
  list(): string[];
204
+ /** The PTY child's pid, or undefined (unknown session, or a factory that
205
+ * does not expose one). */
206
+ pidOf(sessionId: string): number | undefined;
201
207
  /**
202
208
  * fcct-34 — READ-ONLY look at what a session currently shows.
203
209
  *
@@ -367,6 +367,11 @@ class SessionManager {
367
367
  list() {
368
368
  return [...this.sessions.keys()];
369
369
  }
370
+ /** The PTY child's pid, or undefined (unknown session, or a factory that
371
+ * does not expose one). */
372
+ pidOf(sessionId) {
373
+ return this.sessions.get(sessionId)?.pty.pid;
374
+ }
370
375
  /**
371
376
  * fcct-34 — READ-ONLY look at what a session currently shows.
372
377
  *
@@ -52,6 +52,7 @@ exports.resolveBridgeInstallEnv = resolveBridgeInstallEnv;
52
52
  * inside the terminal (their subscription, Anthropic's client) — this
53
53
  * process never drives it (ADR-0028 in wenrwa-arena).
54
54
  */
55
+ const processCpu_1 = require("../bridge/processCpu");
55
56
  const crypto = __importStar(require("crypto"));
56
57
  const fs = __importStar(require("fs"));
57
58
  const os = __importStar(require("os"));
@@ -650,6 +651,9 @@ exports.bridgeCommand = new commander_1.Command('bridge')
650
651
  // Deliberately NOT set on the local bridge below — a local agent has no
651
652
  // hosted lifecycle to reconcile.
652
653
  reportSessions: true,
654
+ // Activity-based sleep: which sessions are burning CPU with no output
655
+ // (a silent build) — null off Linux, which sends no `busy` at all.
656
+ cpuBusyTracker: (0, processCpu_1.createCpuBusyTracker)(),
653
657
  // tchp-02 — per-session working-tree change detector (`changes`
654
658
  // frame). Same posture as reportSessions just above: this IS the
655
659
  // hosted container bridge, so every session is relay-owned by
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wenrwa/marketplace-cli",
3
- "version": "0.8.6",
3
+ "version": "0.8.7",
4
4
  "description": "CLI tool for the Wenrwa Agent Marketplace — monitor bounties, agents, workspaces, and treasury from your terminal",
5
5
  "author": "Wenrwa <dev@wenrwa.com>",
6
6
  "license": "MIT",