chat session context scoping, fix sandboxed pi config mounts, dedupe zai/opencode providers
- Add context/contextId fields to session types, storage, and index - Filter sessions by context (project, workspace, or default chat) - Pass context through websocket, REST API, and frontend hooks - Mount ~/.pi/agent into sandboxed containers for auth + writable sessions - Remove PI_CODING_AGENT_DIR override so container pi uses ~/.pi/agent - Deduplicate zai/opencode models (same service, show as opencode) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -66,6 +66,7 @@ export async function listPiModels(): Promise<ModelInfo[]> {
|
||||
};
|
||||
|
||||
const models: ModelInfo[] = [];
|
||||
const seen = new Set<string>();
|
||||
|
||||
for (let i = 1; i < lines.length; i++) {
|
||||
const line = lines[i]!;
|
||||
@@ -78,11 +79,16 @@ export async function listPiModels(): Promise<ModelInfo[]> {
|
||||
const thinking = extractCol(line, 4);
|
||||
const images = extractCol(line, 5);
|
||||
|
||||
const normalizedProvider = provider === 'zai' ? 'opencode' : provider;
|
||||
// zai and opencode are the same service — prefer opencode, skip zai duplicates
|
||||
const displayProvider = provider === 'zai' ? 'opencode' : provider;
|
||||
const dedupeKey = `${displayProvider}/${model}`;
|
||||
if (seen.has(dedupeKey)) continue;
|
||||
seen.add(dedupeKey);
|
||||
|
||||
models.push({
|
||||
id: `${provider}/${model}`,
|
||||
name: model,
|
||||
provider: normalizedProvider,
|
||||
provider: displayProvider,
|
||||
contextWindow: parseSize(context),
|
||||
maxTokens: parseSize(maxOut),
|
||||
reasoning: thinking === 'yes',
|
||||
|
||||
@@ -241,7 +241,6 @@ export async function spawnPi(
|
||||
await ensureGoogleTokenFile(sandbox.userId, sandbox.email);
|
||||
|
||||
const envFlags = [
|
||||
'-e', `PI_CODING_AGENT_DIR=/officer/pi-config`,
|
||||
'-e', `HOME=${containerHome}`,
|
||||
'-e', `OFFICER_USER_HOME=${containerHome}`,
|
||||
'-e', `OFFICER_USER_ROOT=/officer/user`,
|
||||
|
||||
@@ -51,9 +51,10 @@ piRestRouter.post('/pi/sessions', async (ctx: Context) => {
|
||||
const body = await ctx.req.json().catch(() => ({}));
|
||||
const userHome = getHomeDir(user.email);
|
||||
const filterCwd = body.cwd ? resolveBaseCwd(user.email, body.cwdRoot, body.cwd) : null;
|
||||
const contextFilter = body.context ? { context: body.context as string, contextId: body.contextId as string | undefined } : undefined;
|
||||
|
||||
try {
|
||||
let sessions = await storage.listUserSessions(userHome);
|
||||
let sessions = await storage.listUserSessions(userHome, contextFilter);
|
||||
if (filterCwd) {
|
||||
sessions = sessions.filter((s) => s.cwd === filterCwd);
|
||||
}
|
||||
|
||||
@@ -11,7 +11,9 @@ class SessionManager {
|
||||
email: string,
|
||||
cwd: string,
|
||||
model: string,
|
||||
groupSlug?: string | null
|
||||
groupSlug?: string | null,
|
||||
context?: string,
|
||||
contextId?: string,
|
||||
): UserSession {
|
||||
let session = this.sessions.get(sessionId);
|
||||
|
||||
@@ -35,6 +37,8 @@ class SessionManager {
|
||||
model,
|
||||
cwd,
|
||||
groupSlug: groupSlug || null,
|
||||
context,
|
||||
contextId,
|
||||
createdAt: Date.now(),
|
||||
updatedAt: Date.now(),
|
||||
messageCount: 0,
|
||||
|
||||
@@ -251,6 +251,8 @@ export function messagesToJnlEntries(messages: Message[], meta: SessionMeta): Jn
|
||||
messageCount: meta.messageCount,
|
||||
createdAt: meta.createdAt,
|
||||
updatedAt: meta.updatedAt,
|
||||
context: meta.context,
|
||||
contextId: meta.contextId,
|
||||
},
|
||||
};
|
||||
entries.push(infoEntry);
|
||||
@@ -357,6 +359,8 @@ function indexEntryToMeta(sessionId: string, entry: SessionIndexEntry): SessionM
|
||||
messageCount: entry.messageCount,
|
||||
cost: entry.cost,
|
||||
groupSlug: entry.groupSlug ?? null,
|
||||
context: entry.context,
|
||||
contextId: entry.contextId,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -371,6 +375,8 @@ function metaToIndexEntry(meta: SessionMeta, relFile: string): SessionIndexEntry
|
||||
messageCount: meta.messageCount,
|
||||
cost: meta.cost,
|
||||
groupSlug: meta.groupSlug ?? null,
|
||||
context: meta.context,
|
||||
contextId: meta.contextId,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -495,14 +501,28 @@ export async function deleteSession(
|
||||
await saveIndex(baseCwd, index);
|
||||
}
|
||||
|
||||
export async function listUserSessions(baseCwd: string): Promise<SessionMeta[]> {
|
||||
type SessionFilter = {
|
||||
context?: string;
|
||||
contextId?: string;
|
||||
};
|
||||
|
||||
export async function listUserSessions(baseCwd: string, filter?: SessionFilter): Promise<SessionMeta[]> {
|
||||
let index = await loadIndex(baseCwd);
|
||||
|
||||
if (Object.keys(index).length === 0) {
|
||||
index = await rebuildIndex(baseCwd);
|
||||
}
|
||||
|
||||
const sessions: SessionMeta[] = Object.entries(index).map(([id, entry]) => indexEntryToMeta(id, entry));
|
||||
let sessions: SessionMeta[] = Object.entries(index).map(([id, entry]) => indexEntryToMeta(id, entry));
|
||||
|
||||
if (filter?.context) {
|
||||
if (filter.context === 'chat') {
|
||||
// 'chat' matches sessions with no context or context='chat'
|
||||
sessions = sessions.filter((s) => !s.context || s.context === 'chat');
|
||||
} else {
|
||||
sessions = sessions.filter((s) => s.context === filter.context && s.contextId === filter.contextId);
|
||||
}
|
||||
}
|
||||
|
||||
sessions.sort((a, b) => b.updatedAt - a.updatedAt);
|
||||
return sessions;
|
||||
@@ -748,6 +768,8 @@ export async function rebuildIndex(baseCwd: string): Promise<SessionIndex> {
|
||||
let createdAt = new Date(header.timestamp).getTime();
|
||||
let updatedAt = createdAt;
|
||||
let groupSlug: string | null = null;
|
||||
let context: string | undefined;
|
||||
let contextId: string | undefined;
|
||||
|
||||
// Count message entries and find session_info
|
||||
for (const entry of entries) {
|
||||
@@ -763,6 +785,8 @@ export async function rebuildIndex(baseCwd: string): Promise<SessionIndex> {
|
||||
createdAt = info.officer.createdAt;
|
||||
updatedAt = info.officer.updatedAt;
|
||||
groupSlug = info.officer.groupSlug ?? null;
|
||||
context = info.officer.context;
|
||||
contextId = info.officer.contextId;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -777,6 +801,8 @@ export async function rebuildIndex(baseCwd: string): Promise<SessionIndex> {
|
||||
messageCount,
|
||||
cost,
|
||||
groupSlug,
|
||||
context,
|
||||
contextId,
|
||||
};
|
||||
} catch {
|
||||
continue;
|
||||
|
||||
@@ -28,6 +28,8 @@ export type SessionMeta = {
|
||||
messageCount: number;
|
||||
cost: MessageCost;
|
||||
groupSlug?: string | null;
|
||||
context?: string;
|
||||
contextId?: string;
|
||||
};
|
||||
|
||||
export type GroupMeta = {
|
||||
@@ -53,6 +55,8 @@ export type ClientMessage =
|
||||
groupSlug?: string;
|
||||
attachmentIds?: string[];
|
||||
thinking?: ThinkingLevel;
|
||||
context?: string;
|
||||
contextId?: string;
|
||||
}
|
||||
| {
|
||||
type: "resume";
|
||||
@@ -70,6 +74,8 @@ export type ServerMessage =
|
||||
sessionId: string;
|
||||
model: string;
|
||||
cwd: string;
|
||||
context?: string;
|
||||
contextId?: string;
|
||||
}
|
||||
| {
|
||||
type: "assistant:text";
|
||||
@@ -230,6 +236,8 @@ export type JnlSessionInfoEntry = JnlEntryBase & {
|
||||
messageCount: number;
|
||||
createdAt: number;
|
||||
updatedAt: number;
|
||||
context?: string;
|
||||
contextId?: string;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -247,6 +255,8 @@ export type SessionIndexEntry = {
|
||||
messageCount: number;
|
||||
cost: MessageCost;
|
||||
groupSlug?: string | null;
|
||||
context?: string;
|
||||
contextId?: string;
|
||||
};
|
||||
|
||||
export type SessionIndex = Record<string, SessionIndexEntry>;
|
||||
|
||||
@@ -242,7 +242,7 @@ function createEventHandler(sessionId: string, model: string, cwd: string, stora
|
||||
|
||||
async function handleChat(
|
||||
ws: ServerWebSocket<WSData>,
|
||||
msg: { prompt: string; sessionId?: string; model?: string; cwd?: string; cwdRoot?: string; sandboxed?: boolean; groupSlug?: string; attachmentIds?: string[]; thinking?: string }
|
||||
msg: { prompt: string; sessionId?: string; model?: string; cwd?: string; cwdRoot?: string; sandboxed?: boolean; groupSlug?: string; attachmentIds?: string[]; thinking?: string; context?: string; contextId?: string }
|
||||
): Promise<void> {
|
||||
const { email, username, userId } = ws.data;
|
||||
const sessionId = msg.sessionId || randomUUID();
|
||||
@@ -276,13 +276,13 @@ async function handleChat(
|
||||
? resolveSandboxedCwd(email, msg.cwdRoot, msg.cwd)
|
||||
: resolveHostCwd(msg.cwdRoot, msg.cwd);
|
||||
const groupSlug = msg.groupSlug || null;
|
||||
const session = sessionManager.getOrCreate(sessionId, email, cwd, model, groupSlug);
|
||||
const session = sessionManager.getOrCreate(sessionId, email, cwd, model, groupSlug, msg.context, msg.contextId);
|
||||
session.sandboxed = sandboxed;
|
||||
session.userId = userId;
|
||||
sessionManager.attachWs(sessionId, ws);
|
||||
wsToSessionMap.set(ws as any, sessionId);
|
||||
|
||||
sendToClient(ws, { type: 'session:init', sessionId, model, cwd });
|
||||
sendToClient(ws, { type: 'session:init', sessionId, model, cwd, context: session.meta.context, contextId: session.meta.contextId });
|
||||
if (!session.piProcess) {
|
||||
try {
|
||||
const onEvent = createEventHandler(sessionId, model, cwd, homeDir);
|
||||
@@ -353,7 +353,7 @@ async function handleResume(
|
||||
sessionManager.attachWs(sessionId, ws);
|
||||
wsToSessionMap.set(ws as any, sessionId);
|
||||
|
||||
sendToClient(ws, { type: 'session:init', sessionId, model: session.model, cwd: session.cwd });
|
||||
sendToClient(ws, { type: 'session:init', sessionId, model: session.model, cwd: session.cwd, context: session.meta.context, contextId: session.meta.contextId });
|
||||
|
||||
// Spawn fresh Pi process if needed
|
||||
if (!session.piProcess) {
|
||||
|
||||
Reference in New Issue
Block a user