fix: suppress inherited subagent notifications

Subagent completion notifications respect notification settings
Permission auto-accept now suppresses notifications for known child sessions
Server notification checks use OpenCode session parent semantics
This commit is contained in:
Bohdan Triapitsyn
2026-05-07 23:03:46 +03:00
parent add73ad687
commit 585e2a55f1
2 changed files with 48 additions and 14 deletions
+13 -9
View File
@@ -169,20 +169,24 @@ export const usePermissionStore = create<PermissionStore>()(
return { autoAccept }; return { autoAccept };
}); });
// Mirror state to the server so it can suppress permission const sessionScope = resolveSessionScope(sessionId, sessions);
// notifications at the source (otherwise the 500ms debounce
// races with the client's auto-response and can leak). // Mirror inherited state to the server so it can suppress
void fetch('/api/notifications/auto-accept', { // permission notifications before the client auto-response
method: 'POST', // round-trip. Send known descendants too; server-side
headers: { 'Content-Type': 'application/json' }, // ancestry lookup can lag OpenCode session indexing.
body: JSON.stringify({ sessionId, enabled }), for (const scopedSessionId of sessionScope) {
}).catch(() => { /* best-effort */ }); void fetch('/api/notifications/auto-accept', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ sessionId: scopedSessionId, enabled }),
}).catch(() => { /* best-effort */ });
}
if (!enabled) { if (!enabled) {
return; return;
} }
const sessionScope = resolveSessionScope(sessionId, sessions);
const sessionDirectory = useSessionUIStore.getState().getDirectoryForSession(sessionId); const sessionDirectory = useSessionUIStore.getState().getDirectoryForSession(sessionId);
const directories = new Set<string>(); const directories = new Set<string>();
const currentDirectory = normalizeDirectoryCandidate(opencodeClient.getDirectory()); const currentDirectory = normalizeDirectoryCandidate(opencodeClient.getDirectory());
@@ -60,9 +60,26 @@ export const createNotificationTriggerRuntime = (deps) => {
}; };
const setCachedSessionParentId = (sessionId, parentID) => { const setCachedSessionParentId = (sessionId, parentID) => {
if (!parentID) return;
sessionParentIdCache.set(sessionId, { parentID: parentID ?? null, at: Date.now() }); sessionParentIdCache.set(sessionId, { parentID: parentID ?? null, at: Date.now() });
}; };
const getParentIdFromPayload = (payload) => {
if (!payload || typeof payload !== 'object') return null;
if (payload.type !== 'session.created' && payload.type !== 'session.updated') return null;
const parentID = payload.properties?.info?.parentID ?? null;
return typeof parentID === 'string' && parentID.length > 0 ? parentID : null;
};
const maybeCacheSessionParentFromPayload = (payload) => {
const sessionId = extractSessionIdFromPayload(payload);
if (typeof sessionId !== 'string' || sessionId.length === 0) return;
const parentID = getParentIdFromPayload(payload);
if (parentID) {
setCachedSessionParentId(sessionId, parentID);
}
};
const fetchSessionParentId = async (sessionId) => { const fetchSessionParentId = async (sessionId) => {
if (!sessionId) return undefined; if (!sessionId) return undefined;
@@ -82,12 +99,19 @@ export const createNotificationTriggerRuntime = (deps) => {
return undefined; return undefined;
} }
const data = await response.json().catch(() => null); const data = await response.json().catch(() => null);
if (!Array.isArray(data)) { const sessions = Array.isArray(data)
? data
: Array.isArray(data?.items)
? data.items
: Array.isArray(data?.data)
? data.data
: null;
if (!sessions) {
return undefined; return undefined;
} }
const match = data.find((session) => session && typeof session === 'object' && session.id === sessionId); const match = sessions.find((session) => session && typeof session === 'object' && session.id === sessionId);
const parentID = match?.parentID ? match.parentID : null; const parentID = match?.parentID ?? null;
setCachedSessionParentId(sessionId, parentID); setCachedSessionParentId(sessionId, parentID);
return parentID; return parentID;
} catch { } catch {
@@ -164,6 +188,8 @@ export const createNotificationTriggerRuntime = (deps) => {
return; return;
} }
maybeCacheSessionParentFromPayload(payload);
const sessionId = extractSessionIdFromPayload(payload); const sessionId = extractSessionIdFromPayload(payload);
if (payload.type === 'message.updated') { if (payload.type === 'message.updated') {
const info = payload.properties?.info; const info = payload.properties?.info;
@@ -171,8 +197,7 @@ export const createNotificationTriggerRuntime = (deps) => {
const settings = await readSettingsFromDisk(); const settings = await readSettingsFromDisk();
if (settings.notifyOnSubtasks === false) { if (settings.notifyOnSubtasks === false) {
const sessionInfo = payload.properties?.session; const parentIDFromPayload = getParentIdFromPayload(payload);
const parentIDFromPayload = sessionInfo?.parentID ?? payload.properties?.parentID;
const parentID = parentIDFromPayload const parentID = parentIDFromPayload
? parentIDFromPayload ? parentIDFromPayload
: await fetchSessionParentId(sessionId); : await fetchSessionParentId(sessionId);
@@ -437,6 +462,11 @@ export const createNotificationTriggerRuntime = (deps) => {
const timer = setTimeout(async () => { const timer = setTimeout(async () => {
pushPermissionDebounceTimers.delete(sessionId); pushPermissionDebounceTimers.delete(sessionId);
if (await isSessionAutoAccepting(sessionId)) {
if (requestKey) notifiedPermissionRequests.add(requestKey);
return;
}
const settings = await readSettingsFromDisk(); const settings = await readSettingsFromDisk();
if (settings.notifyOnQuestion === false) { if (settings.notifyOnQuestion === false) {