flowviant 0.89.0 → 0.90.0

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.
@@ -168,6 +168,43 @@ export const humanizeToolUse = humanizeClaudeTool;
168
168
  // Collapse whitespace + clip so a narration/thinking snippet is one tidy feed line.
169
169
  const oneLine = (s, n = 160) => String(s).replace(/\s+/g, ' ').trim().slice(0, n);
170
170
 
171
+ /**
172
+ * WHAT THIS TURN COST, IN THE CLI'S OWN NUMBERS (2026-09-19).
173
+ *
174
+ * The `result` event carries a `usage` object — `input_tokens`,
175
+ * `output_tokens`, `cache_creation_input_tokens`, `cache_read_input_tokens` —
176
+ * and this reads exactly those four and nothing else. A RELAY: nothing here
177
+ * derives, estimates or prices anything.
178
+ *
179
+ * DELIBERATELY NOT `total_cost_usd`, which rides the same event. It is a
180
+ * notional list price that a subscription operator did not pay, so relaying it
181
+ * as "cost" would be the product asserting a figure nobody was charged — the
182
+ * refusal the board's fix pass already made once against the mock's "1.4
183
+ * spent". Tokens are measured; dollars are not.
184
+ *
185
+ * COERCED AND FLOORED, never trusted: a non-number, a NaN, an Infinity or a
186
+ * negative reads as 0, because these are summed into a counter that only ever
187
+ * goes up and one bad field must not poison the other three.
188
+ *
189
+ * Returns null when there is no usage object at all — the three-state rule
190
+ * every readout here keeps: a turn that reported nothing charges nothing, and
191
+ * that is not the same as a turn that reported zeros.
192
+ */
193
+ export function usageFromResult(ev) {
194
+ const u = ev?.usage;
195
+ if (!u || typeof u !== 'object') return null;
196
+ const n = (v) => {
197
+ const x = Number(v);
198
+ return Number.isFinite(x) && x > 0 ? Math.floor(x) : 0;
199
+ };
200
+ return {
201
+ input: n(u.input_tokens),
202
+ output: n(u.output_tokens),
203
+ cacheCreate: n(u.cache_creation_input_tokens),
204
+ cacheRead: n(u.cache_read_input_tokens),
205
+ };
206
+ }
207
+
171
208
  // Parse ONE line of `--output-format stream-json` NDJSON into feed activities.
172
209
  // Surfaces the WHOLE turn — thinking, narration, AND every tool — so neither the
173
210
  // daemon console nor the app cover goes dark while Claude reasons (Opus thinks in
