@agent-team-foundation/first-tree-hub 0.14.9-alpha.292.1 → 0.14.9-alpha.294.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.
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
- import { $ as cleanWorkspaces, A as printResults, B as ClientRuntime, C as migrateLocalAgentDirs, D as checkNodeVersion, E as checkClientConfig, G as removeLocalAgent, I as restartClientService, J as ClientOrgMismatchError, K as fail, L as startClientService, M as getClientServiceStatus, N as installClientService, O as checkServerReachable, P as isServiceSupported, Q as SessionRegistry, R as stopClientService, S as createApiNameResolver, T as checkBackgroundService, U as findStaleAliases, V as handleClientOrgMismatch, W as formatStaleReason, X as FirstTreeHubSDK, Y as ClientUserMismatchError, Z as SdkError, _ as loadOnboardState, a as declineUpdate, b as saveOnboardState, c as detectInstallMode, d as reconcileLocalRuntimeProviders, et as probeCapabilities, f as uploadClientCapabilities, g as formatCheckReport, h as promptMissingFields, i as createExecuteUpdate, j as reconcileAgentConfigs, k as checkWebSocket, l as fetchLatestVersion, m as promptAddAgent, nt as configureClientLoggerForService, o as promptUpdate, p as isInteractive, q as success, r as registerSaaSConnectCommand, s as PACKAGE_NAME, tt as applyClientLoggerConfig, u as installGlobalLatest, v as onboardCheck, w as checkAgentConfigs, x as runHomeMigration, y as onboardCreate } from "../saas-connect-BueXVlnx.mjs";
2
+ import { $ as cleanWorkspaces, A as printResults, B as ClientRuntime, C as migrateLocalAgentDirs, D as checkNodeVersion, E as checkClientConfig, G as removeLocalAgent, I as restartClientService, J as ClientOrgMismatchError, K as fail, L as startClientService, M as getClientServiceStatus, N as installClientService, O as checkServerReachable, P as isServiceSupported, Q as SessionRegistry, R as stopClientService, S as createApiNameResolver, T as checkBackgroundService, U as findStaleAliases, V as handleClientOrgMismatch, W as formatStaleReason, X as FirstTreeHubSDK, Y as ClientUserMismatchError, Z as SdkError, _ as loadOnboardState, a as declineUpdate, b as saveOnboardState, c as detectInstallMode, d as reconcileLocalRuntimeProviders, et as probeCapabilities, f as uploadClientCapabilities, g as formatCheckReport, h as promptMissingFields, i as createExecuteUpdate, j as reconcileAgentConfigs, k as checkWebSocket, l as fetchLatestVersion, m as promptAddAgent, nt as configureClientLoggerForService, o as promptUpdate, p as isInteractive, q as success, r as registerSaaSConnectCommand, s as PACKAGE_NAME, tt as applyClientLoggerConfig, u as installGlobalLatest, v as onboardCheck, w as checkAgentConfigs, x as runHomeMigration, y as onboardCreate } from "../saas-connect-DzbieDfo.mjs";
3
3
  import { C as resolveConfigReadonly, S as resetConfigMeta, _ as initConfig, a as loadCredentials, b as readConfigFile, c as saveAgentConfig, d as DEFAULT_DATA_DIR, f as DEFAULT_HOME_DIR, g as getConfigValue, i as ensureFreshAdminToken, m as clientConfigSchema, p as agentConfigSchema, r as ensureFreshAccessToken, s as resolveServerUrl, u as DEFAULT_CONFIG_DIR, v as loadAgents, w as setConfigValue, x as resetConfig } from "../bootstrap-D6RsdtJg.mjs";
4
4
  import { a as print, n as CLI_USER_AGENT, o as setJsonMode, r as COMMAND_VERSION, t as cliFetch } from "../cli-fetch-BGVItZxo.mjs";
5
- import { n as bindFeishuUser, t as bindFeishuBot } from "../feishu-vc0yRVWj.mjs";
5
+ import { n as bindFeishuUser, t as bindFeishuBot } from "../feishu-CwD_5b9W.mjs";
6
6
  import { join } from "node:path";
