Skip to content
This repository was archived by the owner on Jun 8, 2026. It is now read-only.

Commit 84d0c78

Browse files
authored
Merge pull request #274 from closedloop-ai/feat/fea-1548-identity-columns
FEA-1548: Add session multi-tenant identity columns
2 parents bf7c8bd + da703ce commit 84d0c78

10 files changed

Lines changed: 361 additions & 8 deletions

apps/desktop/src/main/agent-session-sync-contract.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,13 @@ export type SyncedAgentSession = {
7979
awaitingInputSince?: string | null;
8080
metadata?: SyncJsonObject | null;
8181
attribution?: SyncedAgentSessionAttribution;
82+
/**
83+
* FEA-1548: multi-tenant identity columns. Nullable — sessions created before
84+
* account signup have no user/org context. Optional + additive; the relay
85+
* already ignores unknown fields, so this is backward-compatible.
86+
*/
87+
userId?: string | null;
88+
organizationId?: string | null;
8289
agents: SyncedAgentSessionAgent[];
8390
events: SyncedAgentSessionEvent[];
8491
tokenUsageByModel: SyncedAgentSessionTokenUsage[];

apps/desktop/src/main/agent-session-sync-service.ts

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,8 @@ type SessionRow = {
7373
metadata: string | null;
7474
harness: string | null;
7575
billing_mode: string | null;
76+
user_id: string | null;
77+
organization_id: string | null;
7678
};
7779

7880
type AgentRow = {
@@ -921,6 +923,12 @@ export function loadSyncedSessions(
921923
return [];
922924
}
923925

926+
const hasIdentityCols =
927+
columnExists(db, "sessions", "user_id")
928+
&& columnExists(db, "sessions", "organization_id");
929+
const identityColsSql = hasIdentityCols
930+
? "user_id, organization_id"
931+
: "NULL AS user_id, NULL AS organization_id";
924932
const sessionRows = selectRowsByIds<SessionRow>(
925933
db,
926934
`
@@ -936,7 +944,8 @@ export function loadSyncedSessions(
936944
awaiting_input_since,
937945
metadata,
938946
harness,
939-
billing_mode
947+
billing_mode,
948+
${identityColsSql}
940949
FROM sessions
941950
WHERE id IN (__IDS__)
942951
`,
@@ -1044,6 +1053,8 @@ export function loadSyncedSessions(
10441053
awaitingInputSince: row.awaiting_input_since,
10451054
metadata: parseJsonObjectText(row.metadata),
10461055
...(attribution ? { attribution } : {}),
1056+
...(row.user_id != null ? { userId: row.user_id } : {}),
1057+
...(row.organization_id != null ? { organizationId: row.organization_id } : {}),
10471058
agents: (agentsBySessionId.get(id) ?? []).map((agentRow) => ({
10481059
externalAgentId: agentRow.id,
10491060
name: agentRow.name,
@@ -1109,6 +1120,17 @@ export function resolveBillingModeForRow(row: SessionRow): BillingMode {
11091120
});
11101121
}
11111122

1123+
function columnExists(
1124+
db: DatabaseSync,
1125+
table: string,
1126+
column: string,
1127+
): boolean {
1128+
const rows = db
1129+
.prepare(`PRAGMA table_info(${table})`)
1130+
.all() as Array<{ name: string }>;
1131+
return rows.some((row) => row.name === column);
1132+
}
1133+
11121134
function selectRowsByIds<T>(
11131135
db: DatabaseSync,
11141136
sql: string,

apps/desktop/src/main/collectors/import-session.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,11 @@ export interface ImporterDeps {
3737
now?: () => string;
3838
/** Key-free diagnostic sink. */
3939
log?: (message: string) => void;
40+
/**
41+
* FEA-1548: resolve the current authenticated user's identity for stamping
42+
* on new sessions. Returns null when no user is signed in.
43+
*/
44+
getUserIdentity?: () => { userId: string; organizationId: string | null } | null;
4045
}
4146

4247
export interface ImportResult {
@@ -64,8 +69,8 @@ export function createImporter(db: DatabaseSync, deps: ImporterDeps): Importer {
6469
"SELECT id, status, ended_at FROM sessions WHERE id = ?",
6570
);
6671
const insertSessionStmt = db.prepare(`
67-
INSERT INTO sessions (id, name, status, cwd, model, started_at, updated_at, ended_at, harness, billing_mode, metadata)
68-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
72+
INSERT INTO sessions (id, name, status, cwd, model, started_at, updated_at, ended_at, harness, billing_mode, metadata, user_id, organization_id)
73+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
6974
`);
7075
// Fill only missing fields on an existing row; never clobber a live status.
7176
// Always refresh metadata so new fields (diffStats, artifacts, etc.) are populated.
@@ -193,6 +198,7 @@ export function createImporter(db: DatabaseSync, deps: ImporterDeps): Importer {
193198
if (!existing) {
194199
const status = recentlyActive ? "active" : "completed";
195200
const billingMode = safe(() => deps.detectBillingMode(harness)) ?? "unknown";
201+
const identity = safe(() => deps.getUserIdentity?.()) ?? null;
196202
insertSessionStmt.run(
197203
session.sessionId,
198204
session.name ?? null,
@@ -205,6 +211,8 @@ export function createImporter(db: DatabaseSync, deps: ImporterDeps): Importer {
205211
harness,
206212
billingMode,
207213
buildMetadata(session, harness),
214+
identity?.userId ?? null,
215+
identity?.organizationId ?? null,
208216
);
209217
insertAgentStmt.run(
210218
mainId,

apps/desktop/src/main/database/lifecycle.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,11 @@ export interface LifecycleDeps {
5252
log?: (message: string) => void;
5353
/** Minutes of inactivity after which a still-active session is abandoned. */
5454
staleMinutes?: number;
55+
/**
56+
* FEA-1548: resolve the current authenticated user's identity for stamping
57+
* on new sessions. Returns null when no user is signed in.
58+
*/
59+
getUserIdentity?: () => { userId: string; organizationId: string | null } | null;
5560
}
5661

5762
const COMPACTION_RE = /compact|compress|context.*(reduc|truncat|summar)/i;
@@ -85,8 +90,8 @@ export function createLifecycle(db: DatabaseSync, deps: LifecycleDeps) {
8590
"SELECT id, status, harness, billing_mode, model FROM sessions WHERE id = ?",
8691
);
8792
const insertSessionStmt = db.prepare(`
88-
INSERT INTO sessions (id, name, status, cwd, model, started_at, updated_at, harness, billing_mode)
89-
VALUES (?, ?, 'active', ?, ?, ?, ?, ?, ?)
93+
INSERT INTO sessions (id, name, status, cwd, model, started_at, updated_at, harness, billing_mode, user_id, organization_id)
94+
VALUES (?, ?, 'active', ?, ?, ?, ?, ?, ?, ?, ?)
9095
`);
9196
const setSessionStatusStmt = db.prepare(
9297
"UPDATE sessions SET status = ?, updated_at = ?, ended_at = ? WHERE id = ?",
@@ -169,6 +174,7 @@ export function createLifecycle(db: DatabaseSync, deps: LifecycleDeps) {
169174
return existing;
170175
}
171176
const billingMode = safe(() => deps.detectBillingMode(harness)) ?? "unknown";
177+
const identity = safe(() => deps.getUserIdentity?.()) ?? null;
172178
insertSessionStmt.run(
173179
sessionId,
174180
data.session_name ?? null,
@@ -178,6 +184,8 @@ export function createLifecycle(db: DatabaseSync, deps: LifecycleDeps) {
178184
now,
179185
harness,
180186
billingMode,
187+
identity?.userId ?? null,
188+
identity?.organizationId ?? null,
181189
);
182190
// Every session has a synthetic main agent.
183191
insertAgentStmt.run(

apps/desktop/src/main/database/schema.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
export const CURRENT_SCHEMA_VERSION = 5;
1+
export const CURRENT_SCHEMA_VERSION = 6;
22

33
/**
44
* Each migration runs against the DB when user_version < CURRENT_SCHEMA_VERSION.
@@ -121,5 +121,16 @@ ALTER TABLE sessions ADD COLUMN billing_mode TEXT;
121121
`
122122
CREATE INDEX IF NOT EXISTS idx_sessions_started_at ON sessions(started_at DESC);
123123
CREATE INDEX IF NOT EXISTS idx_sessions_status_started_at ON sessions(status, started_at DESC);
124+
`,
125+
126+
// Version 5 -> 6 (FEA-1548 Phase 1): multi-tenant identity columns.
127+
// Nullable because sessions created before account signup have no user/org
128+
// context. Backfilled when the user creates an account and joins an org.
129+
`
130+
ALTER TABLE sessions ADD COLUMN user_id TEXT;
131+
ALTER TABLE sessions ADD COLUMN organization_id TEXT;
132+
133+
CREATE INDEX IF NOT EXISTS idx_sessions_user_id ON sessions(user_id) WHERE user_id IS NOT NULL;
134+
CREATE INDEX IF NOT EXISTS idx_sessions_organization_id ON sessions(organization_id) WHERE organization_id IS NOT NULL;
124135
`,
125136
];

apps/desktop/src/main/database/sessions.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,8 @@ export function createSessionStore(db: DatabaseSync) {
7878
metadata: (raw.metadata as string) ?? null,
7979
harness: (raw.harness as string) ?? null,
8080
billingMode: (raw.billing_mode as string) ?? null,
81+
userId: (raw.user_id as string) ?? null,
82+
organizationId: (raw.organization_id as string) ?? null,
8183
};
8284
}
8385

apps/desktop/src/shared/agent-db-contract.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ export interface SessionRow {
2525
metadata: string | null;
2626
harness: string | null;
2727
billingMode: string | null;
28+
userId: string | null;
29+
organizationId: string | null;
2830
}
2931

3032
export interface AgentRow {

apps/desktop/test/agent-session-sync-service.test.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,9 @@ test("agent-session sync loads normalized session payloads with attribution and
7373
awaiting_input_since TEXT,
7474
metadata TEXT,
7575
harness TEXT NOT NULL,
76-
billing_mode TEXT NOT NULL DEFAULT 'unknown'
76+
billing_mode TEXT NOT NULL DEFAULT 'unknown',
77+
user_id TEXT,
78+
organization_id TEXT
7779
);
7880
CREATE TABLE agents (
7981
id TEXT PRIMARY KEY,

apps/desktop/test/helpers/agent-session-sync-test-utils.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,9 @@ export function createAgentMonitorTestDatabase(rootDir: string): DatabaseSync {
4343
awaiting_input_since TEXT,
4444
metadata TEXT,
4545
harness TEXT NOT NULL,
46-
billing_mode TEXT NOT NULL DEFAULT 'unknown'
46+
billing_mode TEXT NOT NULL DEFAULT 'unknown',
47+
user_id TEXT,
48+
organization_id TEXT
4749
);
4850
CREATE TABLE agents (
4951
id TEXT PRIMARY KEY,

0 commit comments

Comments
 (0)