@@ -180,7 +217,15 @@ const oneLine = (s, n = 160) => String(s).replace(/\s+/g, ' ').trim().slice(0, n
180
217
  // every intermediate text block still NARRATES, but only the final `result`
181
218
  // event contributes text — otherwise the same sentences arrive twice, once as
182
219
  // they stream and once in the result, and the tab posts the duplicate.
183
- function handleStreamLine(line, { cwd, emit, onActivity, onToolEvent, appendText, answerFromResult, onInit }) {
220
+ //
221
+ // EXPORTED FOR ITS TEST ONLY (2026-09-19), and the reason is worth stating: the
222
+ // `onUsage` threading through this function, `runTurn`'s options and `onLine`
223
+ // was four edits and NOT ONE of them was reachable from either suite — delete
224
+ // any one and both stay green, while the only symptom in production is a
225
+ // container that reports no spend, which is indistinguishable from an older
226
+ // daemon by design. A callback that is silent when it breaks has to be called
227
+ // directly by something.
228
+ export function handleStreamLine(line, { cwd, emit, onActivity, onToolEvent, appendText, answerFromResult, onInit, onUsage }) {
184
229
  let ev;
185
230
  try {
186
231
  ev = JSON.parse(line);
@@ -262,6 +307,18 @@ function handleStreamLine(line, { cwd, emit, onActivity, onToolEvent, appendText
262
307
  });
263
308
  }
264
309
  } else if (ev.type === 'result') {
310
+ /**
311
+ * WHAT THE TURN SPENT, BEFORE ANYTHING ELSE IN THIS BRANCH.
312
+ *
313
+ * FIRED ON A FAILED RESULT TOO, and that ordering is the point: a turn that
314
+ * hit a limit, ran out of permission or aborted still sent the requests it
315
+ * sent, and a spend readout that quietly skipped every unhappy turn would
316
+ * under-report exactly the runs somebody is looking at the number to
317
+ * understand. `usageFromResult` returns null when the event carries no
318
+ * usage at all, and a caller that passes no `onUsage` sees no change.
319
+ */
320
+ const usage = usageFromResult(ev);
321
+ if (usage) onUsage?.(usage);
265
322
  // The final assistant text (carries WIKI_DONE / REGROUND_DONE).
266
323
  if (typeof ev.result === 'string') appendText(ev.result + '\n');
267
324
  else if (ev.is_error || ev.subtype) {
@@ -294,7 +351,7 @@ function handleStreamLine(line, { cwd, emit, onActivity, onToolEvent, appendText
294
351
  // returned string for sentinel detection, and each activity is handed to
295
352
  // `onActivity` so the caller can forward progress. Build-agent turns leave it
296
353
  // off and keep the raw text passthrough + line sentinels.
297
- export function runTurn({ prompt, resume, system, cwd, mcpConfig, mcpArgs, mcpEnv, runtime = 'claude', label, onSpawn, streamJson, answerFromResult, onActivity, onToolEvent, onInit, onThreadId, wikiPerm, readOnly, planPerm, vaultDir, resultSchemaArgs, model, effort, adoptResumeId, resumeThreadId, resumeConversationId }) {
354
+ export function runTurn({ prompt, resume, system, cwd, mcpConfig, mcpArgs, mcpEnv, runtime = 'claude', label, onSpawn, streamJson, answerFromResult, onActivity, onToolEvent, onInit, onUsage, onThreadId, wikiPerm, readOnly, planPerm, vaultDir, resultSchemaArgs, model, effort, adoptResumeId, resumeThreadId, resumeConversationId }) {
298
355
  return new Promise((resolve) => {
299
356
  const rt = runtimeById(runtime);
300
357
  if (!rt.args) {
@@ -425,7 +482,7 @@ export function runTurn({ prompt, resume, system, cwd, mcpConfig, mcpArgs, mcpEn
425
482
  /** One line of the child's stdout, in whichever dialect it speaks. */
426
483
  const onLine = (line) => {
427
484
  if (!rt.parse)
428
- return handleStreamLine(line, { cwd, emit, onActivity, onToolEvent, appendText, answerFromResult, onInit });
485
+ return handleStreamLine(line, { cwd, emit, onActivity, onToolEvent, appendText, answerFromResult, onInit, onUsage });
429
486
  const ev = rt.parse(line, cwd);
430
487
  if (!ev) return;
431
488
  // The conversation id, when the runtime announces one (codex's
package/bin/lib/work.mjs CHANGED
@@ -4885,6 +4885,21 @@ export function createWorkManager({
4885
4885
 
4886
4886
  let out = '';
4887
4887
  let child = null;
4888
+ /**
4889
+ * WHAT THIS TURN SPENT, AS THE CLI COUNTED IT (2026-09-19).
4890
+ *
4891
+ * Held across the whole turn so every settle below can carry it — a turn
4892
+ * that hit a limit, produced no parseable outcome or delivered properly
4893
+ * all spent real tokens, and a readout that only charged the happy path
4894
+ * would under-report the runs somebody opens the number to understand.
4895
+ *
4896
+ * The three settles it is spread into are the ones that follow a CLI
4897
+ * actually running. The pre-spawn refusals above (a bad place, a missing
4898
+ * card, the begun-guard) and the teardown sweep below deliberately do NOT
4899
+ * take it: nothing ran there, and `usage` stays null anyway, which is
4900
+ * what makes `...(usage ? … : {})` the whole guard.
4901
+ */
4902
+ let usage = null;
4888
4903
  try {
4889
4904
  out = await runTurn({
4890
4905
  prompt:
@@ -4944,6 +4959,13 @@ export function createWorkManager({
4944
4959
  onToolEvent: (name, input) => {
4945
4960
  trace.tool(toolEventOf(name, input, wt, envScrub));
4946
4961
  },
4962
+ // The CLI's own per-turn token counts, off the `result` event. One
4963
+ // result per turn, so this is a set and not an accumulate — the
4964
+ // adding-up happens SERVER-side, behind the settle's idempotent win,
4965
+ // because a retried settle must not charge the same turn twice.
4966
+ onUsage: (u) => {
4967
+ usage = u;
4968
+ },
4947
4969
  onSpawn: (ch) => {
4948
4970
  child = ch;
4949
4971
  // The AGENT it serves — what the machine snapshot charges this
@@ -5015,7 +5037,14 @@ export function createWorkManager({
5015
5037
  // limit means all of them have. The turn itself is reported as
5016
5038
  // `nothing` — it did not deliver and it did not ask.
5017
5039
  await postAgentParked(limit);
5018
- await postAgentTurn({ turnId, outcome: 'nothing', answer: limit, branch, worktree: wt });
5040
+ await postAgentTurn({
5041
+ turnId,
5042
+ outcome: 'nothing',
5043
+ answer: limit,
5044
+ ...(usage ? { usage } : {}),
5045
+ branch,
5046
+ worktree: wt,
5047
+ });
5019
5048
  return;
5020
5049
  }
5021
5050
 
@@ -5029,6 +5058,7 @@ export function createWorkManager({
5029
5058
  ? envScrub(out).slice(-1500)
5030
5059
  : 'the turn produced no output on the machine — its CLI may be signed out',
5031
5060
  ...(commits.length ? { commits } : {}),
5061
+ ...(usage ? { usage } : {}),
5032
5062
  branch,
5033
5063
  worktree: wt,
5034
5064
  });
@@ -5040,6 +5070,7 @@ export function createWorkManager({
5040
5070
  answer: envScrub(res.answer ?? '').slice(0, 8000),
5041
5071
  ...(commits.length ? { commits } : {}),
5042
5072
  ...(res.raised?.length ? { raised: res.raised } : {}),
5073
+ ...(usage ? { usage } : {}),
5043
5074
  branch,
5044
5075
  worktree: wt,
5045
5076
  });
@@ -5450,6 +5481,17 @@ export function createWorkManager({
5450
5481
  * and is retried ONCE, because unlike a trace batch this body cost a whole
5451
5482
  * model call and losing it to a blip means the operator paid for a label
5452
5483
  * nobody ever sees.
5484
+ *
5485
+ * THE RETRY IS NOT A GUARANTEE THAT THE FIRST ATTEMPT FAILED, and since the
5486
+ * body started carrying `usage` (0.90.0) that matters. The `catch` below
5487
+ * takes the 30-second `AbortSignal` with everything else, so a server that
5488
+ * was merely SLOW — and committed — receives this identical body twice. The
5489
+ * store was always idempotent; the token counts riding it were not, and a
5490
+ * timed-out-but-committed post added them twice. The SERVER closes that: its
5491
+ * write refuses a body whose text already sits on the row, so a re-send
5492
+ * stores the same reading and charges nothing for it. Nothing here needs to
5493
+ * change, and nothing here may start assuming a thrown fetch means the
5494
+ * server never saw this.
5453
5495
  */
5454
5496
  const postPre = async (body) => {
5455
5497
  try {
@@ -5552,6 +5594,21 @@ export function createWorkManager({
5552
5594
  let timer = null;
5553
5595
  /** The cap fired: the CLI was still running when this machine stopped it. */
5554
5596
  let wedged = false;
5597
+ /**
5598
+ * WHAT THE READING SPENT — the agent-turn lane's own relay, on the one
5599
+ * other lane that charges an AGENT (2026-09-19).
5600
+ *
5601
+ * It rides the agent and never a card link: a pre-review belongs to no card
5602
+ * (the same reason `workChildren` gets a null task id below), so the four
5603
+ * counters it adds to are the container's alone. That is also why an
5604
+ * agent's links can never sum to its total, and the schema says so.
5605
+ *
5606
+ * A WEDGED OR UNPARSEABLE READING POSTS NOTHING AT ALL, so it charges
5607
+ * nothing either — which under-reports a turn that really did spend. That
5608
+ * is the honest direction: there is no row to put it on, and inventing a
5609
+ * settle to carry a number would be a post whose only content is a bill.
5610
+ */
5611
+ let usage = null;
5555
5612
  /**
5556
5613
  * THE CONVERSATION THIS READING SPEAKS UNDER — held for exactly one reason:
5557
5614
  * to DELETE the transcript it leaves behind (review, 2026-09-17).
@@ -5612,6 +5669,11 @@ export function createWorkManager({
5612
5669
  if (typeof i.sessionId === 'string' && i.sessionId.trim())
5613
5670
  preSession = i.sessionId.trim();
5614
5671
  },
5672
+ // The CLI's own count for this reading. Set, never accumulated: one
5673
+ // turn is one `result` event, and the adding-up is the server's.
5674
+ onUsage: (u) => {
5675
+ usage = u;
5676
+ },
5615
5677
  onSpawn: (ch) => {
5616
5678
  child = ch;
5617
5679
  // No task id: a pre-review belongs to no card, and the machine
@@ -5710,6 +5772,7 @@ export function createWorkManager({
5710
5772
  const body = {
5711
5773
  agentId,
5712
5774
  ...(headSha ? { headSha } : {}),
5775
+ ...(usage ? { usage } : {}),
5713
5776
  cards: result.cards.map((cd) => ({
5714
5777
  taskId: cd.taskId,
5715
5778
  verdict: cd.verdict,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "flowviant",
3
- "version": "0.89.0",
3
+ "version": "0.90.0",
4
4
  "description": "Run your own coding CLIs as build agents for Flowviant \u2014 Claude Code, Codex or Antigravity, on your own credentials. Holds your sessions, keeps a worktree per tab, and ships branches on your word.",
5
5
  "type": "module",
6
6
  "bin": {