feat: Claude Code Monitor — lanes, pipelines and a merged workspace
Internal SmartGift build of a Claude Code monitoring dashboard. Lanes: a durable unit of parallel agent work, one per working directory, tracked across session restarts. Managed lanes are git worktrees the dashboard provisions and can reset or remove behind a three-check destroy guard and a counted preflight; adopted lanes are directories you already own and are never destroyable. Pipelines: a lane moves through pipeline stages. A stage the agent declares with evidence renders green; a stage inferred from the tool-event stream renders dashed amber and never counts as done. Detection is forward-only within a 30-minute window, and never writes the declared stage. Workspace: one page at /run with a lane grid, the selected lane's pipeline, and a full Claude console behind a disclosure.
This commit is contained in:
@@ -0,0 +1,40 @@
|
||||
/**
|
||||
* @file Per-lane serialization lock. Ensures that only one async operation runs
|
||||
* on a lane at a time, preventing concurrent git checkout races and other
|
||||
* worktree collisions. Implemented as a chain of promises keyed by lane ID;
|
||||
* acquiring a lock waits for the previous holder to settle (success or throw),
|
||||
* then runs the new work, and releases for the next waiter.
|
||||
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
||||
*/
|
||||
|
||||
const chains = new Map();
|
||||
|
||||
/**
|
||||
* Acquire the per-lane lock, run fn, and release.
|
||||
* The lock serialises work per lane_id: the next waiter runs only after the
|
||||
* previous holder settles (success or error). If fn throws, the exception
|
||||
* propagates to the caller; the chain is *not* poisoned — the next waiter
|
||||
* still gets a fresh attempt.
|
||||
*
|
||||
* @param {number|string} id - Lane ID, converted to string for deduplication.
|
||||
* @param {() => Promise<T>} fn - Async function to run while holding the lock.
|
||||
* @returns {Promise<T>} The result of fn, or its thrown error.
|
||||
*/
|
||||
function withLaneLock(id, fn) {
|
||||
const key = String(id);
|
||||
const prev = chains.get(key) || Promise.resolve();
|
||||
const run = prev.then(fn, fn); // run regardless of how the previous holder settled
|
||||
// Keep the chain alive but never let a rejection poison the next waiter.
|
||||
const settled = run.then(
|
||||
() => {},
|
||||
() => {}
|
||||
);
|
||||
chains.set(key, settled);
|
||||
// Clean up the chain entry when it settles to prevent unbounded map growth.
|
||||
settled.then(() => {
|
||||
if (chains.get(key) === settled) chains.delete(key);
|
||||
});
|
||||
return run;
|
||||
}
|
||||
|
||||
module.exports = { withLaneLock };
|
||||
Reference in New Issue
Block a user