mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 05:50:43 +00:00
committed by
GitHub
parent
8d6db59cf7
commit
dd83f72a7f
@@ -73,6 +73,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Google Meet: keep explicit non-Google `realtime.provider` values as the transcription provider compatibility fallback when `realtime.transcriptionProvider` is unset. Thanks @vincentkoc.
|
||||
- Google Meet: make Twilio setup status require an enabled `voice-call` plugin entry instead of treating a missing entry as ready. Thanks @vincentkoc.
|
||||
- Telegram: render shared interactive reply buttons in reply delivery so plugin approval messages show inline keyboards. (#76238) Thanks @keshavbotagent.
|
||||
- Cron/sessions: keep cron metadata rows without an on-disk transcript non-resumable until a transcript exists, so doctor and `sessions cleanup --fix-missing` no longer report or prune pre-transcript cron rows as broken sessions. Refs #77011.
|
||||
- Agents/cli-runner: drop a saved `claude-cli` resume sessionId at preparation time when its on-disk transcript no longer exists in `~/.claude/projects/`, so a stale binding from a half-installed `update.run` cannot trap follow-up runs (auto-reply / Telegram direct) in a `claude --resume` timeout loop; the run starts fresh and the new sessionId is written back through the existing post-run flow. (#77030; refs #77011) Thanks @openperf.
|
||||
- Release validation: install the cross-OS TypeScript harness through Windows-safe Node/npm shims so native Windows package checks reach the OpenClaw smoke suites instead of exiting before artifact capture. Thanks @vincentkoc.
|
||||
- Release validation: let Windows packaged-upgrade checks continue after the shipped 2026.5.2 updater hits its native-module swap cleanup fallback, verifying the fallback-installed candidate through package metadata and downstream smoke instead of crashing on the immediate update-status probe. Thanks @vincentkoc.
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
import crypto from "node:crypto";
|
||||
import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import type { SessionEntry } from "../../config/sessions.js";
|
||||
import { createPersistCronSessionEntry, type MutableCronSession } from "./run-session-state.js";
|
||||
@@ -26,6 +30,7 @@ describe("createPersistCronSessionEntry", () => {
|
||||
it("persists isolated cron state only under the stable cron session key", async () => {
|
||||
const cronSession = makeCronSession(
|
||||
makeSessionEntry({
|
||||
sessionFile: await createTranscriptFile(),
|
||||
status: "running",
|
||||
startedAt: 900,
|
||||
skillsSnapshot: {
|
||||
@@ -56,6 +61,81 @@ describe("createPersistCronSessionEntry", () => {
|
||||
expect(cronSession.store["agent:main:cron:job:run:run-session-id"]).toBeUndefined();
|
||||
});
|
||||
|
||||
it("does not register cron sessions as resumable until the transcript exists", async () => {
|
||||
const missingTranscriptPath = path.join(
|
||||
os.tmpdir(),
|
||||
`openclaw-missing-cron-${crypto.randomUUID()}.jsonl`,
|
||||
);
|
||||
const cronSession = makeCronSession(
|
||||
makeSessionEntry({
|
||||
sessionFile: missingTranscriptPath,
|
||||
label: "Cron: shell-only",
|
||||
status: "running",
|
||||
}),
|
||||
);
|
||||
const updateSessionStore = vi.fn(
|
||||
async (_storePath, update: (store: Record<string, SessionEntry>) => void) => {
|
||||
const store: Record<string, SessionEntry> = {};
|
||||
update(store);
|
||||
expect(store["agent:main:cron:shell-only"]).toEqual(
|
||||
expect.objectContaining({
|
||||
label: "Cron: shell-only",
|
||||
status: "running",
|
||||
updatedAt: 1000,
|
||||
}),
|
||||
);
|
||||
expect(store["agent:main:cron:shell-only"]?.sessionId).toBeUndefined();
|
||||
expect(store["agent:main:cron:shell-only"]?.sessionFile).toBeUndefined();
|
||||
},
|
||||
);
|
||||
|
||||
const persist = createPersistCronSessionEntry({
|
||||
isFastTestEnv: false,
|
||||
cronSession,
|
||||
agentSessionKey: "agent:main:cron:shell-only",
|
||||
updateSessionStore,
|
||||
});
|
||||
|
||||
await persist();
|
||||
|
||||
expect(cronSession.store["agent:main:cron:shell-only"]?.sessionId).toBeUndefined();
|
||||
expect(cronSession.store["agent:main:cron:shell-only"]?.sessionFile).toBeUndefined();
|
||||
});
|
||||
|
||||
it("restores resumable cron fields once the transcript exists", async () => {
|
||||
const transcriptPath = await createTranscriptFile();
|
||||
const cronSession = makeCronSession(
|
||||
makeSessionEntry({
|
||||
sessionFile: transcriptPath,
|
||||
label: "Cron: completed",
|
||||
}),
|
||||
);
|
||||
|
||||
const persist = createPersistCronSessionEntry({
|
||||
isFastTestEnv: false,
|
||||
cronSession,
|
||||
agentSessionKey: "agent:main:cron:completed",
|
||||
updateSessionStore: vi.fn(
|
||||
async (_storePath, update: (store: Record<string, SessionEntry>) => void) => {
|
||||
const store: Record<string, SessionEntry> = {};
|
||||
update(store);
|
||||
expect(store["agent:main:cron:completed"]).toMatchObject({
|
||||
sessionId: "run-session-id",
|
||||
sessionFile: transcriptPath,
|
||||
label: "Cron: completed",
|
||||
});
|
||||
},
|
||||
),
|
||||
});
|
||||
|
||||
await persist();
|
||||
|
||||
expect(cronSession.store["agent:main:cron:completed"]).toMatchObject({
|
||||
sessionId: "run-session-id",
|
||||
sessionFile: transcriptPath,
|
||||
});
|
||||
});
|
||||
|
||||
it("persists explicit session-bound cron state under the requested session key", async () => {
|
||||
const cronSession = makeCronSession();
|
||||
const updateSessionStore = vi.fn(
|
||||
@@ -78,3 +158,10 @@ describe("createPersistCronSessionEntry", () => {
|
||||
expect(cronSession.store["agent:main:session"]).toBe(cronSession.sessionEntry);
|
||||
});
|
||||
});
|
||||
|
||||
async function createTranscriptFile(): Promise<string> {
|
||||
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-cron-session-"));
|
||||
const file = path.join(dir, "session.jsonl");
|
||||
await fs.writeFile(file, `${JSON.stringify({ type: "session", sessionId: "run-session-id" })}\n`);
|
||||
return file;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import fs from "node:fs";
|
||||
import type { LiveSessionModelSelection } from "../../agents/live-model-switch.js";
|
||||
import type { SkillSnapshot } from "../../agents/skills.js";
|
||||
import type { SessionEntry } from "../../config/sessions.js";
|
||||
import { isCronSessionKey } from "../../sessions/session-key-utils.js";
|
||||
import type { resolveCronSession } from "./session.js";
|
||||
|
||||
type MutableSessionStore = Record<string, SessionEntry>;
|
||||
@@ -19,6 +21,23 @@ type UpdateSessionStore = (
|
||||
|
||||
export type PersistCronSessionEntry = () => Promise<void>;
|
||||
|
||||
function cronTranscriptExists(entry: SessionEntry): boolean {
|
||||
const sessionFile = entry.sessionFile?.trim();
|
||||
return Boolean(sessionFile && fs.existsSync(sessionFile));
|
||||
}
|
||||
|
||||
function toNonResumableCronSessionEntry(entry: SessionEntry): SessionEntry {
|
||||
const next = { ...entry } as Partial<SessionEntry>;
|
||||
delete next.sessionId;
|
||||
delete next.sessionFile;
|
||||
delete next.sessionStartedAt;
|
||||
delete next.lastInteractionAt;
|
||||
delete next.cliSessionIds;
|
||||
delete next.cliSessionBindings;
|
||||
delete next.claudeCliSessionId;
|
||||
return next as SessionEntry;
|
||||
}
|
||||
|
||||
export function createPersistCronSessionEntry(params: {
|
||||
isFastTestEnv: boolean;
|
||||
cronSession: MutableCronSession;
|
||||
@@ -29,9 +48,15 @@ export function createPersistCronSessionEntry(params: {
|
||||
if (params.isFastTestEnv) {
|
||||
return;
|
||||
}
|
||||
params.cronSession.store[params.agentSessionKey] = params.cronSession.sessionEntry;
|
||||
const persistedEntry =
|
||||
isCronSessionKey(params.agentSessionKey) &&
|
||||
params.cronSession.sessionEntry.sessionId &&
|
||||
!cronTranscriptExists(params.cronSession.sessionEntry)
|
||||
? toNonResumableCronSessionEntry(params.cronSession.sessionEntry)
|
||||
: params.cronSession.sessionEntry;
|
||||
params.cronSession.store[params.agentSessionKey] = persistedEntry;
|
||||
await params.updateSessionStore(params.cronSession.storePath, (store) => {
|
||||
store[params.agentSessionKey] = params.cronSession.sessionEntry;
|
||||
store[params.agentSessionKey] = persistedEntry;
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user