@openagentsinc/pylon 0.1.13 → 0.1.14

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
@@ -15,6 +15,7 @@ npm install -g @openagentsinc/pylon && pylon
15
15
  bun install -g @openagentsinc/pylon && pylon
16
16
  npx @openagentsinc/pylon --version 0.1.13
17
17
  npx @openagentsinc/pylon --no-launch
18
+ npx @openagentsinc/pylon --no-updates
18
19
  npx @openagentsinc/pylon --download-curated-cache --model gemma-4-e2b --run-diagnostics
19
20
  npx @openagentsinc/pylon --verbose
20
21
  ```
@@ -25,6 +26,7 @@ The launcher:
25
26
  `bun install -g` installs with the same `pylon` command
26
27
  - checks GitHub for the latest tagged `pylon-v...` release on each default run,
27
28
  or resolves a specific tagged `Pylon` version when `--version` is provided
29
+ - only installs releases initiated by `AtlantisPleb` in GitHub Releases
28
30
  - resolves the correct `pylon-v<version>-<os>-<arch>.tar.gz` asset for the
29
31
  current machine
30
32
  - falls back to the exact tagged source checkout and builds `pylon` plus
@@ -57,12 +59,17 @@ The launcher:
57
59
  - starts the installed `pylon-tui` by default after the smoke path; that TUI
58
60
  starts and supervises the earning worker
59
61
  unless `--no-launch` is set
60
- - for hosted homework/training work, use `0.1.13` or newer. That release keeps
61
- the `0.1.12` Mac training-worker launch fix, removes the last legacy runtime
62
- wording from the public bootstrap/runtime path, keeps runtime management
63
- explicit instead of mutating the host automatically, prefers a current
64
- `target/release/psionic-train` binary, and falls back to
65
- `cargo run --release` instead of debug `cargo run`
62
+ - while the TUI is running on the default release track, checks GitHub Releases
63
+ every 30 seconds and restarts the TUI from a newer trusted cached release
64
+ without replacing the global npm/bun command
65
+ - use `--no-updates` to keep the current installed release running without
66
+ background GitHub release checks; `--version` remains a pinned release run
67
+ - for hosted homework/training work, use launcher `0.1.14` or newer so the
68
+ cached standalone binary auto-updates while the dashboard is open. The latest
69
+ trusted standalone binary still carries the `0.1.13` and `0.1.12` runtime
70
+ fixes: no legacy runtime wording, explicit runtime management, current
71
+ `target/release/psionic-train` preference, and `cargo run --release`
72
+ fallback instead of debug `cargo run`
66
73
  - does not try to install or register a local runtime automatically; the
67
74
  bootstrap stays honest about the separate local Gemma runtime
68
75
  prerequisite instead of mutating the host behind the user's back
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openagentsinc/pylon",
3
- "version": "0.1.13",
3
+ "version": "0.1.14",
4
4
  "description": "Bootstrap the standalone OpenAgents Pylon release asset and run first-run smoke checks.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/cli.js CHANGED
@@ -7,6 +7,7 @@ import {
7
7
  bootstrapInstalledPylon,
8
8
  ensureReleaseInstall,
9
9
  launchInstalledPylon,
10
+ launchInstalledPylonWithUpdates,
10
11
  resolveBootstrapOutcome,
11
12
  resolvePlatformTarget,
12
13
  renderBootstrapSummary,
@@ -85,9 +86,10 @@ Description:
85
86
  and build it locally instead. Cache the binaries, run the first-run smoke
86
87
  path, and then start the Pylon terminal UI by default. The terminal UI manages
87
88
  the earning worker and keeps live status visible. The launcher checks GitHub
88
- for newer tagged pylon-v... releases on
89
- each default run, but only caches the standalone binaries under the local
90
- bootstrap root; it does not replace your global npm or bun pylon command.
89
+ for newer tagged pylon-v... releases on each default run and every 30 seconds
90
+ while the dashboard is open. Only releases initiated by AtlantisPleb are
91
+ accepted. New standalone binaries are cached under the local bootstrap root;
92
+ the global npm or bun pylon command is not replaced.
91
93
 
92
94
  Options:
93
95
  --version <x.y.z> Resolve a specific Pylon release.
@@ -107,6 +109,8 @@ Options:
107
109
  --skip-model-download Keep the curated GGUF cache skipped.
108
110
  --skip-diagnostics Keep optional pylon gemma diagnose skipped.
109
111
  --no-launch Do not start pylon-tui after bootstrap.
112
+ --no-updates Disable 30-second GitHub release polling
113
+ and dashboard restart while pylon runs.
110
114
  --verbose Print extra network and recovery detail.
111
115
  --debug-network Alias for --verbose.
112
116
  --json Emit a machine-readable JSON summary.
@@ -132,6 +136,7 @@ export function parseArgs(argv) {
132
136
  skipModelDownload: true,
133
137
  skipDiagnostics: true,
134
138
  noLaunch: false,
139
+ noUpdates: false,
135
140
  verbose: false,
136
141
  json: false,
137
142
  help: false,
@@ -197,6 +202,9 @@ export function parseArgs(argv) {
197
202
  case "--no-launch":
198
203
  options.noLaunch = true;
199
204
  break;
205
+ case "--no-updates":
206
+ options.noUpdates = true;
207
+ break;
200
208
  case "--verbose":
201
209
  case "--debug-network":
202
210
  options.verbose = true;
@@ -232,7 +240,7 @@ export async function main(argv = process.argv.slice(2), dependencies = {}) {
232
240
  const {
233
241
  ensureReleaseInstallImpl = ensureReleaseInstall,
234
242
  bootstrapInstalledPylonImpl = bootstrapInstalledPylon,
235
- launchInstalledPylonImpl = launchInstalledPylon,
243
+ launchInstalledPylonImpl = launchInstalledPylonWithUpdates,
236
244
  createTelemetryClientImpl = createTelemetryClient,
237
245
  } = dependencies;
238
246
  const options = parseArgs(argv);
@@ -318,10 +326,12 @@ export async function main(argv = process.argv.slice(2), dependencies = {}) {
318
326
  ...options,
319
327
  ...install,
320
328
  version: install.version,
329
+ pinnedVersion: Boolean(options.version),
321
330
  },
322
331
  {
323
332
  ...dependencies,
324
333
  onStatus: reporter?.status,
334
+ telemetryClient,
325
335
  },
326
336
  );
327
337
  } else {
package/src/index.js CHANGED
@@ -20,6 +20,8 @@ export const DEFAULT_MODEL_ID = "gemma-4-e4b";
20
20
  export const DEFAULT_DIAGNOSTIC_REPEATS = 3;
21
21
  export const DEFAULT_DIAGNOSTIC_MAX_OUTPUT_TOKENS = 96;
22
22
  export const DEFAULT_FETCH_TIMEOUT_MS = 15_000;
23
+ export const DEFAULT_UPDATE_CHECK_INTERVAL_MS = 30_000;
24
+ export const DEFAULT_TRUSTED_RELEASE_AUTHOR = "AtlantisPleb";
23
25
  const PYLON_RELEASE_TAG_PREFIX = "pylon-v";
24
26
  const RELEASE_ASSET_INSTALL_METHOD = "release_asset";
25
27
  const SOURCE_BUILD_INSTALL_METHOD = "source_build";
@@ -116,6 +118,36 @@ function comparePylonReleaseTags(leftTagName, rightTagName) {
116
118
  return left.normalized.localeCompare(right.normalized);
117
119
  }
118
120
 
121
+ function trustedReleaseAuthor(release, trustedAuthor = DEFAULT_TRUSTED_RELEASE_AUTHOR) {
122
+ if (!trustedAuthor) {
123
+ return true;
124
+ }
125
+ return release?.author?.login === trustedAuthor;
126
+ }
127
+
128
+ export function assertTrustedReleaseAuthor(
129
+ release,
130
+ trustedAuthor = DEFAULT_TRUSTED_RELEASE_AUTHOR,
131
+ ) {
132
+ if (trustedReleaseAuthor(release, trustedAuthor)) {
133
+ return release;
134
+ }
135
+ const tagName = release?.tag_name ?? "unknown";
136
+ const author = release?.author?.login ?? "unknown";
137
+ throw new Error(
138
+ `Refusing ${tagName}: GitHub release author ${author} is not trusted. Expected ${trustedAuthor}.`,
139
+ );
140
+ }
141
+
142
+ function isNewerPylonVersion(candidateVersion, currentVersion) {
143
+ return (
144
+ comparePylonReleaseTags(
145
+ `${PYLON_RELEASE_TAG_PREFIX}${normalizeVersion(candidateVersion)}`,
146
+ `${PYLON_RELEASE_TAG_PREFIX}${normalizeVersion(currentVersion)}`,
147
+ ) > 0
148
+ );
149
+ }
150
+
119
151
  function createBootstrapError(message, context = {}) {
120
152
  const error = new Error(message);
121
153
  Object.assign(error, context);
@@ -619,14 +651,19 @@ export function selectLatestPylonRelease(releases, target = null) {
619
651
  }
620
652
 
621
653
  const candidates = releases
622
- .filter((candidate) => !candidate?.draft && isPylonReleaseTag(candidate?.tag_name))
654
+ .filter(
655
+ (candidate) =>
656
+ !candidate?.draft &&
657
+ isPylonReleaseTag(candidate?.tag_name) &&
658
+ trustedReleaseAuthor(candidate),
659
+ )
623
660
  .sort((left, right) => comparePylonReleaseTags(right.tag_name, left.tag_name));
624
661
  const release =
625
662
  candidates.find((candidate) => releaseHasTargetAssets(candidate, target)) ??
626
663
  candidates[0];
627
664
  if (!release) {
628
665
  throw new Error(
629
- `GitHub release lookup did not find any published ${PYLON_RELEASE_TAG_PREFIX} releases.`,
666
+ `GitHub release lookup did not find any published ${PYLON_RELEASE_TAG_PREFIX} releases initiated by ${DEFAULT_TRUSTED_RELEASE_AUTHOR}.`,
630
667
  );
631
668
  }
632
669
 
@@ -658,7 +695,10 @@ export async function fetchReleaseMetadata({
658
695
  ? "GitHub tagged release lookup"
659
696
  : "GitHub release list lookup",
660
697
  });
661
- return normalizedVersion ? payload : selectLatestPylonRelease(payload, target);
698
+ const release = normalizedVersion
699
+ ? payload
700
+ : selectLatestPylonRelease(payload, target);
701
+ return assertTrustedReleaseAuthor(release);
662
702
  }
663
703
 
664
704
  export function selectReleaseAssets(release, target) {
@@ -1367,6 +1407,45 @@ export async function runProcess(
1367
1407
  });
1368
1408
  }
1369
1409
 
1410
+ function delay(ms) {
1411
+ return new Promise((resolve) => {
1412
+ setTimeout(resolve, ms);
1413
+ });
1414
+ }
1415
+
1416
+ function waitForChildExit(child) {
1417
+ return new Promise((resolve, reject) => {
1418
+ child.once?.("error", reject);
1419
+ child.once?.("close", (code, signal) => {
1420
+ resolve({ code, signal });
1421
+ });
1422
+ });
1423
+ }
1424
+
1425
+ async function stopChild(child, timeoutMs = 5_000) {
1426
+ if (!child || child.killed || child.exitCode != null) {
1427
+ return;
1428
+ }
1429
+
1430
+ const exited = waitForChildExit(child).catch(() => null);
1431
+ child.kill?.("SIGTERM");
1432
+ const result = await Promise.race([
1433
+ exited,
1434
+ delay(timeoutMs).then(() => "timeout"),
1435
+ ]);
1436
+ if (result === "timeout" && child.exitCode == null) {
1437
+ child.kill?.("SIGKILL");
1438
+ await exited;
1439
+ }
1440
+ }
1441
+
1442
+ function spawnPylonTui(pylonTuiPath, options, spawnProcessImpl) {
1443
+ return spawnProcessImpl(pylonTuiPath, [], {
1444
+ env: buildPylonEnv(options),
1445
+ stdio: "inherit",
1446
+ });
1447
+ }
1448
+
1370
1449
  async function extractArchive(archivePath, destinationDir, runProcessImpl) {
1371
1450
  await fs.mkdir(destinationDir, { recursive: true });
1372
1451
  await runProcessImpl("tar", ["-xzf", archivePath, "-C", destinationDir]);
@@ -1937,6 +2016,93 @@ export async function launchInstalledPylon(
1937
2016
 
1938
2017
  export const launchInstalledPylonTui = launchInstalledPylon;
1939
2018
 
2019
+ export async function launchInstalledPylonWithUpdates(
2020
+ options,
2021
+ {
2022
+ ensureReleaseInstallImpl = ensureReleaseInstall,
2023
+ spawnProcessImpl = spawn,
2024
+ updateCheckIntervalMs = DEFAULT_UPDATE_CHECK_INTERVAL_MS,
2025
+ onStatus = null,
2026
+ ...dependencies
2027
+ } = {},
2028
+ ) {
2029
+ if (options.noUpdates || options.pinnedVersion) {
2030
+ return launchInstalledPylon(options, { ...dependencies, onStatus });
2031
+ }
2032
+
2033
+ let current = {
2034
+ ...options,
2035
+ version: normalizeVersion(options.version),
2036
+ };
2037
+ let lastUpdateError = null;
2038
+
2039
+ while (true) {
2040
+ const pylonTuiPath = path.resolve(current.pylonTuiPath);
2041
+ emitStatus(
2042
+ onStatus,
2043
+ "Starting Pylon terminal UI",
2044
+ `${path.basename(pylonTuiPath)} manages the earning worker`,
2045
+ );
2046
+ const child = spawnPylonTui(pylonTuiPath, current, spawnProcessImpl);
2047
+ const childExit = waitForChildExit(child);
2048
+ let restartForUpdate = false;
2049
+
2050
+ while (!restartForUpdate) {
2051
+ const result = await Promise.race([
2052
+ childExit.then((exit) => ({ type: "exit", exit })),
2053
+ delay(updateCheckIntervalMs).then(() => ({ type: "tick" })),
2054
+ ]);
2055
+
2056
+ if (result.type === "exit") {
2057
+ const { code, signal } = result.exit;
2058
+ if (code === 0 || signal === "SIGTERM" || signal === "SIGINT") {
2059
+ return result.exit;
2060
+ }
2061
+ throw new Error(
2062
+ `${path.basename(pylonTuiPath)} exited with code ${
2063
+ code ?? "null"
2064
+ }${signal ? ` signal ${signal}` : ""}`,
2065
+ );
2066
+ }
2067
+
2068
+ try {
2069
+ const install = await ensureReleaseInstallImpl(
2070
+ {
2071
+ ...options,
2072
+ version: null,
2073
+ },
2074
+ {
2075
+ ...dependencies,
2076
+ onStatus: null,
2077
+ },
2078
+ );
2079
+ lastUpdateError = null;
2080
+ if (!isNewerPylonVersion(install.version, current.version)) {
2081
+ continue;
2082
+ }
2083
+ emitStatus(
2084
+ onStatus,
2085
+ "Installed newer Pylon release",
2086
+ `${install.tagName}; restarting dashboard`,
2087
+ );
2088
+ await stopChild(child);
2089
+ current = {
2090
+ ...options,
2091
+ ...install,
2092
+ version: install.version,
2093
+ };
2094
+ restartForUpdate = true;
2095
+ } catch (error) {
2096
+ const message = error instanceof Error ? error.message : String(error);
2097
+ if (message !== lastUpdateError) {
2098
+ emitStatus(onStatus, "Pylon update check failed", message);
2099
+ lastUpdateError = message;
2100
+ }
2101
+ }
2102
+ }
2103
+ }
2104
+ }
2105
+
1940
2106
  export function resolveBootstrapOutcome(summary) {
1941
2107
  const runtimeState =
1942
2108
  summary.status?.snapshot?.runtime?.authoritative_status ?? "unknown";