import { t as tasksSessions, a6 as tasksTaskWorkHistory } from './conduit-Ca_OFWR_.js';
import { a as getDb } from './sqlite-Dsbe0wec.js';
import 'node:crypto';
import { AsyncLocalStorage } from 'node:async_hooks';
import { e as eq, d as desc, a as and, c as isNull } from './logger-ChCwlV5k.js';

//#region ../core/dist/sessions/session-id.js
/**
* Ordered list of session-id environment variables consulted by the canonical
* env-first resolver. First non-empty value wins.
*
* Order (T11347-unified):
* 1. `CLEO_SESSION_ID`   — canonical; set by spawn isolation (T11343),
*    `cleo session start`, and `cleo session adopt`.
* 2. `CLEO_SESSION`      — legacy alias (single legacy reader); kept for one
*    deprecation cycle so older shells keep resolving.
* 3. `CLAUDE_SESSION_ID` — injected by the Claude Code harness.
* 4. `AIDER_SESSION_ID`  — injected by the Aider harness.
*
* @task T9975
* @task T11347
*/
var SESSION_ENV_KEY_PRECEDENCE = [
	"CLEO_SESSION_ID",
	"CLEO_SESSION",
	"CLAUDE_SESSION_ID",
	"AIDER_SESSION_ID"
];
/**
* Resolve the active session ID via environment-variable precedence (T9975).
*
* THE single canonical env-first resolver (T11344, Epic T11284). Every
* session-resolution hot path — `lookupCliSession`, the session-resolver
* middleware, the audit middleware — consults THIS function so the precedence
* logic is never duplicated. It returns the per-agent session id injected into
* the isolation shell by spawn (T11343), making `getActiveSession()` a fallback
* rather than the default identity. This is what dissolves multi-agent
* session-bleed: a short-lived `cleo` call inside a worktree resolves the
* agent's OWN session, not "whoever touched the DB last".
*
* Precedence is defined by {@link SESSION_ENV_KEY_PRECEDENCE}. Empty-string
* values are treated as absent (spawn sets `''` when no per-agent session was
* allocated), so an empty env var transparently falls through to the next key
* and ultimately to the caller's `getActiveSession()` fallback.
*
* This function NEVER reads the database — it is synchronous and safe to call
* in hot paths. Database fallback is the caller's responsibility.
*
* @returns The session ID string when an env var is set, otherwise `null`.
*
* @task T9975
* @task T11344
*/
function resolveSessionIdFromEnv() {
	for (const key of SESSION_ENV_KEY_PRECEDENCE) {
		const value = process.env[key];
		if (value !== void 0 && value !== "") return value;
	}
	return null;
}

