fix: address lifecycle review feedback (#122)

Keep reconstructed activity and activitySignal coherent, preserve stronger lifecycle states when repeated polls only surface weak evidence, and switch release notes from manual changelog edits to a Changesets entry. This also stabilizes the respond/working path investigated in issue #126.
This commit is contained in:
harshitsinghbhandari 2026-04-17 19:38:32 +05:30
parent dd83a11503
commit ca8c4cc7ab
9 changed files with 80 additions and 14 deletions

View File

@ -0,0 +1,5 @@
"@aoagents/ao-core": patch
"@aoagents/ao-web": patch
---
Model activity evidence explicitly across lifecycle inference and dashboard rendering so missing or failed probes cannot spuriously produce idle or stuck interpretations. This also stabilizes repeated polls by preserving stronger prior lifecycle states when the only new evidence is weak or unavailable.

View File

@ -1,11 +1,5 @@
# @composio/ao-core
## Unreleased
### Patch Changes
- Fix lifecycle activity inference to model missing evidence explicitly, prevent null or failed probes from being treated as idle proof, and require valid idle timing before sessions can transition to `stuck`.
## 0.2.0
### Minor Changes

View File

@ -675,6 +675,48 @@ describe("check (single session)", () => {
expect(lm.getStates().get("app-1")).toBe("stuck");
});
it("preserves needs_input state when getActivityState returns null with no terminal evidence", async () => {
vi.mocked(plugins.agent.getActivityState).mockResolvedValue(null);
vi.mocked(plugins.runtime.getOutput).mockResolvedValue("");
const lm = setupCheck("app-1", {
session: makeSession({ status: "needs_input" }),
});
await lm.check("app-1");
expect(lm.getStates().get("app-1")).toBe("needs_input");
});
it("preserves stuck state across repeated polls with unchanged weak evidence", async () => {
vi.mocked(plugins.agent.getActivityState).mockResolvedValue(null);
vi.mocked(plugins.runtime.getOutput).mockResolvedValue("");
const lm = setupCheck("app-1", {
session: makeSession({ status: "stuck" }),
});
await lm.check("app-1");
expect(lm.getStates().get("app-1")).toBe("stuck");
await lm.check("app-1");
expect(lm.getStates().get("app-1")).toBe("stuck");
});
it("preserves needs_input across repeated polls with unchanged weak evidence", async () => {
vi.mocked(plugins.agent.getActivityState).mockResolvedValue(null);
vi.mocked(plugins.runtime.getOutput).mockResolvedValue("");
const lm = setupCheck("app-1", {
session: makeSession({ status: "needs_input" }),
});
await lm.check("app-1");
expect(lm.getStates().get("app-1")).toBe("needs_input");
await lm.check("app-1");
expect(lm.getStates().get("app-1")).toBe("needs_input");
});
it("detects PR states from SCM", async () => {
const mockSCM = createMockSCM({ getCISummary: vi.fn().mockResolvedValue("failing") });
const registry = createMockRegistry({

View File

@ -112,6 +112,10 @@ export function supportsRecentLiveness(signal: ActivitySignal, now: Date = new D
);
}
export function isWeakActivityEvidence(signal: ActivitySignal): boolean {
return signal.state !== "valid";
}
export function formatActivitySignalEvidence(signal: ActivitySignal): string {
const source = signal.source === "none" ? "" : ` via_${signal.source}`;
const activity = signal.activity ? ` activity=${signal.activity}` : "";

View File

@ -129,6 +129,7 @@ export {
createActivitySignal,
formatActivitySignalEvidence,
hasPositiveIdleEvidence,
isWeakActivityEvidence,
summarizeActivityFreshness,
supportsRecentLiveness,
} from "./activity-signal.js";

View File

@ -45,6 +45,7 @@ import {
createActivitySignal,
formatActivitySignalEvidence,
hasPositiveIdleEvidence,
isWeakActivityEvidence,
supportsRecentLiveness,
} from "./activity-signal.js";
import {
@ -899,6 +900,15 @@ export function createLifecycleManager(deps: LifecycleManagerDeps): LifecycleMan
return commit(SESSION_STATUS.STUCK, `idle_beyond_threshold ${activityEvidence}`, 0);
}
if (
isWeakActivityEvidence(activitySignal) &&
(session.status === SESSION_STATUS.DETECTING ||
session.status === SESSION_STATUS.STUCK ||
session.status === SESSION_STATUS.NEEDS_INPUT)
) {
return commit(session.status, activityEvidence, 0);
}
if (
session.status === SESSION_STATUS.SPAWNING ||
session.status === SESSION_STATUS.DETECTING ||

View File

@ -15,6 +15,22 @@ interface SessionFromMetadataOptions {
restoredAt?: Date;
}
function deriveDefaultActivitySignal(options: SessionFromMetadataOptions): ActivitySignal {
if (options.activitySignal) {
return options.activitySignal;
}
if (options.activity === undefined || options.activity === null) {
return createActivitySignal("unavailable");
}
return createActivitySignal("valid", {
activity: options.activity,
timestamp: options.lastActivityAt,
source: options.activity === "exited" ? "runtime" : "native",
});
}
export function sessionFromMetadata(
sessionId: SessionId,
meta: Record<string, string>,
@ -40,7 +56,7 @@ export function sessionFromMetadata(
projectId: meta["project"] ?? options.projectId ?? "",
status,
activity: options.activity ?? null,
activitySignal: options.activitySignal ?? createActivitySignal("unavailable"),
activitySignal: deriveDefaultActivitySignal(options),
lifecycle,
branch: meta["branch"] || null,
issueId: meta["issue"] || null,

View File

@ -1,11 +1,5 @@
# @composio/ao-web
## Unreleased
### Patch Changes
- Update dashboard lifecycle truth rendering to expose explicit activity-signal uncertainty, keep activity/runtime labels aligned with core lifecycle semantics, and cover stale, null, and probe-failure cases in tests.
## 0.2.2
### Patch Changes

View File

@ -76,7 +76,7 @@ export interface DashboardSession {
projectId: string;
status: SessionStatus;
activity: ActivityState | null;
activitySignal: DashboardActivitySignal;
activitySignal?: DashboardActivitySignal;
lifecycle?: DashboardLifecycle;
branch: string | null;
issueId: string | null; // Deprecated: use issueUrl instead