agileflow 4.0.0-alpha.23 → 4.0.0-alpha.24
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/package.json
CHANGED
|
@@ -1155,6 +1155,11 @@ async function runInternalSnapshotSession(sessionName, deps = {}) {
|
|
|
1155
1155
|
const runner = deps.runner || defaultTmuxRunner();
|
|
1156
1156
|
const DELIM = "\x1f";
|
|
1157
1157
|
const fmt = `#{window_index}${DELIM}#{window_name}${DELIM}#{pane_current_path}`;
|
|
1158
|
+
// Capture timestamp BEFORE list-windows so concurrent subprocesses
|
|
1159
|
+
// produce a stable ordering. The registry rejects writes whose
|
|
1160
|
+
// capturedAt is older than what's already stored — without this,
|
|
1161
|
+
// out-of-order lock acquisition can clobber newer state with older.
|
|
1162
|
+
const capturedAt = Date.now();
|
|
1158
1163
|
const result = runner.runSync(["list-windows", "-t", sessionName, "-F", fmt]);
|
|
1159
1164
|
if (result.status !== 0) return;
|
|
1160
1165
|
const lines = (result.stdout || "")
|
|
@@ -1177,7 +1182,7 @@ async function runInternalSnapshotSession(sessionName, deps = {}) {
|
|
|
1177
1182
|
const {
|
|
1178
1183
|
updateSession,
|
|
1179
1184
|
} = require("../../runtime/launch/session-registry.js");
|
|
1180
|
-
updateSession(sessionName, { windows });
|
|
1185
|
+
updateSession(sessionName, { windows, windowsCapturedAt: capturedAt });
|
|
1181
1186
|
} catch {
|
|
1182
1187
|
// Registry might not exist or session might have been forgotten;
|
|
1183
1188
|
// either way we can't usefully recover. Stay silent.
|
|
@@ -137,6 +137,10 @@ function withRegistryLock(home, fn) {
|
|
|
137
137
|
* by tmux hooks; replayed on
|
|
138
138
|
* restore so reboots bring back
|
|
139
139
|
* every tab in its original cwd
|
|
140
|
+
* @property {number} [windowsCapturedAt] - epoch ms when `windows` was
|
|
141
|
+
* captured. Newer captures
|
|
142
|
+
* always win when concurrent
|
|
143
|
+
* hook subprocesses race.
|
|
140
144
|
*
|
|
141
145
|
* @typedef {Object} RegistryShape
|
|
142
146
|
* @property {1} version
|
|
@@ -233,6 +237,8 @@ function loadRegistry(home) {
|
|
|
233
237
|
}
|
|
234
238
|
: undefined,
|
|
235
239
|
windows,
|
|
240
|
+
windowsCapturedAt:
|
|
241
|
+
typeof s.windowsCapturedAt === "number" ? s.windowsCapturedAt : 0,
|
|
236
242
|
});
|
|
237
243
|
}
|
|
238
244
|
return { version: 1, sessions: sane };
|
|
@@ -369,7 +375,22 @@ function updateSession(name, patch, home) {
|
|
|
369
375
|
if (patch.worktree !== undefined) s.worktree = patch.worktree;
|
|
370
376
|
if (patch.pinned !== undefined) s.pinned = patch.pinned === true;
|
|
371
377
|
if (patch.windows !== undefined) {
|
|
372
|
-
|
|
378
|
+
// Stale-snapshot guard: hooks fire on every tab change, so
|
|
379
|
+
// when the user spams Alt+t multiple subprocesses can race.
|
|
380
|
+
// Lock acquisition isn't FIFO so an older capture can be
|
|
381
|
+
// committed after a newer one. Reject any update whose
|
|
382
|
+
// capturedAt timestamp is older than what's already stored.
|
|
383
|
+
const incomingTs =
|
|
384
|
+
typeof patch.windowsCapturedAt === "number"
|
|
385
|
+
? patch.windowsCapturedAt
|
|
386
|
+
: Date.now();
|
|
387
|
+
if (!s.windowsCapturedAt || incomingTs >= s.windowsCapturedAt) {
|
|
388
|
+
s.windows = Array.isArray(patch.windows)
|
|
389
|
+
? patch.windows
|
|
390
|
+
: undefined;
|
|
391
|
+
s.windowsCapturedAt = incomingTs;
|
|
392
|
+
}
|
|
393
|
+
// else: silently drop the stale write
|
|
373
394
|
}
|
|
374
395
|
updated = true;
|
|
375
396
|
}
|
|
@@ -344,9 +344,35 @@ function applyTabFormat(sessionName, runner, opts = {}) {
|
|
|
344
344
|
*/
|
|
345
345
|
function installSessionHooks(sessionName, runner, opts = {}) {
|
|
346
346
|
const agileflowBin = opts.agileflowBin || resolveAgileflowBin();
|
|
347
|
-
|
|
347
|
+
// Double-quote the binary path and session name so paths with
|
|
348
|
+
// spaces (e.g., /Users/x with space/agileflow) work. The shell
|
|
349
|
+
// command is itself wrapped in double quotes for run-shell to
|
|
350
|
+
// accept it as a single argument.
|
|
351
|
+
const shellCmd = `"${agileflowBin}" launch __snapshot-session "${sessionName}"`;
|
|
352
|
+
const snapshotCmd = `run-shell -b "${shellCmd.replace(/"/g, '\\"')}"`;
|
|
353
|
+
/** @type {Array<{ event: string, stderr: string }>} */
|
|
354
|
+
const failures = [];
|
|
348
355
|
for (const event of ["window-linked", "window-unlinked", "window-renamed"]) {
|
|
349
|
-
runner.runSync([
|
|
356
|
+
const r = runner.runSync([
|
|
357
|
+
"set-hook",
|
|
358
|
+
"-t",
|
|
359
|
+
sessionName,
|
|
360
|
+
event,
|
|
361
|
+
snapshotCmd,
|
|
362
|
+
]);
|
|
363
|
+
if (r.status !== 0) {
|
|
364
|
+
failures.push({ event, stderr: (r.stderr || "").trim() });
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
if (failures.length > 0) {
|
|
368
|
+
// eslint-disable-next-line no-console
|
|
369
|
+
console.error(
|
|
370
|
+
`agileflow launch: tab persistence — ${failures.length} of 3 tmux hooks failed to install:`,
|
|
371
|
+
);
|
|
372
|
+
for (const f of failures) {
|
|
373
|
+
// eslint-disable-next-line no-console
|
|
374
|
+
console.error(` ${f.event}: ${f.stderr || "(no error message)"}`);
|
|
375
|
+
}
|
|
350
376
|
}
|
|
351
377
|
}
|
|
352
378
|
|