diff --git a/.changeset/fix-activity-unavailable-stuck.md b/.changeset/fix-activity-unavailable-stuck.md new file mode 100644 index 000000000..1ad7e562d --- /dev/null +++ b/.changeset/fix-activity-unavailable-stuck.md @@ -0,0 +1,5 @@ +--- +"@aoagents/ao-core": patch +--- + +Stop carrying forward `stuck` / `probe_failure` session truth when the runtime is still confirmed alive and activity is merely unavailable, and degrade that combination to `detecting` until stronger evidence arrives. diff --git a/packages/core/src/__tests__/lifecycle-manager.test.ts b/packages/core/src/__tests__/lifecycle-manager.test.ts index 3e7052710..9f19cf47d 100644 --- a/packages/core/src/__tests__/lifecycle-manager.test.ts +++ b/packages/core/src/__tests__/lifecycle-manager.test.ts @@ -542,6 +542,27 @@ describe("check (single session)", () => { expect(meta?.["lifecycleEvidence"]).toContain("activity_signal=probe_failure"); }); + it("degrades stuck probe-failure sessions to detecting when runtime is alive but activity is unavailable", async () => { + const registryWithoutAgent: PluginRegistry = { + ...mockRegistry, + get: vi.fn().mockImplementation((slot: string) => { + if (slot === "runtime") return plugins.runtime; + return null; + }), + }; + + const lm = setupCheck("app-1", { + session: makeSession({ status: "stuck" }), + registry: registryWithoutAgent, + }); + + await lm.check("app-1"); + + expect(lm.getStates().get("app-1")).toBe("detecting"); + const meta = readMetadataRaw(env.sessionsDir, "app-1"); + expect(meta?.["lifecycleEvidence"]).toContain("activity_signal=unavailable"); + }); + it("detects needs_input from agent", async () => { vi.mocked(plugins.agent.getActivityState).mockResolvedValue({ state: "waiting_input" }); diff --git a/packages/core/src/lifecycle-manager.ts b/packages/core/src/lifecycle-manager.ts index b23fb66c4..51eb4f2c6 100644 --- a/packages/core/src/lifecycle-manager.ts +++ b/packages/core/src/lifecycle-manager.ts @@ -909,6 +909,17 @@ export function createLifecycleManager(deps: LifecycleManagerDeps): LifecycleMan lifecycle.session.state === "stuck" || lifecycle.session.state === "needs_input") ) { + const preservingProbeFailureStuck = + activitySignal.state === "unavailable" && + lifecycle.session.state === "stuck" && + lifecycle.session.reason === "probe_failure" && + lifecycle.runtime.state === "alive"; + + if (preservingProbeFailureStuck) { + setSessionState("detecting", "probe_failure"); + return commit(SESSION_STATUS.DETECTING, activityEvidence, 0); + } + return commit(deriveLegacyStatus(lifecycle, session.status), activityEvidence, 0); }