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:
parent
dd83a11503
commit
ca8c4cc7ab
|
|
@ -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.
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -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}` : "";
|
||||
|
|
|
|||
|
|
@ -129,6 +129,7 @@ export {
|
|||
createActivitySignal,
|
||||
formatActivitySignalEvidence,
|
||||
hasPositiveIdleEvidence,
|
||||
isWeakActivityEvidence,
|
||||
summarizeActivityFreshness,
|
||||
supportsRecentLiveness,
|
||||
} from "./activity-signal.js";
|
||||
|
|
|
|||
|
|
@ -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 ||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in New Issue