@tiens.nguyen/gonext-local-worker 1.0.293 → 1.0.295

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.
@@ -1997,6 +1997,10 @@ async function runAgentChatJob(job) {
1997
1997
  let latestCodeTokens = 0;
1998
1998
  // Task #84: peak single-request prompt-token count seen this turn (not the sum).
1999
1999
  let latestMaxCodeTokens = 0;
2000
+ // Task #104: running total of OUTPUT (completion) tokens the CODE model generated this
2001
+ // turn. Persisted to the job doc; the REPL folds this turn's total into the lifetime
2002
+ // per-workspace + per-(user, coding-URL) counters at completion.
2003
+ let latestCodeOutputTokens = 0;
2000
2004
  // Raw model-stream tokens (event.type === "stream") are Python code + `Thought:`
2001
2005
  // prose + <code>/<|"|> fragments. The web Thought panel renders reasoning as
2002
2006
  // markdown, which turns `#`-prefixed code comments into <h1> headings and
@@ -2133,14 +2137,24 @@ async function runAgentChatJob(job) {
2133
2137
  const nextMax = Number.isFinite(event.codeMax)
2134
2138
  ? Math.max(latestMaxCodeTokens, event.codeMax)
2135
2139
  : latestMaxCodeTokens;
2136
- if (nextTotal !== latestCodeTokens || nextMax !== latestMaxCodeTokens) {
2140
+ // Task #104: output tokens climb monotonically across the turn too.
2141
+ const nextOut = Number.isFinite(event.codeOutput)
2142
+ ? Math.max(latestCodeOutputTokens, event.codeOutput)
2143
+ : latestCodeOutputTokens;
2144
+ if (
2145
+ nextTotal !== latestCodeTokens ||
2146
+ nextMax !== latestMaxCodeTokens ||
2147
+ nextOut !== latestCodeOutputTokens
2148
+ ) {
2137
2149
  latestCodeTokens = nextTotal;
2138
2150
  latestMaxCodeTokens = nextMax;
2151
+ latestCodeOutputTokens = nextOut;
2139
2152
  workerFetch(`/api/worker/jobs/${jobId}`, {
2140
2153
  method: "PATCH",
2141
2154
  body: JSON.stringify({
2142
2155
  agentCodeTokens: latestCodeTokens,
2143
2156
  agentMaxCodeTokens: latestMaxCodeTokens,
2157
+ agentOutputTokens: latestCodeOutputTokens,
2144
2158
  }),
2145
2159
  }).catch(() => {});
2146
2160
  }
@@ -2196,6 +2210,7 @@ async function runAgentChatJob(job) {
2196
2210
  tokenCount: Math.max(1, Math.ceil((finalText || fullText).length / 4)),
2197
2211
  ...(latestCodeTokens > 0 ? { agentCodeTokens: latestCodeTokens } : {}),
2198
2212
  ...(latestMaxCodeTokens > 0 ? { agentMaxCodeTokens: latestMaxCodeTokens } : {}),
2213
+ ...(latestCodeOutputTokens > 0 ? { agentOutputTokens: latestCodeOutputTokens } : {}),
2199
2214
  totalTimeSeconds,
2200
2215
  }),
2201
2216
  });
package/gonext-repl.mjs CHANGED
@@ -193,6 +193,8 @@ const COMMANDS = [
193
193
  { name: "/server", desc: "pick a deployment server (host/user) for this session" },
194
194
  { name: "/revert", usage: "/revert [runId]", desc: "undo the agent's file edits (latest run)" },
195
195
  { name: "/reset", aliases: ["/new"], desc: "forget this folder's saved conversation and start fresh" },
196
+ { name: "/output-token", desc: "show the agent code-model OUTPUT-token total for this workspace" },
197
+ { name: "/reset-output-token-global", desc: "reset the GLOBAL (you + coding server) output-token total to 0" },
196
198
  { name: "/help", desc: "list these commands" },
197
199
  { name: "/exit", aliases: ["/quit"], desc: "quit" },
198
200
  ];