//#region ../core/dist/store/parsers.js
/**
* Shared JSON parsing helpers for SQLite store modules.
*
* Eliminates duplication across sqlite-data-accessor.ts, tasks-sqlite.ts,
* and session-store.ts.
*/
/**
* Parse a JSON string, returning undefined on null/undefined input or parse error.
*/
function safeParseJson(str) {
	if (!str) return void 0;
	try {
		return JSON.parse(str);
	} catch {
		return;
	}
}
/**
* Parse a JSON string expected to contain an array.
* Returns undefined for null/undefined input, empty arrays, or parse errors.
*/
function safeParseJsonArray(str) {
	if (!str) return void 0;
	try {
		const arr = JSON.parse(str);
		if (Array.isArray(arr) && arr.length === 0) return void 0;
		return arr;
	} catch {
		return;
	}
}
//#endregion
//#region ../core/dist/store/converters.js
/**
* Shared row <-> domain conversion functions for SQLite store modules.
*
* Eliminates duplication across sqlite-data-accessor.ts, tasks-sqlite.ts,
* and session-store.ts.
*/
/** Convert a database TaskRow to a domain Task object. */
function rowToTask(row) {
	return {
		id: row.id,
		title: row.title,
		status: row.status,
		priority: row.priority,
		type: row.type ?? void 0,
		parentId: row.parentId ?? void 0,
		phase: row.phase ?? void 0,
		size: row.size ?? void 0,
		position: row.position ?? void 0,
		positionVersion: row.positionVersion ?? void 0,
		description: row.description ?? "",
		labels: safeParseJsonArray(row.labelsJson),
		notes: safeParseJsonArray(row.notesJson),
		acceptance: safeParseJsonArray(row.acceptanceJson),
		files: safeParseJsonArray(row.filesJson),
		depends: void 0,
		origin: row.origin ?? void 0,
		blockedBy: row.blockedBy ?? void 0,
		epicLifecycle: row.epicLifecycle ?? void 0,
		noAutoComplete: row.noAutoComplete ?? void 0,
		createdAt: row.createdAt,
		updatedAt: row.updatedAt ?? void 0,
		completedAt: row.completedAt ?? void 0,
		cancelledAt: row.cancelledAt ?? void 0,
		cancellationReason: row.cancellationReason ?? void 0,
		verification: row.verificationJson ? safeParseJson(row.verificationJson) : void 0,
		provenance: row.createdBy || row.modifiedBy || row.sessionId ? {
			createdBy: row.createdBy ?? null,
			modifiedBy: row.modifiedBy ?? null,
			sessionId: row.sessionId ?? null
		} : void 0,
		pipelineStage: row.pipelineStage ?? void 0,
		assignee: row.assignee ?? void 0,
		kind: row.kind ?? void 0,
		scope: row.scope ?? void 0,
		severity: row.severity ?? void 0
	};
}
/** Convert a domain Task to a database row for insert/upsert.
*
* T877 invariant: when a task has a terminal status but no explicit
* pipelineStage, derive it so the INSERT satisfies the SQLite trigger
* `trg_tasks_status_pipeline_insert` (status=done requires
* pipeline_stage IN (contribution, cancelled); status=cancelled requires
* pipeline_stage=cancelled). This mirrors the runtime behaviour in
* complete/cancel-ops and means callers never have to think about the
* invariant — the canonical converter enforces it.
*/
function taskToRow(task) {
	const status = task.status ?? "pending";
	let pipelineStage = task.pipelineStage ?? null;
	if (pipelineStage === null || pipelineStage === void 0) {
		if (status === "done") pipelineStage = "contribution";
		else if (status === "cancelled") pipelineStage = "cancelled";
	}
	return {
		id: task.id,
		title: task.title ?? "",
		description: task.description ?? null,
		status,
		priority: task.priority ?? "medium",
		type: task.type ?? null,
		parentId: task.parentId ?? null,
		phase: task.phase ?? null,
		size: task.size ?? null,
		position: task.position ?? null,
		positionVersion: task.positionVersion ?? 0,
		labelsJson: task.labels ? JSON.stringify(task.labels) : "[]",
		notesJson: task.notes ? JSON.stringify(task.notes) : "[]",
		acceptanceJson: task.acceptance ? JSON.stringify(task.acceptance) : "[]",
		filesJson: task.files ? JSON.stringify(task.files) : "[]",
		origin: task.origin ?? null,
		blockedBy: task.blockedBy ?? null,
		epicLifecycle: task.epicLifecycle ?? null,
		noAutoComplete: task.noAutoComplete ?? null,
		createdAt: task.createdAt ?? (/* @__PURE__ */ new Date()).toISOString(),
		updatedAt: task.updatedAt ?? null,
		completedAt: task.completedAt ?? null,
		cancelledAt: task.cancelledAt ?? null,
		cancellationReason: task.cancellationReason ?? null,
		verificationJson: task.verification ? JSON.stringify(task.verification) : null,
		createdBy: task.provenance?.createdBy ?? null,
		modifiedBy: task.provenance?.modifiedBy ?? null,
		sessionId: task.provenance?.sessionId ?? null,
		pipelineStage,
		assignee: task.assignee ?? null,
		kind: task.kind ?? void 0,
		scope: task.scope ?? void 0,
		severity: task.severity ?? void 0
	};
}
/** Convert a domain Task to a row suitable for archived tasks. */
function archivedTaskToRow(task) {
	const row = taskToRow(task);
	row.status = "archived";
	if (!row["archivedAt"]) row["archivedAt"] = task.completedAt ?? (/* @__PURE__ */ new Date()).toISOString();
	return row;
}
/** Convert a SessionRow to a domain Session. */
function rowToSession(row) {
	const taskWork = {
		taskId: row.currentTask ?? null,
		setAt: row.taskStartedAt ?? null
	};
	return {
		id: row.id,
		name: row.name,
		status: row.status,
		scope: safeParseJson(row.scopeJson) ?? { type: "global" },
		taskWork,
		startedAt: row.startedAt,
		endedAt: row.endedAt ?? void 0,
		agent: row.agent ?? void 0,
		notes: safeParseJsonArray(row.notesJson),
		tasksCompleted: safeParseJsonArray(row.tasksCompletedJson),
		tasksCreated: safeParseJsonArray(row.tasksCreatedJson),
		handoffJson: row.handoffJson ?? null,
		previousSessionId: row.previousSessionId ?? null,
		nextSessionId: row.nextSessionId ?? null,
		parentSessionId: row.parentSessionId ?? null,
		agentIdentifier: row.agentIdentifier ?? null,
		handoffConsumedAt: row.handoffConsumedAt ?? null,
		handoffConsumedBy: row.handoffConsumedBy ?? null,
		debriefJson: row.debriefJson ?? null,
		stats: row.statsJson ? safeParseJson(row.statsJson) : void 0,
		resumeCount: row.resumeCount ?? void 0,
		gradeMode: row.gradeMode ? Boolean(row.gradeMode) : void 0,
		agentHandle: row.agentHandle ?? null,
		scopeKind: row.scopeKind ?? null,
		scopeId: row.scopeId ?? null,
		lastActivity: row.lastActivity ?? null
	};
}
//#endregion
//#region ../core/dist/sessions/connection-session-handle.js
/**
* Connection-scoped session handle registry (T11640 · Epic T11638).
*
* The single-process CLI binds exactly one session per process via the dispatch
* `session-context` singleton — but a warm **daemon** serves many concurrent
* clients over one long-lived process, so a module singleton would collapse
* every connection's identity onto "whoever bound last". This module gives the
* daemon a per-connection identity surface WITHOUT changing the dependency
* direction (`runtime` → `core`):
*
*  1. An in-memory `Map<connId, sessionId>` registry the RPC transport writes at
*     **accept-time** (the moment a connection first declares its session) and
*     clears on socket close. Bind/unbind/lookup are O(1) and synchronous.
*  2. An {@link AsyncLocalStorage} "current connection" channel so that, while a
*     request frame is being dispatched, ANY code in `core` (notably
*     {@link resolveCurrentSession}) can ask "which connection am I serving?"
*     and resolve that connection's bound session — with zero parameter
*     threading through the dispatch call graph.
*
* Because this lives in `core`, the identity resolver
* (`store/session-store.ts:resolveCurrentSession`) can consult it directly while
* `runtime` (which depends on `core`) writes to it from the transport boundary.
* The CLI single-process path never binds a connection, so
* {@link getCurrentConnectionSessionId} returns `null` there and resolution
* transparently falls through to the env-first + `getActiveSession()` fallback.
*
* This module is import-time side-effect-free: it constructs only an empty `Map`
* and an `AsyncLocalStorage` instance (both inert), never touching the DB, the
* filesystem, the network, or any logger at import time.
*
* @packageDocumentation
* @module @cleocode/core/sessions/connection-session-handle
*
* @task T11640
* @epic T11638
* @saga T11243
*/
/**
* Process-local `connId → sessionId` map.
*
* Module-private; mutated only through {@link bindConnectionSession} /
* {@link unbindConnectionSession} so the lifecycle stays auditable and the map
* can never leak across the public surface.
*/
var connectionSessionRegistry = /* @__PURE__ */ new Map();
/**
* The async-context channel carrying the {@link ConnectionHandleContext} for the
* request currently being dispatched on a connection.
*/
var connectionHandleStore = new AsyncLocalStorage();
/**
* Look up the session id bound to a specific connection id.
*
* @param connId - The connection id to resolve.
* @returns The bound session id, or `null` when the connection is unbound.
* @task T11640
*/
function getConnectionSessionId(connId) {
	return connectionSessionRegistry.get(connId) ?? null;
}
/**
* Resolve the session id of the connection currently being served, if any.
*
* Reads the {@link AsyncLocalStorage} channel to find the in-flight connection
* handle, then resolves the session in this precedence:
*  1. the handle's per-frame `sessionId` snapshot, when present — authoritative
*     for this dispatch and immune to concurrent re-binds of the same `connId`
*     by sibling frames (the intra-connection bleed fix);
*  2. an on-demand registry lookup by `connId` — so a frame that declared no
*     session of its own still honours a (possibly late) connection binding.
* Returns `null` outside any connection-handle scope (the single-process CLI
* path, or daemon code running outside a request frame), so callers
* transparently fall through to their next resolution tier.
*
* This NEVER reads the database — it is synchronous and safe in hot identity
* paths, mirroring {@link resolveSessionIdFromEnv}.
*
* @returns The current connection's session id, or `null`.
* @task T11640
*/
function getCurrentConnectionSessionId() {
	const ctx = connectionHandleStore.getStore();
	if (!ctx) return null;
	if (ctx.sessionId) return ctx.sessionId;
	return getConnectionSessionId(ctx.connId);
}
//#endregion
//#region ../core/dist/store/session-store.js
/**
* SQLite-backed session store operations.
*
* CRUD operations for sessions and task work tracking backed by tasks.db.
*
* @epic T4454
* @task W1-T4
* @task T1609 insertHandoffEntry — write-once INSERT path for handoff data
*/
/** Create a new session. */
async function createSession(session, cwd) {
	const db = await getDb(cwd);
	const tw = session.taskWork;
	db.insert(tasksSessions).values({
		id: session.id,
		name: session.name,
		status: session.status,
		scopeJson: JSON.stringify(session.scope),
		currentTask: tw?.taskId,
		taskStartedAt: tw?.setAt,
		agent: session.agent,
		notesJson: session.notes ? JSON.stringify(session.notes) : "[]",
		tasksCompletedJson: session.tasksCompleted ? JSON.stringify(session.tasksCompleted) : "[]",
		tasksCreatedJson: session.tasksCreated ? JSON.stringify(session.tasksCreated) : "[]",
		startedAt: session.startedAt,
		endedAt: session.endedAt,
		parentSessionId: session.parentSessionId ?? null
	}).run();
	return session;
}
/** Get a session by ID. */
async function getSession(sessionId, cwd) {
	const rows = await (await getDb(cwd)).select().from(tasksSessions).where(eq(tasksSessions.id, sessionId)).all();
	if (rows.length === 0) return null;
	return rowToSession(rows[0]);
}
/** Update a session. */
async function updateSession(sessionId, updates, cwd) {
	const db = await getDb(cwd);
	if (!await getSession(sessionId, cwd)) return null;
	const updateRow = {};
	if (updates.name !== void 0) updateRow.name = updates.name;
	if (updates.status !== void 0) updateRow.status = updates.status;
	if (updates.scope !== void 0) updateRow.scopeJson = JSON.stringify(updates.scope);
	if (updates.endedAt !== void 0) updateRow.endedAt = updates.endedAt;
	if (updates.agent !== void 0) updateRow.agent = updates.agent;
	if (updates.notes !== void 0) updateRow.notesJson = JSON.stringify(updates.notes);
	if (updates.tasksCompleted !== void 0) updateRow.tasksCompletedJson = JSON.stringify(updates.tasksCompleted);
	if (updates.tasksCreated !== void 0) updateRow.tasksCreatedJson = JSON.stringify(updates.tasksCreated);
	if (updates.previousSessionId !== void 0) updateRow.previousSessionId = updates.previousSessionId;
	if (updates.nextSessionId !== void 0) updateRow.nextSessionId = updates.nextSessionId;
	if (updates.parentSessionId !== void 0) updateRow.parentSessionId = updates.parentSessionId;
	if (updates.agentIdentifier !== void 0) updateRow.agentIdentifier = updates.agentIdentifier;
	if (updates.handoffConsumedAt !== void 0) updateRow.handoffConsumedAt = updates.handoffConsumedAt;
	if (updates.handoffConsumedBy !== void 0) updateRow.handoffConsumedBy = updates.handoffConsumedBy;
	if (updates.debriefJson !== void 0) updateRow.debriefJson = updates.debriefJson;
	if (updates.handoffJson !== void 0) updateRow.handoffJson = updates.handoffJson;
	db.update(tasksSessions).set(updateRow).where(eq(tasksSessions.id, sessionId)).run();
	return getSession(sessionId, cwd);
}
/** List sessions with optional filters. */
async function listSessions(filters, cwd) {
	const db = await getDb(cwd);
	const conditions = [];
	if (filters?.active) conditions.push(eq(tasksSessions.status, "active"));
	const query = db.select().from(tasksSessions).where(conditions.length > 0 ? and(...conditions) : void 0).orderBy(desc(tasksSessions.startedAt));
	return (filters?.limit ? await query.limit(filters.limit).all() : await query.all()).map(rowToSession);
}
/** End a session. */
async function endSession(sessionId, note, cwd) {
	const session = await getSession(sessionId, cwd);
	if (!session) return null;
	const updates = {
		status: "ended",
		endedAt: (/* @__PURE__ */ new Date()).toISOString()
	};
	if (note) {
		const notes = session.notes ?? [];
		notes.push(note);
		updates.notes = notes;
	}
	return updateSession(sessionId, updates, cwd);
}
/** Start working on a task within a session. */
async function startTask(sessionId, taskId, cwd) {
	const db = await getDb(cwd);
	const now = (/* @__PURE__ */ new Date()).toISOString();
	db.update(tasksTaskWorkHistory).set({ clearedAt: now }).where(and(eq(tasksTaskWorkHistory.sessionId, sessionId), isNull(tasksTaskWorkHistory.clearedAt))).run();
	db.insert(tasksTaskWorkHistory).values({
		sessionId,
		taskId,
		setAt: now
	}).run();
	db.update(tasksSessions).set({
		currentTask: taskId,
		taskStartedAt: now
	}).where(eq(tasksSessions.id, sessionId)).run();
}
/** Get current task for a session. */
async function getCurrentTask(sessionId, cwd) {
	const rows = await (await getDb(cwd)).select({
		currentTask: tasksSessions.currentTask,
		taskStartedAt: tasksSessions.taskStartedAt
	}).from(tasksSessions).where(eq(tasksSessions.id, sessionId)).all();
	if (rows.length === 0) return {
		taskId: null,
		since: null
	};
	return {
		taskId: rows[0].currentTask,
		since: rows[0].taskStartedAt
	};
}
/** Stop working on the current task for a session. */
async function stopTask(sessionId, cwd) {
	const db = await getDb(cwd);
	const now = (/* @__PURE__ */ new Date()).toISOString();
	db.update(tasksTaskWorkHistory).set({ clearedAt: now }).where(and(eq(tasksTaskWorkHistory.sessionId, sessionId), isNull(tasksTaskWorkHistory.clearedAt))).run();
	db.update(tasksSessions).set({
		currentTask: null,
		taskStartedAt: null
	}).where(eq(tasksSessions.id, sessionId)).run();
}
/**
* Get the currently active session — the most-recent `status='active'` row.
*
* @internal LEGACY single-process TTY fallback ONLY (T11640 · Epic T11638).
*
* This resolves "whoever wrote an active session to the DB most recently",
* which is the WRONG identity in any multi-tenant context: under the warm
* daemon (many concurrent connections) and under multi-agent spawn isolation
* (many worktrees writing the same DB) it collapses every caller's identity
* onto the last writer, causing session-bleed and memory scope-leakage.
*
* Identity-meaning callers — anything that means "the session of whoever is
* calling THIS request" — MUST use {@link resolveCurrentSession} /
* {@link resolveCurrentSessionId}, which resolve, in order:
*   1. the daemon connection handle ({@link getCurrentConnectionSessionId}),
*   2. the env-named session (`CLEO_SESSION_ID`, via
*      {@link resolveSessionIdFromEnv}),
*   3. and only THEN fall back to this most-recent-active row.
*
* `getActiveSession` survives as that final tier-3 fallback and for genuine
* SCAN-meaning callers (e.g. "is there any active session at all?",
* `gcSessions`, the data-accessor pass-throughs). The
* `lint-no-bare-get-active-session.mjs` gate bans NEW bare callsites so the
* identity-vs-scan split cannot silently regress — annotate a justified scan
* callsite with a trailing `// get-active-session-allowed: <reason>`.
*
* @param cwd - Working directory for DB resolution.
* @returns The most-recent active session, or `null`.
*/
async function getActiveSession(cwd) {
	const rows = await (await getDb(cwd)).select().from(tasksSessions).where(eq(tasksSessions.status, "active")).orderBy(desc(tasksSessions.startedAt)).limit(1).all();
	if (rows.length === 0) return null;
	return rowToSession(rows[0]);
}
/**
* Resolve the CALLER's current session (T11344/T11640 · Epics T11284, T11638).
*
* THE canonical identity-resolution helper. Replaces bare {@link getActiveSession}
* for any consumer that means "the session of whoever is calling THIS request".
* Resolution precedence (first hit wins):
*
* 1. **Daemon connection handle** ({@link getCurrentConnectionSessionId}) — when
*    dispatching a request frame on a warm-daemon connection, the connection's
*    accept-time-bound session is authoritative. A row with that id is honoured
*    when present; when the bound id has no row yet, resolution proceeds (the
*    connection asserted an identity that the env/active tiers may still
*    satisfy). This is the seam that gives the multi-connection daemon a stable
*    per-connection identity (T11640).
* 2. **Env-named session** — `resolveSessionIdFromEnv()` (`CLEO_SESSION_ID`
*    injected by spawn isolation, T11343); the spawned agent's OWN session.
* 3. **Most-recent active row** — {@link getActiveSession}, the legacy
*    single-process fallback.
*
* Making most-recent-active the FALLBACK rather than the default identity is
* what dissolves multi-agent session-bleed AND memory scope-leakage: a
* short-lived `cleo` call in agent A's worktree no longer resolves agent B's
* session just because B wrote to the DB more recently.
*
* The connection-handle and env-named sessions are honoured even when their
* status is not `active` (the caller explicitly asserted that id), so callers
* that need an active-only guarantee should check `.status` on the result.
*
* @param cwd - Working directory for DB resolution.
* @returns The resolved session, or `null` when none can be resolved.
* @task T11344
* @task T11640
*/
async function resolveCurrentSession(cwd) {
	const connId = getCurrentConnectionSessionId();
	if (connId) {
		const byConn = await getSession(connId, cwd);
		if (byConn) return byConn;
	}
	const envId = resolveSessionIdFromEnv();
	if (envId) {
		const byEnv = await getSession(envId, cwd);
		if (byEnv) return byEnv;
	}
	return getActiveSession(cwd);
}
/**
* Resolve the CALLER's current session id (T11344/T11640).
*
* Thin id-only convenience over {@link resolveCurrentSession} sharing its
* connection-handle → env → most-recent-active precedence. Prefer this over
* `(await getActiveSession())?.id` in identity-resolution hot paths.
*
* @param cwd - Working directory for DB resolution.
* @returns The resolved session id, or `null`.
* @task T11344
* @task T11640
*/
async function resolveCurrentSessionId(cwd) {
	const connId = getCurrentConnectionSessionId();
	if (connId) {
		const byConn = await getSession(connId, cwd);
		if (byConn) return byConn.id;
	}
	const envId = resolveSessionIdFromEnv();
	if (envId) {
		const byEnv = await getSession(envId, cwd);
		if (byEnv) return byEnv.id;
	}
	return (await getActiveSession(cwd))?.id ?? null;
}

export { getSession as a, stopTask as b, createSession as c, rowToTask as d, endSession as e, resolveCurrentSession as f, getCurrentTask as g, rowToSession as h, archivedTaskToRow as i, listSessions as l, resolveCurrentSessionId as r, startTask as s, taskToRow as t, updateSession as u };
//# sourceMappingURL=session-store-OwQWMSf_.js.map
