flowviant 0.86.0 → 0.87.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.
@@ -108,9 +108,27 @@ export function parseProposal(text) {
108
108
  ...(Number.isFinite(g.pointsBudget) && g.pointsBudget > 0
109
109
  ? { pointsBudget: Math.min(Math.round(g.pointsBudget), 100_000) }
110
110
  : {}),
111
- ...(Array.isArray(g.waitsOn)
112
- ? { waitsOn: g.waitsOn.filter((w) => typeof w === 'string' && w).slice(0, 20) }
113
- : {}),
111
+ // NO `waitsOn`, AND ITS ABSENCE IS THE FEATURE (2026-09-16).
112
+ //
113
+ // The planner's schema used to carry it — tempIds of agents that had to
114
+ // MERGE first — and the owner deleted the idea: "whats the point of
115
+ // dividing up the agents if one of the agents rely on waiting for one to
116
+ // finish? if thats the case have it be in the same agent." An agent
117
+ // already works its cards in order, so a chain split across two agents
118
+ // buys a second worktree, a second branch and a second review and then
119
+ // idles one of them; the only thing a split buys is SIMULTANEOUS work.
120
+ // SYSTEM_PLAN no longer mentions the key at all.
121
+ //
122
+ // So a model that emits it anyway is answering a schema it was not given
123
+ // — an older prompt cached in a resumed conversation, or invention — and
124
+ // reading it would create a silently-waiting agent through the exact door
125
+ // the prompt just closed. DROPPED, not REFUSED: this file's own law is
126
+ // lenient packaging, strict shape, and a stray key is packaging. The
127
+ // proposal is otherwise good work the operator already paid for.
128
+ //
129
+ // The SERVER still accepts and honours `waitsOn` on the wire — 0.86.0
130
+ // daemons are still proposing it and existing agents still carry it. This
131
+ // is the end of PROPOSING one, not the end of reading one.
114
132
  ...(typeof g.intoAgentId === 'string' && g.intoAgentId
115
133
  ? { intoAgentId: g.intoAgentId.slice(0, 64) }
116
134
  : {}),
@@ -13,7 +13,7 @@
13
13
 
14
14
  import { spawn } from 'node:child_process';
15
15
  import { SAFE } from './config.mjs';
16
- import { runtimeById, humanizeClaudeTool } from './runtimes.mjs';
16
+ import { runtimeById, humanizeClaudeTool, THINK_MARKER } from './runtimes.mjs';
17
17
 
18
18
  // Every prompt/kickoff constant lives in prompts.mjs and is re-exported here:
19
19
  // a dozen call sites import them from claude.mjs, and none of them care where
@@ -197,12 +197,39 @@ function handleStreamLine(line, { cwd, emit, onActivity, onToolEvent, appendText
197
197
  if (ev.type === 'assistant' && Array.isArray(ev.message?.content)) {
198
198
  for (const b of ev.message.content) {
199
199
  if (b.type === 'thinking' || b.type === 'redacted_thinking') {
200
- // The `thinking` text is usually redacted (signature only), so emit a
201
- // marker enough to show Claude is actively reasoning, not hung.
202
- push({ kind: 'think', label: b.thinking ? `thinking: ${oneLine(b.thinking)}` : 'thinking…' });
200
+ /**
201
+ * NO THINKING TEXT ARRIVES TODAY, AND THAT IS MEASURED (2026-09-16).
202
+ *
203
+ * This used to say the text is "usually redacted", which was a guess
204
+ * doing the work of a fact. The fact: across three real transcripts, 93
205
+ * thinking blocks, EVERY ONE of them carried an empty `thinking` — and
206
+ * a live probe with MAX_THINKING_TOKENS set and
207
+ * `--include-partial-messages` on returned an empty `thinking_delta`
208
+ * and a complete block of length zero (signature only). So the CLI
209
+ * emits the FACT that it reasoned and not a word of the reasoning, and
210
+ * "show the full thinking" cannot be conjured from this stream.
211
+ *
212
+ * THE BRANCH BELOW EXISTS ANYWAY, and deliberately. The shape is the
213
+ * whole point: when text is present it rides `full` UNCLIPPED, so the
214
+ * day a CLI release starts emitting it, the trace carries the thought
215
+ * whole with no daemon change and no version floor — the report's own
216
+ * presence is the capability. Until then every block takes the marker
217
+ * arm, and `trace.mjs` collapses a run of identical markers so the
218
+ * absence reads as one quiet step rather than forty.
219
+ */
220
+ push(
221
+ b.thinking
222
+ ? { kind: 'think', label: `thinking: ${oneLine(b.thinking)}`, full: b.thinking }
223
+ : { kind: 'think', label: THINK_MARKER }
224
+ );
203
225
  } else if (b.type === 'text' && b.text?.trim()) {
204
226
  if (!answerFromResult) appendText(b.text + '\n');
205
- push({ kind: 'say', label: oneLine(b.text) });
227
+ // `label` for the console and the pulse — one collapsed 160-char line,
228
+ // byte-identical to what it has always printed. `full` for the trace:
229
+ // a `say` is the agent NARRATING, several sentences at a time, and the
230
+ // clip at 160 was landing mid-sentence on the one thing a person opens
231
+ // the page to read.
232
+ push({ kind: 'say', label: oneLine(b.text), full: b.text });
206
233
  } else if (b.type === 'tool_use') {
207
234
  push(humanizeToolUse(b.name, b.input || {}, cwd));
208
235
  // The STRUCTURED form of the same event, for the transcript's tool
package/bin/lib/fleet.mjs CHANGED
@@ -82,6 +82,7 @@ import {
82
82
  probeSkillsOnce,
83
83
  recordSkills,
84
84
  RUNTIMES,
85
+ THINK_MARKER,
85
86
  } from './runtimes.mjs';
86
87
  import { createWorkManager } from './work.mjs';
87
88
  import { scanLocalSessions, ourConversationIds } from './localSessions.mjs';
@@ -1598,7 +1599,13 @@ export async function runFleetDaemon() {
1598
1599
  pagesSeen.add(a.path || a.label);
1599
1600
  }
1600
1601
  // Collapse runs of bare "thinking…" so the feed doesn't fill with it.
1601
- if (!(a.label === 'thinking…' && feed[feed.length - 1] === 'thinking…')) {
1602
+ // Keyed on the SHARED constant (runtimes.mjs), not on the literal: the
1603
+ // labels being compared here are the ones claude.mjs now builds from
1604
+ // that constant, so a reworded marker would leave this comparison
1605
+ // matching nothing and the 48-slot feed filling with the repeat — the
1606
+ // exact noise this line exists to stop, and silent, because a collapse
1607
+ // that stops collapsing fails no test.
1608
+ if (!(a.label === THINK_MARKER && feed[feed.length - 1] === THINK_MARKER)) {
1602
1609
  feed.push(a.label);
1603
1610
  if (feed.length > 48) feed.shift();
1604
1611
  }
@@ -588,6 +588,35 @@ export const REGROUND_KICKOFF = ({ sha, title, files, vaultDir, predictedPages =
588
588
  * TOUCHED. Everything else — how long something will take, who should own it,
589
589
  * whether it is a good idea — is not a question a planner can answer from a
590
590
  * repository, and asking for it produces confident invention.
591
+ *
592
+ * ── THERE IS NO WAY TO SAY "THIS ONE WAITS" (2026-09-16) ──
593
+ *
594
+ * The answer schema used to carry `waitsOn`, a list of tempIds an agent had to
595
+ * see MERGE before it could start, and the owner deleted the idea outright:
596
+ *
597
+ * "for the planning agent, whats the point of dividing up the agents if one
598
+ * of the agents rely on waiting for one to finish? if thats the case have it
599
+ * be in the same agent. the point of having it divide into various agent is
600
+ * so that its capable of parallel and simultaneous work."
601
+ *
602
+ * That is not a preference about a field, it is what an agent IS. An agent is
603
+ * one CLI in one worktree working its cards IN ORDER — ordering is the thing it
604
+ * already does, for free, with no branch to merge in between. So a planner that
605
+ * splits a chain across two agents has bought nothing and paid twice: a second
606
+ * worktree, a second branch, a second review, and a second agent sitting idle
607
+ * until the first one lands. The ONLY thing a split buys is two CLIs typing at
608
+ * the same time, and work that waits cannot do that by definition.
609
+ *
610
+ * So the vocabulary is gone rather than discouraged. A rule the model can still
611
+ * express a violation of is a rule it will sometimes express a violation of;
612
+ * removing the key removes the move. Rule 1 below carries the reasoning in the
613
+ * planner's own terms, and `agentPlan.mjs` drops the key if a model on an older
614
+ * prompt sends it anyway.
615
+ *
616
+ * WHAT DID NOT CHANGE, on purpose: the SERVER still accepts `waitsOn` on the
617
+ * wire and still honours it on stored proposals, because 0.86.0 daemons are
618
+ * still running and agents created under the old prompt still exist. This is a
619
+ * change to what is PROPOSED, not to what can be read.
591
620
  */
592
621
  export const SYSTEM_PLAN = `You are the human's own Claude, planning a batch of work in their repository.
593
622
 
@@ -602,8 +631,13 @@ as a single reviewable branch.
602
631
  THE RULES THAT MATTER:
603
632
 
604
633
  1. SEQUENTIAL WORK BELONGS IN ONE AGENT. If B needs A's code to exist, put them
605
- in the same agent, A first. Splitting a chain across agents means the second
606
- one waits for the first to MERGE before it can even start.
634
+ in the same agent, A first an agent works its cards in the order you give,
635
+ so ordering is free and costs no merge in between. THE ONLY REASON TO SPLIT
636
+ IS WORK THAT CAN RUN AT THE SAME TIME. There is no way to say that one agent
637
+ waits for another, and that is deliberate: an agent that has to wait is a
638
+ split done wrong. Splitting a chain buys a second worktree, a second branch
639
+ and a second review, and the second agent sits idle until the first one
640
+ merges — slower than one agent doing both in order.
607
641
 
608
642
  2. SPLIT ONLY WHAT CAN GENUINELY RUN AT THE SAME TIME. Two agents editing the
609
643
  same files land two branches that conflict, and somebody resolves it by hand.
@@ -641,7 +675,6 @@ after it. Wrap it in a \`\`\`json fence:
641
675
  "name": "auth",
642
676
  "taskIds": ["<card id>", "<card id>"],
643
677
  "pointsBudget": 8,
644
- "waitsOn": [],
645
678
  "intoAgentId": null
646
679
  }
647
680
  ]
@@ -649,7 +682,7 @@ after it. Wrap it in a \`\`\`json fence:
649
682
  \`\`\`
650
683
 
651
684
  Every selected card id must appear EXACTLY ONCE across all agents. Use the ids
652
- exactly as given. "waitsOn" holds tempIds of agents that must MERGE first.`;
685
+ exactly as given.`;
653
686
 
654
687
  /**
655
688
  * The planner's turn.
@@ -46,6 +46,41 @@ import { SAFE, MODEL, USER_AGENT } from './config.mjs';
46
46
  const oneLine = (s, n = 140) =>
47
47
  String(s ?? '').replace(/\s+/g, ' ').trim().slice(0, n);
48
48
 
49
+ /**
50
+ * `label` IS THE READOUT; `full` IS THE RELAY (2026-09-16) — the convention
51
+ * every prose activity in this daemon follows, stated once here because three
52
+ * parsers produce one and three consumers read them.
53
+ *
54
+ * `label` is one collapsed, clamped line, because its first two consumers are a
55
+ * terminal console and a one-line pulse that is overwritten every two seconds,
56
+ * and neither can show a paragraph. The turn TRACE is a third consumer with the
57
+ * opposite need: it is scrollback, and clipping a sentence at 140 characters
58
+ * there is the product summarizing its own agent.
59
+ *
60
+ * So a prose activity may ALSO carry `full`: the text EXACTLY as the CLI
61
+ * emitted it, uncollapsed and unclipped. Nothing derives it and nothing but the
62
+ * trace reads it — `label` is untouched, so the console and the pulse are
63
+ * byte-identical to before. Absent means there was no fuller text than the
64
+ * label (a tool line, a bare thinking marker), and the trace falls back to the
65
+ * label — which is what every pre-0.87.0 daemon does for everything.
66
+ *
67
+ * THE MARKER BELOW IS THE label-ONLY CASE, and one definition with four
68
+ * readers: claude.mjs writes it, this file's codex parser falls back to it, and
69
+ * TWO collapses key on it — `trace.mjs` for the turn trace, `fleet.mjs` for the
70
+ * wiki sweep's feed. A collapse keyed on a string typed out four times would
71
+ * silently stop collapsing the first time somebody reworded one of them, and a
72
+ * collapse that stops collapsing fails no test — it just fills a feed.
73
+ *
74
+ * It exists at all because the CLIs do not hand over the thinking itself:
75
+ * measured 2026-09-16 across three real transcripts, 93 thinking blocks, every
76
+ * one of them empty (signature only), and a live probe with MAX_THINKING_TOKENS
77
+ * and `--include-partial-messages` returned an empty `thinking_delta` and a
78
+ * zero-length complete block. So the marker is the honest whole of what is
79
+ * known — "it is reasoning, not hung" — and the day text does arrive it rides
80
+ * `full` and this marker is simply not used.
81
+ */
82
+ export const THINK_MARKER = 'thinking…';
83
+
49
84
  const shortPath = (p, cwd) => {
50
85
  const s = String(p ?? '');
51
86
  return cwd && s.startsWith(cwd) ? s.slice(cwd.length).replace(/^\//, '') : s;
@@ -239,10 +274,22 @@ export const CLAUDE_TOOL_PROSE_KINDS = new Set([
239
274
  */
240
275
  function humanizeCodexItem(item = {}, cwd = '') {
241
276
  switch (item.item_type ?? item.type) {
242
- case 'agent_message':
243
- return { kind: 'say', label: oneLine(item.text ?? item.message) };
244
- case 'reasoning':
245
- return { kind: 'think', label: oneLine(item.text) || 'thinking…' };
277
+ // `full` rides beside the label wherever the item carries more than the
278
+ // label can hold — see the `label`/`full` note above. Codex is the runtime
279
+ // that actually SENDS reasoning text today, so its `think` is the one place
280
+ // in this daemon where a real thought reaches the trace whole.
281
+ case 'agent_message': {
282
+ const text = String(item.text ?? item.message ?? '');
283
+ return { kind: 'say', label: oneLine(text), ...(text ? { full: text } : {}) };
284
+ }
285
+ case 'reasoning': {
286
+ const text = String(item.text ?? '');
287
+ return {
288
+ kind: 'think',
289
+ label: oneLine(text) || THINK_MARKER,
290
+ ...(text.trim() ? { full: text } : {}),
291
+ };
292
+ }
246
293
  case 'command_execution':
247
294
  return {
248
295
  kind: 'bash',
@@ -301,11 +348,28 @@ function parseCodexLine(line, cwd) {
301
348
  : '';
302
349
  return { activity, text };
303
350
  }
304
- case 'turn.failed':
351
+ case 'turn.failed': {
352
+ // An error is prose too, and the sentence that explains a failed turn is
353
+ // routinely longer than a 140-char label — a stack-shaped message loses
354
+ // its cause exactly where somebody is reading to find it.
355
+ //
356
+ // THE LABEL KEEPS ITS OWN `??`, not `msg || …`, and the difference is one
357
+ // input: an EXPLICITLY EMPTY message. `??` lets `''` through as an empty
358
+ // label, which every consumer swallows (`if (!label)`); `||` would print
359
+ // "turn failed" and fire a pulse where this daemon printed nothing. That
360
+ // may well be the better readout, but it is a change to the console and
361
+ // the pulse, and `full` shipped on the promise that neither moved — so it
362
+ // is argued for on its own day, not smuggled in beside a relay field.
363
+ const msg = String(ev.error?.message ?? '');
305
364
  return {
306
- activity: { kind: 'error', label: oneLine(ev.error?.message ?? 'turn failed') },
365
+ activity: {
366
+ kind: 'error',
367
+ label: oneLine(ev.error?.message ?? 'turn failed'),
368
+ ...(msg.trim() ? { full: msg } : {}),
369
+ },
307
370
  text: '',
308
371
  };
372
+ }
309
373
  // A bare `error` event — the shape an auth failure arrives in ("401
310
374
  // Unauthorized: Missing bearer…", observed against 0.147.0 with no
311
375
  // credentials). It used to fall through to `default` and be dropped, which
@@ -314,11 +378,19 @@ function parseCodexLine(line, cwd) {
314
378
  // gave up rather than that the CLI is not signed in. The message goes into
315
379
  // `text` so it reaches the operator's console AND the usage-limit
316
380
  // classifier, which reads exactly this stream.
317
- case 'error':
381
+ case 'error': {
382
+ // Same split as the arm above: `??` for the label (untouched behaviour),
383
+ // `msg` for the relay field only.
384
+ const msg = String(ev.message ?? '');
318
385
  return {
319
- activity: { kind: 'error', label: oneLine(ev.message ?? 'error') },
386
+ activity: {
387
+ kind: 'error',
388
+ label: oneLine(ev.message ?? 'error'),
389
+ ...(msg.trim() ? { full: msg } : {}),
390
+ },
320
391
  text: `${ev.message ?? ''}\n`,
321
392
  };
393
+ }
322
394
  default:
323
395
  return null; // turn.started / item.started / item.updated
324
396
  }
@@ -376,7 +448,14 @@ function parseAgyLine(line, cwd) {
376
448
  const ti = su.tool_info;
377
449
  if (!ti) return null;
378
450
  const err = ti.error?.message;
379
- if (err) return { activity: { kind: 'error', label: oneLine(err) }, text: '' };
451
+ // Same rule as codex's error lines: the label is the console's, `full` is
452
+ // the trace's, and a tool error's message is exactly the kind of sentence a
453
+ // 140-char clamp cuts the cause out of.
454
+ if (err)
455
+ return {
456
+ activity: { kind: 'error', label: oneLine(err), full: String(err) },
457
+ text: '',
458
+ };
380
459
  // Each tool is reported twice — once ACTIVE, once DONE — so only the
381
460
  // terminal state emits, otherwise every action appears in the thread twice.
382
461
  if (su.state && su.state !== 'DONE') return null;
@@ -387,7 +466,9 @@ function parseAgyLine(line, cwd) {
387
466
  // The final answer is the ONLY sentinel-bearing text: agy has no incremental
388
467
  // assistant-message event, so a turn's whole verdict arrives here at once.
389
468
  return {
390
- activity: r.error ? { kind: 'error', label: oneLine(r.error) } : null,
469
+ activity: r.error
470
+ ? { kind: 'error', label: oneLine(r.error), full: String(r.error) }
471
+ : null,
391
472
  text: `${r.response ?? ''}${r.error ? `\n${r.error}` : ''}\n`,
392
473
  };
393
474
  }
package/bin/lib/trace.mjs CHANGED
@@ -54,6 +54,8 @@
54
54
  * tail of a trace, never the settle behind it.
55
55
  */
56
56
 
57
+ import { THINK_MARKER } from './runtimes.mjs';
58
+
57
59
  /** One batch every two seconds — the discipline the tab's narrator keeps, for
58
60
  * the same reason: a turn emits hundreds of entries and nobody is reading them
59
61
  * faster than that. */
@@ -64,9 +66,22 @@ export const TRACE_BATCH = 40;
64
66
  * scrollback, and the newest steps are the ones somebody watching wants. The
65
67
  * drop is not silent — see the seq rule above. */
66
68
  export const TRACE_BUFFER = 120;
67
- /** Longest prose entry. The server clamps to the same number; doing it here too
68
- * means a pathological line never becomes the POST. */
69
- export const TRACE_PROSE_CAP = 300;
69
+ /**
70
+ * Longest prose entry. The server clamps to the same number
71
+ * (`agentTrace.ts.TRACE_PROSE_CAP`); doing it here too means a pathological line
72
+ * never becomes the POST. The two must move together — daemon at or below the
73
+ * server's, or the server silently does the cutting and this file's caps stop
74
+ * describing what ships.
75
+ *
76
+ * 300 UNTIL 2026-09-16, when it was the thing clipping sentences. A trace entry
77
+ * used to be the humanized 160-char label the console prints, so a 300 cap could
78
+ * not bite; it now carries the FULL text of what the CLI said or thought, and at
79
+ * 300 a paragraph of narration was cut mid-sentence. The real bound is the
80
+ * TURN's (400 entries / 96KB server-side, oldest shed with the count said out
81
+ * loud) — a long thought spends budget older steps would have held, and an
82
+ * admitted shed beats a silent clip.
83
+ */
84
+ export const TRACE_PROSE_CAP = 4_000;
70
85
 
71
86
  /**
72
87
  * ONE ATTEMPT AT ONE TURN, named.
@@ -93,6 +108,11 @@ export function makeTraceRelay({ agentId, turnId, post, scrub = (s) => s, run =
93
108
  * server's business: it rebases each run onto its own high-water mark. */
94
109
  const queue = [];
95
110
  let base = 0;
111
+ /** The last entry this relay ACCEPTED, kept across flushes and across a shed
112
+ * buffer — `queue[queue.length - 1]` is not the same thing, because a flush
113
+ * empties the queue and a batch boundary is not a change in the stream. Read
114
+ * by the bare-marker collapse in `prose()` and nothing else. */
115
+ let lastEntry = null;
96
116
  let dirty = false;
97
117
  let timer = null;
98
118
  let stopped = false;
@@ -111,6 +131,7 @@ export function makeTraceRelay({ agentId, turnId, post, scrub = (s) => s, run =
111
131
 
112
132
  const push = (entry) => {
113
133
  if (stopped || !entry) return;
134
+ lastEntry = entry;
114
135
  queue.push(entry);
115
136
  while (queue.length > TRACE_BUFFER) {
116
137
  queue.shift();
@@ -157,17 +178,59 @@ export function makeTraceRelay({ agentId, turnId, post, scrub = (s) => s, run =
157
178
  };
158
179
 
159
180
  return {
160
- /** A prose line from the stream. `kind` is the daemon's activity vocabulary
161
- * (runtimes.mjs); anything that is not thinking or the model speaking is a
162
- * `note` the honest bucket for a codex error line or an agy tool name,
163
- * rather than a wire value invented per runtime. */
181
+ /**
182
+ * A prose line from the stream. `kind` is the daemon's activity vocabulary
183
+ * (runtimes.mjs); anything that is not thinking or the model speaking is a
184
+ * `note` — the honest bucket for a codex error line or an agy tool name,
185
+ * rather than a wire value invented per runtime.
186
+ *
187
+ * THE NEWLINES SURVIVE (2026-09-16). This used to be `\s+ → ' '`, which was
188
+ * right while an entry WAS a one-line label and wrong the moment the caller
189
+ * started handing over the whole of what the CLI said: a model writes in
190
+ * paragraphs and lists, and flattening them here is the relay deciding how
191
+ * the agent's own words should be shaped. So only HORIZONTAL runs collapse,
192
+ * and a wall of blank lines becomes one — that second rule is not cosmetic,
193
+ * it stops padding eating the cap that the real sentences need.
194
+ *
195
+ * The scrub still runs over the WHOLE text, before the cap: this is the
196
+ * CLI's own stdout, and a secret in a thought must not ride further than it
197
+ * did when a thought was 300 characters.
198
+ */
164
199
  prose(kind, text) {
165
200
  const t = scrub(String(text ?? ''))
166
- .replace(/\s+/g, ' ')
201
+ // One line ending, whatever the CLI printed.
202
+ .replace(/\r\n?/g, '\n')
203
+ // Spaces and tabs collapse; `\n` is deliberately excluded from the class.
204
+ .replace(/[^\S\n]+/g, ' ')
205
+ // Three or more breaks in a row — two or more blank lines — become one.
206
+ .replace(/\n{3,}/g, '\n\n')
167
207
  .trim()
168
208
  .slice(0, TRACE_PROSE_CAP);
169
209
  if (!t) return;
170
- push({ k: kind === 'think' ? 'think' : kind === 'say' ? 'say' : 'note', t });
210
+ const k = kind === 'think' ? 'think' : kind === 'say' ? 'say' : 'note';
211
+ /**
212
+ * A RUN OF BARE "thinking…" IS ONE STEP, not forty.
213
+ *
214
+ * Claude emits a thinking block per burst and (measured 2026-09-16, three
215
+ * real transcripts: 93 blocks, all of them empty) carries no text in any
216
+ * of them — so the trace filled with the identical marker repeated, which
217
+ * is noise standing exactly where the thought would have been. The wiki
218
+ * feed collapses the same run for the same reason (fleet.mjs: "Collapse
219
+ * runs of bare 'thinking…' so the feed doesn't fill with it").
220
+ *
221
+ * ONLY THE BARE MARKER, and that is the whole safety of it: a think WITH
222
+ * text is never equal to it, so no real thought is ever eaten — the day
223
+ * the CLI starts emitting thinking text, every one of those blocks lands
224
+ * whole beside the others.
225
+ *
226
+ * A collapsed marker is NOT a drop: it never becomes an entry, so it
227
+ * never takes a seq, exactly like the empty line above it. The "N earlier
228
+ * steps are not shown" count stays a count of steps that existed.
229
+ */
230
+ if (k === 'think' && t === THINK_MARKER && lastEntry?.k === 'think' && lastEntry.t === t) {
231
+ return;
232
+ }
233
+ push({ k, t });
171
234
  },
172
235
  /** One structured tool event, exactly as `toolEventOf` built it. A tool this
173
236
  * builder does not know returns null there and nothing is pushed here — a
package/bin/lib/work.mjs CHANGED
@@ -4888,7 +4888,16 @@ export function createWorkManager({
4888
4888
  // seconds. Two channels, one stream, and the drop-sampler stays a
4889
4889
  // drop-sampler — buffering the pulse would make a stale line look
4890
4890
  // fresh, which is the one thing it exists to answer.
4891
- if (!doubledKinds || !doubledKinds.has(a.kind)) trace.prose(a.kind, line);
4891
+ //
4892
+ // …AND THE TRACE TAKES THE WHOLE LINE. `label` is a 160-char
4893
+ // console readout; `full` is what the CLI actually said, when the
4894
+ // parser had more than the label could hold (see runtimes.mjs).
4895
+ // The fallback is not a degradation — it is what every activity
4896
+ // without a fuller form carries, and what an entire pre-0.87.0
4897
+ // daemon carried for all of them. The PULSE below is deliberately
4898
+ // still `line`: it is one overwritten line on a board, and a
4899
+ // paragraph there would be a paragraph nobody can read.
4900
+ if (!doubledKinds || !doubledKinds.has(a.kind)) trace.prose(a.kind, a.full ?? line);
4892
4901
  const now = Date.now();
4893
4902
  if (now - (lastAgentBeat.get(agentId) ?? 0) < 2_000) return;
4894
4903
  lastAgentBeat.set(agentId, now);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "flowviant",
3
- "version": "0.86.0",
3
+ "version": "0.87.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": {