fix(server): restore release-blocking lifecycle checks

This commit is contained in:
Mohamed Boudra
2026-04-22 18:03:37 +07:00
parent e51fdada51
commit 713c90ea42
5 changed files with 113 additions and 2 deletions
@@ -7,6 +7,7 @@ import {
function state(overrides: Partial<ClientPresenceState>): ClientPresenceState {
return {
appVisible: true,
focusedAgentId: null,
lastActivityAtMs: null,
...overrides,
@@ -54,6 +55,23 @@ describe("computeNotificationPlan", () => {
).toEqual({ inAppRecipientIndex: null, shouldPush: false });
});
it("does not suppress notifications when a focused client is backgrounded", () => {
const backgroundFocused = state({
appVisible: false,
focusedAgentId: "agent-1",
lastActivityAtMs: presentAtMs,
});
expect(
computeNotificationPlan({
allStates: [backgroundFocused],
agentId: "agent-1",
reason: "finished",
nowMs,
}),
).toEqual({ inAppRecipientIndex: 0, shouldPush: false });
});
it("treats present clients focused on different agents as eligible", () => {
expect(
computeNotificationPlan({
@@ -3,6 +3,7 @@ import type { AgentAttentionReason } from "../shared/agent-attention-notificatio
export const PRESENCE_THRESHOLD_MS = 180_000;
export interface ClientPresenceState {
appVisible: boolean;
lastActivityAtMs: number | null;
focusedAgentId: string | null;
}
@@ -38,7 +39,7 @@ export function computeNotificationPlan({
continue;
}
if (state.focusedAgentId === agentId) {
if (state.appVisible && state.focusedAgentId === agentId) {
return { inAppRecipientIndex: null, shouldPush: false };
}
+38 -1
View File
@@ -165,6 +165,7 @@ import { DownloadTokenStore } from "./file-download/token-store.js";
import { PushTokenStore } from "./push/token-store.js";
import { type WorktreeConfig } from "../utils/worktree.js";
import { runAsyncWorktreeBootstrap } from "./worktree-bootstrap.js";
import { WorkspaceReconciliationService } from "./workspace-reconciliation-service.js";
import type { ScriptRouteStore } from "./script-proxy.js";
import {
checkoutResolvedBranch,
@@ -6440,7 +6441,43 @@ export class Session {
}
private async reconcileActiveWorkspaceRecords(): Promise<Set<string>> {
return new Set();
const service = new WorkspaceReconciliationService({
projectRegistry: this.projectRegistry,
workspaceRegistry: this.workspaceRegistry,
logger: this.sessionLogger,
workspaceGitService: this.workspaceGitService,
});
const result = await service.runOnce();
const changedWorkspaceIds = new Set<string>();
const changedProjectIds = new Set<string>();
for (const change of result.changesApplied) {
switch (change.kind) {
case "workspace_archived":
await this.removeWorkspaceGitWatchTarget(change.directory);
this.scriptRuntimeStore?.removeForWorkspace(change.directory);
this.removeWorkspaceGitSubscription(change.workspaceId);
changedWorkspaceIds.add(change.workspaceId);
break;
case "workspace_updated":
changedWorkspaceIds.add(change.workspaceId);
break;
case "project_archived":
case "project_updated":
changedProjectIds.add(change.projectId);
break;
}
}
if (changedProjectIds.size > 0) {
for (const workspace of await this.workspaceRegistry.list()) {
if (changedProjectIds.has(workspace.projectId)) {
changedWorkspaceIds.add(workspace.workspaceId);
}
}
}
return changedWorkspaceIds;
}
private async emitWorkspaceUpdatesForWorkspaceIds(
@@ -311,6 +311,59 @@ function createSessionForWorkspaceTests(
}
describe("workspace aggregation", () => {
test("workspace reconciliation reports archived workspaces to subscribed clients", async () => {
const missingCwd = path.join(tmpdir(), `paseo-missing-workspace-${Date.now()}`);
rmSync(missingCwd, { recursive: true, force: true });
const projects = new Map([
[
"proj-missing",
createPersistedProjectRecord({
projectId: "proj-missing",
rootPath: missingCwd,
kind: "non_git",
displayName: "missing",
createdAt: "2026-03-01T12:00:00.000Z",
updatedAt: "2026-03-01T12:00:00.000Z",
}),
],
]);
const workspaces = new Map([
[
"ws-missing",
createPersistedWorkspaceRecord({
workspaceId: "ws-missing",
projectId: "proj-missing",
cwd: missingCwd,
kind: "directory",
displayName: "missing",
createdAt: "2026-03-01T12:00:00.000Z",
updatedAt: "2026-03-01T12:00:00.000Z",
}),
],
]);
const session = createSessionForWorkspaceTests() as any;
session.projectRegistry.list = async () => Array.from(projects.values());
session.projectRegistry.archive = async (projectId: string, archivedAt: string) => {
const project = projects.get(projectId);
if (project) {
projects.set(projectId, { ...project, archivedAt });
}
};
session.workspaceRegistry.list = async () => Array.from(workspaces.values());
session.workspaceRegistry.archive = async (workspaceId: string, archivedAt: string) => {
const workspace = workspaces.get(workspaceId);
if (workspace) {
workspaces.set(workspaceId, { ...workspace, archivedAt });
}
};
const changedWorkspaceIds = await session.reconcileActiveWorkspaceRecords();
expect(changedWorkspaceIds).toEqual(new Set(["ws-missing"]));
expect(workspaces.get("ws-missing")?.archivedAt).toBeTruthy();
expect(projects.get("proj-missing")?.archivedAt).toBeTruthy();
});
test("agent_update placement does not refresh git snapshots", async () => {
const emitted: SessionOutboundMessage[] = [];
const getSnapshot = vi.fn(async () => {
@@ -1524,12 +1524,14 @@ export class VoiceAssistantWebSocketServer {
const activity = session.getClientActivity();
if (!activity) {
return {
appVisible: false,
focusedAgentId: null,
lastActivityAtMs: null,
};
}
return {
appVisible: activity.appVisible,
focusedAgentId: activity.focusedAgentId,
lastActivityAtMs: activity.lastActivityAt.getTime(),
};