flowviant 0.59.0 → 0.61.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.
package/bin/lib/fleet.mjs CHANGED
@@ -824,6 +824,7 @@ export async function runFleetDaemon() {
824
824
  // retirement — lives in work.mjs; this hands it the loop's mutable state.
825
825
  const {
826
826
  flushWorkReports,
827
+ learnPlaces,
827
828
  processWorkTurns,
828
829
  processShipJobs,
829
830
  processDiffJobs,
@@ -1549,6 +1550,10 @@ export async function runFleetDaemon() {
1549
1550
  void flushWorkReports();
1550
1551
  processMergeJobs(roster.mergeJobs);
1551
1552
  processPatchRevertJobs(roster.patchRevertJobs);
1553
+ // WHERE each live tab works, BEFORE anything measures one: the sweep below
1554
+ // and every preview check ask `placeOf`, and a tab nobody has typed into
1555
+ // yet has taught it nothing.
1556
+ learnPlaces(roster.sessionPlaces);
1552
1557
  processWorkTurns(roster.workTurnJobs);
1553
1558
  // The roster's live-session list rides along: an ENDED session's ship
1554
1559
  // must not be refused by checks whose remedies need a live tab.
package/bin/lib/work.mjs CHANGED
@@ -337,6 +337,51 @@ export function createWorkManager({ repoRoot, baseDir, baseRef, getMcpUrl, getLe
337
337
  * network, and a teammate's push being visible within three minutes is the
338
338
  * same promise the rest of the product makes. */
339
339
  const WORKTREE_FETCH_MS = 3 * 60_000;
340
+ /**
341
+ * WHERE EACH SESSION WORKS, learned from the turns we are handed.
342
+ *
343
+ * Every other beat — the worktree sweep, ship, the preview re-check — has to
344
+ * ask the SAME directory the turn ran in, and only the turn job carries
345
+ * `place`. Caching it here is what keeps them agreeing without a second
346
+ * server→daemon field: a session absent from this map has never run a turn,
347
+ * and its own id is the right answer for that case anyway (it is the default
348
+ * place, and a session with no turn has no worktree either).
349
+ *
350
+ * A tab standing in the CHECKOUT is the case this exists for: its directory
351
+ * is not `sessions/<id>` and never will be, so a sweep that assumed the
352
+ * default would measure a directory that does not exist and report nothing —
353
+ * which is exactly why "I still do not see a preview URL" was true of a tab
354
+ * opened in the checkout.
355
+ */
356
+ const sessionPlaces = new Map();
357
+ const placeOf = (sessionId) => sessionPlaces.get(sessionId) ?? sessionId;
358
+ /**
359
+ * Places straight off the roster, so a tab is measured the moment it EXISTS
360
+ * rather than after somebody types into it.
361
+ *
362
+ * Learning only from turn jobs meant a tab nobody had spoken to yet was
363
+ * measured at `sessions/<id>` — a directory that does not exist for a tab
364
+ * working in the checkout — so it reported no branch, no diffstat and no
365
+ * ports, and every control reading those had nothing to render.
366
+ *
367
+ * The roster is authoritative and turn jobs still agree with it; a session
368
+ * the server does not name keeps whatever a turn taught, and failing that its
369
+ * own id, which is the pre-places default.
370
+ */
371
+ const learnPlaces = (map) => {
372
+ if (!map || typeof map !== 'object') return;
373
+ for (const [sid, place] of Object.entries(map)) {
374
+ if (typeof place === 'string' && place) sessionPlaces.set(sid, place);
375
+ }
376
+ };
377
+ /** The DIRECTORY a session works in. Every path that used to build
378
+ * `sessions/<id>` by hand goes through here, or a tab in the checkout gets
379
+ * measured against a directory that does not exist. */
380
+ const placeDir = (sessionId) => {
381
+ const place = placeOf(sessionId);
382
+ return place === REPO_PLACE ? repoRoot : join(baseDir, 'sessions', place);
383
+ };
384
+
340
385
  let lastWorktreeSweep = 0;
341
386
  let lastWorktreeFetch = 0;
342
387
  let sweepingWorktrees = false;
@@ -358,8 +403,11 @@ export function createWorkManager({ repoRoot, baseDir, baseRef, getMcpUrl, getLe
358
403
  }
359
404
  };
360
405
  const sessionWorktreeReport = (sessionId) => {
361
- if (!isSafePathSegment(sessionId)) return null;
362
- const wt = join(baseDir, 'sessions', sessionId);
406
+ // The session's PLACE, not its name: a tab in the checkout is measured in
407
+ // the checkout, and a tab sharing another tab's worktree is measured there.
408
+ const place = placeOf(sessionId);
409
+ if (place !== REPO_PLACE && !isSafePathSegment(place)) return null;
410
+ const wt = placeDir(sessionId);
363
411
  const d = worktreeDiff(wt, baseRef);
364
412
  if (!d) return null;
365
413
  // WHAT IS LISTENING in this worktree, attributed by the CWD of the process
@@ -585,7 +633,7 @@ export function createWorkManager({ repoRoot, baseDir, baseRef, getMcpUrl, getLe
585
633
  void (async () => {
586
634
  try {
587
635
  if (!(await claimPreview(sessionId))) return; // somebody else has it
588
- const wt = join(baseDir, 'sessions', sessionId);
636
+ const wt = placeDir(sessionId);
589
637
  // RE-VALIDATE the attribution here, not just the liveness. The server
590
638
  // checked this port against a report up to a minute old; more
591
639
  // importantly, checking `listenersIn` again is what keeps the answer
@@ -773,7 +821,7 @@ export function createWorkManager({ repoRoot, baseDir, baseRef, getMcpUrl, getLe
773
821
  void (async () => {
774
822
  try {
775
823
  if (!(await claimDevRun(sessionId))) return; // somebody else has it
776
- const wt = join(baseDir, 'sessions', sessionId);
824
+ const wt = placeDir(sessionId);
777
825
  const r = await startDevServer({
778
826
  sessionId,
779
827
  worktree: wt,
@@ -845,7 +893,7 @@ export function createWorkManager({ repoRoot, baseDir, baseRef, getMcpUrl, getLe
845
893
  const adoptDevRuns = (activeIds) => {
846
894
  let adopted = 0;
847
895
  for (const entry of reapOrphanDevRuns(activeIds, note)) {
848
- const wt = join(baseDir, 'sessions', entry.sessionId);
896
+ const wt = placeDir(entry.sessionId);
849
897
  // The recorded cwd must still be this session's worktree. A recycled pid
850
898
  // pointing anywhere else is somebody else's process.
851
899
  if (entry.cwd !== wt) continue;
@@ -1647,6 +1695,9 @@ export function createWorkManager({ repoRoot, baseDir, baseRef, getMcpUrl, getLe
1647
1695
  // Serialized by PLACE: two tabs sharing a worktree take turns in it
1648
1696
  // rather than editing the same files at the same time.
1649
1697
  const place = job.place || job.sessionId;
1698
+ // Remembered for every other beat — the sweep, ship, the preview
1699
+ // re-check — so they all ask the same directory this turn runs in.
1700
+ sessionPlaces.set(job.sessionId, place);
1650
1701
  chainFor(place, async () => {
1651
1702
  try {
1652
1703
  const tries = workAttempts.get(job.id) ?? 0;
@@ -2273,7 +2324,9 @@ export function createWorkManager({ repoRoot, baseDir, baseRef, getMcpUrl, getLe
2273
2324
  * so the recorded name is the fallback — the one case where the name
2274
2325
  * is the only thing there is.
2275
2326
  */
2276
- const wt = join(baseDir, 'sessions', job.sessionId);
2327
+ // The session's PLACE the directory it actually works in.
2328
+ const shipPlace = placeOf(job.sessionId);
2329
+ const wt = shipPlace === REPO_PLACE ? repoRoot : join(baseDir, 'sessions', shipPlace);
2277
2330
  let branch = `session/${job.sessionId}`;
2278
2331
  let detached = false;
2279
2332
  if (existsSync(wt)) {
@@ -2459,7 +2512,7 @@ export function createWorkManager({ repoRoot, baseDir, baseRef, getMcpUrl, getLe
2459
2512
  ok(`${c.cyan('ship')} ${c.dim(`— ${commits.length} commit${commits.length === 1 ? '' : 's'} on main`)}`);
2460
2513
  return;
2461
2514
  }
2462
- const dir = sessionWtFor(job.sessionId);
2515
+ const dir = placeWtFor(shipPlace);
2463
2516
  if (!dir) {
2464
2517
  await done({
2465
2518
  ok: false,
@@ -2583,6 +2636,7 @@ export function createWorkManager({ repoRoot, baseDir, baseRef, getMcpUrl, getLe
2583
2636
 
2584
2637
  return {
2585
2638
  flushWorkReports,
2639
+ learnPlaces,
2586
2640
  processWorkTurns,
2587
2641
  processShipJobs,
2588
2642
  processDiffJobs,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "flowviant",
3
- "version": "0.59.0",
3
+ "version": "0.61.0",
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": {