@blockrun/franklin 3.15.92 → 3.15.94

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.
@@ -1011,10 +1011,26 @@ export async function interactiveSession(config, getUserInput, onEvent, onAbortR
1011
1011
  // where input-replay tax has clearly started biting; the
1012
1012
  // fire-once-per-turn flag still bounds the worst case at one
1013
1013
  // extra summary call (~$0.005).
1014
+ //
1015
+ // 2026-05-11: added a high-cost early-exit. The original
1016
+ // (>15 calls AND >$0.03) gate works well for cheap models
1017
+ // where 15 calls clears the $0.03 floor trivially. For Opus-
1018
+ // class models, cost climbs much faster than call count —
1019
+ // verified in production from a real session:
1020
+ // `Research-bloat compacted at 16 calls / $9.4552: ~3129
1021
+ // tokens`. By the time the 16-call gate fired, $9.45 was
1022
+ // already spent on input-replay. With an early-exit at
1023
+ // $1.00 turn-cost, the compact would have fired around
1024
+ // call 4-5, saving ~$8 on that turn. The cost cap is
1025
+ // intentionally conservative — even extended-thinking Opus
1026
+ // shouldn't legitimately need >$1 of context-replay before
1027
+ // compacting (the compact itself runs on a cheaper model
1028
+ // and costs <$0.05).
1029
+ const TURN_COST_CAP_FOR_EARLY_COMPACT = 1.00;
1014
1030
  if (!bloatCompactedThisTurn &&
1015
1031
  compactFailures < 3 &&
1016
- turnToolCalls > 15 &&
1017
- turnCostUsd > 0.03) {
1032
+ ((turnToolCalls > 15 && turnCostUsd > 0.03) ||
1033
+ turnCostUsd > TURN_COST_CAP_FOR_EARLY_COMPACT)) {
1018
1034
  try {
1019
1035
  const beforeTokens = estimateHistoryTokens(history);
1020
1036
  const { history: compacted, compacted: didCompact } = await forceCompact(history, config.model, client, config.debug);
@@ -17,9 +17,17 @@ import os from 'node:os';
17
17
  import { setupAgentWallet, setupAgentSolanaWallet, } from '@blockrun/llm';
18
18
  import { loadChain, API_URLS, VERSION, BLOCKRUN_DIR } from '../config.js';
19
19
  import { isTelemetryEnabled, readAllRecords } from '../telemetry/store.js';
20
- import { getAvailableUpdate, kickoffVersionCheck } from '../version-check.js';
20
+ import { getAvailableUpdateFresh, kickoffVersionCheck } from '../version-check.js';
21
21
  async function runChecks() {
22
22
  const out = [];
23
+ // Kick off the authoritative version fetch FIRST, in parallel with the
24
+ // other checks. Doctor is a diagnostic — the user just asked "am I
25
+ // healthy?" — so a 24h-stale cache is the wrong answer. The fetch is
26
+ // bounded by the same 2s timeout the background check uses, and falls
27
+ // back to the cached value on failure. By the time we render the
28
+ // Franklin-version check below, the fetch has typically settled in
29
+ // <300ms (npm is fast) and we have a current answer.
30
+ const freshUpdatePromise = getAvailableUpdateFresh();
23
31
  // ── 1. Runtime ────────────────────────────────────────────────────
24
32
  const nodeVer = process.versions.node;
25
33
  const nodeMajor = parseInt(nodeVer.split('.')[0], 10);
@@ -30,10 +38,10 @@ async function runChecks() {
30
38
  remedy: nodeMajor >= 20 ? undefined : 'Upgrade Node.js: https://nodejs.org',
31
39
  });
32
40
  // ── 2. Franklin version ───────────────────────────────────────────
33
- // Kick the daily cache refresh so subsequent doctor runs carry fresh
34
- // data. Current run uses whatever's already cached.
41
+ // Keep kickoffVersionCheck() so non-doctor entry points (banner etc.)
42
+ // still warm the cache through their normal daily refresh path.
35
43
  kickoffVersionCheck();
36
- const update = getAvailableUpdate();
44
+ const update = await freshUpdatePromise;
37
45
  out.push({
38
46
  name: 'Franklin',
39
47
  status: update ? 'warn' : 'ok',
@@ -37,3 +37,17 @@ export interface UpdateInfo {
37
37
  * background check settles — returns null (we don't speculate).
38
38
  */
39
39
  export declare function getAvailableUpdate(): UpdateInfo | null;
40
+ /**
41
+ * Authoritative check that forces a fresh fetch (up to FETCH_TIMEOUT_MS).
42
+ * Use for on-demand diagnostics like `franklin doctor` where the user
43
+ * explicitly asked "am I up to date?" and a 24h-stale cache is the wrong
44
+ * answer. Verified 2026-05-11: between two same-day releases (3.15.91 →
45
+ * 3.15.92), the daily cache made `franklin doctor` show green for a user
46
+ * who was actually 4 versions behind (3.15.88), because they ran doctor
47
+ * in the brief gap between npm publish and the next cache refresh.
48
+ *
49
+ * Falls back to the cached value if the fetch fails (offline, slow npm,
50
+ * etc.) — same behavior as the cached check, just refreshed when
51
+ * possible.
52
+ */
53
+ export declare function getAvailableUpdateFresh(): Promise<UpdateInfo | null>;
@@ -132,3 +132,30 @@ export function getAvailableUpdate() {
132
132
  }
133
133
  return null;
134
134
  }
135
+ /**
136
+ * Authoritative check that forces a fresh fetch (up to FETCH_TIMEOUT_MS).
137
+ * Use for on-demand diagnostics like `franklin doctor` where the user
138
+ * explicitly asked "am I up to date?" and a 24h-stale cache is the wrong
139
+ * answer. Verified 2026-05-11: between two same-day releases (3.15.91 →
140
+ * 3.15.92), the daily cache made `franklin doctor` show green for a user
141
+ * who was actually 4 versions behind (3.15.88), because they ran doctor
142
+ * in the brief gap between npm publish and the next cache refresh.
143
+ *
144
+ * Falls back to the cached value if the fetch fails (offline, slow npm,
145
+ * etc.) — same behavior as the cached check, just refreshed when
146
+ * possible.
147
+ */
148
+ export async function getAvailableUpdateFresh() {
149
+ if (isDisabled())
150
+ return getAvailableUpdate();
151
+ const latest = await fetchLatestVersion();
152
+ if (latest) {
153
+ writeCache({ latestVersion: latest, checkedAt: Date.now() });
154
+ if (compareSemver(latest, VERSION) > 0) {
155
+ return { current: VERSION, latest };
156
+ }
157
+ return null;
158
+ }
159
+ // Fetch failed — fall back to whatever the cache says.
160
+ return getAvailableUpdate();
161
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blockrun/franklin",
3
- "version": "3.15.92",
3
+ "version": "3.15.94",
4
4
  "description": "Franklin — The AI agent with a wallet. Spends USDC autonomously to get real work done. Pay per action, no subscriptions.",
5
5
  "type": "module",
6
6
  "exports": {