Archived
fix(sessions): stop counting session startup as work in progress
Startup progress rides the per-session activity channel with an "active" phase, because a startup phase really is in progress. But isSessionActive() treated any active activity as work, so a session that was merely *opening* enabled "Stop Active Work", disabled "Reload from disk" with the misleading "Stop current session activity before reloading" tooltip, showed the row's active-work indicator, and — for any caller that hands a startup activity to WorkspaceActivityService — reported the whole workspace as busy. Selecting an archived, read-only session reported active work while it opened. Starting is not working. publishStartupProgress now marks its reports with a new optional SessionActivity.startup field, and isSessionActive() does not count a marked activity. Every affected consumer — the session list, the core actions, the app's activity-transition handling, and the server's workspace aggregation — reads that one helper, so the correction lands in all of them at once. The marker is a new field rather than a new phase on purpose: six readers test phase === "active" directly, including the pending row's "creating · " prefix, the chat dock's active styling, and the daemon's own heartbeat re-publication. It can only ever remove the activity-phase reason for being active, so streaming, bash, compaction, and queued prompts still report as active through the status even while a startup report is the latest activity. The chat dock still shows the startup text; this changes what counts as work, not what is shown. The browser's own pending-create row keeps its previous appearance: it borrows only the daemon's phase text and drops the marker, since that row stands for a create the user is waiting on rather than a session the daemon is opening.
This commit is contained in:
@@ -32,6 +32,19 @@ describe("WorkspaceActivityService", () => {
|
||||
expect(events.at(-1)).toMatchObject({ type: "workspace.activity", activity: { cwd: "/repo", hasSessionActivity: false, hasTerminalActivity: false } });
|
||||
});
|
||||
|
||||
it("does not report a workspace active for a session that is only starting up", () => {
|
||||
const events: RealtimeEvent[] = [];
|
||||
const service = new WorkspaceActivityService({ publishRealtime: (event) => events.push(event) });
|
||||
|
||||
// Startup progress names a phase the daemon is inside; it is not work, so the
|
||||
// workspace (and the project indicators and remote machines that read it)
|
||||
// must not be reported as busy because of it.
|
||||
service.applySessionActivity("/repo", { sessionId: "s1", phase: "active", label: "Opening session", detail: "Starting the Pi session", at: "now", startup: true });
|
||||
|
||||
expect(service.snapshot().workspaces).toEqual([]);
|
||||
expect(events.at(-1)).toMatchObject({ type: "workspace.activity", activity: { cwd: "/repo", hasSessionActivity: false } });
|
||||
});
|
||||
|
||||
it("clears stale active activity when an idle status arrives", () => {
|
||||
const events: RealtimeEvent[] = [];
|
||||
const service = new WorkspaceActivityService({ publishRealtime: (event) => events.push(event) });
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { PiSessionService, type PiSessionRuntime } from "./piSessionService.js";
|
||||
import { CapturingSessionEventHub, emptyArchiveStore, fakeRuntime, sessionGateway, sessionRecord, sessionRef, testModelRuntime } from "./piSessionService.testSupport.js";
|
||||
import { isSessionActive } from "../../shared/activity.js";
|
||||
import type { SessionActivity, SessionStartupProgressEvent } from "../../shared/apiTypes.js";
|
||||
|
||||
const TEST_AGENT_DIR = "/tmp/pi-web-test-agent";
|
||||
@@ -235,6 +236,34 @@ describe("PiSessionService session startup progress", () => {
|
||||
await service.dispose();
|
||||
});
|
||||
|
||||
it("reports startup progress as starting rather than as work in progress", async () => {
|
||||
const { hub, service } = startupService();
|
||||
|
||||
await service.start("/workspace");
|
||||
|
||||
// Startup phases are published with an "active" phase so the waiting user
|
||||
// sees them, but opening a session is not work: nothing that decides whether
|
||||
// work is in progress may count them.
|
||||
const phases = startupEvents(hub).filter((event) => event.activity.phase === "active");
|
||||
expect(phases).toHaveLength(2);
|
||||
expect(phases.map((event) => isSessionActive(undefined, event.activity))).toEqual([false, false]);
|
||||
await service.dispose();
|
||||
});
|
||||
|
||||
it("still reports a real activity published during startup as work", async () => {
|
||||
const { hub, fake, service } = startupService();
|
||||
|
||||
await service.start("/workspace");
|
||||
fake.emit({ type: "tool_execution_start", toolName: "bash" });
|
||||
|
||||
// The marker belongs to the startup channel alone; an ordinary activity for
|
||||
// the same session still counts, or the fix would hide real work.
|
||||
const running = activityUpdates(hub).filter((activity) => activity.phase === "active");
|
||||
expect(running.length).toBeGreaterThan(0);
|
||||
expect(running.every((activity) => isSessionActive(undefined, activity))).toBe(true);
|
||||
await service.dispose();
|
||||
});
|
||||
|
||||
it("keeps startup reporting event-only, writing no session or workspace activity state", async () => {
|
||||
const recorder = recordingWorkspaceActivity();
|
||||
const failure = new Error("runtime unavailable");
|
||||
|
||||
@@ -3039,10 +3039,15 @@ export class PiSessionService implements SessionRouteService {
|
||||
* Unlike {@link publishActivity} this deliberately records nothing: no
|
||||
* `activities` entry, no workspace activity, no unread observation. There is
|
||||
* no session to own that state, and a failed creation would leave it stranded.
|
||||
*
|
||||
* Every report is marked `startup`, which is what keeps a session that is
|
||||
* merely opening from counting as one doing work. This is the only publisher
|
||||
* that sets the marker, and because it writes no `activities` entry no later
|
||||
* heartbeat re-publication can carry it.
|
||||
*/
|
||||
private publishStartupProgress(sessionId: string, startupToken: string | undefined, label: string, phase: "active" | "idle", detail: string | undefined): void {
|
||||
const at = new Date().toISOString();
|
||||
const activity = detail === undefined ? { sessionId, phase, label, at } : { sessionId, phase, label, detail, at };
|
||||
const activity = detail === undefined ? { sessionId, phase, label, at, startup: true } : { sessionId, phase, label, detail, at, startup: true };
|
||||
this.events.publishGlobal(startupToken === undefined ? { type: "session.startup", activity } : { type: "session.startup", startupToken, activity });
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user