diff --git a/apps/gateway/src/agent/__tests__/runtime-provider-registry.service.test.ts b/apps/gateway/src/agent/__tests__/runtime-provider-registry.service.test.ts index a521b5d..a70eafe 100644 --- a/apps/gateway/src/agent/__tests__/runtime-provider-registry.service.test.ts +++ b/apps/gateway/src/agent/__tests__/runtime-provider-registry.service.test.ts @@ -22,6 +22,8 @@ import { type RuntimeApprovalVerifier, } from '../runtime-provider-registry.service.js'; +process.env['MOSAIC_AGENT_NAME'] ??= 'test-runtime-agent'; + const OWNER_SCOPE: ActorTenantScope = { userId: 'owner-1', tenantId: 'tenant-1' }; const CONTEXT = { actorScope: OWNER_SCOPE, @@ -243,6 +245,7 @@ describe('RuntimeProviderService security boundary', (): void => { tenantId: OWNER_SCOPE.tenantId, channelId: CONTEXT.channelId, correlationId: CONTEXT.correlationId, + agentName: process.env['MOSAIC_AGENT_NAME'], }); expect(provider.terminateCalls).toBe(1); }); diff --git a/apps/gateway/src/agent/runtime-provider-registry.service.ts b/apps/gateway/src/agent/runtime-provider-registry.service.ts index c56906f..cb16a29 100644 --- a/apps/gateway/src/agent/runtime-provider-registry.service.ts +++ b/apps/gateway/src/agent/runtime-provider-registry.service.ts @@ -64,12 +64,19 @@ export interface RuntimeTerminationAction { tenantId: string; channelId: string; correlationId: string; + agentName: string; } export interface RuntimeApprovalVerifier { consume(approvalRef: string, action: RuntimeTerminationAction): Promise; } +function configuredAgentName(): string { + const agentName = process.env['MOSAIC_AGENT_NAME']?.trim(); + if (!agentName) throw new RuntimeApprovalDeniedError(); + return agentName; +} + class RuntimeApprovalDeniedError extends Error { constructor() { super('Runtime termination approval denied'); @@ -261,6 +268,7 @@ export class RuntimeProviderService { tenantId: scope.tenantId, channelId: scope.channelId, correlationId: scope.correlationId, + agentName: configuredAgentName(), }); if (!approved) { throw new RuntimeApprovalDeniedError(); diff --git a/apps/gateway/src/agent/tess-durable-session.repository.test.ts b/apps/gateway/src/agent/tess-durable-session.repository.test.ts index c9c4336..1cd1d3b 100644 --- a/apps/gateway/src/agent/tess-durable-session.repository.test.ts +++ b/apps/gateway/src/agent/tess-durable-session.repository.test.ts @@ -1,14 +1,15 @@ import { mkdtempSync, rmSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; -import { eq, sql, tessInbox } from '@mosaicstack/db'; +import { eq, sql, interactionInbox } from '@mosaicstack/db'; import { DurableSessionCoordinator, type DurableSessionIdentity } from '@mosaicstack/agent'; -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest'; import { createPgliteDb, runPgliteMigrations, type DbHandle } from '@mosaicstack/db'; import { TessDurableSessionRepository } from './tess-durable-session.repository.js'; import { TessDurableSessionService } from './tess-durable-session.service.js'; const IDENTITY: DurableSessionIdentity = { + agentName: 'Nova', sessionId: 'tess-pglite-session', tenantId: 'tenant-pglite', ownerId: 'tess-owner', @@ -18,27 +19,34 @@ const IDENTITY: DurableSessionIdentity = { describe('TessDurableSessionRepository', () => { let dataDir: string | undefined; - let handle: DbHandle | undefined; + let handle: DbHandle; let previousAuthSecret: string | undefined; - beforeEach((): void => { + beforeAll(async (): Promise => { previousAuthSecret = process.env['BETTER_AUTH_SECRET']; process.env['BETTER_AUTH_SECRET'] = 'tess-durable-state-test-sealing-key'; + dataDir = mkdtempSync(join(tmpdir(), 'tess-durable-state-')); + handle = createPgliteDb(dataDir); + await runPgliteMigrations(handle); + await seedOwner(handle); + }, 30_000); + + beforeEach(async (): Promise => { + await handle.db.execute(sql`DELETE FROM interaction_handoffs`); + await handle.db.execute(sql`DELETE FROM interaction_checkpoints`); + await handle.db.execute(sql`DELETE FROM interaction_inbox`); + await handle.db.execute(sql`DELETE FROM interaction_outbox`); + await handle.db.execute(sql`DELETE FROM interaction_sessions`); }); - afterEach(async (): Promise => { - await handle?.close(); + afterAll(async (): Promise => { + await handle.close(); if (dataDir) rmSync(dataDir, { recursive: true, force: true }); if (previousAuthSecret === undefined) delete process.env['BETTER_AUTH_SECRET']; else process.env['BETTER_AUTH_SECRET'] = previousAuthSecret; }); it('survives a full PGlite close/reopen mid-session without duplicate inbox or outbox side effects', async () => { - dataDir = mkdtempSync(join(tmpdir(), 'tess-durable-recovery-')); - handle = createPgliteDb(dataDir); - await runPgliteMigrations(handle); - await seedOwner(handle); - const beforeRestart = new DurableSessionCoordinator( new TessDurableSessionRepository(handle.db), ); @@ -81,7 +89,7 @@ describe('TessDurableSessionRepository', () => { }); await handle.close(); - handle = createPgliteDb(dataDir); + handle = createPgliteDb(dataDir!); const afterRestart = new DurableSessionCoordinator(new TessDurableSessionRepository(handle.db)); const recovered = await afterRestart.recover(IDENTITY.sessionId); @@ -111,11 +119,6 @@ describe('TessDurableSessionRepository', () => { }, 30_000); it('redacts sensitive durable payloads before persistence', async () => { - dataDir = mkdtempSync(join(tmpdir(), 'tess-durable-redaction-')); - handle = createPgliteDb(dataDir); - await runPgliteMigrations(handle); - await seedOwner(handle); - const coordinator = new DurableSessionCoordinator(new TessDurableSessionRepository(handle.db)); await coordinator.create(IDENTITY); await coordinator.receive({ @@ -142,9 +145,9 @@ describe('TessDurableSessionRepository', () => { const snapshot = await coordinator.snapshot(IDENTITY.sessionId); const [persisted] = await handle.db - .select({ content: tessInbox.content }) - .from(tessInbox) - .where(eq(tessInbox.idempotencyKey, 'redacted-inbox')); + .select({ content: interactionInbox.content }) + .from(interactionInbox) + .where(eq(interactionInbox.idempotencyKey, 'redacted-inbox')); expect(JSON.stringify(snapshot)).not.toContain('super-secret-canary'); expect(JSON.stringify(snapshot)).not.toContain('operator@example.test'); @@ -153,11 +156,6 @@ describe('TessDurableSessionRepository', () => { }, 30_000); it('rejects database inbox and outbox idempotency-key conflicts', async () => { - dataDir = mkdtempSync(join(tmpdir(), 'tess-durable-idempotency-conflict-')); - handle = createPgliteDb(dataDir); - await runPgliteMigrations(handle); - await seedOwner(handle); - const coordinator = new DurableSessionCoordinator(new TessDurableSessionRepository(handle.db)); await coordinator.create(IDENTITY); await coordinator.receive({ @@ -196,11 +194,6 @@ describe('TessDurableSessionRepository', () => { }, 30_000); it('does not requeue a live outbox claim during a normal scoped dispatch', async () => { - dataDir = mkdtempSync(join(tmpdir(), 'tess-durable-live-claim-')); - handle = createPgliteDb(dataDir); - await runPgliteMigrations(handle); - await seedOwner(handle); - const repository = new TessDurableSessionRepository(handle.db); const coordinator = new DurableSessionCoordinator(repository); const runtimeProviders = { sendMessage: vi.fn().mockResolvedValue(undefined) }; @@ -241,11 +234,6 @@ describe('TessDurableSessionRepository', () => { }, 30_000); it('rejects an outbox correlation mismatch before claiming the pending effect', async () => { - dataDir = mkdtempSync(join(tmpdir(), 'tess-durable-outbox-mismatch-')); - handle = createPgliteDb(dataDir); - await runPgliteMigrations(handle); - await seedOwner(handle); - const repository = new TessDurableSessionRepository(handle.db); const coordinator = new DurableSessionCoordinator(repository); const runtimeProviders = { sendMessage: vi.fn().mockResolvedValue(undefined) }; @@ -279,11 +267,6 @@ describe('TessDurableSessionRepository', () => { }, 30_000); it('dispatches only the outbox record bound to the supplied correlation and channel', async () => { - dataDir = mkdtempSync(join(tmpdir(), 'tess-durable-outbox-scope-')); - handle = createPgliteDb(dataDir); - await runPgliteMigrations(handle); - await seedOwner(handle); - const repository = new TessDurableSessionRepository(handle.db); const coordinator = new DurableSessionCoordinator(repository); const runtimeProviders = { sendMessage: vi.fn().mockResolvedValue(undefined) }; diff --git a/apps/gateway/src/agent/tess-durable-session.repository.ts b/apps/gateway/src/agent/tess-durable-session.repository.ts index 76f07e1..9daf324 100644 --- a/apps/gateway/src/agent/tess-durable-session.repository.ts +++ b/apps/gateway/src/agent/tess-durable-session.repository.ts @@ -1,14 +1,15 @@ +import { createHash } from 'node:crypto'; import { Inject, Injectable } from '@nestjs/common'; import { and, asc, desc, eq, - tessCheckpoints, - tessHandoffs, - tessInbox, - tessOutbox, - tessSessions, + interactionCheckpoints, + interactionHandoffs, + interactionInbox, + interactionOutbox, + interactionSessions, type Db, } from '@mosaicstack/db'; import { seal, unseal } from '@mosaicstack/auth'; @@ -37,9 +38,10 @@ export class TessDurableSessionRepository implements DurableSessionStore { async create(identity: DurableSessionIdentity): Promise { await this.db - .insert(tessSessions) + .insert(interactionSessions) .values({ id: identity.sessionId, + agentName: identity.agentName, tenantId: identity.tenantId, ownerId: identity.ownerId, providerId: identity.providerId, @@ -60,25 +62,28 @@ export class TessDurableSessionRepository implements DurableSessionStore { const [inbox, outbox, checkpoints, handoffs] = await Promise.all([ this.db .select() - .from(tessInbox) - .where(eq(tessInbox.sessionId, sessionId)) - .orderBy(asc(tessInbox.createdAt)), + .from(interactionInbox) + .where(eq(interactionInbox.sessionId, sessionId)) + .orderBy(asc(interactionInbox.createdAt)), this.db .select() - .from(tessOutbox) - .where(eq(tessOutbox.sessionId, sessionId)) - .orderBy(asc(tessOutbox.createdAt)), + .from(interactionOutbox) + .where(eq(interactionOutbox.sessionId, sessionId)) + .orderBy(asc(interactionOutbox.createdAt)), this.db .select() - .from(tessCheckpoints) - .where(eq(tessCheckpoints.sessionId, sessionId)) - .orderBy(desc(tessCheckpoints.compactionEpoch), desc(tessCheckpoints.createdAt)) + .from(interactionCheckpoints) + .where(eq(interactionCheckpoints.sessionId, sessionId)) + .orderBy( + desc(interactionCheckpoints.compactionEpoch), + desc(interactionCheckpoints.createdAt), + ) .limit(1), this.db .select() - .from(tessHandoffs) - .where(eq(tessHandoffs.sessionId, sessionId)) - .orderBy(asc(tessHandoffs.createdAt)), + .from(interactionHandoffs) + .where(eq(interactionHandoffs.sessionId, sessionId)) + .orderBy(asc(interactionHandoffs.createdAt)), ]); const checkpoint = checkpoints[0]; @@ -92,30 +97,36 @@ export class TessDurableSessionRepository implements DurableSessionStore { } async enqueueInbox(input: DurableInboxInput): Promise> { + const digest = contentDigest(input.content); const record: DurableInboxInput = { ...input, content: redactSensitiveContent(input.content).content, }; const inserted = await this.db - .insert(tessInbox) - .values({ ...record, content: seal(record.content), status: 'pending' }) + .insert(interactionInbox) + .values({ + ...record, + content: seal(record.content), + contentDigest: digest, + status: 'pending', + }) .onConflictDoNothing() - .returning({ status: tessInbox.status }); + .returning({ status: interactionInbox.status }); if (inserted[0]) return { accepted: true, status: inserted[0].status }; const existing = await this.db .select() - .from(tessInbox) + .from(interactionInbox) .where( and( - eq(tessInbox.sessionId, input.sessionId), - eq(tessInbox.idempotencyKey, input.idempotencyKey), + eq(interactionInbox.sessionId, input.sessionId), + eq(interactionInbox.idempotencyKey, input.idempotencyKey), ), ) .limit(1); if (!existing[0]) throw new Error(`Durable Tess inbox enqueue failed: ${input.idempotencyKey}`); const entry = toInbox(existing[0]); - if (!sameInbox(entry, record)) { + if (!sameInbox(entry, record) || existing[0].contentDigest !== digest) { throw new Error(`Durable Tess inbox idempotency conflict: ${input.idempotencyKey}`); } return { accepted: false, status: entry.status }; @@ -125,16 +136,18 @@ export class TessDurableSessionRepository implements DurableSessionStore { for (let attempt = 0; attempt < 3; attempt += 1) { const candidate = await this.db .select() - .from(tessInbox) - .where(and(eq(tessInbox.sessionId, sessionId), eq(tessInbox.status, 'pending'))) - .orderBy(asc(tessInbox.createdAt)) + .from(interactionInbox) + .where( + and(eq(interactionInbox.sessionId, sessionId), eq(interactionInbox.status, 'pending')), + ) + .orderBy(asc(interactionInbox.createdAt)) .limit(1); const entry = candidate[0]; if (!entry) return null; const claimed = await this.db - .update(tessInbox) + .update(interactionInbox) .set({ status: 'processing', updatedAt: new Date() }) - .where(and(eq(tessInbox.id, entry.id), eq(tessInbox.status, 'pending'))) + .where(and(eq(interactionInbox.id, entry.id), eq(interactionInbox.status, 'pending'))) .returning(); if (claimed[0]) return toInbox(claimed[0]); } @@ -143,26 +156,26 @@ export class TessDurableSessionRepository implements DurableSessionStore { async completeInbox(sessionId: string, idempotencyKey: string): Promise { await this.db - .update(tessInbox) + .update(interactionInbox) .set({ status: 'processed', updatedAt: new Date() }) .where( and( - eq(tessInbox.sessionId, sessionId), - eq(tessInbox.idempotencyKey, idempotencyKey), - eq(tessInbox.status, 'processing'), + eq(interactionInbox.sessionId, sessionId), + eq(interactionInbox.idempotencyKey, idempotencyKey), + eq(interactionInbox.status, 'processing'), ), ); } async releaseInbox(sessionId: string, idempotencyKey: string): Promise { await this.db - .update(tessInbox) + .update(interactionInbox) .set({ status: 'pending', updatedAt: new Date() }) .where( and( - eq(tessInbox.sessionId, sessionId), - eq(tessInbox.idempotencyKey, idempotencyKey), - eq(tessInbox.status, 'processing'), + eq(interactionInbox.sessionId, sessionId), + eq(interactionInbox.idempotencyKey, idempotencyKey), + eq(interactionInbox.status, 'processing'), ), ); } @@ -170,31 +183,37 @@ export class TessDurableSessionRepository implements DurableSessionStore { async enqueueOutbox( input: DurableOutboxInput, ): Promise> { + const digest = contentDigest(input.content); const record: DurableOutboxInput = { ...input, content: redactSensitiveContent(input.content).content, }; const inserted = await this.db - .insert(tessOutbox) - .values({ ...record, content: seal(record.content), status: 'pending' }) + .insert(interactionOutbox) + .values({ + ...record, + content: seal(record.content), + contentDigest: digest, + status: 'pending', + }) .onConflictDoNothing() - .returning({ status: tessOutbox.status }); + .returning({ status: interactionOutbox.status }); if (inserted[0]) return { accepted: true, status: inserted[0].status }; const existing = await this.db .select() - .from(tessOutbox) + .from(interactionOutbox) .where( and( - eq(tessOutbox.sessionId, input.sessionId), - eq(tessOutbox.idempotencyKey, input.idempotencyKey), + eq(interactionOutbox.sessionId, input.sessionId), + eq(interactionOutbox.idempotencyKey, input.idempotencyKey), ), ) .limit(1); if (!existing[0]) throw new Error(`Durable Tess outbox enqueue failed: ${input.idempotencyKey}`); const entry = toOutbox(existing[0]); - if (!sameOutbox(entry, record)) { + if (!sameOutbox(entry, record) || existing[0].contentDigest !== digest) { throw new Error(`Durable Tess outbox idempotency conflict: ${input.idempotencyKey}`); } return { accepted: false, status: entry.status }; @@ -204,16 +223,18 @@ export class TessDurableSessionRepository implements DurableSessionStore { for (let attempt = 0; attempt < 3; attempt += 1) { const candidate = await this.db .select() - .from(tessOutbox) - .where(and(eq(tessOutbox.sessionId, sessionId), eq(tessOutbox.status, 'pending'))) - .orderBy(asc(tessOutbox.createdAt)) + .from(interactionOutbox) + .where( + and(eq(interactionOutbox.sessionId, sessionId), eq(interactionOutbox.status, 'pending')), + ) + .orderBy(asc(interactionOutbox.createdAt)) .limit(1); const entry = candidate[0]; if (!entry) return null; const claimed = await this.db - .update(tessOutbox) + .update(interactionOutbox) .set({ status: 'processing', updatedAt: new Date() }) - .where(and(eq(tessOutbox.id, entry.id), eq(tessOutbox.status, 'pending'))) + .where(and(eq(interactionOutbox.id, entry.id), eq(interactionOutbox.status, 'pending'))) .returning(); if (claimed[0]) return toOutbox(claimed[0]); } @@ -225,13 +246,13 @@ export class TessDurableSessionRepository implements DurableSessionStore { idempotencyKey: string, ): Promise { const claimed = await this.db - .update(tessOutbox) + .update(interactionOutbox) .set({ status: 'processing', updatedAt: new Date() }) .where( and( - eq(tessOutbox.sessionId, sessionId), - eq(tessOutbox.idempotencyKey, idempotencyKey), - eq(tessOutbox.status, 'pending'), + eq(interactionOutbox.sessionId, sessionId), + eq(interactionOutbox.idempotencyKey, idempotencyKey), + eq(interactionOutbox.status, 'pending'), ), ) .returning(); @@ -240,26 +261,26 @@ export class TessDurableSessionRepository implements DurableSessionStore { async completeOutbox(sessionId: string, idempotencyKey: string): Promise { await this.db - .update(tessOutbox) + .update(interactionOutbox) .set({ status: 'delivered', updatedAt: new Date() }) .where( and( - eq(tessOutbox.sessionId, sessionId), - eq(tessOutbox.idempotencyKey, idempotencyKey), - eq(tessOutbox.status, 'processing'), + eq(interactionOutbox.sessionId, sessionId), + eq(interactionOutbox.idempotencyKey, idempotencyKey), + eq(interactionOutbox.status, 'processing'), ), ); } async releaseOutbox(sessionId: string, idempotencyKey: string): Promise { await this.db - .update(tessOutbox) + .update(interactionOutbox) .set({ status: 'pending', updatedAt: new Date() }) .where( and( - eq(tessOutbox.sessionId, sessionId), - eq(tessOutbox.idempotencyKey, idempotencyKey), - eq(tessOutbox.status, 'processing'), + eq(interactionOutbox.sessionId, sessionId), + eq(interactionOutbox.idempotencyKey, idempotencyKey), + eq(interactionOutbox.status, 'processing'), ), ); } @@ -271,14 +292,14 @@ export class TessDurableSessionRepository implements DurableSessionStore { summary: redactSensitiveContent(input.summary).content, }; const inserted = await this.db - .insert(tessCheckpoints) + .insert(interactionCheckpoints) .values({ ...checkpoint, cursor: seal(checkpoint.cursor), summary: seal(checkpoint.summary), }) .onConflictDoNothing() - .returning({ checkpointId: tessCheckpoints.checkpointId }); + .returning({ checkpointId: interactionCheckpoints.checkpointId }); if (inserted[0]) return; const existing = await this.findCheckpoint(input.sessionId, input.checkpointId); @@ -290,11 +311,11 @@ export class TessDurableSessionRepository implements DurableSessionStore { async findCheckpoint(sessionId: string, checkpointId: string): Promise { const checkpoints = await this.db .select() - .from(tessCheckpoints) + .from(interactionCheckpoints) .where( and( - eq(tessCheckpoints.sessionId, sessionId), - eq(tessCheckpoints.checkpointId, checkpointId), + eq(interactionCheckpoints.sessionId, sessionId), + eq(interactionCheckpoints.checkpointId, checkpointId), ), ) .limit(1); @@ -308,10 +329,10 @@ export class TessDurableSessionRepository implements DurableSessionStore { throw new Error(`Durable Tess handoff checkpoint is unavailable: ${input.checkpointId}`); } const inserted = await this.db - .insert(tessHandoffs) + .insert(interactionHandoffs) .values({ ...input }) .onConflictDoNothing() - .returning({ handoffId: tessHandoffs.handoffId }); + .returning({ handoffId: interactionHandoffs.handoffId }); if (inserted[0]) return; const existing = await this.findHandoff(input.handoffId); @@ -323,8 +344,8 @@ export class TessDurableSessionRepository implements DurableSessionStore { async findHandoff(handoffId: string): Promise { const handoffs = await this.db .select() - .from(tessHandoffs) - .where(eq(tessHandoffs.handoffId, handoffId)) + .from(interactionHandoffs) + .where(eq(interactionHandoffs.handoffId, handoffId)) .limit(1); const handoff = handoffs[0]; return handoff ? toHandoff(handoff) : null; @@ -335,20 +356,23 @@ export class TessDurableSessionRepository implements DurableSessionStore { // reached an external target before a crash, so it is deliberately not // replayed by generic recovery. await this.db - .update(tessInbox) + .update(interactionInbox) .set({ status: 'pending', updatedAt: new Date() }) - .where(and(eq(tessInbox.sessionId, sessionId), eq(tessInbox.status, 'processing'))); + .where( + and(eq(interactionInbox.sessionId, sessionId), eq(interactionInbox.status, 'processing')), + ); } private async session(sessionId: string): Promise { const sessions = await this.db .select() - .from(tessSessions) - .where(eq(tessSessions.id, sessionId)) + .from(interactionSessions) + .where(eq(interactionSessions.id, sessionId)) .limit(1); const session = sessions[0]; return session ? { + agentName: session.agentName, sessionId: session.id, tenantId: session.tenantId, ownerId: session.ownerId, @@ -359,8 +383,13 @@ export class TessDurableSessionRepository implements DurableSessionStore { } } +function contentDigest(content: string): string { + return createHash('sha256').update(content).digest('hex'); +} + function sameIdentity(left: DurableSessionIdentity, right: DurableSessionIdentity): boolean { return ( + left.agentName === right.agentName && left.sessionId === right.sessionId && left.tenantId === right.tenantId && left.ownerId === right.ownerId && @@ -410,7 +439,7 @@ function sameHandoff(left: DurableHandoff, right: DurableHandoffInput): boolean ); } -function toInbox(row: typeof tessInbox.$inferSelect): DurableInboxEntry { +function toInbox(row: typeof interactionInbox.$inferSelect): DurableInboxEntry { return { sessionId: row.sessionId, idempotencyKey: row.idempotencyKey, @@ -420,7 +449,7 @@ function toInbox(row: typeof tessInbox.$inferSelect): DurableInboxEntry { }; } -function toOutbox(row: typeof tessOutbox.$inferSelect): DurableOutboxEntry { +function toOutbox(row: typeof interactionOutbox.$inferSelect): DurableOutboxEntry { return { sessionId: row.sessionId, idempotencyKey: row.idempotencyKey, @@ -432,7 +461,7 @@ function toOutbox(row: typeof tessOutbox.$inferSelect): DurableOutboxEntry { }; } -function toCheckpoint(row: typeof tessCheckpoints.$inferSelect): DurableCheckpoint { +function toCheckpoint(row: typeof interactionCheckpoints.$inferSelect): DurableCheckpoint { return { sessionId: row.sessionId, checkpointId: row.checkpointId, @@ -442,7 +471,7 @@ function toCheckpoint(row: typeof tessCheckpoints.$inferSelect): DurableCheckpoi }; } -function toHandoff(row: typeof tessHandoffs.$inferSelect): DurableHandoff { +function toHandoff(row: typeof interactionHandoffs.$inferSelect): DurableHandoff { return { sessionId: row.sessionId, handoffId: row.handoffId, diff --git a/apps/gateway/src/commands/command-authorization.service.spec.ts b/apps/gateway/src/commands/command-authorization.service.spec.ts index 831ffbb..315d902 100644 --- a/apps/gateway/src/commands/command-authorization.service.spec.ts +++ b/apps/gateway/src/commands/command-authorization.service.spec.ts @@ -70,11 +70,12 @@ describe('CommandAuthorizationService', () => { tenantId: 'tenant-1', channelId: 'discord:operator', correlationId: 'correlation-malformed-expiry', + agentName: 'Nova', }; const service = createService('admin', entries); const approval = await service.createRuntimeTerminationApproval(action); expect(approval).not.toBeNull(); - const key = `tess:command-approval:${approval!.approvalId}`; + const key = `agent:Nova:command-approval:${approval!.approvalId}`; const stored = entries.get(key); expect(stored).toBeDefined(); entries.set(key, JSON.stringify({ ...JSON.parse(stored!), expiresAt: 'not-a-date' })); @@ -93,6 +94,7 @@ describe('CommandAuthorizationService', () => { tenantId: 'tenant-1', channelId: 'discord:operator', correlationId: 'correlation-1', + agentName: 'Nova', }; const beforeRestart = createService('admin', entries); const approval = await beforeRestart.createRuntimeTerminationApproval(action); diff --git a/apps/gateway/src/commands/command-authorization.service.ts b/apps/gateway/src/commands/command-authorization.service.ts index 4228a18..a9f829e 100644 --- a/apps/gateway/src/commands/command-authorization.service.ts +++ b/apps/gateway/src/commands/command-authorization.service.ts @@ -23,6 +23,8 @@ export interface RuntimeTerminationApprovalAction { tenantId: string; channelId: string; correlationId: string; + /** Provisioned roster identity; isolates approvals between interaction agents. */ + agentName: string; } export interface RuntimeTerminationApproval extends RuntimeTerminationApprovalAction { @@ -92,7 +94,7 @@ export class CommandAuthorizationService { } /** - * Uses the same `tess:command-approval:*` store and one-time deletion rule as + * Uses the same `interaction:command-approval:*` store and one-time deletion rule as * command approvals. This deliberately avoids a parallel approval database. */ async createRuntimeTerminationApproval( @@ -108,7 +110,12 @@ export class CommandAuthorizationService { ...action, expiresAt: new Date(Date.now() + 5 * 60_000).toISOString(), }; - await this.redis.set(this.key(approval.approvalId), JSON.stringify(approval), 'EX', '300'); + await this.redis.set( + this.runtimeKey(action.agentName, approval.approvalId), + JSON.stringify(approval), + 'EX', + '300', + ); return approval; } @@ -116,7 +123,7 @@ export class CommandAuthorizationService { approvalId: string, action: RuntimeTerminationApprovalAction, ): Promise { - const encoded = await this.redis.get(this.key(approvalId)); + const encoded = await this.redis.get(this.runtimeKey(action.agentName, approvalId)); if (!encoded) return false; let approval: unknown; try { @@ -134,7 +141,7 @@ export class CommandAuthorizationService { return false; } if ((await this.resolveRole(approval.actorId)) !== 'admin') return false; - return (await this.redis.del(this.key(approvalId))) === 1; + return (await this.redis.del(this.runtimeKey(action.agentName, approvalId))) === 1; } private async resolveRole(actorId: string): Promise { @@ -196,6 +203,7 @@ export class CommandAuthorizationService { action.tenantId, action.channelId, action.correlationId, + action.agentName, ].every((value: string): boolean => value.trim().length > 0); } @@ -209,6 +217,7 @@ export class CommandAuthorizationService { tenantId: action.tenantId, channelId: action.channelId, correlationId: action.correlationId, + agentName: action.agentName, }), ) .digest('hex'); @@ -244,11 +253,16 @@ export class CommandAuthorizationService { 'sessionId' in value && 'channelId' in value && 'correlationId' in value && + 'agentName' in value && 'expiresAt' in value ); } private key(approvalId: string): string { - return `tess:command-approval:${approvalId}`; + return `interaction:command-approval:${approvalId}`; + } + + private runtimeKey(agentName: string, approvalId: string): string { + return `agent:${encodeURIComponent(agentName)}:command-approval:${approvalId}`; } } diff --git a/apps/gateway/src/commands/runtime-approval-verifier.ts b/apps/gateway/src/commands/runtime-approval-verifier.ts index b16294c..47e0973 100644 --- a/apps/gateway/src/commands/runtime-approval-verifier.ts +++ b/apps/gateway/src/commands/runtime-approval-verifier.ts @@ -7,7 +7,7 @@ import { CommandAuthorizationService } from './command-authorization.service.js' /** * Adapter from the provider registry's exact termination action to the shared, - * Redis-backed `tess:command-approval:*` store. It has no separate approval + * Redis-backed `interaction:command-approval:*` store. It has no separate approval * persistence or replay semantics. */ @Injectable() diff --git a/apps/gateway/src/gc/session-gc.service.spec.ts b/apps/gateway/src/gc/session-gc.service.spec.ts index c6f3948..8c5ab6a 100644 --- a/apps/gateway/src/gc/session-gc.service.spec.ts +++ b/apps/gateway/src/gc/session-gc.service.spec.ts @@ -108,7 +108,7 @@ describe('SessionGCService', () => { } as never; const payload = { command: 'gc', conversationId: 'owned' }; const approval = await authorization.createApproval(command, payload, 'admin-1'); - const approvalKey = `tess:command-approval:${approval!.approvalId}`; + const approvalKey = `interaction:command-approval:${approval!.approvalId}`; const gc = new SessionGCService(redis as never, mockLogService as unknown as LogService); await gc.collect('owned'); diff --git a/docs/tess/ARCHITECTURE.md b/docs/tess/ARCHITECTURE.md index 858945e..31ae36d 100644 --- a/docs/tess/ARCHITECTURE.md +++ b/docs/tess/ARCHITECTURE.md @@ -41,7 +41,7 @@ Every call receives an immutable, server-derived actor/tenant/channel scope and `@mosaicstack/agent` owns the explicit `AgentRuntimeProviderRegistry`; duplicate provider IDs are rejected rather than replaced. Gateway owns `RuntimeProviderService`, which creates a frozen `RuntimeScope` from authenticated `ActorTenantScope` and trusted ingress channel/correlation metadata before every provider call. The service checks the declared provider capability before invoking a side effect and records metadata-only audit events (`providerId`, operation, outcome, actor/tenant/channel, correlation, and resource ID). It never records message bodies, idempotency keys, or approval references. -Termination is fail-closed: a runtime approval verifier consumes a one-time, exact action binding for the provider, session, actor, tenant, channel, and correlation ID before `terminate` reaches a provider. The verifier reuses the Redis-backed `tess:command-approval:*` store and its expiry/delete-on-consume semantics; it has no parallel approval store. This internal service introduces no HTTP endpoint; later Discord, CLI, MCP, and provider adapters consume the same gateway boundary. +Termination is fail-closed: a runtime approval verifier consumes a one-time, exact action binding for the provider, session, actor, tenant, channel, and correlation ID before `terminate` reaches a provider. The verifier reuses the Redis-backed `interaction:command-approval:*` store and its expiry/delete-on-consume semantics; it has no parallel approval store. This internal service introduces no HTTP endpoint; later Discord, CLI, MCP, and provider adapters consume the same gateway boundary. ## Authority Model @@ -61,7 +61,7 @@ Valkey holds the existing short-lived, one-time command-approval records; Postgr ### M2 Durable Recovery `@mosaicstack/agent` owns a transport-neutral state machine and `apps/gateway` provides its -PostgreSQL adapter. `tess_sessions` holds immutable identity; inbox/outbox records use a +PostgreSQL adapter. `interaction_sessions` holds immutable identity; inbox/outbox records use a per-session unique idempotency key and transition `pending → processing → processed|delivered`. Checkpoints are immutable history scoped by session and checkpoint ID: the latest checkpoint supports compaction recovery, while a handoff always resolves the exact checkpoint it references. diff --git a/packages/agent/src/tess-durable-session.test.ts b/packages/agent/src/tess-durable-session.test.ts index dd2c500..64e5ca8 100644 --- a/packages/agent/src/tess-durable-session.test.ts +++ b/packages/agent/src/tess-durable-session.test.ts @@ -6,6 +6,7 @@ import { } from './tess-durable-session.js'; const IDENTITY: DurableSessionIdentity = { + agentName: 'Nova', sessionId: 'tess-session-1', tenantId: 'tenant-1', ownerId: 'owner-1', diff --git a/packages/agent/src/tess-durable-session.ts b/packages/agent/src/tess-durable-session.ts index d259640..039fe06 100644 --- a/packages/agent/src/tess-durable-session.ts +++ b/packages/agent/src/tess-durable-session.ts @@ -1,4 +1,6 @@ export interface DurableSessionIdentity { + /** Provisioned roster identity; isolates durable state between named agents. */ + agentName: string; sessionId: string; tenantId: string; ownerId: string; @@ -220,7 +222,13 @@ export class DurableSessionCoordinator { } private assertIdentity(identity: DurableSessionIdentity): void { - this.assertRecord(identity.sessionId, identity.tenantId, identity.ownerId, identity.providerId); + this.assertRecord( + identity.agentName, + identity.sessionId, + identity.tenantId, + identity.ownerId, + identity.providerId, + ); if (identity.runtimeSessionId.trim().length === 0) { throw new Error('Durable runtime session identity is required'); } @@ -410,6 +418,7 @@ export class InMemoryDurableSessionStore implements DurableSessionStore { function identitiesEqual(left: DurableSessionIdentity, right: DurableSessionIdentity): boolean { return ( + left.agentName === right.agentName && left.sessionId === right.sessionId && left.tenantId === right.tenantId && left.ownerId === right.ownerId && diff --git a/packages/db/drizzle/0012_interaction_durable_state.sql b/packages/db/drizzle/0012_interaction_durable_state.sql new file mode 100644 index 0000000..36d8680 --- /dev/null +++ b/packages/db/drizzle/0012_interaction_durable_state.sql @@ -0,0 +1,71 @@ +CREATE TYPE "public"."interaction_handoff_status" AS ENUM('pending', 'accepted');--> statement-breakpoint +CREATE TYPE "public"."interaction_inbox_status" AS ENUM('pending', 'processing', 'processed');--> statement-breakpoint +CREATE TYPE "public"."interaction_outbox_status" AS ENUM('pending', 'processing', 'delivered');--> statement-breakpoint +CREATE TABLE "interaction_checkpoints" ( + "session_id" text PRIMARY KEY NOT NULL, + "checkpoint_id" text NOT NULL, + "cursor" text NOT NULL, + "summary" text NOT NULL, + "compaction_epoch" integer NOT NULL, + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "updated_at" timestamp with time zone DEFAULT now() NOT NULL, + CONSTRAINT "interaction_checkpoints_checkpoint_id_unique" UNIQUE("checkpoint_id") +); +--> statement-breakpoint +CREATE TABLE "interaction_handoffs" ( + "handoff_id" text PRIMARY KEY NOT NULL, + "session_id" text NOT NULL, + "destination" text NOT NULL, + "correlation_id" text NOT NULL, + "checkpoint_id" text NOT NULL, + "status" "interaction_handoff_status" DEFAULT 'pending' NOT NULL, + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "updated_at" timestamp with time zone DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE TABLE "interaction_inbox" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "session_id" text NOT NULL, + "idempotency_key" text NOT NULL, + "correlation_id" text NOT NULL, + "content" text NOT NULL, + "content_digest" text NOT NULL, + "status" "interaction_inbox_status" DEFAULT 'pending' NOT NULL, + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "updated_at" timestamp with time zone DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE TABLE "interaction_outbox" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "session_id" text NOT NULL, + "idempotency_key" text NOT NULL, + "correlation_id" text NOT NULL, + "kind" text NOT NULL, + "content" text NOT NULL, + "content_digest" text NOT NULL, + "status" "interaction_outbox_status" DEFAULT 'pending' NOT NULL, + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "updated_at" timestamp with time zone DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE TABLE "interaction_sessions" ( + "id" text PRIMARY KEY NOT NULL, + "agent_name" text NOT NULL, + "tenant_id" text NOT NULL, + "owner_id" text NOT NULL, + "provider_id" text NOT NULL, + "runtime_session_id" text NOT NULL, + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "updated_at" timestamp with time zone DEFAULT now() NOT NULL +); +--> statement-breakpoint +ALTER TABLE "interaction_checkpoints" ADD CONSTRAINT "interaction_checkpoints_session_id_interaction_sessions_id_fk" FOREIGN KEY ("session_id") REFERENCES "public"."interaction_sessions"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "interaction_handoffs" ADD CONSTRAINT "interaction_handoffs_session_id_interaction_sessions_id_fk" FOREIGN KEY ("session_id") REFERENCES "public"."interaction_sessions"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "interaction_inbox" ADD CONSTRAINT "interaction_inbox_session_id_interaction_sessions_id_fk" FOREIGN KEY ("session_id") REFERENCES "public"."interaction_sessions"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "interaction_outbox" ADD CONSTRAINT "interaction_outbox_session_id_interaction_sessions_id_fk" FOREIGN KEY ("session_id") REFERENCES "public"."interaction_sessions"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "interaction_sessions" ADD CONSTRAINT "interaction_sessions_owner_id_users_id_fk" FOREIGN KEY ("owner_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +CREATE INDEX "interaction_handoffs_session_status_idx" ON "interaction_handoffs" USING btree ("session_id","status");--> statement-breakpoint +CREATE UNIQUE INDEX "interaction_inbox_session_idempotency_idx" ON "interaction_inbox" USING btree ("session_id","idempotency_key");--> statement-breakpoint +CREATE INDEX "interaction_inbox_session_status_created_idx" ON "interaction_inbox" USING btree ("session_id","status","created_at");--> statement-breakpoint +CREATE UNIQUE INDEX "interaction_outbox_session_idempotency_idx" ON "interaction_outbox" USING btree ("session_id","idempotency_key");--> statement-breakpoint +CREATE INDEX "interaction_outbox_session_status_created_idx" ON "interaction_outbox" USING btree ("session_id","status","created_at"); \ No newline at end of file diff --git a/packages/db/drizzle/0012_tess_durable_state.sql b/packages/db/drizzle/0012_tess_durable_state.sql deleted file mode 100644 index e89f873..0000000 --- a/packages/db/drizzle/0012_tess_durable_state.sql +++ /dev/null @@ -1,68 +0,0 @@ -CREATE TYPE "public"."tess_handoff_status" AS ENUM('pending', 'accepted');--> statement-breakpoint -CREATE TYPE "public"."tess_inbox_status" AS ENUM('pending', 'processing', 'processed');--> statement-breakpoint -CREATE TYPE "public"."tess_outbox_status" AS ENUM('pending', 'processing', 'delivered');--> statement-breakpoint -CREATE TABLE "tess_checkpoints" ( - "session_id" text PRIMARY KEY NOT NULL, - "checkpoint_id" text NOT NULL, - "cursor" text NOT NULL, - "summary" text NOT NULL, - "compaction_epoch" integer NOT NULL, - "created_at" timestamp with time zone DEFAULT now() NOT NULL, - "updated_at" timestamp with time zone DEFAULT now() NOT NULL, - CONSTRAINT "tess_checkpoints_checkpoint_id_unique" UNIQUE("checkpoint_id") -); ---> statement-breakpoint -CREATE TABLE "tess_handoffs" ( - "handoff_id" text PRIMARY KEY NOT NULL, - "session_id" text NOT NULL, - "destination" text NOT NULL, - "correlation_id" text NOT NULL, - "checkpoint_id" text NOT NULL, - "status" "tess_handoff_status" DEFAULT 'pending' NOT NULL, - "created_at" timestamp with time zone DEFAULT now() NOT NULL, - "updated_at" timestamp with time zone DEFAULT now() NOT NULL -); ---> statement-breakpoint -CREATE TABLE "tess_inbox" ( - "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, - "session_id" text NOT NULL, - "idempotency_key" text NOT NULL, - "correlation_id" text NOT NULL, - "content" text NOT NULL, - "status" "tess_inbox_status" DEFAULT 'pending' NOT NULL, - "created_at" timestamp with time zone DEFAULT now() NOT NULL, - "updated_at" timestamp with time zone DEFAULT now() NOT NULL -); ---> statement-breakpoint -CREATE TABLE "tess_outbox" ( - "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, - "session_id" text NOT NULL, - "idempotency_key" text NOT NULL, - "correlation_id" text NOT NULL, - "kind" text NOT NULL, - "content" text NOT NULL, - "status" "tess_outbox_status" DEFAULT 'pending' NOT NULL, - "created_at" timestamp with time zone DEFAULT now() NOT NULL, - "updated_at" timestamp with time zone DEFAULT now() NOT NULL -); ---> statement-breakpoint -CREATE TABLE "tess_sessions" ( - "id" text PRIMARY KEY NOT NULL, - "tenant_id" text NOT NULL, - "owner_id" text NOT NULL, - "provider_id" text NOT NULL, - "runtime_session_id" text NOT NULL, - "created_at" timestamp with time zone DEFAULT now() NOT NULL, - "updated_at" timestamp with time zone DEFAULT now() NOT NULL -); ---> statement-breakpoint -ALTER TABLE "tess_checkpoints" ADD CONSTRAINT "tess_checkpoints_session_id_tess_sessions_id_fk" FOREIGN KEY ("session_id") REFERENCES "public"."tess_sessions"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint -ALTER TABLE "tess_handoffs" ADD CONSTRAINT "tess_handoffs_session_id_tess_sessions_id_fk" FOREIGN KEY ("session_id") REFERENCES "public"."tess_sessions"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint -ALTER TABLE "tess_inbox" ADD CONSTRAINT "tess_inbox_session_id_tess_sessions_id_fk" FOREIGN KEY ("session_id") REFERENCES "public"."tess_sessions"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint -ALTER TABLE "tess_outbox" ADD CONSTRAINT "tess_outbox_session_id_tess_sessions_id_fk" FOREIGN KEY ("session_id") REFERENCES "public"."tess_sessions"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint -ALTER TABLE "tess_sessions" ADD CONSTRAINT "tess_sessions_owner_id_users_id_fk" FOREIGN KEY ("owner_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint -CREATE INDEX "tess_handoffs_session_status_idx" ON "tess_handoffs" USING btree ("session_id","status");--> statement-breakpoint -CREATE UNIQUE INDEX "tess_inbox_session_idempotency_idx" ON "tess_inbox" USING btree ("session_id","idempotency_key");--> statement-breakpoint -CREATE INDEX "tess_inbox_session_status_created_idx" ON "tess_inbox" USING btree ("session_id","status","created_at");--> statement-breakpoint -CREATE UNIQUE INDEX "tess_outbox_session_idempotency_idx" ON "tess_outbox" USING btree ("session_id","idempotency_key");--> statement-breakpoint -CREATE INDEX "tess_outbox_session_status_created_idx" ON "tess_outbox" USING btree ("session_id","status","created_at"); \ No newline at end of file diff --git a/packages/db/drizzle/0013_interaction_checkpoint_history.sql b/packages/db/drizzle/0013_interaction_checkpoint_history.sql new file mode 100644 index 0000000..7fc8573 --- /dev/null +++ b/packages/db/drizzle/0013_interaction_checkpoint_history.sql @@ -0,0 +1,5 @@ +ALTER TABLE "interaction_checkpoints" DROP CONSTRAINT "interaction_checkpoints_checkpoint_id_unique";--> statement-breakpoint +ALTER TABLE "interaction_checkpoints" DROP CONSTRAINT "interaction_checkpoints_pkey";--> statement-breakpoint +ALTER TABLE "interaction_checkpoints" ADD COLUMN "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL;--> statement-breakpoint +CREATE UNIQUE INDEX "interaction_checkpoints_session_idempotency_idx" ON "interaction_checkpoints" USING btree ("session_id","checkpoint_id");--> statement-breakpoint +CREATE INDEX "interaction_checkpoints_session_epoch_idx" ON "interaction_checkpoints" USING btree ("session_id","compaction_epoch"); \ No newline at end of file diff --git a/packages/db/drizzle/0013_tess_checkpoint_history.sql b/packages/db/drizzle/0013_tess_checkpoint_history.sql deleted file mode 100644 index f863ddc..0000000 --- a/packages/db/drizzle/0013_tess_checkpoint_history.sql +++ /dev/null @@ -1,5 +0,0 @@ -ALTER TABLE "tess_checkpoints" DROP CONSTRAINT "tess_checkpoints_checkpoint_id_unique";--> statement-breakpoint -ALTER TABLE "tess_checkpoints" DROP CONSTRAINT "tess_checkpoints_pkey";--> statement-breakpoint -ALTER TABLE "tess_checkpoints" ADD COLUMN "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL;--> statement-breakpoint -CREATE UNIQUE INDEX "tess_checkpoints_session_idempotency_idx" ON "tess_checkpoints" USING btree ("session_id","checkpoint_id");--> statement-breakpoint -CREATE INDEX "tess_checkpoints_session_epoch_idx" ON "tess_checkpoints" USING btree ("session_id","compaction_epoch"); \ No newline at end of file diff --git a/packages/db/drizzle/0014_interaction_outbox_channel_scope.sql b/packages/db/drizzle/0014_interaction_outbox_channel_scope.sql new file mode 100644 index 0000000..af44cf5 --- /dev/null +++ b/packages/db/drizzle/0014_interaction_outbox_channel_scope.sql @@ -0,0 +1,5 @@ +ALTER TABLE "interaction_outbox" ADD COLUMN "channel_id" text; +--> statement-breakpoint +UPDATE "interaction_outbox" SET "channel_id" = 'legacy:unknown' WHERE "channel_id" IS NULL; +--> statement-breakpoint +ALTER TABLE "interaction_outbox" ALTER COLUMN "channel_id" SET NOT NULL; diff --git a/packages/db/drizzle/0014_tess_outbox_channel_scope.sql b/packages/db/drizzle/0014_tess_outbox_channel_scope.sql deleted file mode 100644 index 32e3c73..0000000 --- a/packages/db/drizzle/0014_tess_outbox_channel_scope.sql +++ /dev/null @@ -1,5 +0,0 @@ -ALTER TABLE "tess_outbox" ADD COLUMN "channel_id" text; ---> statement-breakpoint -UPDATE "tess_outbox" SET "channel_id" = 'legacy:unknown' WHERE "channel_id" IS NULL; ---> statement-breakpoint -ALTER TABLE "tess_outbox" ALTER COLUMN "channel_id" SET NOT NULL; diff --git a/packages/db/drizzle/meta/0012_snapshot.json b/packages/db/drizzle/meta/0012_snapshot.json index 7ade9e7..721391c 100644 --- a/packages/db/drizzle/meta/0012_snapshot.json +++ b/packages/db/drizzle/meta/0012_snapshot.json @@ -3354,8 +3354,8 @@ "checkConstraints": {}, "isRLSEnabled": false }, - "public.tess_checkpoints": { - "name": "tess_checkpoints", + "public.interaction_checkpoints": { + "name": "interaction_checkpoints", "schema": "", "columns": { "session_id": { @@ -3405,10 +3405,10 @@ }, "indexes": {}, "foreignKeys": { - "tess_checkpoints_session_id_tess_sessions_id_fk": { - "name": "tess_checkpoints_session_id_tess_sessions_id_fk", - "tableFrom": "tess_checkpoints", - "tableTo": "tess_sessions", + "interaction_checkpoints_session_id_interaction_sessions_id_fk": { + "name": "interaction_checkpoints_session_id_interaction_sessions_id_fk", + "tableFrom": "interaction_checkpoints", + "tableTo": "interaction_sessions", "columnsFrom": [ "session_id" ], @@ -3421,8 +3421,8 @@ }, "compositePrimaryKeys": {}, "uniqueConstraints": { - "tess_checkpoints_checkpoint_id_unique": { - "name": "tess_checkpoints_checkpoint_id_unique", + "interaction_checkpoints_checkpoint_id_unique": { + "name": "interaction_checkpoints_checkpoint_id_unique", "nullsNotDistinct": false, "columns": [ "checkpoint_id" @@ -3433,8 +3433,8 @@ "checkConstraints": {}, "isRLSEnabled": false }, - "public.tess_handoffs": { - "name": "tess_handoffs", + "public.interaction_handoffs": { + "name": "interaction_handoffs", "schema": "", "columns": { "handoff_id": { @@ -3469,7 +3469,7 @@ }, "status": { "name": "status", - "type": "tess_handoff_status", + "type": "interaction_handoff_status", "typeSchema": "public", "primaryKey": false, "notNull": true, @@ -3491,8 +3491,8 @@ } }, "indexes": { - "tess_handoffs_session_status_idx": { - "name": "tess_handoffs_session_status_idx", + "interaction_handoffs_session_status_idx": { + "name": "interaction_handoffs_session_status_idx", "columns": [ { "expression": "session_id", @@ -3514,10 +3514,10 @@ } }, "foreignKeys": { - "tess_handoffs_session_id_tess_sessions_id_fk": { - "name": "tess_handoffs_session_id_tess_sessions_id_fk", - "tableFrom": "tess_handoffs", - "tableTo": "tess_sessions", + "interaction_handoffs_session_id_interaction_sessions_id_fk": { + "name": "interaction_handoffs_session_id_interaction_sessions_id_fk", + "tableFrom": "interaction_handoffs", + "tableTo": "interaction_sessions", "columnsFrom": [ "session_id" ], @@ -3534,8 +3534,8 @@ "checkConstraints": {}, "isRLSEnabled": false }, - "public.tess_inbox": { - "name": "tess_inbox", + "public.interaction_inbox": { + "name": "interaction_inbox", "schema": "", "columns": { "id": { @@ -3571,7 +3571,7 @@ }, "status": { "name": "status", - "type": "tess_inbox_status", + "type": "interaction_inbox_status", "typeSchema": "public", "primaryKey": false, "notNull": true, @@ -3593,8 +3593,8 @@ } }, "indexes": { - "tess_inbox_session_idempotency_idx": { - "name": "tess_inbox_session_idempotency_idx", + "interaction_inbox_session_idempotency_idx": { + "name": "interaction_inbox_session_idempotency_idx", "columns": [ { "expression": "session_id", @@ -3614,8 +3614,8 @@ "method": "btree", "with": {} }, - "tess_inbox_session_status_created_idx": { - "name": "tess_inbox_session_status_created_idx", + "interaction_inbox_session_status_created_idx": { + "name": "interaction_inbox_session_status_created_idx", "columns": [ { "expression": "session_id", @@ -3643,10 +3643,10 @@ } }, "foreignKeys": { - "tess_inbox_session_id_tess_sessions_id_fk": { - "name": "tess_inbox_session_id_tess_sessions_id_fk", - "tableFrom": "tess_inbox", - "tableTo": "tess_sessions", + "interaction_inbox_session_id_interaction_sessions_id_fk": { + "name": "interaction_inbox_session_id_interaction_sessions_id_fk", + "tableFrom": "interaction_inbox", + "tableTo": "interaction_sessions", "columnsFrom": [ "session_id" ], @@ -3663,8 +3663,8 @@ "checkConstraints": {}, "isRLSEnabled": false }, - "public.tess_outbox": { - "name": "tess_outbox", + "public.interaction_outbox": { + "name": "interaction_outbox", "schema": "", "columns": { "id": { @@ -3706,7 +3706,7 @@ }, "status": { "name": "status", - "type": "tess_outbox_status", + "type": "interaction_outbox_status", "typeSchema": "public", "primaryKey": false, "notNull": true, @@ -3728,8 +3728,8 @@ } }, "indexes": { - "tess_outbox_session_idempotency_idx": { - "name": "tess_outbox_session_idempotency_idx", + "interaction_outbox_session_idempotency_idx": { + "name": "interaction_outbox_session_idempotency_idx", "columns": [ { "expression": "session_id", @@ -3749,8 +3749,8 @@ "method": "btree", "with": {} }, - "tess_outbox_session_status_created_idx": { - "name": "tess_outbox_session_status_created_idx", + "interaction_outbox_session_status_created_idx": { + "name": "interaction_outbox_session_status_created_idx", "columns": [ { "expression": "session_id", @@ -3778,10 +3778,10 @@ } }, "foreignKeys": { - "tess_outbox_session_id_tess_sessions_id_fk": { - "name": "tess_outbox_session_id_tess_sessions_id_fk", - "tableFrom": "tess_outbox", - "tableTo": "tess_sessions", + "interaction_outbox_session_id_interaction_sessions_id_fk": { + "name": "interaction_outbox_session_id_interaction_sessions_id_fk", + "tableFrom": "interaction_outbox", + "tableTo": "interaction_sessions", "columnsFrom": [ "session_id" ], @@ -3798,8 +3798,8 @@ "checkConstraints": {}, "isRLSEnabled": false }, - "public.tess_sessions": { - "name": "tess_sessions", + "public.interaction_sessions": { + "name": "interaction_sessions", "schema": "", "columns": { "id": { @@ -3849,9 +3849,9 @@ }, "indexes": {}, "foreignKeys": { - "tess_sessions_owner_id_users_id_fk": { - "name": "tess_sessions_owner_id_users_id_fk", - "tableFrom": "tess_sessions", + "interaction_sessions_owner_id_users_id_fk": { + "name": "interaction_sessions_owner_id_users_id_fk", + "tableFrom": "interaction_sessions", "tableTo": "users", "columnsFrom": [ "owner_id" @@ -4132,16 +4132,16 @@ "revoked" ] }, - "public.tess_handoff_status": { - "name": "tess_handoff_status", + "public.interaction_handoff_status": { + "name": "interaction_handoff_status", "schema": "public", "values": [ "pending", "accepted" ] }, - "public.tess_inbox_status": { - "name": "tess_inbox_status", + "public.interaction_inbox_status": { + "name": "interaction_inbox_status", "schema": "public", "values": [ "pending", @@ -4149,8 +4149,8 @@ "processed" ] }, - "public.tess_outbox_status": { - "name": "tess_outbox_status", + "public.interaction_outbox_status": { + "name": "interaction_outbox_status", "schema": "public", "values": [ "pending", diff --git a/packages/db/drizzle/meta/0013_snapshot.json b/packages/db/drizzle/meta/0013_snapshot.json index 4a124f5..6c85c86 100644 --- a/packages/db/drizzle/meta/0013_snapshot.json +++ b/packages/db/drizzle/meta/0013_snapshot.json @@ -3354,8 +3354,8 @@ "checkConstraints": {}, "isRLSEnabled": false }, - "public.tess_checkpoints": { - "name": "tess_checkpoints", + "public.interaction_checkpoints": { + "name": "interaction_checkpoints", "schema": "", "columns": { "id": { @@ -3411,8 +3411,8 @@ } }, "indexes": { - "tess_checkpoints_session_idempotency_idx": { - "name": "tess_checkpoints_session_idempotency_idx", + "interaction_checkpoints_session_idempotency_idx": { + "name": "interaction_checkpoints_session_idempotency_idx", "columns": [ { "expression": "session_id", @@ -3432,8 +3432,8 @@ "method": "btree", "with": {} }, - "tess_checkpoints_session_epoch_idx": { - "name": "tess_checkpoints_session_epoch_idx", + "interaction_checkpoints_session_epoch_idx": { + "name": "interaction_checkpoints_session_epoch_idx", "columns": [ { "expression": "session_id", @@ -3455,10 +3455,10 @@ } }, "foreignKeys": { - "tess_checkpoints_session_id_tess_sessions_id_fk": { - "name": "tess_checkpoints_session_id_tess_sessions_id_fk", - "tableFrom": "tess_checkpoints", - "tableTo": "tess_sessions", + "interaction_checkpoints_session_id_interaction_sessions_id_fk": { + "name": "interaction_checkpoints_session_id_interaction_sessions_id_fk", + "tableFrom": "interaction_checkpoints", + "tableTo": "interaction_sessions", "columnsFrom": [ "session_id" ], @@ -3475,8 +3475,8 @@ "checkConstraints": {}, "isRLSEnabled": false }, - "public.tess_handoffs": { - "name": "tess_handoffs", + "public.interaction_handoffs": { + "name": "interaction_handoffs", "schema": "", "columns": { "handoff_id": { @@ -3511,7 +3511,7 @@ }, "status": { "name": "status", - "type": "tess_handoff_status", + "type": "interaction_handoff_status", "typeSchema": "public", "primaryKey": false, "notNull": true, @@ -3533,8 +3533,8 @@ } }, "indexes": { - "tess_handoffs_session_status_idx": { - "name": "tess_handoffs_session_status_idx", + "interaction_handoffs_session_status_idx": { + "name": "interaction_handoffs_session_status_idx", "columns": [ { "expression": "session_id", @@ -3556,10 +3556,10 @@ } }, "foreignKeys": { - "tess_handoffs_session_id_tess_sessions_id_fk": { - "name": "tess_handoffs_session_id_tess_sessions_id_fk", - "tableFrom": "tess_handoffs", - "tableTo": "tess_sessions", + "interaction_handoffs_session_id_interaction_sessions_id_fk": { + "name": "interaction_handoffs_session_id_interaction_sessions_id_fk", + "tableFrom": "interaction_handoffs", + "tableTo": "interaction_sessions", "columnsFrom": [ "session_id" ], @@ -3576,8 +3576,8 @@ "checkConstraints": {}, "isRLSEnabled": false }, - "public.tess_inbox": { - "name": "tess_inbox", + "public.interaction_inbox": { + "name": "interaction_inbox", "schema": "", "columns": { "id": { @@ -3613,7 +3613,7 @@ }, "status": { "name": "status", - "type": "tess_inbox_status", + "type": "interaction_inbox_status", "typeSchema": "public", "primaryKey": false, "notNull": true, @@ -3635,8 +3635,8 @@ } }, "indexes": { - "tess_inbox_session_idempotency_idx": { - "name": "tess_inbox_session_idempotency_idx", + "interaction_inbox_session_idempotency_idx": { + "name": "interaction_inbox_session_idempotency_idx", "columns": [ { "expression": "session_id", @@ -3656,8 +3656,8 @@ "method": "btree", "with": {} }, - "tess_inbox_session_status_created_idx": { - "name": "tess_inbox_session_status_created_idx", + "interaction_inbox_session_status_created_idx": { + "name": "interaction_inbox_session_status_created_idx", "columns": [ { "expression": "session_id", @@ -3685,10 +3685,10 @@ } }, "foreignKeys": { - "tess_inbox_session_id_tess_sessions_id_fk": { - "name": "tess_inbox_session_id_tess_sessions_id_fk", - "tableFrom": "tess_inbox", - "tableTo": "tess_sessions", + "interaction_inbox_session_id_interaction_sessions_id_fk": { + "name": "interaction_inbox_session_id_interaction_sessions_id_fk", + "tableFrom": "interaction_inbox", + "tableTo": "interaction_sessions", "columnsFrom": [ "session_id" ], @@ -3705,8 +3705,8 @@ "checkConstraints": {}, "isRLSEnabled": false }, - "public.tess_outbox": { - "name": "tess_outbox", + "public.interaction_outbox": { + "name": "interaction_outbox", "schema": "", "columns": { "id": { @@ -3748,7 +3748,7 @@ }, "status": { "name": "status", - "type": "tess_outbox_status", + "type": "interaction_outbox_status", "typeSchema": "public", "primaryKey": false, "notNull": true, @@ -3770,8 +3770,8 @@ } }, "indexes": { - "tess_outbox_session_idempotency_idx": { - "name": "tess_outbox_session_idempotency_idx", + "interaction_outbox_session_idempotency_idx": { + "name": "interaction_outbox_session_idempotency_idx", "columns": [ { "expression": "session_id", @@ -3791,8 +3791,8 @@ "method": "btree", "with": {} }, - "tess_outbox_session_status_created_idx": { - "name": "tess_outbox_session_status_created_idx", + "interaction_outbox_session_status_created_idx": { + "name": "interaction_outbox_session_status_created_idx", "columns": [ { "expression": "session_id", @@ -3820,10 +3820,10 @@ } }, "foreignKeys": { - "tess_outbox_session_id_tess_sessions_id_fk": { - "name": "tess_outbox_session_id_tess_sessions_id_fk", - "tableFrom": "tess_outbox", - "tableTo": "tess_sessions", + "interaction_outbox_session_id_interaction_sessions_id_fk": { + "name": "interaction_outbox_session_id_interaction_sessions_id_fk", + "tableFrom": "interaction_outbox", + "tableTo": "interaction_sessions", "columnsFrom": [ "session_id" ], @@ -3840,8 +3840,8 @@ "checkConstraints": {}, "isRLSEnabled": false }, - "public.tess_sessions": { - "name": "tess_sessions", + "public.interaction_sessions": { + "name": "interaction_sessions", "schema": "", "columns": { "id": { @@ -3891,9 +3891,9 @@ }, "indexes": {}, "foreignKeys": { - "tess_sessions_owner_id_users_id_fk": { - "name": "tess_sessions_owner_id_users_id_fk", - "tableFrom": "tess_sessions", + "interaction_sessions_owner_id_users_id_fk": { + "name": "interaction_sessions_owner_id_users_id_fk", + "tableFrom": "interaction_sessions", "tableTo": "users", "columnsFrom": [ "owner_id" @@ -4174,16 +4174,16 @@ "revoked" ] }, - "public.tess_handoff_status": { - "name": "tess_handoff_status", + "public.interaction_handoff_status": { + "name": "interaction_handoff_status", "schema": "public", "values": [ "pending", "accepted" ] }, - "public.tess_inbox_status": { - "name": "tess_inbox_status", + "public.interaction_inbox_status": { + "name": "interaction_inbox_status", "schema": "public", "values": [ "pending", @@ -4191,8 +4191,8 @@ "processed" ] }, - "public.tess_outbox_status": { - "name": "tess_outbox_status", + "public.interaction_outbox_status": { + "name": "interaction_outbox_status", "schema": "public", "values": [ "pending", diff --git a/packages/db/drizzle/meta/0014_snapshot.json b/packages/db/drizzle/meta/0014_snapshot.json index 249e99a..b60ca23 100644 --- a/packages/db/drizzle/meta/0014_snapshot.json +++ b/packages/db/drizzle/meta/0014_snapshot.json @@ -3354,8 +3354,8 @@ "checkConstraints": {}, "isRLSEnabled": false }, - "public.tess_checkpoints": { - "name": "tess_checkpoints", + "public.interaction_checkpoints": { + "name": "interaction_checkpoints", "schema": "", "columns": { "id": { @@ -3411,8 +3411,8 @@ } }, "indexes": { - "tess_checkpoints_session_idempotency_idx": { - "name": "tess_checkpoints_session_idempotency_idx", + "interaction_checkpoints_session_idempotency_idx": { + "name": "interaction_checkpoints_session_idempotency_idx", "columns": [ { "expression": "session_id", @@ -3432,8 +3432,8 @@ "method": "btree", "with": {} }, - "tess_checkpoints_session_epoch_idx": { - "name": "tess_checkpoints_session_epoch_idx", + "interaction_checkpoints_session_epoch_idx": { + "name": "interaction_checkpoints_session_epoch_idx", "columns": [ { "expression": "session_id", @@ -3455,10 +3455,10 @@ } }, "foreignKeys": { - "tess_checkpoints_session_id_tess_sessions_id_fk": { - "name": "tess_checkpoints_session_id_tess_sessions_id_fk", - "tableFrom": "tess_checkpoints", - "tableTo": "tess_sessions", + "interaction_checkpoints_session_id_interaction_sessions_id_fk": { + "name": "interaction_checkpoints_session_id_interaction_sessions_id_fk", + "tableFrom": "interaction_checkpoints", + "tableTo": "interaction_sessions", "columnsFrom": [ "session_id" ], @@ -3475,8 +3475,8 @@ "checkConstraints": {}, "isRLSEnabled": false }, - "public.tess_handoffs": { - "name": "tess_handoffs", + "public.interaction_handoffs": { + "name": "interaction_handoffs", "schema": "", "columns": { "handoff_id": { @@ -3511,7 +3511,7 @@ }, "status": { "name": "status", - "type": "tess_handoff_status", + "type": "interaction_handoff_status", "typeSchema": "public", "primaryKey": false, "notNull": true, @@ -3533,8 +3533,8 @@ } }, "indexes": { - "tess_handoffs_session_status_idx": { - "name": "tess_handoffs_session_status_idx", + "interaction_handoffs_session_status_idx": { + "name": "interaction_handoffs_session_status_idx", "columns": [ { "expression": "session_id", @@ -3556,10 +3556,10 @@ } }, "foreignKeys": { - "tess_handoffs_session_id_tess_sessions_id_fk": { - "name": "tess_handoffs_session_id_tess_sessions_id_fk", - "tableFrom": "tess_handoffs", - "tableTo": "tess_sessions", + "interaction_handoffs_session_id_interaction_sessions_id_fk": { + "name": "interaction_handoffs_session_id_interaction_sessions_id_fk", + "tableFrom": "interaction_handoffs", + "tableTo": "interaction_sessions", "columnsFrom": [ "session_id" ], @@ -3576,8 +3576,8 @@ "checkConstraints": {}, "isRLSEnabled": false }, - "public.tess_inbox": { - "name": "tess_inbox", + "public.interaction_inbox": { + "name": "interaction_inbox", "schema": "", "columns": { "id": { @@ -3613,7 +3613,7 @@ }, "status": { "name": "status", - "type": "tess_inbox_status", + "type": "interaction_inbox_status", "typeSchema": "public", "primaryKey": false, "notNull": true, @@ -3635,8 +3635,8 @@ } }, "indexes": { - "tess_inbox_session_idempotency_idx": { - "name": "tess_inbox_session_idempotency_idx", + "interaction_inbox_session_idempotency_idx": { + "name": "interaction_inbox_session_idempotency_idx", "columns": [ { "expression": "session_id", @@ -3656,8 +3656,8 @@ "method": "btree", "with": {} }, - "tess_inbox_session_status_created_idx": { - "name": "tess_inbox_session_status_created_idx", + "interaction_inbox_session_status_created_idx": { + "name": "interaction_inbox_session_status_created_idx", "columns": [ { "expression": "session_id", @@ -3685,10 +3685,10 @@ } }, "foreignKeys": { - "tess_inbox_session_id_tess_sessions_id_fk": { - "name": "tess_inbox_session_id_tess_sessions_id_fk", - "tableFrom": "tess_inbox", - "tableTo": "tess_sessions", + "interaction_inbox_session_id_interaction_sessions_id_fk": { + "name": "interaction_inbox_session_id_interaction_sessions_id_fk", + "tableFrom": "interaction_inbox", + "tableTo": "interaction_sessions", "columnsFrom": [ "session_id" ], @@ -3705,8 +3705,8 @@ "checkConstraints": {}, "isRLSEnabled": false }, - "public.tess_outbox": { - "name": "tess_outbox", + "public.interaction_outbox": { + "name": "interaction_outbox", "schema": "", "columns": { "id": { @@ -3754,7 +3754,7 @@ }, "status": { "name": "status", - "type": "tess_outbox_status", + "type": "interaction_outbox_status", "typeSchema": "public", "primaryKey": false, "notNull": true, @@ -3776,8 +3776,8 @@ } }, "indexes": { - "tess_outbox_session_idempotency_idx": { - "name": "tess_outbox_session_idempotency_idx", + "interaction_outbox_session_idempotency_idx": { + "name": "interaction_outbox_session_idempotency_idx", "columns": [ { "expression": "session_id", @@ -3797,8 +3797,8 @@ "method": "btree", "with": {} }, - "tess_outbox_session_status_created_idx": { - "name": "tess_outbox_session_status_created_idx", + "interaction_outbox_session_status_created_idx": { + "name": "interaction_outbox_session_status_created_idx", "columns": [ { "expression": "session_id", @@ -3826,10 +3826,10 @@ } }, "foreignKeys": { - "tess_outbox_session_id_tess_sessions_id_fk": { - "name": "tess_outbox_session_id_tess_sessions_id_fk", - "tableFrom": "tess_outbox", - "tableTo": "tess_sessions", + "interaction_outbox_session_id_interaction_sessions_id_fk": { + "name": "interaction_outbox_session_id_interaction_sessions_id_fk", + "tableFrom": "interaction_outbox", + "tableTo": "interaction_sessions", "columnsFrom": [ "session_id" ], @@ -3846,8 +3846,8 @@ "checkConstraints": {}, "isRLSEnabled": false }, - "public.tess_sessions": { - "name": "tess_sessions", + "public.interaction_sessions": { + "name": "interaction_sessions", "schema": "", "columns": { "id": { @@ -3897,9 +3897,9 @@ }, "indexes": {}, "foreignKeys": { - "tess_sessions_owner_id_users_id_fk": { - "name": "tess_sessions_owner_id_users_id_fk", - "tableFrom": "tess_sessions", + "interaction_sessions_owner_id_users_id_fk": { + "name": "interaction_sessions_owner_id_users_id_fk", + "tableFrom": "interaction_sessions", "tableTo": "users", "columnsFrom": [ "owner_id" @@ -4180,16 +4180,16 @@ "revoked" ] }, - "public.tess_handoff_status": { - "name": "tess_handoff_status", + "public.interaction_handoff_status": { + "name": "interaction_handoff_status", "schema": "public", "values": [ "pending", "accepted" ] }, - "public.tess_inbox_status": { - "name": "tess_inbox_status", + "public.interaction_inbox_status": { + "name": "interaction_inbox_status", "schema": "public", "values": [ "pending", @@ -4197,8 +4197,8 @@ "processed" ] }, - "public.tess_outbox_status": { - "name": "tess_outbox_status", + "public.interaction_outbox_status": { + "name": "interaction_outbox_status", "schema": "public", "values": [ "pending", diff --git a/packages/db/drizzle/meta/_journal.json b/packages/db/drizzle/meta/_journal.json index b5b1dd1..5d842b5 100644 --- a/packages/db/drizzle/meta/_journal.json +++ b/packages/db/drizzle/meta/_journal.json @@ -90,21 +90,21 @@ "idx": 12, "version": "7", "when": 1783911983447, - "tag": "0012_tess_durable_state", + "tag": "0012_interaction_durable_state", "breakpoints": true }, { "idx": 13, "version": "7", "when": 1783913232578, - "tag": "0013_tess_checkpoint_history", + "tag": "0013_interaction_checkpoint_history", "breakpoints": true }, { "idx": 14, "version": "7", "when": 1783913398006, - "tag": "0014_tess_outbox_channel_scope", + "tag": "0014_interaction_outbox_channel_scope", "breakpoints": true } ] diff --git a/packages/db/src/schema.ts b/packages/db/src/schema.ts index c085a7c..abd0ff8 100644 --- a/packages/db/src/schema.ts +++ b/packages/db/src/schema.ts @@ -491,20 +491,24 @@ export const agentLogs = pgTable( // PostgreSQL is canonical for restart-safe Tess session recovery. The state // machine lives in @mosaicstack/agent; these records are its durable adapter. -export const tessInboxStatusEnum = pgEnum('tess_inbox_status', [ +export const interactionInboxStatusEnum = pgEnum('interaction_inbox_status', [ 'pending', 'processing', 'processed', ]); -export const tessOutboxStatusEnum = pgEnum('tess_outbox_status', [ +export const interactionOutboxStatusEnum = pgEnum('interaction_outbox_status', [ 'pending', 'processing', 'delivered', ]); -export const tessHandoffStatusEnum = pgEnum('tess_handoff_status', ['pending', 'accepted']); +export const interactionHandoffStatusEnum = pgEnum('interaction_handoff_status', [ + 'pending', + 'accepted', +]); -export const tessSessions = pgTable('tess_sessions', { +export const interactionSessions = pgTable('interaction_sessions', { id: text('id').primaryKey(), + agentName: text('agent_name').notNull(), tenantId: text('tenant_id').notNull(), ownerId: text('owner_id') .notNull() @@ -515,55 +519,57 @@ export const tessSessions = pgTable('tess_sessions', { updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(), }); -export const tessInbox = pgTable( - 'tess_inbox', +export const interactionInbox = pgTable( + 'interaction_inbox', { id: uuid('id').primaryKey().defaultRandom(), sessionId: text('session_id') .notNull() - .references(() => tessSessions.id, { onDelete: 'cascade' }), + .references(() => interactionSessions.id, { onDelete: 'cascade' }), idempotencyKey: text('idempotency_key').notNull(), correlationId: text('correlation_id').notNull(), content: text('content').notNull(), - status: tessInboxStatusEnum('status').notNull().default('pending'), + contentDigest: text('content_digest').notNull(), + status: interactionInboxStatusEnum('status').notNull().default('pending'), createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(), updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(), }, (t) => [ - uniqueIndex('tess_inbox_session_idempotency_idx').on(t.sessionId, t.idempotencyKey), - index('tess_inbox_session_status_created_idx').on(t.sessionId, t.status, t.createdAt), + uniqueIndex('interaction_inbox_session_idempotency_idx').on(t.sessionId, t.idempotencyKey), + index('interaction_inbox_session_status_created_idx').on(t.sessionId, t.status, t.createdAt), ], ); -export const tessOutbox = pgTable( - 'tess_outbox', +export const interactionOutbox = pgTable( + 'interaction_outbox', { id: uuid('id').primaryKey().defaultRandom(), sessionId: text('session_id') .notNull() - .references(() => tessSessions.id, { onDelete: 'cascade' }), + .references(() => interactionSessions.id, { onDelete: 'cascade' }), idempotencyKey: text('idempotency_key').notNull(), correlationId: text('correlation_id').notNull(), channelId: text('channel_id').notNull(), kind: text('kind').notNull(), content: text('content').notNull(), - status: tessOutboxStatusEnum('status').notNull().default('pending'), + contentDigest: text('content_digest').notNull(), + status: interactionOutboxStatusEnum('status').notNull().default('pending'), createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(), updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(), }, (t) => [ - uniqueIndex('tess_outbox_session_idempotency_idx').on(t.sessionId, t.idempotencyKey), - index('tess_outbox_session_status_created_idx').on(t.sessionId, t.status, t.createdAt), + uniqueIndex('interaction_outbox_session_idempotency_idx').on(t.sessionId, t.idempotencyKey), + index('interaction_outbox_session_status_created_idx').on(t.sessionId, t.status, t.createdAt), ], ); -export const tessCheckpoints = pgTable( - 'tess_checkpoints', +export const interactionCheckpoints = pgTable( + 'interaction_checkpoints', { id: uuid('id').primaryKey().defaultRandom(), sessionId: text('session_id') .notNull() - .references(() => tessSessions.id, { onDelete: 'cascade' }), + .references(() => interactionSessions.id, { onDelete: 'cascade' }), checkpointId: text('checkpoint_id').notNull(), cursor: text('cursor').notNull(), summary: text('summary').notNull(), @@ -572,26 +578,26 @@ export const tessCheckpoints = pgTable( updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(), }, (t) => [ - uniqueIndex('tess_checkpoints_session_idempotency_idx').on(t.sessionId, t.checkpointId), - index('tess_checkpoints_session_epoch_idx').on(t.sessionId, t.compactionEpoch), + uniqueIndex('interaction_checkpoints_session_idempotency_idx').on(t.sessionId, t.checkpointId), + index('interaction_checkpoints_session_epoch_idx').on(t.sessionId, t.compactionEpoch), ], ); -export const tessHandoffs = pgTable( - 'tess_handoffs', +export const interactionHandoffs = pgTable( + 'interaction_handoffs', { handoffId: text('handoff_id').primaryKey(), sessionId: text('session_id') .notNull() - .references(() => tessSessions.id, { onDelete: 'cascade' }), + .references(() => interactionSessions.id, { onDelete: 'cascade' }), destination: text('destination').notNull(), correlationId: text('correlation_id').notNull(), checkpointId: text('checkpoint_id').notNull(), - status: tessHandoffStatusEnum('status').notNull().default('pending'), + status: interactionHandoffStatusEnum('status').notNull().default('pending'), createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(), updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(), }, - (t) => [index('tess_handoffs_session_status_idx').on(t.sessionId, t.status)], + (t) => [index('interaction_handoffs_session_status_idx').on(t.sessionId, t.status)], ); // ─── Skills ─────────────────────────────────────────────────────────────────