fix: start fresh cron sessions each run
This commit is contained in:
@@ -29,6 +29,7 @@
|
|||||||
- UI: show gateway auth guidance + doc link on unauthorized Control UI connections.
|
- UI: show gateway auth guidance + doc link on unauthorized Control UI connections.
|
||||||
- Security: warn on weak model tiers (Haiku, below GPT-5, below Claude 4.5) in `clawdbot security audit`.
|
- Security: warn on weak model tiers (Haiku, below GPT-5, below Claude 4.5) in `clawdbot security audit`.
|
||||||
- Apps: store node auth tokens encrypted (Keychain/SecurePrefs).
|
- Apps: store node auth tokens encrypted (Keychain/SecurePrefs).
|
||||||
|
- Cron: isolated cron jobs now start a fresh session id on every run to prevent context buildup.
|
||||||
- Daemon: share profile/state-dir resolution across service helpers and honor `CLAWDBOT_STATE_DIR` for Windows task scripts.
|
- Daemon: share profile/state-dir resolution across service helpers and honor `CLAWDBOT_STATE_DIR` for Windows task scripts.
|
||||||
- Docs: clarify multi-gateway rescue bot guidance. (#969) — thanks @bjesuiter.
|
- Docs: clarify multi-gateway rescue bot guidance. (#969) — thanks @bjesuiter.
|
||||||
- Agents: add Current Date & Time system prompt section with configurable time format (auto/12/24).
|
- Agents: add Current Date & Time system prompt section with configurable time format (auto/12/24).
|
||||||
|
|||||||
@@ -78,6 +78,7 @@ Isolated jobs run a dedicated agent turn in session `cron:<jobId>`.
|
|||||||
|
|
||||||
Key behaviors:
|
Key behaviors:
|
||||||
- Prompt is prefixed with `[cron:<jobId> <job name>]` for traceability.
|
- Prompt is prefixed with `[cron:<jobId> <job name>]` for traceability.
|
||||||
|
- Each run starts a **fresh session id** (no prior conversation carry-over).
|
||||||
- A summary is posted to the main session (prefix `Cron`, configurable).
|
- A summary is posted to the main session (prefix `Cron`, configurable).
|
||||||
- `wakeMode: "now"` triggers an immediate heartbeat after posting the summary.
|
- `wakeMode: "now"` triggers an immediate heartbeat after posting the summary.
|
||||||
- If `payload.deliver: true`, output is delivered to a channel; otherwise it stays internal.
|
- If `payload.deliver: true`, output is delivered to a channel; otherwise it stays internal.
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ the workspace is writable. See [Memory](/concepts/memory) and
|
|||||||
- Idle expiry: `session.idleMinutes` (default 60). After the timeout a new `sessionId` is minted on the next message.
|
- Idle expiry: `session.idleMinutes` (default 60). After the timeout a new `sessionId` is minted on the next message.
|
||||||
- Reset triggers: exact `/new` or `/reset` (plus any extras in `resetTriggers`) start a fresh session id and pass the remainder of the message through. If `/new` or `/reset` is sent alone, Clawdbot runs a short “hello” greeting turn to confirm the reset.
|
- Reset triggers: exact `/new` or `/reset` (plus any extras in `resetTriggers`) start a fresh session id and pass the remainder of the message through. If `/new` or `/reset` is sent alone, Clawdbot runs a short “hello” greeting turn to confirm the reset.
|
||||||
- Manual reset: delete specific keys from the store or remove the JSONL transcript; the next message recreates them.
|
- Manual reset: delete specific keys from the store or remove the JSONL transcript; the next message recreates them.
|
||||||
|
- Isolated cron jobs always mint a fresh `sessionId` per run (no idle reuse).
|
||||||
|
|
||||||
## Send policy (optional)
|
## Send policy (optional)
|
||||||
Block delivery for specific session types without listing individual ids.
|
Block delivery for specific session types without listing individual ids.
|
||||||
|
|||||||
@@ -48,6 +48,12 @@ async function writeSessionStore(home: string) {
|
|||||||
return storePath;
|
return storePath;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function readSessionEntry(storePath: string, key: string) {
|
||||||
|
const raw = await fs.readFile(storePath, "utf-8");
|
||||||
|
const store = JSON.parse(raw) as Record<string, { sessionId?: string }>;
|
||||||
|
return store[key];
|
||||||
|
}
|
||||||
|
|
||||||
function makeCfg(
|
function makeCfg(
|
||||||
home: string,
|
home: string,
|
||||||
storePath: string,
|
storePath: string,
|
||||||
@@ -466,4 +472,51 @@ describe("runCronIsolatedAgentTurn", () => {
|
|||||||
expect(deps.sendMessageWhatsApp).not.toHaveBeenCalled();
|
expect(deps.sendMessageWhatsApp).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("starts a fresh session id for each cron run", async () => {
|
||||||
|
await withTempHome(async (home) => {
|
||||||
|
const storePath = await writeSessionStore(home);
|
||||||
|
const deps: CliDeps = {
|
||||||
|
sendMessageWhatsApp: vi.fn(),
|
||||||
|
sendMessageTelegram: vi.fn(),
|
||||||
|
sendMessageDiscord: vi.fn(),
|
||||||
|
sendMessageSignal: vi.fn(),
|
||||||
|
sendMessageIMessage: vi.fn(),
|
||||||
|
};
|
||||||
|
vi.mocked(runEmbeddedPiAgent).mockResolvedValue({
|
||||||
|
payloads: [{ text: "ok" }],
|
||||||
|
meta: {
|
||||||
|
durationMs: 5,
|
||||||
|
agentMeta: { sessionId: "s", provider: "p", model: "m" },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const cfg = makeCfg(home, storePath);
|
||||||
|
const job = makeJob({ kind: "agentTurn", message: "ping", deliver: false });
|
||||||
|
|
||||||
|
await runCronIsolatedAgentTurn({
|
||||||
|
cfg,
|
||||||
|
deps,
|
||||||
|
job,
|
||||||
|
message: "ping",
|
||||||
|
sessionKey: "cron:job-1",
|
||||||
|
lane: "cron",
|
||||||
|
});
|
||||||
|
const first = await readSessionEntry(storePath, "agent:main:cron:job-1");
|
||||||
|
|
||||||
|
await runCronIsolatedAgentTurn({
|
||||||
|
cfg,
|
||||||
|
deps,
|
||||||
|
job,
|
||||||
|
message: "ping",
|
||||||
|
sessionKey: "cron:job-1",
|
||||||
|
lane: "cron",
|
||||||
|
});
|
||||||
|
const second = await readSessionEntry(storePath, "agent:main:cron:job-1");
|
||||||
|
|
||||||
|
expect(first?.sessionId).toBeDefined();
|
||||||
|
expect(second?.sessionId).toBeDefined();
|
||||||
|
expect(second?.sessionId).not.toBe(first?.sessionId);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -206,8 +206,10 @@ export async function runCronIsolatedAgentTurn(params: {
|
|||||||
const base = `[cron:${params.job.id} ${params.job.name}] ${params.message}`.trim();
|
const base = `[cron:${params.job.id} ${params.job.name}] ${params.message}`.trim();
|
||||||
const commandBody = base;
|
const commandBody = base;
|
||||||
|
|
||||||
const needsSkillsSnapshot = cronSession.isNewSession || !cronSession.sessionEntry.skillsSnapshot;
|
const existingSnapshot = cronSession.sessionEntry.skillsSnapshot;
|
||||||
const skillsSnapshotVersion = getSkillsSnapshotVersion(workspaceDir);
|
const skillsSnapshotVersion = getSkillsSnapshotVersion(workspaceDir);
|
||||||
|
const needsSkillsSnapshot =
|
||||||
|
!existingSnapshot || existingSnapshot.version !== skillsSnapshotVersion;
|
||||||
const skillsSnapshot = needsSkillsSnapshot
|
const skillsSnapshot = needsSkillsSnapshot
|
||||||
? buildWorkspaceSkillSnapshot(workspaceDir, {
|
? buildWorkspaceSkillSnapshot(workspaceDir, {
|
||||||
config: cfgWithAgentDefaults,
|
config: cfgWithAgentDefaults,
|
||||||
|
|||||||
@@ -1,12 +1,7 @@
|
|||||||
import crypto from "node:crypto";
|
import crypto from "node:crypto";
|
||||||
|
|
||||||
import type { ClawdbotConfig } from "../../config/config.js";
|
import type { ClawdbotConfig } from "../../config/config.js";
|
||||||
import {
|
import { loadSessionStore, resolveStorePath, type SessionEntry } from "../../config/sessions.js";
|
||||||
DEFAULT_IDLE_MINUTES,
|
|
||||||
loadSessionStore,
|
|
||||||
resolveStorePath,
|
|
||||||
type SessionEntry,
|
|
||||||
} from "../../config/sessions.js";
|
|
||||||
|
|
||||||
export function resolveCronSession(params: {
|
export function resolveCronSession(params: {
|
||||||
cfg: ClawdbotConfig;
|
cfg: ClawdbotConfig;
|
||||||
@@ -15,16 +10,13 @@ export function resolveCronSession(params: {
|
|||||||
agentId: string;
|
agentId: string;
|
||||||
}) {
|
}) {
|
||||||
const sessionCfg = params.cfg.session;
|
const sessionCfg = params.cfg.session;
|
||||||
const idleMinutes = Math.max(sessionCfg?.idleMinutes ?? DEFAULT_IDLE_MINUTES, 1);
|
|
||||||
const idleMs = idleMinutes * 60_000;
|
|
||||||
const storePath = resolveStorePath(sessionCfg?.store, {
|
const storePath = resolveStorePath(sessionCfg?.store, {
|
||||||
agentId: params.agentId,
|
agentId: params.agentId,
|
||||||
});
|
});
|
||||||
const store = loadSessionStore(storePath);
|
const store = loadSessionStore(storePath);
|
||||||
const entry = store[params.sessionKey];
|
const entry = store[params.sessionKey];
|
||||||
const fresh = entry && params.nowMs - entry.updatedAt <= idleMs;
|
const sessionId = crypto.randomUUID();
|
||||||
const sessionId = fresh ? entry.sessionId : crypto.randomUUID();
|
const systemSent = false;
|
||||||
const systemSent = fresh ? Boolean(entry.systemSent) : false;
|
|
||||||
const sessionEntry: SessionEntry = {
|
const sessionEntry: SessionEntry = {
|
||||||
sessionId,
|
sessionId,
|
||||||
updatedAt: params.nowMs,
|
updatedAt: params.nowMs,
|
||||||
@@ -36,6 +28,8 @@ export function resolveCronSession(params: {
|
|||||||
sendPolicy: entry?.sendPolicy,
|
sendPolicy: entry?.sendPolicy,
|
||||||
lastChannel: entry?.lastChannel,
|
lastChannel: entry?.lastChannel,
|
||||||
lastTo: entry?.lastTo,
|
lastTo: entry?.lastTo,
|
||||||
|
lastAccountId: entry?.lastAccountId,
|
||||||
|
skillsSnapshot: entry?.skillsSnapshot,
|
||||||
};
|
};
|
||||||
return { storePath, store, sessionEntry, systemSent, isNewSession: !fresh };
|
return { storePath, store, sessionEntry, systemSent, isNewSession: true };
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user