fix: preserve session_end reasons

This commit is contained in:
Josh Lehman
2026-04-02 15:55:13 -07:00
parent 628eb1e825
commit b3ef62b973
3 changed files with 74 additions and 11 deletions

View File

@@ -31,6 +31,7 @@ Docs: https://docs.openclaw.ai
- Plugins/runtime: reuse compatible active registries for `web_search` and `web_fetch` provider snapshot resolution so repeated runtime reads do not re-import the same bundled plugin set on each agent message. Related #48380.
- Infra/tailscale: ignore `OPENCLAW_TEST_TAILSCALE_BINARY` outside explicit test environments and block it from workspace `.env`, so test-only binary overrides cannot be injected through trusted repository state. (#58468) Thanks @eleqtrizit.
- Agents/tool policy: preserve restrictive plugin-only allowlists instead of silently widening access to core tools, and keep allowlist warnings aligned with the enforced policy. (#58476) Thanks @eleqtrizit.
- Hooks/session_end: preserve deterministic reason metadata for custom reset aliases and overlapping idle-plus-daily rollovers so plugins can rely on lifecycle reason reporting. (#59715) Thanks @jalehman.
## 2026.4.2

View File

@@ -150,6 +150,34 @@ describe("session hook context wiring", () => {
expect(event).toMatchObject({ reason: "reset" });
});
it("maps custom reset trigger aliases to the new-session reason", async () => {
const sessionKey = "agent:main:telegram:direct:alias";
const storePath = await createStorePath("openclaw-session-hook-reset-alias");
const transcriptPath = await writeTranscript(storePath, "alias-session", "alias me");
await writeStore(storePath, {
[sessionKey]: {
sessionId: "alias-session",
sessionFile: transcriptPath,
updatedAt: Date.now(),
},
});
const cfg = {
session: {
store: storePath,
resetTriggers: ["/fresh"],
},
} as OpenClawConfig;
await initSessionState({
ctx: { Body: "/fresh", SessionKey: sessionKey },
cfg,
commandAuthorized: true,
});
const [event] = hookRunnerMocks.runSessionEnd.mock.calls[0] ?? [];
expect(event).toMatchObject({ reason: "new" });
});
it("marks daily stale rollovers and exposes the archived transcript path", async () => {
vi.useFakeTimers();
try {
@@ -221,4 +249,42 @@ describe("session hook context wiring", () => {
vi.useRealTimers();
}
});
it("prefers idle over daily when both rollover conditions are true", async () => {
vi.useFakeTimers();
try {
vi.setSystemTime(new Date(2026, 0, 18, 5, 30, 0));
const sessionKey = "agent:main:telegram:direct:overlap";
const storePath = await createStorePath("openclaw-session-hook-overlap");
const transcriptPath = await writeTranscript(storePath, "overlap-session", "overlap");
await writeStore(storePath, {
[sessionKey]: {
sessionId: "overlap-session",
sessionFile: transcriptPath,
updatedAt: new Date(2026, 0, 18, 4, 45, 0).getTime(),
},
});
const cfg = {
session: {
store: storePath,
reset: {
mode: "daily",
atHour: 4,
idleMinutes: 30,
},
},
} as OpenClawConfig;
await initSessionState({
ctx: { Body: "hello", SessionKey: sessionKey },
cfg,
commandAuthorized: true,
});
const [event] = hookRunnerMocks.runSessionEnd.mock.calls[0] ?? [];
expect(event).toMatchObject({ reason: "idle" });
} finally {
vi.useRealTimers();
}
});
});

View File

@@ -63,14 +63,7 @@ function loadSessionArchiveRuntime() {
function resolveExplicitSessionEndReason(
matchedResetTriggerLower?: string,
): PluginHookSessionEndReason {
switch (matchedResetTriggerLower) {
case "/new":
return "new";
case "/reset":
return "reset";
default:
return "unknown";
}
return matchedResetTriggerLower === "/reset" ? "reset" : "new";
}
function resolveStaleSessionEndReason(params: {
@@ -85,10 +78,13 @@ function resolveStaleSessionEndReason(params: {
params.freshness.dailyResetAt != null && params.entry.updatedAt < params.freshness.dailyResetAt;
const staleIdle =
params.freshness.idleExpiresAt != null && params.now > params.freshness.idleExpiresAt;
if (staleDaily === staleIdle) {
return staleDaily ? "unknown" : undefined;
if (staleIdle) {
return "idle";
}
return staleDaily ? "daily" : "idle";
if (staleDaily) {
return "daily";
}
return undefined;
}
export type SessionInitResult = {