flowviant 0.62.0 → 0.63.1

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.
@@ -115,7 +115,7 @@ export function pickCommand(text) {
115
115
  * the caller's only job with a failure is to relay it, and an exception at this
116
116
  * boundary would strand the row.
117
117
  */
118
- export async function resolveDevCommandOnMachine({ cwd, model, log }) {
118
+ export async function resolveDevCommandOnMachine({ cwd, model, log, onActivity }) {
119
119
  let sessionId = null;
120
120
  let timer;
121
121
  try {
@@ -127,6 +127,18 @@ export async function resolveDevCommandOnMachine({ cwd, model, log }) {
127
127
  answerFromResult: true,
128
128
  model,
129
129
  label: 'dev',
130
+ /**
131
+ * THE TURN'S OWN HUMANIZED TAIL, forwarded so a browser can watch it.
132
+ *
133
+ * This is the only feature in the product where a Claude does work
134
+ * nobody can see — no transcript, by design — and the driver's answer
135
+ * to that is the right one: "we could have it stream the output for
136
+ * transparency on the menu." So the same `read …` / `+ npm install …`
137
+ * lines a tab relays are forwarded here. It is the CLI's own stdout
138
+ * humanized, never an inference about it, which is the standing rule
139
+ * for every activity readout in this product.
140
+ */
141
+ onActivity,
130
142
  onInit: (i) => {
131
143
  if (i?.sessionId) sessionId = i.sessionId;
132
144
  },
package/bin/lib/work.mjs CHANGED
@@ -112,6 +112,7 @@ export function createWorkManager({ repoRoot, baseDir, baseRef, getMcpUrl, getLe
112
112
  const DEV_RUN_CLAIM_URL = FLEET_URL.replace(/\/agents\/?$/, '/dev-run-claim');
113
113
  const DEV_RUN_DONE_URL = FLEET_URL.replace(/\/agents\/?$/, '/dev-run-done');
114
114
  const DEV_RUN_RESOLVED_URL = FLEET_URL.replace(/\/agents\/?$/, '/dev-run-resolved');
115
+ const DEV_RUN_PROGRESS_URL = FLEET_URL.replace(/\/agents\/?$/, '/dev-run-progress');
115
116
  const SESSION_COMMANDS_URL = FLEET_URL.replace(/\/agents\/?$/, '/session-commands');
116
117
  const ATTACHMENT_URL = FLEET_URL.replace(/\/agents\/?$/, '/attachment');
117
118
  const workAnswering = new Set(); // turn ids currently queued/running here
@@ -784,6 +785,65 @@ export function createWorkManager({ repoRoot, baseDir, baseRef, getMcpUrl, getLe
784
785
  * a second implementation of that policy inside the one component a deploy
785
786
  * cannot upgrade is exactly the drift this product keeps closing.
786
787
  */
788
+ /**
789
+ * THE RESOLVE TURN'S OUTPUT, streamed while it happens.
790
+ *
791
+ * A headless turn is the one place in this product where a Claude works and
792
+ * nobody can see it — deliberately, since it must not reach the transcript —
793
+ * and the answer to that is transparency rather than a promise: "we could
794
+ * have it stream the output for transparency on the menu."
795
+ *
796
+ * THROTTLED, and the tail is BOUNDED at the machine. This is a per-line hook
797
+ * on a turn that may run for minutes; posting each line would be hundreds of
798
+ * requests, and sending the whole transcript would grow without limit. Same
799
+ * shape as the session activity relay, for the same reasons.
800
+ */
801
+ const devProgress = new Map(); // sessionId → { lines, at, timer }
802
+ const DEV_PROGRESS_MS = 2_000;
803
+ const DEV_PROGRESS_LINES = 40;
804
+
805
+ const flushDevProgress = async (sessionId) => {
806
+ const st = devProgress.get(sessionId);
807
+ if (!st || !st.lines.length) return;
808
+ st.at = Date.now();
809
+ const logTail = st.lines.join('\n').slice(-4000);
810
+ try {
811
+ await fetch(DEV_RUN_PROGRESS_URL, {
812
+ method: 'POST',
813
+ headers: {
814
+ Authorization: `Bearer ${FLEET_TOKEN}`,
815
+ 'User-Agent': USER_AGENT,
816
+ 'Content-Type': 'application/json',
817
+ },
818
+ signal: AbortSignal.timeout(15_000),
819
+ body: JSON.stringify({ sessionId, instance: DAEMON_INSTANCE, logTail }),
820
+ });
821
+ } catch {
822
+ /* progress is best-effort — the row's own state is the truth */
823
+ }
824
+ };
825
+
826
+ const noteDevProgress = (sessionId, label) => {
827
+ if (!label) return;
828
+ const st = devProgress.get(sessionId) ?? { lines: [], at: 0, timer: null };
829
+ st.lines.push(String(label).slice(0, 300));
830
+ if (st.lines.length > DEV_PROGRESS_LINES) st.lines.splice(0, st.lines.length - DEV_PROGRESS_LINES);
831
+ devProgress.set(sessionId, st);
832
+ // Leading-edge post, then a trailing one — so the FIRST line appears at
833
+ // once (an empty panel for two seconds reads as nothing happening) and the
834
+ // last line is never left unsent.
835
+ if (Date.now() - st.at > DEV_PROGRESS_MS) {
836
+ void flushDevProgress(sessionId);
837
+ return;
838
+ }
839
+ if (st.timer) return;
840
+ st.timer = setTimeout(() => {
841
+ st.timer = null;
842
+ void flushDevProgress(sessionId);
843
+ }, DEV_PROGRESS_MS);
844
+ st.timer.unref?.();
845
+ };
846
+
787
847
  const postDevResolved = async (body) => {
788
848
  try {
789
849
  await fetch(DEV_RUN_RESOLVED_URL, {
@@ -847,12 +907,27 @@ export function createWorkManager({ repoRoot, baseDir, baseRef, getMcpUrl, getLe
847
907
  * long as the server lived.
848
908
  */
849
909
  if (job?.action === 'resolve') {
850
- if (liveDevRuns.has(sessionId)) continue;
851
- if (devRunClaiming.has(sessionId)) continue;
910
+ /**
911
+ * EVERY SKIP SAYS SO. These were silent `continue`s, and that cost real
912
+ * time: a request sat unclaimed and the only sentence anyone got was
913
+ * "the machine did not pick this up — it may be offline", which was
914
+ * false — the machine was polling throughout. A job dropped without a
915
+ * trace in the row OR the log is undiagnosable from either end, so the
916
+ * skips are narrated even though they are all legitimate states.
917
+ */
918
+ if (liveDevRuns.has(sessionId)) {
919
+ note(`dev ${sessionId.slice(0, 8)}: already serving this tab — ignoring the request`);
920
+ continue;
921
+ }
922
+ if (devRunClaiming.has(sessionId)) continue; // in flight this tick; not news
852
923
  devRunClaiming.add(sessionId);
853
924
  void (async () => {
854
925
  try {
855
- if (!(await claimDevRun(sessionId))) return; // somebody else has it
926
+ if (!(await claimDevRun(sessionId))) {
927
+ note(`dev ${sessionId.slice(0, 8)}: another process holds this request`);
928
+ return;
929
+ }
930
+ note(`dev ${sessionId.slice(0, 8)}: working out how to start this project…`);
856
931
  const wt = placeDir(sessionId);
857
932
  const out = await chainFor(placeOf(sessionId), () =>
858
933
  resolveDevCommandOnMachine({
@@ -865,6 +940,8 @@ export function createWorkManager({ repoRoot, baseDir, baseRef, getMcpUrl, getLe
865
940
  log: (m) => note(`${sessionId.slice(0, 8)}: ${m}`),
866
941
  })
867
942
  );
943
+ // The last lines, before the row leaves the resolving state.
944
+ await flushDevProgress(sessionId);
868
945
  await postDevResolved({
869
946
  sessionId,
870
947
  command: out?.command ?? null,
@@ -877,6 +954,9 @@ export function createWorkManager({ repoRoot, baseDir, baseRef, getMcpUrl, getLe
877
954
  error: `the machine could not run that turn: ${e?.message ?? 'unknown error'}`,
878
955
  });
879
956
  } finally {
957
+ const st = devProgress.get(sessionId);
958
+ if (st?.timer) clearTimeout(st.timer);
959
+ devProgress.delete(sessionId);
880
960
  devRunClaiming.delete(sessionId);
881
961
  }
882
962
  })();
@@ -898,14 +978,21 @@ export function createWorkManager({ repoRoot, baseDir, baseRef, getMcpUrl, getLe
898
978
  continue;
899
979
  }
900
980
  // Already serving this tab. Re-starting would kill a server somebody is
901
- // looking at right now.
902
- if (liveDevRuns.has(sessionId)) continue;
903
- if (devRunClaiming.has(sessionId)) continue;
981
+ // looking at right now. Narrated for the same reason the resolve branch
982
+ // is: a silent skip is invisible from the browser AND from the machine.
983
+ if (liveDevRuns.has(sessionId)) {
984
+ note(`dev ${sessionId.slice(0, 8)}: already serving this tab — ignoring the request`);
985
+ continue;
986
+ }
987
+ if (devRunClaiming.has(sessionId)) continue; // in flight this tick; not news
904
988
  devRunClaiming.add(sessionId);
905
989
 
906
990
  void (async () => {
907
991
  try {
908
- if (!(await claimDevRun(sessionId))) return; // somebody else has it
992
+ if (!(await claimDevRun(sessionId))) {
993
+ note(`dev ${sessionId.slice(0, 8)}: another process holds this request`);
994
+ return;
995
+ }
909
996
  const wt = placeDir(sessionId);
910
997
  const r = await startDevServer({
911
998
  sessionId,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "flowviant",
3
- "version": "0.62.0",
3
+ "version": "0.63.1",
4
4
  "description": "Run your own coding CLIs as build agents for Flowviant — 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": {