flowviant 0.59.0 → 0.60.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.
Files changed (2) hide show
  1. package/bin/lib/work.mjs +41 -7
  2. package/package.json +1 -1
package/bin/lib/work.mjs CHANGED
@@ -337,6 +337,32 @@ 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
+ /** The DIRECTORY a session works in. Every path that used to build
359
+ * `sessions/<id>` by hand goes through here, or a tab in the checkout gets
360
+ * measured against a directory that does not exist. */
361
+ const placeDir = (sessionId) => {
362
+ const place = placeOf(sessionId);
363
+ return place === REPO_PLACE ? repoRoot : join(baseDir, 'sessions', place);
364
+ };
365
+
340
366
  let lastWorktreeSweep = 0;
341
367
  let lastWorktreeFetch = 0;
342
368
  let sweepingWorktrees = false;
@@ -358,8 +384,11 @@ export function createWorkManager({ repoRoot, baseDir, baseRef, getMcpUrl, getLe
358
384
  }
359
385
  };
360
386
  const sessionWorktreeReport = (sessionId) => {
361
- if (!isSafePathSegment(sessionId)) return null;
362
- const wt = join(baseDir, 'sessions', sessionId);
387
+ // The session's PLACE, not its name: a tab in the checkout is measured in
388
+ // the checkout, and a tab sharing another tab's worktree is measured there.
389
+ const place = placeOf(sessionId);
390
+ if (place !== REPO_PLACE && !isSafePathSegment(place)) return null;
391
+ const wt = placeDir(sessionId);
363
392
  const d = worktreeDiff(wt, baseRef);
364
393
  if (!d) return null;
365
394
  // WHAT IS LISTENING in this worktree, attributed by the CWD of the process
@@ -585,7 +614,7 @@ export function createWorkManager({ repoRoot, baseDir, baseRef, getMcpUrl, getLe
585
614
  void (async () => {
586
615
  try {
587
616
  if (!(await claimPreview(sessionId))) return; // somebody else has it
588
- const wt = join(baseDir, 'sessions', sessionId);
617
+ const wt = placeDir(sessionId);
589
618
  // RE-VALIDATE the attribution here, not just the liveness. The server
590
619
  // checked this port against a report up to a minute old; more
591
620
  // importantly, checking `listenersIn` again is what keeps the answer
@@ -773,7 +802,7 @@ export function createWorkManager({ repoRoot, baseDir, baseRef, getMcpUrl, getLe
773
802
  void (async () => {
774
803
  try {
775
804
  if (!(await claimDevRun(sessionId))) return; // somebody else has it
776
- const wt = join(baseDir, 'sessions', sessionId);
805
+ const wt = placeDir(sessionId);
777
806
  const r = await startDevServer({
778
807
  sessionId,
779
808
  worktree: wt,
@@ -845,7 +874,7 @@ export function createWorkManager({ repoRoot, baseDir, baseRef, getMcpUrl, getLe
845
874
  const adoptDevRuns = (activeIds) => {
846
875
  let adopted = 0;
847
876
  for (const entry of reapOrphanDevRuns(activeIds, note)) {
848
- const wt = join(baseDir, 'sessions', entry.sessionId);
877
+ const wt = placeDir(entry.sessionId);
849
878
  // The recorded cwd must still be this session's worktree. A recycled pid
850
879
  // pointing anywhere else is somebody else's process.
851
880
  if (entry.cwd !== wt) continue;
@@ -1647,6 +1676,9 @@ export function createWorkManager({ repoRoot, baseDir, baseRef, getMcpUrl, getLe
1647
1676
  // Serialized by PLACE: two tabs sharing a worktree take turns in it
1648
1677
  // rather than editing the same files at the same time.
1649
1678
  const place = job.place || job.sessionId;
1679
+ // Remembered for every other beat — the sweep, ship, the preview
1680
+ // re-check — so they all ask the same directory this turn runs in.
1681
+ sessionPlaces.set(job.sessionId, place);
1650
1682
  chainFor(place, async () => {
1651
1683
  try {
1652
1684
  const tries = workAttempts.get(job.id) ?? 0;
@@ -2273,7 +2305,9 @@ export function createWorkManager({ repoRoot, baseDir, baseRef, getMcpUrl, getLe
2273
2305
  * so the recorded name is the fallback — the one case where the name
2274
2306
  * is the only thing there is.
2275
2307
  */
2276
- const wt = join(baseDir, 'sessions', job.sessionId);
2308
+ // The session's PLACE the directory it actually works in.
2309
+ const shipPlace = placeOf(job.sessionId);
2310
+ const wt = shipPlace === REPO_PLACE ? repoRoot : join(baseDir, 'sessions', shipPlace);
2277
2311
  let branch = `session/${job.sessionId}`;
2278
2312
  let detached = false;
2279
2313
  if (existsSync(wt)) {
@@ -2459,7 +2493,7 @@ export function createWorkManager({ repoRoot, baseDir, baseRef, getMcpUrl, getLe
2459
2493
  ok(`${c.cyan('ship')} ${c.dim(`— ${commits.length} commit${commits.length === 1 ? '' : 's'} on main`)}`);
2460
2494
  return;
2461
2495
  }
2462
- const dir = sessionWtFor(job.sessionId);
2496
+ const dir = placeWtFor(shipPlace);
2463
2497
  if (!dir) {
2464
2498
  await done({
2465
2499
  ok: false,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "flowviant",
3
- "version": "0.59.0",
3
+ "version": "0.60.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": {