Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -340,6 +340,7 @@ gx sync
```sh
gx agents start # review monitor + stale cleanup
gx agents stop
gx agents stop --pid 12345
gx agents status

# tuning
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# agent-codex-active-agents-bugfix-pass-2026-04-22-16-54 (minimal / T1)

Branch: `agent/codex/active-agents-bugfix-pass-2026-04-22-16-54`

Patch the shipped VS Code `Active Agents` companion bugs that remain after the grouped-session rollout. Keep the scope on the real defects: duplicate provider methods, expensive clean-worktree activity scans, stop-session process handling, and blocking diff rendering.

Scope:
- Update `vscode/guardex-active-agents/session-schema.js` to bound and cache clean-worktree activity checks.
- Update `vscode/guardex-active-agents/extension.js` to remove the duplicate lock-registry methods, route stop through `gx`, replace the blocking diff dump with Git-native change opens, and drop the emoji lock label.
- Update the focused extension/CLI regressions so they cover the live `vscode/` source instead of the stale template copy, then add metadata parity coverage so the mirrored JS sources do not drift again.

Verification:
- `node --test test/vscode-active-agents-session-state.test.js test/agents.test.js`
- `node --test test/metadata.test.js`

## Handoff

- Handoff: change=`agent-codex-active-agents-bugfix-pass-2026-04-22-16-54`; branch=`agent/codex/active-agents-bugfix-pass-2026-04-22-16-54`; scope=`vscode/guardex-active-agents/*, src/cli/{args.js,main.js}, test/{vscode-active-agents-session-state.test.js,agents.test.js}, openspec/changes/agent-codex-active-agents-bugfix-pass-2026-04-22-16-54/notes.md`; action=`fix the remaining Active Agents extension bugs, verify with targeted Node tests, then finish via PR merge + cleanup`.
- Copy prompt: Continue `agent-codex-active-agents-bugfix-pass-2026-04-22-16-54` on branch `agent/codex/active-agents-bugfix-pass-2026-04-22-16-54`. Work inside the existing sandbox, review `openspec/changes/agent-codex-active-agents-bugfix-pass-2026-04-22-16-54/notes.md`, continue from the current state instead of creating a new sandbox, and when the work is done run `gx branch finish --branch agent/codex/active-agents-bugfix-pass-2026-04-22-16-54 --base main --via-pr --wait-for-merge --cleanup`.

## Cleanup

- [ ] Run: `gx branch finish --branch agent/codex/active-agents-bugfix-pass-2026-04-22-16-54 --base main --via-pr --wait-for-merge --cleanup`
- [ ] Record PR URL + `MERGED` state in the completion handoff.
- [ ] Confirm sandbox worktree is gone (`git worktree list`, `git branch -a`).
17 changes: 17 additions & 0 deletions src/cli/args.js
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,7 @@ function parseAgentsArgs(rawArgs) {
reviewIntervalSeconds: 30,
cleanupIntervalSeconds: 60,
idleMinutes: DEFAULT_SHADOW_CLEANUP_IDLE_MINUTES,
pid: null,
};

for (let index = 0; index < rest.length; index += 1) {
Expand Down Expand Up @@ -314,12 +315,28 @@ function parseAgentsArgs(rawArgs) {
index += 1;
continue;
}
if (arg === '--pid') {
const next = rest[index + 1];
if (!next) {
throw new Error('--pid requires a positive integer value');
}
const parsedValue = Number.parseInt(next, 10);
if (!Number.isInteger(parsedValue) || parsedValue <= 0) {
throw new Error('--pid must be a positive integer');
}
options.pid = parsedValue;
index += 1;
continue;
}
throw new Error(`Unknown option: ${arg}`);
}

if (!['start', 'stop', 'status'].includes(options.subcommand)) {
throw new Error(`Unknown agents subcommand: ${options.subcommand}`);
}
if (options.pid !== null && options.subcommand !== 'stop') {
throw new Error('--pid is only supported with `gx agents stop`');
}

return options;
}
Expand Down
25 changes: 24 additions & 1 deletion src/cli/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -2219,10 +2219,15 @@ function processAlive(pid) {
}
try {
process.kill(normalizedPid, 0);
return true;
} catch (_error) {
return false;
}

const state = readProcessState(normalizedPid);
if (state.startsWith('Z')) {
return false;
}
return true;
}

function sleepSeconds(seconds) {
Expand All @@ -2240,6 +2245,14 @@ function readProcessCommand(pid) {
return String(result.stdout || '').trim();
}

function readProcessState(pid) {
const result = run('ps', ['-o', 'stat=', '-p', String(pid)]);
if (isSpawnFailure(result) || result.status !== 0) {
return '';
}
return String(result.stdout || '').trim();
}

function stopAgentProcessByPid(pid, expectedToken = '') {
const normalizedPid = Number.parseInt(String(pid || ''), 10);
if (!Number.isInteger(normalizedPid) || normalizedPid <= 0) {
Expand Down Expand Up @@ -2431,6 +2444,16 @@ function agents(rawArgs) {
}

if (options.subcommand === 'stop') {
if (options.pid) {
const stopResult = stopAgentProcessByPid(options.pid);
const success = ['stopped', 'not-running'].includes(stopResult.status);
console.log(
`[${TOOL_NAME}] Stopped agent pid ${options.pid} (${stopResult.status}).`,
);
process.exitCode = success ? 0 : 1;
return;
}

const existingState = readAgentsState(repoRoot);
if (!existingState) {
console.log(`[${TOOL_NAME}] Repo agents are not running for ${repoRoot}.`);
Expand Down
120 changes: 92 additions & 28 deletions templates/vscode/guardex-active-agents/extension.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ const ACTIVE_AGENTS_MANIFEST_RELATIVE = path.join('vscode', 'guardex-active-agen
const ACTIVE_AGENTS_INSTALL_SCRIPT_RELATIVE = path.join('scripts', 'install-vscode-active-agents-extension.js');
const RELOAD_WINDOW_ACTION = 'Reload Window';
const UPDATE_LATER_ACTION = 'Later';
const REFRESH_POLL_INTERVAL_MS = 30_000;
const SESSION_ACTIVITY_GROUPS = [
{ kind: 'blocked', label: 'BLOCKED' },
{ kind: 'working', label: 'WORKING NOW' },
Expand Down Expand Up @@ -293,7 +294,7 @@ class SessionItem extends vscode.TreeItem {
constructor(session, items = []) {
const lockCount = Number.isFinite(session.lockCount) ? session.lockCount : 0;
super(
`${session.label} 🔒 ${lockCount}`,
session.label,
items.length > 0 ? vscode.TreeItemCollapsibleState.Expanded : vscode.TreeItemCollapsibleState.None,
);
this.session = session;
Expand All @@ -304,6 +305,9 @@ class SessionItem extends vscode.TreeItem {
descriptionParts.push(session.activityCountLabel);
}
descriptionParts.push(session.elapsedLabel || formatElapsedFrom(session.startedAt));
if (lockCount > 0) {
descriptionParts.push(`${lockCount} $(lock)`);
}
this.description = descriptionParts.join(' · ');
const tooltipLines = [
session.branch,
Expand Down Expand Up @@ -437,6 +441,20 @@ function syncSession(session) {
runSessionTerminalCommand(session, 'Sync', 'sync', 'gx sync');
}

function execFileAsync(command, args, options = {}) {
return new Promise((resolve, reject) => {
cp.execFile(command, args, options, (error, stdout = '', stderr = '') => {
if (error) {
error.stdout = stdout;
error.stderr = stderr;
reject(error);
return;
}
resolve({ stdout, stderr });
});
});
}

async function stopSession(session, refresh) {
const pid = Number(session?.pid);
if (!Number.isInteger(pid) || pid <= 0) {
Expand All @@ -450,20 +468,69 @@ async function stopSession(session, refresh) {

const confirmed = await vscode.window.showWarningMessage(
`Stop ${sessionDisplayLabel(session)}?`,
{ modal: true, detail: `Ask gx to send SIGTERM to pid ${pid}.` },
{ modal: true, detail: `Run gx agents stop --pid ${pid}.` },
'Stop',
);
if (confirmed !== 'Stop') {
return;
}

runSessionTerminalCommand(
session,
'Stop',
'debug-stop',
`gx internal stop-session --branch ${shellQuote(session.branch)}`,
);
refresh();
try {
const commandCwd = session?.repoRoot || sessionWorktreePath(session) || process.cwd();
const args = ['agents', 'stop', '--pid', String(pid)];
if (session?.repoRoot) {
args.push('--target', session.repoRoot);
}
await execFileAsync('gx', args, {
cwd: commandCwd,
encoding: 'utf8',
maxBuffer: 1024 * 1024,
});
refresh();
} catch (error) {
showSessionMessage(
`Failed to stop session ${sessionDisplayLabel(session)}: ${formatGitCommandFailure(error)}`,
);
}
}

function sessionChangedPaths(session) {
const directPaths = Array.isArray(session?.changedPaths)
? session.changedPaths.map(normalizeRelativePath).filter(Boolean)
: [];
if (directPaths.length > 0) {
return [...new Set(directPaths)];
}
if (!session?.repoRoot || !session?.branch) {
return [];
}

const liveSession = readActiveSessions(session.repoRoot)
.find((entry) => sessionSelectionKey(entry) === sessionSelectionKey(session));
return Array.isArray(liveSession?.changedPaths)
? [...new Set(liveSession.changedPaths.map(normalizeRelativePath).filter(Boolean))]
: [];
}

async function pickSessionDiffPath(session) {
const changedPaths = sessionChangedPaths(session);
if (changedPaths.length === 0) {
return '';
}
if (changedPaths.length === 1 || !vscode.window.showQuickPick) {
return changedPaths[0];
}

const picks = changedPaths.map((relativePath) => ({
label: path.basename(relativePath),
description: relativePath,
relativePath,
}));
const selection = await vscode.window.showQuickPick(picks, {
placeHolder: `Select a changed file for ${sessionDisplayLabel(session)}`,
ignoreFocusOut: true,
});
return selection?.relativePath || '';
}

async function openSessionDiff(session) {
Expand All @@ -472,27 +539,24 @@ async function openSessionDiff(session) {
return;
}

let diffOutput = '';
try {
diffOutput = cp.execFileSync('git', ['-C', worktreePath, 'diff'], {
encoding: 'utf8',
stdio: ['ignore', 'pipe', 'pipe'],
});
} catch (error) {
const detail = [
error?.stdout,
error?.stderr,
error?.message,
].find((value) => typeof value === 'string' && value.trim().length > 0) || 'git diff failed.';
showSessionMessage(`Failed to open diff for ${sessionDisplayLabel(session)}: ${detail.trim()}`);
const relativePath = await pickSessionDiffPath(session);
if (!relativePath) {
showSessionMessage(`No changed files to diff for ${sessionDisplayLabel(session)}.`);
return;
}

const document = await vscode.workspace.openTextDocument({
language: 'diff',
content: diffOutput,
});
await vscode.window.showTextDocument(document, { preview: false });
const repoRoot = session?.repoRoot || worktreePath;
const absolutePath = path.resolve(repoRoot, relativePath);
const resourceUri = vscode.Uri.file(absolutePath);
try {
await vscode.commands.executeCommand('git.openChange', resourceUri);
} catch (error) {
if (fs.existsSync(absolutePath)) {
await vscode.commands.executeCommand('vscode.open', resourceUri);
return;
}
showSessionMessage(`Failed to open diff for ${sessionDisplayLabel(session)}: ${formatGitCommandFailure(error)}`);
}
}

function repoRootFromSessionFile(filePath) {
Expand Down Expand Up @@ -1461,7 +1525,7 @@ function activate(context) {
command: 'gitguardex.activeAgents.commitSelectedSession',
title: 'Commit Selected Session',
};
const interval = setInterval(refresh, 5_000);
const interval = setInterval(refresh, REFRESH_POLL_INTERVAL_MS);
const refreshLockRegistry = (uri) => {
if (uri?.fsPath) {
provider.refreshLockRegistryForFile(uri.fsPath);
Expand Down
2 changes: 1 addition & 1 deletion templates/vscode/guardex-active-agents/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"displayName": "GitGuardex Active Agents",
"description": "Shows live Guardex sandbox sessions and repo changes inside VS Code Source Control.",
"publisher": "recodeee",
"version": "0.0.4",
"version": "0.0.5",
"license": "MIT",
"icon": "icon.png",
"engines": {
Expand Down
Loading