Implement task-scoped sessions, queued run chaining, and session reset API

Heartbeat service now resolves session state per-task using agentTaskSessions,
with resolveNextSessionState handling codec-based serialization and fallback
to legacy sessionId. Queued runs are chained — when a run finishes or is reaped,
the next queued run for the same agent starts automatically. Queued runs for
an agent with an already-running run wait instead of failing.

Add task-sessions list endpoint and extend reset-session to accept optional
taskKey for targeted session clearing. Block pending_approval agents from
API key auth. Update agent/company delete cascades to include task sessions.
Update spec docs with task-session architecture.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Forgotten
2026-02-19 14:02:17 -06:00
parent d56618e9fe
commit 2acf28a51a
6 changed files with 467 additions and 46 deletions

View File

@@ -6,6 +6,7 @@ import {
createAgentKeySchema,
createAgentHireSchema,
createAgentSchema,
resetAgentSessionSchema,
updateAgentPermissionsSchema,
wakeAgentSchema,
updateAgentSchema,
@@ -357,7 +358,7 @@ export function agentRoutes(db: Db) {
res.json(state);
});
router.post("/agents/:id/runtime-state/reset-session", async (req, res) => {
router.get("/agents/:id/task-sessions", async (req, res) => {
assertBoard(req);
const id = req.params.id as string;
const agent = await svc.getById(id);
@@ -367,7 +368,30 @@ export function agentRoutes(db: Db) {
}
assertCompanyAccess(req, agent.companyId);
const state = await heartbeat.resetRuntimeSession(id);
const sessions = await heartbeat.listTaskSessions(id);
res.json(
sessions.map((session) => ({
...session,
sessionParamsJson: redactEventPayload(session.sessionParamsJson ?? null),
})),
);
});
router.post("/agents/:id/runtime-state/reset-session", validate(resetAgentSessionSchema), async (req, res) => {
assertBoard(req);
const id = req.params.id as string;
const agent = await svc.getById(id);
if (!agent) {
res.status(404).json({ error: "Agent not found" });
return;
}
assertCompanyAccess(req, agent.companyId);
const taskKey =
typeof req.body.taskKey === "string" && req.body.taskKey.trim().length > 0
? req.body.taskKey.trim()
: null;
const state = await heartbeat.resetRuntimeSession(id, { taskKey });
await logActivity(db, {
companyId: agent.companyId,
@@ -376,6 +400,7 @@ export function agentRoutes(db: Db) {
action: "agent.runtime_session_reset",
entityType: "agent",
entityId: id,
details: { taskKey: taskKey ?? null },
});
res.json(state);