flowviant 0.77.0 → 0.77.2
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/work.mjs +85 -5
- package/package.json +1 -1
package/bin/lib/work.mjs
CHANGED
|
@@ -2275,6 +2275,36 @@ export function createWorkManager({
|
|
|
2275
2275
|
for (const id of ids) {
|
|
2276
2276
|
if (live.has(id)) continue;
|
|
2277
2277
|
if (peers.has(id)) continue; // another daemon's tab — not ours to retire
|
|
2278
|
+
/**
|
|
2279
|
+
* A HARD STOP REACHES THE CLI HERE, and this is the whole of it.
|
|
2280
|
+
*
|
|
2281
|
+
* `stopAgent` marked the agent abandoned in D1 and nothing on the wire
|
|
2282
|
+
* told the machine, so the CLI went on working, editing the worktree and
|
|
2283
|
+
* spending the operator's quota — while the confirm dialog said the agent
|
|
2284
|
+
* was stopped. It also never got cleaned up, because the check below
|
|
2285
|
+
* skips any place whose lock is held and a running turn holds it: a
|
|
2286
|
+
* stopped agent mid-turn kept its directory forever.
|
|
2287
|
+
*
|
|
2288
|
+
* No new wire field and no version floor: an abandoned agent simply drops
|
|
2289
|
+
* out of `activeWorkSessions` (that list is built from LIVE statuses), so
|
|
2290
|
+
* the server ALREADY says everything needed. Absence means "nothing here
|
|
2291
|
+
* is live any more", and the honest response to that is to stop what we
|
|
2292
|
+
* are running in it and then take the directory.
|
|
2293
|
+
*
|
|
2294
|
+
* SIGTERM the CHILD, never its group — the rule teardown keeps, so an
|
|
2295
|
+
* unattended sweep cannot take the driver's dev server with it. The lock
|
|
2296
|
+
* is released when the child exits, so the removal happens on the next
|
|
2297
|
+
* pass rather than this one.
|
|
2298
|
+
*/
|
|
2299
|
+
const running = agentChildren.get(id);
|
|
2300
|
+
if (running) {
|
|
2301
|
+
try {
|
|
2302
|
+
running.kill('SIGTERM');
|
|
2303
|
+
} catch {
|
|
2304
|
+
/* already gone */
|
|
2305
|
+
}
|
|
2306
|
+
agentChildren.delete(id);
|
|
2307
|
+
}
|
|
2278
2308
|
// Asked of the PLACE, not the session: the lock is keyed by directory,
|
|
2279
2309
|
// and a session sharing one with a busy peer is not ours to retire
|
|
2280
2310
|
// either — its worktree is the peer's working directory.
|
|
@@ -3604,6 +3634,15 @@ export function createWorkManager({
|
|
|
3604
3634
|
// do is run a CLI twice in one worktree, which is what this in-flight set and
|
|
3605
3635
|
// the place lock prevent — the same discipline `processWorkTurns` keeps.
|
|
3606
3636
|
const agentTurns = new Set(); // turn ids in flight on this tick
|
|
3637
|
+
/**
|
|
3638
|
+
* The CLI a live agent turn is running in, by PLACE.
|
|
3639
|
+
*
|
|
3640
|
+
* `workChildren` is keyed by the child itself, which answers "kill everything
|
|
3641
|
+
* at teardown" and cannot answer "kill THIS agent's". Keyed by place rather
|
|
3642
|
+
* than by agent id because the one consumer — the retire sweep — walks
|
|
3643
|
+
* directory names, and those are places.
|
|
3644
|
+
*/
|
|
3645
|
+
const agentChildren = new Map(); // placeId -> child process
|
|
3607
3646
|
|
|
3608
3647
|
/** Returns the server's reply, because it carries ONE instruction the machine
|
|
3609
3648
|
* can act on immediately: `review: true` means the agent's queue just
|
|
@@ -3747,10 +3786,31 @@ export function createWorkManager({
|
|
|
3747
3786
|
return;
|
|
3748
3787
|
}
|
|
3749
3788
|
const wt = dir.wt;
|
|
3789
|
+
/**
|
|
3790
|
+
* NEITHER OF THESE MAY THROW. `git()` throws on a non-zero exit, and both
|
|
3791
|
+
* of these exit non-zero in states a real worktree reaches: `symbolic-ref`
|
|
3792
|
+
* on a DETACHED HEAD (an agent that ran `git checkout <sha>`, a rebase
|
|
3793
|
+
* left half-done), and `rev-parse HEAD` on a branch with no commits yet.
|
|
3794
|
+
* An escape here happens BEFORE any settle, so the turn stays pending
|
|
3795
|
+
* until the six-hour expiry while the agent sits in Working with nothing
|
|
3796
|
+
* behind it — the wedge this whole lane exists to avoid.
|
|
3797
|
+
*
|
|
3798
|
+
* `runAgentMerge` learned this two commits ago and says so in its own
|
|
3799
|
+
* comment; this is the same call, in the same file, left unguarded. Both
|
|
3800
|
+
* facts are OPTIONAL to a turn — they describe the branch, they do not
|
|
3801
|
+
* gate the work — so failing to read them costs a null, not the turn.
|
|
3802
|
+
*/
|
|
3803
|
+
const readGit = (args) => {
|
|
3804
|
+
try {
|
|
3805
|
+
return (git(args, wt) || '').trim() || null;
|
|
3806
|
+
} catch {
|
|
3807
|
+
return null;
|
|
3808
|
+
}
|
|
3809
|
+
};
|
|
3750
3810
|
// WHERE THE BRANCH WAS BEFORE THIS TURN, so the commits reported are the
|
|
3751
3811
|
// ones this turn actually made.
|
|
3752
|
-
const before = (
|
|
3753
|
-
const branch = (
|
|
3812
|
+
const before = readGit(['rev-parse', 'HEAD']);
|
|
3813
|
+
const branch = readGit(['symbolic-ref', '--quiet', '--short', 'HEAD']);
|
|
3754
3814
|
|
|
3755
3815
|
const rt = job.runtime || 'claude';
|
|
3756
3816
|
if (!canRun(RUNTIMES[rt], 'build')) {
|
|
@@ -3822,10 +3882,15 @@ export function createWorkManager({
|
|
|
3822
3882
|
child = ch;
|
|
3823
3883
|
workChildren.set(ch, null);
|
|
3824
3884
|
noteSessionGroup(agentId, ch.pid);
|
|
3885
|
+
// Keyed by PLACE, because the retire sweep iterates directory names
|
|
3886
|
+
// and a place id IS one. It is what lets a hard stop actually reach
|
|
3887
|
+
// the CLI — see `retireWorkSessions`.
|
|
3888
|
+
agentChildren.set(place, ch);
|
|
3825
3889
|
},
|
|
3826
3890
|
});
|
|
3827
3891
|
} finally {
|
|
3828
3892
|
if (child) workChildren.delete(child);
|
|
3893
|
+
if (agentChildren.get(place) === child) agentChildren.delete(place);
|
|
3829
3894
|
if (ranMarker) {
|
|
3830
3895
|
try {
|
|
3831
3896
|
writeFileSync(ranMarker, '1');
|
|
@@ -4209,7 +4274,15 @@ export function createWorkManager({
|
|
|
4209
4274
|
*/
|
|
4210
4275
|
if (job.prMode) {
|
|
4211
4276
|
try {
|
|
4212
|
-
|
|
4277
|
+
// EVERY `gh` CALL IS TIMED OUT. `execFileSync` blocks the daemon's
|
|
4278
|
+
// whole event loop, and this one runs INSIDE the place writer lock —
|
|
4279
|
+
// so a `gh` that hangs (an expired token prompting, a network black
|
|
4280
|
+
// hole, a hung credential helper) stops every turn on the machine,
|
|
4281
|
+
// not merely this merge, and holds the lock while it does.
|
|
4282
|
+
execFileSync('gh', ['auth', 'status'], {
|
|
4283
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
4284
|
+
timeout: 20_000,
|
|
4285
|
+
});
|
|
4213
4286
|
} catch (e) {
|
|
4214
4287
|
await postAgentMerge({
|
|
4215
4288
|
agentId,
|
|
@@ -4233,7 +4306,12 @@ export function createWorkManager({
|
|
|
4233
4306
|
try {
|
|
4234
4307
|
git(['push', '-u', 'origin', branch], wt);
|
|
4235
4308
|
} catch (e) {
|
|
4236
|
-
|
|
4309
|
+
// SCRUBBED. Every other failure on this path relays `gh`'s own words,
|
|
4310
|
+
// but a push writes the REMOTE URL to stderr and a remote can carry a
|
|
4311
|
+
// token in its userinfo — so this one line is the only place on the
|
|
4312
|
+
// agent merge path that can leak a credential into a stored,
|
|
4313
|
+
// team-visible `mergeError`.
|
|
4314
|
+
await postAgentMerge({ agentId, ok: false, detail: envScrub(ghFirstLine(e)) });
|
|
4237
4315
|
return;
|
|
4238
4316
|
}
|
|
4239
4317
|
let prUrl = null;
|
|
@@ -4242,6 +4320,7 @@ export function createWorkManager({
|
|
|
4242
4320
|
execFileSync('gh', ['pr', 'view', branch, '--json', 'url,state'], {
|
|
4243
4321
|
cwd: repoRoot,
|
|
4244
4322
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
4323
|
+
timeout: 30_000,
|
|
4245
4324
|
}).toString()
|
|
4246
4325
|
);
|
|
4247
4326
|
if (j?.state === 'OPEN' && typeof j?.url === 'string') prUrl = j.url.trim();
|
|
@@ -4254,7 +4333,7 @@ export function createWorkManager({
|
|
|
4254
4333
|
'gh',
|
|
4255
4334
|
// baseBranchName, not baseRef: gh 422s on a remote-tracking name.
|
|
4256
4335
|
['pr', 'create', '--head', branch, '--base', prBase, '--fill'],
|
|
4257
|
-
{ cwd: repoRoot, stdio: ['ignore', 'pipe', 'pipe'] }
|
|
4336
|
+
{ cwd: repoRoot, stdio: ['ignore', 'pipe', 'pipe'], timeout: 60_000 }
|
|
4258
4337
|
)
|
|
4259
4338
|
.toString()
|
|
4260
4339
|
.trim();
|
|
@@ -4268,6 +4347,7 @@ export function createWorkManager({
|
|
|
4268
4347
|
execFileSync('gh', ['pr', 'merge', branch, '--merge'], {
|
|
4269
4348
|
cwd: repoRoot,
|
|
4270
4349
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
4350
|
+
timeout: 120_000,
|
|
4271
4351
|
});
|
|
4272
4352
|
} catch (e) {
|
|
4273
4353
|
const line = ghFirstLine(e);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "flowviant",
|
|
3
|
-
"version": "0.77.
|
|
3
|
+
"version": "0.77.2",
|
|
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": {
|