fix(server): restore release-blocking lifecycle checks
This commit is contained in:
@@ -7,6 +7,7 @@ import {
|
|||||||
|
|
||||||
function state(overrides: Partial<ClientPresenceState>): ClientPresenceState {
|
function state(overrides: Partial<ClientPresenceState>): ClientPresenceState {
|
||||||
return {
|
return {
|
||||||
|
appVisible: true,
|
||||||
focusedAgentId: null,
|
focusedAgentId: null,
|
||||||
lastActivityAtMs: null,
|
lastActivityAtMs: null,
|
||||||
...overrides,
|
...overrides,
|
||||||
@@ -54,6 +55,23 @@ describe("computeNotificationPlan", () => {
|
|||||||
).toEqual({ inAppRecipientIndex: null, shouldPush: false });
|
).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", () => {
|
it("treats present clients focused on different agents as eligible", () => {
|
||||||
expect(
|
expect(
|
||||||
computeNotificationPlan({
|
computeNotificationPlan({
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import type { AgentAttentionReason } from "../shared/agent-attention-notificatio
|
|||||||
export const PRESENCE_THRESHOLD_MS = 180_000;
|
export const PRESENCE_THRESHOLD_MS = 180_000;
|
||||||
|
|
||||||
export interface ClientPresenceState {
|
export interface ClientPresenceState {
|
||||||
|
appVisible: boolean;
|
||||||
lastActivityAtMs: number | null;
|
lastActivityAtMs: number | null;
|
||||||
focusedAgentId: string | null;
|
focusedAgentId: string | null;
|
||||||
}
|
}
|
||||||
@@ -38,7 +39,7 @@ export function computeNotificationPlan({
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (state.focusedAgentId === agentId) {
|
if (state.appVisible && state.focusedAgentId === agentId) {
|
||||||
return { inAppRecipientIndex: null, shouldPush: false };
|
return { inAppRecipientIndex: null, shouldPush: false };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -165,6 +165,7 @@ import { DownloadTokenStore } from "./file-download/token-store.js";
|
|||||||
import { PushTokenStore } from "./push/token-store.js";
|
import { PushTokenStore } from "./push/token-store.js";
|
||||||
import { type WorktreeConfig } from "../utils/worktree.js";
|
import { type WorktreeConfig } from "../utils/worktree.js";
|
||||||
import { runAsyncWorktreeBootstrap } from "./worktree-bootstrap.js";
|
import { runAsyncWorktreeBootstrap } from "./worktree-bootstrap.js";
|
||||||
|
import { WorkspaceReconciliationService } from "./workspace-reconciliation-service.js";
|
||||||
import type { ScriptRouteStore } from "./script-proxy.js";
|
import type { ScriptRouteStore } from "./script-proxy.js";
|
||||||
import {
|
import {
|
||||||
checkoutResolvedBranch,
|
checkoutResolvedBranch,
|
||||||
@@ -6440,7 +6441,43 @@ export class Session {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async reconcileActiveWorkspaceRecords(): Promise<Set<string>> {
|
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(
|
private async emitWorkspaceUpdatesForWorkspaceIds(
|
||||||
|
|||||||
@@ -311,6 +311,59 @@ function createSessionForWorkspaceTests(
|
|||||||
}
|
}
|
||||||
|
|
||||||
describe("workspace aggregation", () => {
|
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 () => {
|
test("agent_update placement does not refresh git snapshots", async () => {
|
||||||
const emitted: SessionOutboundMessage[] = [];
|
const emitted: SessionOutboundMessage[] = [];
|
||||||
const getSnapshot = vi.fn(async () => {
|
const getSnapshot = vi.fn(async () => {
|
||||||
|
|||||||
@@ -1524,12 +1524,14 @@ export class VoiceAssistantWebSocketServer {
|
|||||||
const activity = session.getClientActivity();
|
const activity = session.getClientActivity();
|
||||||
if (!activity) {
|
if (!activity) {
|
||||||
return {
|
return {
|
||||||
|
appVisible: false,
|
||||||
focusedAgentId: null,
|
focusedAgentId: null,
|
||||||
lastActivityAtMs: null,
|
lastActivityAtMs: null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
appVisible: activity.appVisible,
|
||||||
focusedAgentId: activity.focusedAgentId,
|
focusedAgentId: activity.focusedAgentId,
|
||||||
lastActivityAtMs: activity.lastActivityAt.getTime(),
|
lastActivityAtMs: activity.lastActivityAt.getTime(),
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user