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 +5 -0
- package/bin/lib/work.mjs +61 -7
- package/package.json +1 -1
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
|
-
|
|
362
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
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.
|
|
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": {
|