@tiens.nguyen/gonext-local-worker 1.0.334 → 1.0.337

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.
Files changed (2) hide show
  1. package/gonext-repl.mjs +60 -13
  2. package/package.json +1 -1
package/gonext-repl.mjs CHANGED
@@ -688,15 +688,33 @@ let wsSync = { enabled: false, name: "", path: "" };
688
688
  const workspaceKeyFor = (cwd) =>
689
689
  createHash("sha256").update(cwd).digest("hex").slice(0, 32);
690
690
 
691
- // Task #104: agent CODE-model OUTPUT-token counters. The API keys the global total on
692
- // (userId + coding-model URL from settings) and the workspace total on workspaceKey; the
693
- // REPL just supplies workspaceKey + this turn's delta. All best-effort (never block a turn).
691
+ // Task #104: agent CODE-model OUTPUT-token counters. The API keys the workspace total on
692
+ // workspaceKey and the GLOBAL total on (userId + coding-model URL). Task #125: the REPL
693
+ // now supplies the URL of the backend in EFFECT this session (activeCodingUrl) so the
694
+ // global total follows a /model backend switch; empty → the API falls back to the
695
+ // account-default URL from settings (older behaviour). All best-effort (never block a turn).
696
+ const activeCodingUrl = () => sessionCodingUrl || defaultCodingUrl;
697
+
698
+ // Task #126: the footer's global "↓" for the CURRENT backend. The saved-full-responses sum
699
+ // (savedResponsesTotal) is authoritative per-URL — and correct even for turns before #125,
700
+ // because each saved row carries its own url — so PREFER it when it has data. Fall back to
701
+ // the #125 incremental counter (globalTotal) when Save-Full-Response is off (no saved rows,
702
+ // so savedResponsesTotal is 0), which keeps a correct non-zero number instead of a
703
+ // surprising 0. Returns null when the response had neither, so callers keep the old value.
704
+ function pickGlobalTotal(t) {
705
+ if (!t) return null;
706
+ const saved = Number.isFinite(t.savedResponsesTotal) ? t.savedResponsesTotal : 0;
707
+ if (saved > 0) return saved;
708
+ return Number.isFinite(t.globalTotal) ? t.globalTotal : null;
709
+ }
710
+
694
711
  async function fetchOutputTokenTotals(cwd) {
695
712
  try {
696
- const r = await fetch(
697
- `${apiBase}/api/worker/output-tokens?workspaceKey=${workspaceKeyFor(cwd)}`,
698
- { headers: { "X-Worker-Key": workerKey } }
699
- );
713
+ const qs = new URLSearchParams({ workspaceKey: workspaceKeyFor(cwd) });
714
+ if (activeCodingUrl()) qs.set("codingUrl", activeCodingUrl());
715
+ const r = await fetch(`${apiBase}/api/worker/output-tokens?${qs.toString()}`, {
716
+ headers: { "X-Worker-Key": workerKey },
717
+ });
700
718
  if (!r.ok) return null;
701
719
  return await r.json();
702
720
  } catch {
@@ -710,7 +728,11 @@ async function addOutputTokens(cwd, delta) {
710
728
  const r = await fetch(`${apiBase}/api/worker/output-tokens`, {
711
729
  method: "POST",
712
730
  headers: { "Content-Type": "application/json", "X-Worker-Key": workerKey },
713
- body: JSON.stringify({ workspaceKey: workspaceKeyFor(cwd), delta }),
731
+ body: JSON.stringify({
732
+ workspaceKey: workspaceKeyFor(cwd),
733
+ delta,
734
+ codingUrl: activeCodingUrl(),
735
+ }),
714
736
  });
715
737
  if (!r.ok) return null;
716
738
  return await r.json();
@@ -724,6 +746,7 @@ async function resetGlobalOutputTokens() {
724
746
  const r = await fetch(`${apiBase}/api/worker/output-tokens/reset-global`, {
725
747
  method: "POST",
726
748
  headers: { "Content-Type": "application/json", "X-Worker-Key": workerKey },
749
+ body: JSON.stringify({ codingUrl: activeCodingUrl() }),
727
750
  });
728
751
  return r.ok;
729
752
  } catch {
@@ -918,10 +941,20 @@ async function chooseModel() {
918
941
  // Remember the friendly name for the token footer, which should never show a raw
919
942
  // "<kind>::<model>" wire id.
920
943
  sessionCodingLabel = chosen.id === def ? "" : chosen.model;
944
+ // Task #125: remember the chosen backend's coding URL so the GLOBAL output-token total is
945
+ // keyed to it. Default pick → "" so activeCodingUrl() falls back to defaultCodingUrl.
946
+ sessionCodingUrl = chosen.id === def ? "" : (chosen.url ?? "");
921
947
  const where = showKind ? ` (${KIND_LABEL[chosen.kind ?? ""] ?? chosen.kind})` : "";
922
948
  console.log(
923
949
  green(` ✓ coding model → ${chosen.model}${where}${chosen.id === def ? " (default)" : ""}\n`)
924
950
  );
951
+ // Task #125: the GLOBAL lifetime total is per-backend, so re-fetch it for the newly
952
+ // selected backend NOW — otherwise the footer would keep showing the previous backend's
953
+ // total until the next turn. (The live per-turn tail, latestOutputTokens, is local to
954
+ // runAgentTurn and is already 0 here between turns — /model can't run mid-turn — so there
955
+ // is nothing to zero, and it isn't in scope from here.)
956
+ const t = await fetchOutputTokenTotals(resolve(process.cwd()));
957
+ { const g = pickGlobalTotal(t); if (g !== null) globalOutputTokens = g; }
925
958
  }
926
959
 
927
960
  // Probe whether SSH KEY auth already works for a server (so we can tell the user to run
@@ -1282,6 +1315,15 @@ let sessionCodingLabel = "";
1282
1315
  // per-turn token footer next to the GLOBAL output total so it's clear which model that
1283
1316
  // lifetime count belongs to. The /model override (sessionCodingModel) wins when set.
1284
1317
  let defaultCodingModel = "";
1318
+ // Task #125: the coding-server URL of the backend in effect this session — the /model
1319
+ // pick's backend when one is chosen, else the account default. Sent to the output-token
1320
+ // endpoints so the GLOBAL lifetime total is keyed to the RIGHT backend (a /model switch to
1321
+ // another backend must not keep showing the previous backend's total). Empty = let the API
1322
+ // fall back to the account-default URL from settings (older behaviour).
1323
+ let sessionCodingUrl = "";
1324
+ // The account-default backend's URL (from the startup probe's codingChoices), used when no
1325
+ // /model override is active. Set once at startup alongside defaultCodingModel.
1326
+ let defaultCodingUrl = "";
1285
1327
  // Auto-test mode (/test-auto): when on, the agent verifies its work (build/run/curl/…)
1286
1328
  // and fixes → re-tests until it passes before finishing. Default off; remembered per
1287
1329
  // folder in the session file; sent to /agent-ask as autoTest so python drives the loop.
@@ -2008,7 +2050,7 @@ async function runAgentTurn(history) {
2008
2050
  if (latestOutputTokens > 0 && !outputTokensPosted) {
2009
2051
  outputTokensPosted = true;
2010
2052
  void addOutputTokens(resolve(process.cwd()), latestOutputTokens).then((t) => {
2011
- if (t && Number.isFinite(t.globalTotal)) globalOutputTokens = t.globalTotal;
2053
+ { const g = pickGlobalTotal(t); if (g !== null) globalOutputTokens = g; }
2012
2054
  // This turn's total is now folded into globalOutputTokens — zero the live
2013
2055
  // per-turn tally so the ↓ (global + latest) doesn't double-count it.
2014
2056
  latestOutputTokens = 0;
@@ -2166,14 +2208,17 @@ async function main() {
2166
2208
  // Task #104: seed the ↓ global OUTPUT-token total (user + coding-URL) from the API.
2167
2209
  {
2168
2210
  const t = await fetchOutputTokenTotals(resolve(process.cwd()));
2169
- if (t && Number.isFinite(t.globalTotal)) globalOutputTokens = t.globalTotal;
2211
+ { const g = pickGlobalTotal(t); if (g !== null) globalOutputTokens = g; }
2170
2212
  }
2171
2213
  // Show which models will answer, straight from user settings (also validates auth).
2172
2214
  try {
2173
2215
  const probe = await fetchAgentPayload([{ role: "user", content: "ping" }]);
2174
2216
  const p = probe?.payload ?? {};
2175
- // Remember the default coder id for the token footer's global-total label.
2217
+ // Remember the default coder id for the token footer's global-total label, and the
2218
+ // default backend's coding URL so the GLOBAL output-token total is keyed to it (task
2219
+ // #125) when no /model override is active.
2176
2220
  if (p.codingModelId) defaultCodingModel = p.codingModelId;
2221
+ if (p.codingBaseURL) defaultCodingUrl = p.codingBaseURL;
2177
2222
  // RAG storage choice (task #97): ask ONCE per folder, and only when RAG is actually
2178
2223
  // enabled in Settings. Default Yes = LOCAL (kept on this machine, no S3 needed).
2179
2224
  if (p.ragEnabled && sessionRagMode === null) {
@@ -2322,9 +2367,11 @@ async function main() {
2322
2367
  if (line === "/output-token") {
2323
2368
  // Task #104: print the OUTPUT-token total for THIS workspace (and the global one).
2324
2369
  const t = await fetchOutputTokenTotals(cwd);
2325
- if (t && Number.isFinite(t.globalTotal)) globalOutputTokens = t.globalTotal;
2370
+ { const g = pickGlobalTotal(t); if (g !== null) globalOutputTokens = g; }
2326
2371
  const ws = t && Number.isFinite(t.workspaceTotal) ? t.workspaceTotal : 0;
2327
- const gl = t && Number.isFinite(t.globalTotal) ? t.globalTotal : globalOutputTokens;
2372
+ // Task #126: same saved-else-counter rule as the footer (pickGlobalTotal), not the
2373
+ // raw counter — so /output-token and the live ↓ footer agree.
2374
+ const gl = pickGlobalTotal(t) ?? globalOutputTokens;
2328
2375
  console.log(
2329
2376
  cyan(`↓ output tokens`) +
2330
2377
  dim(` — this workspace: `) +
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tiens.nguyen/gonext-local-worker",
3
- "version": "1.0.334",
3
+ "version": "1.0.337",
4
4
  "description": "Polls GoNext cloud API for async local LLM jobs and runs them against Ollama/OpenAI-compatible servers on this Mac",
5
5
  "type": "module",
6
6
  "license": "MIT",