7
7
  import { existsSync, mkdirSync, readFileSync, readdirSync } from "node:fs";
8
8
  import * as semver from "semver";
@@ -682,12 +682,37 @@ z.object({
682
682
  * mandatory on this server build.
683
683
  */
684
684
  const clientWireCapabilitiesSchema = z.object({ wsInboxDeliver: z.boolean().default(false) }).partial();
685
+ /**
686
+ * Outcome of the client's last self-update attempt. Carried on
687
+ * `client:register` so the server can persist it into
688
+ * `clients.metadata.lastUpdateAttempt`, surfacing in the admin
689
+ * dashboard whichever clients are failing to auto-update — without
690
+ * needing per-machine SSH to grep `client.log`.
691
+ *
692
+ * Length caps protect the WS frame budget: even a verbose npm stderr
693
+ * gets truncated client-side before persisting, but `.max()` here is the
694
+ * server-side guard against a hostile or buggy client sending a
695
+ * megabyte-long reason.
696
+ */
697
+ const updateAttemptSchema = z.object({
698
+ result: z.enum([
699
+ "ok",
700
+ "failed",
701
+ "blocked"
702
+ ]),
703
+ target: z.string().min(1).max(64),
704
+ currentBefore: z.string().min(1).max(64),
705
+ installedVersion: z.string().min(1).max(64).nullable(),
706
+ reason: z.string().max(500).nullable(),
707
+ at: z.string().min(1).max(40)
708
+ });
685
709
  z.object({
686
710
  clientId: z.string().min(1).max(100),
687
711
  hostname: z.string().max(100).optional(),
688
712
  os: z.string().max(50).optional(),
689
713
  sdkVersion: z.string().max(50).optional(),
690
- wireCapabilities: clientWireCapabilitiesSchema.optional()
714
+ wireCapabilities: clientWireCapabilitiesSchema.optional(),
715
+ lastUpdateAttempt: updateAttemptSchema.optional()
691
716
  });
692
717
  z.object({
693
718
  clientId: z.string(),
@@ -1826,4 +1851,4 @@ async function bindFeishuUser(serverUrl, accessToken, agentId, humanAgentId, fei
1826
1851
  }
1827
1852
  }
1828
1853
  //#endregion
1829
- export { bindFeishuUser as n, bindFeishuBot as t };
1854
+ export { bindFeishuUser as n, updateAttemptSchema as r, bindFeishuBot as t };
@@ -1,3 +1,3 @@
1
1
  import "./cli-fetch-BGVItZxo.mjs";
2
- import { t as bindFeishuBot } from "./feishu-vc0yRVWj.mjs";
2
+ import { t as bindFeishuBot } from "./feishu-CwD_5b9W.mjs";
3
3
  export { bindFeishuBot };
package/dist/index.mjs CHANGED
@@ -1,5 +1,5 @@
1
- import { A as printResults, B as ClientRuntime, D as checkNodeVersion, E as checkClientConfig, F as resolveCliInvocation, H as rotateClientIdWithBackup, I as restartClientService, L as startClientService, M as getClientServiceStatus, N as installClientService, O as checkServerReachable, P as isServiceSupported, R as stopClientService, V as handleClientOrgMismatch, X as FirstTreeHubSDK, Z as SdkError, g as formatCheckReport, h as promptMissingFields, k as checkWebSocket, m as promptAddAgent, n as deriveHubUrlFromToken, p as isInteractive, t as HubUrlDerivationError, v as onboardCheck, w as checkAgentConfigs, x as runHomeMigration, y as onboardCreate, z as uninstallClientService } from "./saas-connect-BueXVlnx.mjs";
1
+ import { A as printResults, B as ClientRuntime, D as checkNodeVersion, E as checkClientConfig, F as resolveCliInvocation, H as rotateClientIdWithBackup, I as restartClientService, L as startClientService, M as getClientServiceStatus, N as installClientService, O as checkServerReachable, P as isServiceSupported, R as stopClientService, V as handleClientOrgMismatch, X as FirstTreeHubSDK, Z as SdkError, g as formatCheckReport, h as promptMissingFields, k as checkWebSocket, m as promptAddAgent, n as deriveHubUrlFromToken, p as isInteractive, t as HubUrlDerivationError, v as onboardCheck, w as checkAgentConfigs, x as runHomeMigration, y as onboardCreate, z as uninstallClientService } from "./saas-connect-DzbieDfo.mjs";
2
2
  import { i as ensureFreshAdminToken, n as AuthRefreshRateLimitedError, o as resolveAccessToken, r as ensureFreshAccessToken, s as resolveServerUrl, t as AuthRefreshFailedError } from "./bootstrap-D6RsdtJg.mjs";
3
3
  import { i as blank, s as status } from "./cli-fetch-BGVItZxo.mjs";
4
- import { n as bindFeishuUser, t as bindFeishuBot } from "./feishu-vc0yRVWj.mjs";
4
+ import { n as bindFeishuUser, t as bindFeishuBot } from "./feishu-CwD_5b9W.mjs";
5
5
  export { AuthRefreshFailedError, AuthRefreshRateLimitedError, ClientRuntime, FirstTreeHubSDK, HubUrlDerivationError, SdkError, bindFeishuBot, bindFeishuUser, blank, checkAgentConfigs, checkClientConfig, checkNodeVersion, checkServerReachable, checkWebSocket, deriveHubUrlFromToken, ensureFreshAccessToken, ensureFreshAdminToken, formatCheckReport, getClientServiceStatus, handleClientOrgMismatch, installClientService, isInteractive, isServiceSupported, onboardCheck, onboardCreate, printResults, promptAddAgent, promptMissingFields, resolveAccessToken, resolveCliInvocation, resolveServerUrl, restartClientService, rotateClientIdWithBackup, runHomeMigration, startClientService, status, stopClientService, uninstallClientService };
@@ -1,5 +1,6 @@
1
1
  import { C as resolveConfigReadonly, S as resetConfigMeta, _ as initConfig, a as loadCredentials, c as saveAgentConfig, d as DEFAULT_DATA_DIR$1, f as DEFAULT_HOME_DIR$1, h as collectMissingPrompts, l as saveCredentials, m as clientConfigSchema, p as agentConfigSchema, r as ensureFreshAccessToken, s as resolveServerUrl, u as DEFAULT_CONFIG_DIR, v as loadAgents, w as setConfigValue, x as resetConfig, y as migrateLegacyHome } from "./bootstrap-D6RsdtJg.mjs";
2
2
  import { a as print, i as blank, n as CLI_USER_AGENT, r as COMMAND_VERSION, t as cliFetch } from "./cli-fetch-BGVItZxo.mjs";
3
+ import { r as updateAttemptSchema$1 } from "./feishu-CwD_5b9W.mjs";
3
4
  import { createRequire } from "node:module";
4
5
  import { z } from "zod";
5
6
  import { basename, delimiter, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
@@ -4509,12 +4510,37 @@ z.object({
4509
4510
  * mandatory on this server build.
4510
4511
  */
4511
4512
  const clientWireCapabilitiesSchema = z.object({ wsInboxDeliver: z.boolean().default(false) }).partial();
4513
+ /**
4514
+ * Outcome of the client's last self-update attempt. Carried on
4515
+ * `client:register` so the server can persist it into
4516
+ * `clients.metadata.lastUpdateAttempt`, surfacing in the admin
4517
+ * dashboard whichever clients are failing to auto-update — without
4518
+ * needing per-machine SSH to grep `client.log`.
4519
+ *
4520
+ * Length caps protect the WS frame budget: even a verbose npm stderr
4521
+ * gets truncated client-side before persisting, but `.max()` here is the
4522
+ * server-side guard against a hostile or buggy client sending a
4523
+ * megabyte-long reason.
4524
+ */
4525
+ const updateAttemptSchema = z.object({
4526
+ result: z.enum([
4527
+ "ok",
4528
+ "failed",
4529
+ "blocked"
4530
+ ]),
4531
+ target: z.string().min(1).max(64),
4532
+ currentBefore: z.string().min(1).max(64),
4533
+ installedVersion: z.string().min(1).max(64).nullable(),
4534
+ reason: z.string().max(500).nullable(),
4535
+ at: z.string().min(1).max(40)
4536
+ });
4512
4537
  z.object({
4513
4538
  clientId: z.string().min(1).max(100),
4514
4539
  hostname: z.string().max(100).optional(),
4515
4540
  os: z.string().max(50).optional(),
4516
4541
  sdkVersion: z.string().max(50).optional(),
4517
- wireCapabilities: clientWireCapabilitiesSchema.optional()
4542
+ wireCapabilities: clientWireCapabilitiesSchema.optional(),
4543
+ lastUpdateAttempt: updateAttemptSchema.optional()
4518
4544
  });
4519
4545
  z.object({
4520
4546
  clientId: z.string(),
@@ -6290,6 +6316,7 @@ var ClientConnection = class extends EventEmitter {
6290
6316
  sdkVersion;
6291
6317
  userAgent;
6292
6318
  getAccessToken;
6319
+ getLastUpdateAttempt;
6293
6320
  heartbeatIntervalMs;
6294
6321
  heartbeatTimeoutMs;
6295
6322
  ws = null;
@@ -6353,6 +6380,7 @@ var ClientConnection = class extends EventEmitter {
6353
6380
  this.sdkVersion = config.sdkVersion;
6354
6381
  this.userAgent = config.userAgent;
6355
6382
  this.getAccessToken = config.getAccessToken;
6383
+ this.getLastUpdateAttempt = config.getLastUpdateAttempt;
6356
6384
  this.heartbeatIntervalMs = config.heartbeatIntervalMs ?? HEARTBEAT_INTERVAL_MS;
6357
6385
  this.heartbeatTimeoutMs = config.heartbeatTimeoutMs ?? HEARTBEAT_TIMEOUT_MS;
6358
6386
  this.wsLogger = createLogger("ws").child({ clientId: this.clientId });
@@ -6604,12 +6632,19 @@ var ClientConnection = class extends EventEmitter {
6604
6632
  const type = msg.type;
6605
6633
  if (type === "auth:ok") {
6606
6634
  this.authLogger.info("auth accepted, registering client");
6635
+ let lastUpdateAttempt = null;
6636
+ try {
6637
+ lastUpdateAttempt = this.getLastUpdateAttempt?.() ?? null;
6638
+ } catch (err) {
6639
+ this.authLogger.warn({ err }, "getLastUpdateAttempt threw; omitting from register frame");
6640
+ }
6607
6641
  this.ws?.send(JSON.stringify({
6608
6642
  type: "client:register",
6609
6643
  clientId: this.clientId,
6610
6644
  hostname: hostname(),
6611
6645
  os: platform(),
6612
- sdkVersion: this.sdkVersion
6646
+ sdkVersion: this.sdkVersion,
6647
+ ...lastUpdateAttempt ? { lastUpdateAttempt } : {}
6613
6648
  }));
6614
6649
  return;
6615
6650
  }
@@ -11781,6 +11816,54 @@ async function handleClientOrgMismatch(err, opts) {
11781
11816
  }
11782
11817
  }
11783
11818
  //#endregion
11819
+ //#region src/core/update-state.ts
11820
+ /**
11821
+ * Override-able location of the state file. Production code uses the
11822
+ * default; tests pass a temp path so they don't stomp on the real
11823
+ * `~/.first-tree/hub/state/update-state.json`.
11824
+ */
11825
+ function defaultUpdateStatePath() {
11826
+ return join(DEFAULT_HOME_DIR$1, "state", "update-state.json");
11827
+ }
11828
+ /** Read the most recent attempt, or `null` if no attempt has ever been recorded. */
11829
+ function readUpdateState(path = defaultUpdateStatePath()) {
11830
+ if (!existsSync(path)) return null;
11831
+ try {
11832
+ const raw = readFileSync(path, "utf8");
11833
+ const parsed = JSON.parse(raw);
11834
+ if (!parsed || typeof parsed !== "object") return null;
11835
+ const last = updateAttemptSchema$1.safeParse(parsed.last);
11836
+ if (!last.success) return null;
11837
+ return { last: last.data };
11838
+ } catch {
11839
+ return null;
11840
+ }
11841
+ }
11842
+ /**
11843
+ * Persist the given attempt as the most recent record. Atomic in the
11844
+ * single-writer sense — only `update-glue.createExecuteUpdate` writes to
11845
+ * this file, and a CLI process never runs two `executeUpdate` calls
11846
+ * concurrently (UpdateManager's `updateInFlight` lock guarantees it).
11847
+ */
11848
+ function recordUpdateAttempt(attempt, path = defaultUpdateStatePath()) {
11849
+ mkdirSync(dirname(path), { recursive: true });
11850
+ const payload = { last: attempt };
11851
+ writeFileSync(path, `${JSON.stringify(payload, null, 2)}\n`, { mode: 384 });
11852
+ }
11853
+ /**
11854
+ * `true` when the last recorded attempt was `blocked` for *this exact
11855
+ * target version*. Server moving on to a different version clears the
11856
+ * block automatically — only the specific (target, machine) pair stays
11857
+ * frozen. Loop guard does not block on `failed`: those should retry
11858
+ * (transient EACCES / network), and the UpdateManager already handles
11859
+ * the back-off via "next welcome frame".
11860
+ */
11861
+ function isLoopGuarded(target, path = defaultUpdateStatePath()) {
11862
+ const state = readUpdateState(path);
11863
+ if (!state) return false;
11864
+ return state.last.result === "blocked" && state.last.target === target;
11865
+ }
11866
+ //#endregion
11784
11867
  //#region src/core/client-runtime.ts
11785
11868
  /**
11786
11869
  * Client runtime — one shared ClientConnection, multiple agents multiplexed.
@@ -11825,7 +11908,8 @@ var ClientRuntime = class {
11825
11908
  clientId,
11826
11909
  sdkVersion: options.currentVersion,
11827
11910
  userAgent: CLI_USER_AGENT,
11828
- getAccessToken: (opts) => ensureFreshAccessToken(opts)
11911
+ getAccessToken: (opts) => ensureFreshAccessToken(opts),
11912
+ getLastUpdateAttempt: () => readUpdateState()?.last ?? null
11829
11913
  });
11830
11914
  this.gitMirrorManager = createGitMirrorManager({
11831
11915
  dataDir: DEFAULT_DATA_DIR$1,
@@ -13322,7 +13406,7 @@ async function onboardCreate(args) {
13322
13406
  }
13323
13407
  const runtimeAgent = args.type === "human" ? args.assistant : args.id;
13324
13408
  if (args.feishuBotAppId && args.feishuBotAppSecret) {
13325
- const { bindFeishuBot } = await import("./feishu-De4xqSeY.mjs");
13409
+ const { bindFeishuBot } = await import("./feishu-DXF_NJHg.mjs");
13326
13410
  const targetAgentUuid = args.type === "human" ? assistantUuid : primary.uuid;
13327
13411
  if (!targetAgentUuid) print.line(`Warning: Cannot bind Feishu bot — no runtime agent available for "${args.id}".\n`);
13328
13412
  else {
@@ -13784,7 +13868,7 @@ const declineUpdate = async () => false;
13784
13868
  * operator restarts manually.
13785
13869
  */
13786
13870
  function createExecuteUpdate({ managed }) {
13787
- return async ({ targetVersion }) => {
13871
+ return async ({ currentVersion, targetVersion }) => {
13788
13872
  const mode = detectInstallMode();
13789
13873
  if (mode === "source") {
13790
13874
  print.line(" [update] Running from source checkout — self-update skipped. Use `git pull` instead.\n");
@@ -13794,18 +13878,57 @@ function createExecuteUpdate({ managed }) {
13794
13878
  print.line(" [update] Cannot self-update — not launched from a global npm install.\n Run `npm i -g @agent-team-foundation/first-tree-hub` manually.\n");
13795
13879
  return { installed: false };
13796
13880
  }
13881
+ if (isLoopGuarded(targetVersion)) {
13882
+ print.line(` [update] Refusing to retry ${targetVersion} — a previous attempt completed without\n advancing the on-disk version. The most likely cause is npm's \`latest\`
13883
+ dist-tag resolving to the same version this client is already running.
13884
+ Operator action: manually run \`npm install -g @agent-team-foundation/first-tree-hub@latest\`,
13885
+ then restart the service.
13886
+ `);
13887
+ return { installed: true };
13888
+ }
13797
13889
  print.line(` [update] Running \`npm install -g @agent-team-foundation/first-tree-hub@${targetVersion}\`...\n`);
13798
13890
  const result = await installGlobalSpec(targetVersion);
13799
13891
  if (!result.ok) {
13800
13892
  print.line(` [update] Install failed: ${result.reason}\n`);
13893
+ recordUpdateAttempt({
13894
+ result: "failed",
13895
+ target: targetVersion,
13896
+ currentBefore: currentVersion,
13897
+ installedVersion: null,
13898
+ reason: result.reason,
13899
+ at: (/* @__PURE__ */ new Date()).toISOString()
13900
+ });
13801
13901
  return { installed: false };
13802
13902
  }
13803
- const installed = result.installedVersion ?? targetVersion;
13903
+ const installed = result.installedVersion;
13904
+ if (installed && semver.valid(installed) && semver.valid(targetVersion) && semver.lt(installed, targetVersion)) {
13905
+ const reason = `npm reported install of ${installed}, but the server-advertised target was ${targetVersion} (running ${currentVersion})`;
13906
+ print.line(` [update] WARNING: ${reason}\n`);
13907
+ print.line(" [update] Skipping restart to avoid an exit-75 → reboot loop. Loop guard armed.\n");
13908
+ recordUpdateAttempt({
13909
+ result: "blocked",
13910
+ target: targetVersion,
13911
+ currentBefore: currentVersion,
13912
+ installedVersion: installed,
13913
+ reason,
13914
+ at: (/* @__PURE__ */ new Date()).toISOString()
13915
+ });
13916
+ return { installed: true };
13917
+ }
13918
+ const installedLabel = installed ?? targetVersion;
13919
+ recordUpdateAttempt({
13920
+ result: "ok",
13921
+ target: targetVersion,
13922
+ currentBefore: currentVersion,
13923
+ installedVersion: installed,
13924
+ reason: null,
13925
+ at: (/* @__PURE__ */ new Date()).toISOString()
13926
+ });
13804
13927
  if (managed) {
13805
- print.line(` [update] Installed ${installed}. Restarting (exit 75).\n`);
13928
+ print.line(` [update] Installed ${installedLabel}. Restarting (exit 75).\n`);
13806
13929
  process.exit(75);
13807
13930
  }
13808
- print.line(` [update] Installed ${installed}. Restart the client manually (Ctrl+C then \`first-tree-hub client start\`) to pick up the new version.\n`);
13931
+ print.line(` [update] Installed ${installedLabel}. Restart the client manually (Ctrl+C then \`first-tree-hub client start\`) to pick up the new version.\n`);
13809
13932
  return { installed: true };
13810
13933
  };
13811
13934
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-team-foundation/first-tree-hub",
3
- "version": "0.14.9-alpha.292.1",
3
+ "version": "0.14.9-alpha.294.1",
4
4
  "type": "module",
5
5
  "description": "First Tree Hub — unified CLI for client and agent management",
6
6
  "exports": {