refactor(session): scope pi sessions to agents instead of tasks
Sessions were keyed by (task, agent). Every task switch started a fresh
transcript — the agent lost everything it had learned on the previous
task. That breaks the product premise: a CEO should remember its past
decisions, an engineer should carry context between bugs.
Move pi's AgentMessage transcript to be agent-scoped. One session per
agent (per company). Tasks enter the ongoing conversation as user turns;
rotation/compaction boundaries follow the agent's long-running session.
Additive schema only — no row rewrites on the live production DB.
- Migration v34: `ALTER TABLE agents ADD COLUMN session_id TEXT` +
`idx_agents_session_id`. `ensureOptionalColumns` mirror for fresh DBs.
- `AgentRecord.sessionId?: string | null` in the read model; included
in every full-agent SELECT (entity-crud, snapshot, agents-hierarchy
chain-of-command / direct-reports / heartbeat-eligible).
- `db.ensureAgentSession({ agentId, companyId, adapterType })` —
idempotent: returns existing `agents.session_id` if set, else inserts
a new `task_sessions` row with an empty `task_id` sentinel (agent-
scoped, task-id is now metadata) and stores the id on the agent.
- `db.setAgentSessionId(agentId, sessionId)` — used by rotation.
- `db.updateTasksActiveSessionForAgent(agentId, sessionId)` — mirrors
the agent's current session onto every task the agent owns, so
existing per-task readers (`tasks.active_session_id`) still work.
- `turn-context.resolveSession`: pulls the agent's session via
`ensureAgentSession` instead of `getActiveTaskSession(task.id)`,
then points the current task at it so UI continues to resolve.
- `session-management-service.rotateSession`: new session carries
`taskId:""`, then `setAgentSessionId` + `updateTasksActiveSessionForAgent`
so every task the agent owns follows the rotation boundary.
- `session-entries.listActive`: `ORDER BY created_at, sequence` — the
created_at timestamp is authoritative; sequence stays as same-ms
tiebreaker. Forward-safety for future merge scenarios.
Existing agents' first post-upgrade run creates a fresh agent-scoped
session. Their pre-migration per-task transcripts remain in the DB as
historical records but are not loaded for new runs. This is a one-time
transcript reset, accepted over a risky lossy merge on the live DB.
Documented in a code comment on `ensureAgentSession`.
This commit is contained in:
1 parent
5a3647e98e
commit
92795f7bfb
10 files changed
+127
-16
No files matched your search
@@ -440,6 +440,71 @@ export function buildTaskCrudMethods(
|
||||
.run(ts, reason, handoffMarkdown, ts, sessionId);
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns the agent's active session id, creating one lazily on first
|
||||
* call. Session is scoped to the agent (not to any particular task);
|
||||
* switching tasks emits a user turn on the ongoing transcript rather
|
||||
* than spawning a new session.
|
||||
*
|
||||
* Existing agents with pre-migration per-task sessions start fresh
|
||||
* after this upgrade: their first run creates a new agent-scoped
|
||||
* session. The old task_sessions + session_entries rows remain in the
|
||||
* DB as historical records but are not loaded for new runs. This is a
|
||||
* one-time transcript reset, accepted over a risky merge on a live
|
||||
* production DB.
|
||||
*/
|
||||
ensureAgentSession(input: {
|
||||
agentId: string;
|
||||
companyId: string;
|
||||
adapterType: string;
|
||||
}): string {
|
||||
const agentRow = getDb()
|
||||
.prepare("SELECT session_id FROM agents WHERE id = ?")
|
||||
.get(input.agentId) as { session_id: string | null } | undefined;
|
||||
if (agentRow?.session_id) return agentRow.session_id;
|
||||
|
||||
const sessionId = randomUUID();
|
||||
const ts = nowIso();
|
||||
transaction(() => {
|
||||
// task_id is left as empty string — the session is agent-scoped;
|
||||
// the column persists for backwards-compatible schema but no
|
||||
// longer keys the session to a particular task.
|
||||
getDb()
|
||||
.prepare(
|
||||
`INSERT INTO task_sessions (id, company_id, task_id, agent_id, adapter_type,
|
||||
session_params_json, session_display_id, run_count, total_input_tokens,
|
||||
total_output_tokens, created_at, updated_at)
|
||||
VALUES (?, ?, '', ?, ?, NULL, NULL, 0, 0, 0, ?, ?)`,
|
||||
)
|
||||
.run(sessionId, input.companyId, input.agentId, input.adapterType, ts, ts);
|
||||
getDb()
|
||||
.prepare("UPDATE agents SET session_id = ?, updated_at = ? WHERE id = ?")
|
||||
.run(sessionId, ts, input.agentId);
|
||||
});
|
||||
return sessionId;
|
||||
},
|
||||
|
||||
setAgentSessionId(agentId: string, sessionId: string): void {
|
||||
const ts = nowIso();
|
||||
getDb()
|
||||
.prepare("UPDATE agents SET session_id = ?, updated_at = ? WHERE id = ?")
|
||||
.run(sessionId, ts, agentId);
|
||||
},
|
||||
|
||||
/**
|
||||
* Point every task currently assigned to an agent at the agent's new
|
||||
* active session. Called after session rotation so UI paths reading
|
||||
* per-task active_session_id still see the agent's current session.
|
||||
*/
|
||||
updateTasksActiveSessionForAgent(agentId: string, sessionId: string): void {
|
||||
const ts = nowIso();
|
||||
getDb()
|
||||
.prepare(
|
||||
"UPDATE tasks SET active_session_id = ?, updated_at = ? WHERE assignee_agent_id = ?",
|
||||
)
|
||||
.run(sessionId, ts, agentId);
|
||||
},
|
||||
|
||||
createNewSession(input: {
|
||||
taskId: string;
|
||||
companyId: string;
|
||||
|
||||
Reference in new issue
Block a user