@@ -548,6 +550,49 @@ let wsSync = { enabled: false, name: "", path: "" };
548
550
  const workspaceKeyFor = (cwd) =>
549
551
  createHash("sha256").update(cwd).digest("hex").slice(0, 32);
550
552
 
553
+ // Task #104: agent CODE-model OUTPUT-token counters. The API keys the global total on
554
+ // (userId + coding-model URL from settings) and the workspace total on workspaceKey; the
555
+ // REPL just supplies workspaceKey + this turn's delta. All best-effort (never block a turn).
556
+ async function fetchOutputTokenTotals(cwd) {
557
+ try {
558
+ const r = await fetch(
559
+ `${apiBase}/api/worker/output-tokens?workspaceKey=${workspaceKeyFor(cwd)}`,
560
+ { headers: { "X-Worker-Key": workerKey } }
561
+ );
562
+ if (!r.ok) return null;
563
+ return await r.json();
564
+ } catch {
565
+ return null;
566
+ }
567
+ }
568
+
569
+ async function addOutputTokens(cwd, delta) {
570
+ if (!(delta > 0)) return null;
571
+ try {
572
+ const r = await fetch(`${apiBase}/api/worker/output-tokens`, {
573
+ method: "POST",
574
+ headers: { "Content-Type": "application/json", "X-Worker-Key": workerKey },
575
+ body: JSON.stringify({ workspaceKey: workspaceKeyFor(cwd), delta }),
576
+ });
577
+ if (!r.ok) return null;
578
+ return await r.json();
579
+ } catch {
580
+ return null;
581
+ }
582
+ }
583
+
584
+ async function resetGlobalOutputTokens() {
585
+ try {
586
+ const r = await fetch(`${apiBase}/api/worker/output-tokens/reset-global`, {
587
+ method: "POST",
588
+ headers: { "Content-Type": "application/json", "X-Worker-Key": workerKey },
589
+ });
590
+ return r.ok;
591
+ } catch {
592
+ return false;
593
+ }
594
+ }
595
+
551
596
  async function pushWorkspaceSync(cwd, history, { del = false } = {}) {
552
597
  if (!wsSync.enabled) return;
553
598
  const body = del
@@ -1023,6 +1068,10 @@ let selectedServer = null;
1023
1068
  // as a max across turns and persisted per folder in the session file. Shown in orange
1024
1069
  // next to the live token count.
1025
1070
  let sessionMaxCodeTokens = 0;
1071
+ // Task #104: GLOBAL lifetime OUTPUT-token total for (this user + the agent coding-model
1072
+ // API URL), fetched from the API. Shown with a ↓ next to the input/max counts. Updated
1073
+ // after each turn (the API returns the new total) and refreshed at startup.
1074
+ let globalOutputTokens = 0;
1026
1075
 
1027
1076
  // While a turn runs, mute readline's own echo/redraws so keystrokes can't corrupt the
1028
1077
  // live status display and there's no way to type a new question — only Ctrl+C works.
@@ -1235,6 +1284,10 @@ async function runAgentTurn(history) {
1235
1284
  // Running total of prompt tokens sent to the agent CODE model this turn (task #83),
1236
1285
  // read from the job doc each poll. Rendered on the thinking line after the playful word.
1237
1286
  let latestCodeTokens = 0;
1287
+ // Task #104: this turn's OUTPUT-token total, read from the job doc each poll. Folded into
1288
+ // the lifetime per-workspace + global counters once the turn completes.
1289
+ let latestOutputTokens = 0;
1290
+ let outputTokensPosted = false; // guard: increment the lifetime counters once per turn
1238
1291
  // First few KB of the raw stream as consumed — a cheap fingerprint to verify the
1239
1292
  // completed resultText really EXTENDS what we streamed (an old worker replaces it
1240
1293
  // with the shorter bare answer, which must not be sliced by shownChars).
@@ -1321,7 +1374,9 @@ async function runAgentTurn(history) {
1321
1374
  const fit = (s) => (s.length > max ? s.slice(0, max - 1) + "…" : s);
1322
1375
  const tokLabel =
1323
1376
  dim(` · ${fmtTokens(latestCodeTokens)} tok`) +
1324
- orange(` · ${fmtTokens(sessionMaxCodeTokens)} max`);
1377
+ orange(` · ${fmtTokens(sessionMaxCodeTokens)} max`) +
1378
+ // Task #104: global (user + coding-URL) lifetime OUTPUT-token total, ↓ = generated.
1379
+ (globalOutputTokens > 0 ? cyan(` · ↓ ${fmtTokens(globalOutputTokens)}`) : "");
1325
1380
  // Task #96: the thought line is "clickable" only when the fuller thought has MORE text than
1326
1381
  // fits on line 1 (a live command / bare "Thinking" / a thought that already fits has nothing
1327
1382
  // to reveal). Line 1 (what's happening now): a live command > the model's current Thought >
@@ -1686,8 +1741,21 @@ async function runAgentTurn(history) {
1686
1741
  if (Number.isFinite(job.agentMaxCodeTokens) && job.agentMaxCodeTokens > sessionMaxCodeTokens) {
1687
1742
  sessionMaxCodeTokens = job.agentMaxCodeTokens;
1688
1743
  }
1744
+ // This turn's OUTPUT-token total (task #104), monotonic across polls.
1745
+ if (Number.isFinite(job.agentOutputTokens) && job.agentOutputTokens > latestOutputTokens) {
1746
+ latestOutputTokens = job.agentOutputTokens;
1747
+ }
1689
1748
  const text = typeof job.resultText === "string" ? job.resultText : "";
1690
1749
  if (job.jobStatus === "completed") {
1750
+ // Task #104: fold this turn's output-token total into the lifetime per-workspace +
1751
+ // global (user + coding-URL) counters, exactly once. Best-effort; the API returns
1752
+ // the new global total, which drives the ↓ header indicator.
1753
+ if (latestOutputTokens > 0 && !outputTokensPosted) {
1754
+ outputTokensPosted = true;
1755
+ void addOutputTokens(resolve(process.cwd()), latestOutputTokens).then((t) => {
1756
+ if (t && Number.isFinite(t.globalTotal)) globalOutputTokens = t.globalTotal;
1757
+ });
1758
+ }
1691
1759
  // The job can flip to "completed" between polls with authoritative resultText that
1692
1760
  // the live stream hasn't caught up to yet (a debounced last chunk, or the final
1693
1761
  // PATCH landing before its chunk flush was polled). resultText is the source of
@@ -1810,6 +1878,11 @@ async function main() {
1810
1878
  selectedServer = await loadSessionServer(resolve(process.cwd()));
1811
1879
  sessionMaxCodeTokens = await loadSessionMaxCodeTokens(resolve(process.cwd()));
1812
1880
  sessionRagMode = await loadSessionRagMode(resolve(process.cwd())); // task #97: local/cloud/null
1881
+ // Task #104: seed the ↓ global OUTPUT-token total (user + coding-URL) from the API.
1882
+ {
1883
+ const t = await fetchOutputTokenTotals(resolve(process.cwd()));
1884
+ if (t && Number.isFinite(t.globalTotal)) globalOutputTokens = t.globalTotal;
1885
+ }
1813
1886
  // Show which models will answer, straight from user settings (also validates auth).
1814
1887
  try {
1815
1888
  const probe = await fetchAgentPayload([{ role: "user", content: "ping" }]);
@@ -1957,6 +2030,40 @@ async function main() {
1957
2030
  console.log(dim("Session cleared — starting fresh.\n"));
1958
2031
  continue;
1959
2032
  }
2033
+ if (line === "/output-token") {
2034
+ // Task #104: print the OUTPUT-token total for THIS workspace (and the global one).
2035
+ const t = await fetchOutputTokenTotals(cwd);
2036
+ if (t && Number.isFinite(t.globalTotal)) globalOutputTokens = t.globalTotal;
2037
+ const ws = t && Number.isFinite(t.workspaceTotal) ? t.workspaceTotal : 0;
2038
+ const gl = t && Number.isFinite(t.globalTotal) ? t.globalTotal : globalOutputTokens;
2039
+ console.log(
2040
+ cyan(`↓ output tokens`) +
2041
+ dim(` — this workspace: `) +
2042
+ `${fmtTokens(ws)}` +
2043
+ dim(` · global (you + coding server): `) +
2044
+ `${fmtTokens(gl)}\n`
2045
+ );
2046
+ continue;
2047
+ }
2048
+ if (line === "/reset-output-token-global") {
2049
+ // Task #104: zero the GLOBAL (user + coding-URL) output-token counter after confirm.
2050
+ const ok = await askYesNo(
2051
+ `Reset the GLOBAL output-token total (currently ${fmtTokens(globalOutputTokens)}) to 0?`,
2052
+ false
2053
+ );
2054
+ if (!ok) {
2055
+ console.log(dim("Cancelled — global output-token total unchanged.\n"));
2056
+ continue;
2057
+ }
2058
+ const done = await resetGlobalOutputTokens();
2059
+ if (done) {
2060
+ globalOutputTokens = 0;
2061
+ console.log(green("✓ global output-token total reset to 0.\n"));
2062
+ } else {
2063
+ console.log(dim("Could not reset the global output-token total (API error).\n"));
2064
+ }
2065
+ continue;
2066
+ }
1960
2067
  if (line.startsWith("/revert")) {
1961
2068
  const runId = line.split(/\s+/)[1] ?? "";
1962
2069
  await new Promise((res) => {
@@ -4886,6 +4886,10 @@ def run_agent_chat(cfg):
4886
4886
  # Largest SINGLE-request prompt-token count seen this turn (task #84) — the
4887
4887
  # peak context size, not the sum. The REPL keeps a per-workspace max of this.
4888
4888
  self._code_tokens_max = 0
4889
+ # Running total of OUTPUT (completion) tokens the CODE model GENERATED across
4890
+ # the turn (task #104). Reported by the server usage; the REPL adds this turn's
4891
+ # total into the per-workspace + per-(user, coding-URL) global lifetime counters.
4892
+ self._code_output_tokens_total = 0
4889
4893
  # Loop-breaker (#95 B1): how many CONSECUTIVE generations have produced a code
4890
4894
  # block that STILL won't compile after our repair. A weak coder can get stuck
4891
4895
  # re-emitting the same unparseable tool call (seen live: create_file with a
@@ -5225,6 +5229,23 @@ def run_agent_chat(cfg):
5225
5229
  })
5226
5230
  except Exception: # noqa: BLE001
5227
5231
  pass
5232
+ # Task #104: accumulate OUTPUT (completion) tokens this step and report the
5233
+ # running total. Unlike input, there's no request-time estimate — output is
5234
+ # only known once the model finished, so we emit after the call returns.
5235
+ try:
5236
+ tu = getattr(msg, "token_usage", None)
5237
+ step_out = int(getattr(tu, "output_tokens", 0) or 0) if tu else 0
5238
+ if step_out > 0:
5239
+ self._code_output_tokens_total += step_out
5240
+ _emit({
5241
+ "type": "tokens",
5242
+ "codeInput": self._code_tokens_total,
5243
+ "codeMax": self._code_tokens_max,
5244
+ "codeOutput": self._code_output_tokens_total,
5245
+ "stepOutput": step_out,
5246
+ })
5247
+ except Exception: # noqa: BLE001
5248
+ pass
5228
5249
  try:
5229
5250
  content = getattr(msg, "content", None)
5230
5251
  if content is None:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tiens.nguyen/gonext-local-worker",
3
- "version": "1.0.293",
3
+ "version": "1.0.295",
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",