flowviant 0.71.0 → 0.71.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.
Files changed (2) hide show
  1. package/bin/lib/work.mjs +64 -3
  2. package/package.json +1 -1
package/bin/lib/work.mjs CHANGED
@@ -41,7 +41,7 @@ import {
41
41
  import { git, gitRaw, splitNul, baseBranchName, isSafePathSegment } from './git.mjs';
42
42
  import { listenersIn, measureListeners, listenersSupported } from './listeners.mjs';
43
43
  import { measureProcesses, liveGroups, processesSupported } from './processes.mjs';
44
- import { mutateRegistry, readRegistry } from './procRegistry.mjs';
44
+ import { mutateRegistry, processAlive, readRegistry } from './procRegistry.mjs';
45
45
  import { createPlaceLock } from './placeLock.mjs';
46
46
  import { sweepMergedBranch } from './shipSweep.mjs';
47
47
  import { mergeOutward as shipMergeOutward } from './shipMerge.mjs';
@@ -1070,6 +1070,30 @@ export function createWorkManager({ repoRoot, baseDir, getBaseRef, getMcpUrl, ge
1070
1070
  return false;
1071
1071
  };
1072
1072
 
1073
+ /**
1074
+ * How long to watch for the process to actually go before answering.
1075
+ *
1076
+ * SIGTERM is a REQUEST, not an event: a dev server traps it and tears down
1077
+ * its children, which takes a beat. Answering the instant the signal returns
1078
+ * would report "signalled" over a process that is about to die, and the
1079
+ * surface would then offer Force stop on something already on its way out.
1080
+ *
1081
+ * Four seconds is long enough for the ordinary teardown and short enough that
1082
+ * a person is still looking at the row. Past it the honest answer is that the
1083
+ * signal landed and the thing is still there — which is a real state, and the
1084
+ * one where escalating actually means something.
1085
+ */
1086
+ const KILL_GRACE_MS = 4000;
1087
+
1088
+ const waitForExit = async (pid) => {
1089
+ const until = Date.now() + KILL_GRACE_MS;
1090
+ while (Date.now() < until) {
1091
+ if (!processAlive(pid)) return true;
1092
+ await new Promise((r) => setTimeout(r, 200));
1093
+ }
1094
+ return !processAlive(pid);
1095
+ };
1096
+
1073
1097
  const runKill = async (job) => {
1074
1098
  const id = String(job.id);
1075
1099
  const sessionId = String(job.sessionId || '');
@@ -1083,20 +1107,57 @@ export function createWorkManager({ repoRoot, baseDir, getBaseRef, getMcpUrl, ge
1083
1107
  if (!killTargetOk(sessionId, pid)) {
1084
1108
  // Not a lie and not a failure: the process is genuinely no longer one of
1085
1109
  // this tab's, which is the common case when somebody clicks a row that
1086
- // has since exited. The asker gets that sentence rather than a spinner.
1110
+ // has since exited. The asker gets that sentence rather than a spinner
1111
+ // and the RE-MEASURE below is what takes the stale row off their screen,
1112
+ // since a row you can click for something already gone is the readout
1113
+ // being behind, not the person being wrong.
1087
1114
  await postKill({ id, outcome: 'not_found' });
1115
+ await remeasureAfterKill(sessionId);
1088
1116
  return;
1089
1117
  }
1090
1118
  if (!(await claimKill(id))) return;
1091
1119
  try {
1092
1120
  process.kill(pid, signal);
1093
- await postKill({ id, outcome: 'signalled', signal });
1121
+ // WHAT HAPPENED, not what we did. "We sent a signal" is a fact about us;
1122
+ // "it stopped" is a fact about the machine, and the machine is standing
1123
+ // right here able to check. Reporting the weaker word would also make the
1124
+ // Force stop offer wrong for the whole window, since escalating only
1125
+ // means something while the process is genuinely still there.
1126
+ const gone = await waitForExit(pid);
1127
+ await postKill({ id, outcome: gone ? 'stopped' : 'signalled', signal });
1094
1128
  } catch (e) {
1095
1129
  // EPERM is the daemon-on-its-own-user posture doing exactly what it is
1096
1130
  // for. Report it as its own word: "we may not" and "it was gone" are
1097
1131
  // different sentences and the surface says which.
1098
1132
  await postKill({ id, outcome: e?.code === 'ESRCH' ? 'not_found' : 'error', detail: String(e?.code || e) });
1099
1133
  }
1134
+ await remeasureAfterKill(sessionId);
1135
+ };
1136
+
1137
+ /**
1138
+ * THE LIST THE PERSON IS LOOKING AT WAS MEASURED BEFORE ANY OF THIS.
1139
+ *
1140
+ * Without this the row survives the thing it describes: the panel renders the
1141
+ * last sweep's `listening`, the sweep is on a SIXTY-SECOND beat, and the
1142
+ * reported outcome sits next to a port row still claiming to be live. The
1143
+ * first person to use it said exactly that — "i clicked stop on the listening
1144
+ * but its still running… then it finally disappears".
1145
+ *
1146
+ * The rule it was missing is one this file already keeps everywhere else: an
1147
+ * action that changes what the machine would measure must cause a new
1148
+ * measurement. A turn settling does it; a kill did not. `reportSessionWorktree`
1149
+ * is the un-throttled per-session path built for precisely this and it was
1150
+ * being called from exactly one place.
1151
+ *
1152
+ * Never awaited by the caller's answer path: the outcome is posted first, so
1153
+ * a slow re-measure can delay the list but never the sentence.
1154
+ */
1155
+ const remeasureAfterKill = async (sessionId) => {
1156
+ try {
1157
+ await reportSessionWorktree(sessionId);
1158
+ } catch {
1159
+ /* the 60s sweep still carries it — this only makes it prompt */
1160
+ }
1100
1161
  };
1101
1162
 
1102
1163
  const processKillJobs = (jobs) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "flowviant",
3
- "version": "0.71.0",
3
+ "version": "0.71.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": {