feat(tess): persist durable session state #729
@@ -22,6 +22,8 @@ import {
|
|||||||
type RuntimeApprovalVerifier,
|
type RuntimeApprovalVerifier,
|
||||||
} from '../runtime-provider-registry.service.js';
|
} 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 OWNER_SCOPE: ActorTenantScope = { userId: 'owner-1', tenantId: 'tenant-1' };
|
||||||
const CONTEXT = {
|
const CONTEXT = {
|
||||||
actorScope: OWNER_SCOPE,
|
actorScope: OWNER_SCOPE,
|
||||||
@@ -243,6 +245,7 @@ describe('RuntimeProviderService security boundary', (): void => {
|
|||||||
tenantId: OWNER_SCOPE.tenantId,
|
tenantId: OWNER_SCOPE.tenantId,
|
||||||
channelId: CONTEXT.channelId,
|
channelId: CONTEXT.channelId,
|
||||||
correlationId: CONTEXT.correlationId,
|
correlationId: CONTEXT.correlationId,
|
||||||
|
agentName: process.env['MOSAIC_AGENT_NAME'],
|
||||||
});
|
});
|
||||||
expect(provider.terminateCalls).toBe(1);
|
expect(provider.terminateCalls).toBe(1);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -64,12 +64,19 @@ export interface RuntimeTerminationAction {
|
|||||||
tenantId: string;
|
tenantId: string;
|
||||||
channelId: string;
|
channelId: string;
|
||||||
correlationId: string;
|
correlationId: string;
|
||||||
|
agentName: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RuntimeApprovalVerifier {
|
export interface RuntimeApprovalVerifier {
|
||||||
consume(approvalRef: string, action: RuntimeTerminationAction): Promise<boolean>;
|
consume(approvalRef: string, action: RuntimeTerminationAction): Promise<boolean>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function configuredAgentName(): string {
|
||||||
|
const agentName = process.env['MOSAIC_AGENT_NAME']?.trim();
|
||||||
|
if (!agentName) throw new RuntimeApprovalDeniedError();
|
||||||
|
return agentName;
|
||||||
|
}
|
||||||
|
|
||||||
class RuntimeApprovalDeniedError extends Error {
|
class RuntimeApprovalDeniedError extends Error {
|
||||||
constructor() {
|
constructor() {
|
||||||
super('Runtime termination approval denied');
|
super('Runtime termination approval denied');
|
||||||
@@ -261,6 +268,7 @@ export class RuntimeProviderService {
|
|||||||
tenantId: scope.tenantId,
|
tenantId: scope.tenantId,
|
||||||
channelId: scope.channelId,
|
channelId: scope.channelId,
|
||||||
correlationId: scope.correlationId,
|
correlationId: scope.correlationId,
|
||||||
|
agentName: configuredAgentName(),
|
||||||
});
|
});
|
||||||
if (!approved) {
|
if (!approved) {
|
||||||
throw new RuntimeApprovalDeniedError();
|
throw new RuntimeApprovalDeniedError();
|
||||||
|
|||||||
@@ -1,14 +1,15 @@
|
|||||||
import { mkdtempSync, rmSync } from 'node:fs';
|
import { mkdtempSync, rmSync } from 'node:fs';
|
||||||
import { tmpdir } from 'node:os';
|
import { tmpdir } from 'node:os';
|
||||||
import { join } from 'node:path';
|
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 { 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 { createPgliteDb, runPgliteMigrations, type DbHandle } from '@mosaicstack/db';
|
||||||
import { TessDurableSessionRepository } from './tess-durable-session.repository.js';
|
import { TessDurableSessionRepository } from './tess-durable-session.repository.js';
|
||||||
import { TessDurableSessionService } from './tess-durable-session.service.js';
|
import { TessDurableSessionService } from './tess-durable-session.service.js';
|
||||||
|
|
||||||
const IDENTITY: DurableSessionIdentity = {
|
const IDENTITY: DurableSessionIdentity = {
|
||||||
|
agentName: 'Nova',
|
||||||
sessionId: 'tess-pglite-session',
|
sessionId: 'tess-pglite-session',
|
||||||
tenantId: 'tenant-pglite',
|
tenantId: 'tenant-pglite',
|
||||||
ownerId: 'tess-owner',
|
ownerId: 'tess-owner',
|
||||||
@@ -18,27 +19,34 @@ const IDENTITY: DurableSessionIdentity = {
|
|||||||
|
|
||||||
describe('TessDurableSessionRepository', () => {
|
describe('TessDurableSessionRepository', () => {
|
||||||
let dataDir: string | undefined;
|
let dataDir: string | undefined;
|
||||||
let handle: DbHandle | undefined;
|
let handle: DbHandle;
|
||||||
let previousAuthSecret: string | undefined;
|
let previousAuthSecret: string | undefined;
|
||||||
|
|
||||||
beforeEach((): void => {
|
beforeAll(async (): Promise<void> => {
|
||||||
previousAuthSecret = process.env['BETTER_AUTH_SECRET'];
|
previousAuthSecret = process.env['BETTER_AUTH_SECRET'];
|
||||||
process.env['BETTER_AUTH_SECRET'] = 'tess-durable-state-test-sealing-key';
|
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<void> => {
|
||||||
|
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<void> => {
|
afterAll(async (): Promise<void> => {
|
||||||
await handle?.close();
|
await handle.close();
|
||||||
if (dataDir) rmSync(dataDir, { recursive: true, force: true });
|
if (dataDir) rmSync(dataDir, { recursive: true, force: true });
|
||||||
if (previousAuthSecret === undefined) delete process.env['BETTER_AUTH_SECRET'];
|
if (previousAuthSecret === undefined) delete process.env['BETTER_AUTH_SECRET'];
|
||||||
else process.env['BETTER_AUTH_SECRET'] = previousAuthSecret;
|
else process.env['BETTER_AUTH_SECRET'] = previousAuthSecret;
|
||||||
});
|
});
|
||||||
|
|
||||||
it('survives a full PGlite close/reopen mid-session without duplicate inbox or outbox side effects', async () => {
|
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(
|
const beforeRestart = new DurableSessionCoordinator(
|
||||||
new TessDurableSessionRepository(handle.db),
|
new TessDurableSessionRepository(handle.db),
|
||||||
);
|
);
|
||||||
@@ -81,7 +89,7 @@ describe('TessDurableSessionRepository', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
await handle.close();
|
await handle.close();
|
||||||
handle = createPgliteDb(dataDir);
|
handle = createPgliteDb(dataDir!);
|
||||||
|
|
||||||
const afterRestart = new DurableSessionCoordinator(new TessDurableSessionRepository(handle.db));
|
const afterRestart = new DurableSessionCoordinator(new TessDurableSessionRepository(handle.db));
|
||||||
const recovered = await afterRestart.recover(IDENTITY.sessionId);
|
const recovered = await afterRestart.recover(IDENTITY.sessionId);
|
||||||
@@ -111,11 +119,6 @@ describe('TessDurableSessionRepository', () => {
|
|||||||
}, 30_000);
|
}, 30_000);
|
||||||
|
|
||||||
it('redacts sensitive durable payloads before persistence', async () => {
|
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));
|
const coordinator = new DurableSessionCoordinator(new TessDurableSessionRepository(handle.db));
|
||||||
await coordinator.create(IDENTITY);
|
await coordinator.create(IDENTITY);
|
||||||
await coordinator.receive({
|
await coordinator.receive({
|
||||||
@@ -142,9 +145,9 @@ describe('TessDurableSessionRepository', () => {
|
|||||||
|
|
||||||
const snapshot = await coordinator.snapshot(IDENTITY.sessionId);
|
const snapshot = await coordinator.snapshot(IDENTITY.sessionId);
|
||||||
const [persisted] = await handle.db
|
const [persisted] = await handle.db
|
||||||
.select({ content: tessInbox.content })
|
.select({ content: interactionInbox.content })
|
||||||
.from(tessInbox)
|
.from(interactionInbox)
|
||||||
.where(eq(tessInbox.idempotencyKey, 'redacted-inbox'));
|
.where(eq(interactionInbox.idempotencyKey, 'redacted-inbox'));
|
||||||
|
|
||||||
expect(JSON.stringify(snapshot)).not.toContain('super-secret-canary');
|
expect(JSON.stringify(snapshot)).not.toContain('super-secret-canary');
|
||||||
expect(JSON.stringify(snapshot)).not.toContain('operator@example.test');
|
expect(JSON.stringify(snapshot)).not.toContain('operator@example.test');
|
||||||
@@ -153,11 +156,6 @@ describe('TessDurableSessionRepository', () => {
|
|||||||
}, 30_000);
|
}, 30_000);
|
||||||
|
|
||||||
it('rejects database inbox and outbox idempotency-key conflicts', async () => {
|
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));
|
const coordinator = new DurableSessionCoordinator(new TessDurableSessionRepository(handle.db));
|
||||||
await coordinator.create(IDENTITY);
|
await coordinator.create(IDENTITY);
|
||||||
await coordinator.receive({
|
await coordinator.receive({
|
||||||
@@ -196,11 +194,6 @@ describe('TessDurableSessionRepository', () => {
|
|||||||
}, 30_000);
|
}, 30_000);
|
||||||
|
|
||||||
it('does not requeue a live outbox claim during a normal scoped dispatch', async () => {
|
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 repository = new TessDurableSessionRepository(handle.db);
|
||||||
const coordinator = new DurableSessionCoordinator(repository);
|
const coordinator = new DurableSessionCoordinator(repository);
|
||||||
const runtimeProviders = { sendMessage: vi.fn().mockResolvedValue(undefined) };
|
const runtimeProviders = { sendMessage: vi.fn().mockResolvedValue(undefined) };
|
||||||
@@ -241,11 +234,6 @@ describe('TessDurableSessionRepository', () => {
|
|||||||
}, 30_000);
|
}, 30_000);
|
||||||
|
|
||||||
it('rejects an outbox correlation mismatch before claiming the pending effect', async () => {
|
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 repository = new TessDurableSessionRepository(handle.db);
|
||||||
const coordinator = new DurableSessionCoordinator(repository);
|
const coordinator = new DurableSessionCoordinator(repository);
|
||||||
const runtimeProviders = { sendMessage: vi.fn().mockResolvedValue(undefined) };
|
const runtimeProviders = { sendMessage: vi.fn().mockResolvedValue(undefined) };
|
||||||
@@ -279,11 +267,6 @@ describe('TessDurableSessionRepository', () => {
|
|||||||
}, 30_000);
|
}, 30_000);
|
||||||
|
|
||||||
it('dispatches only the outbox record bound to the supplied correlation and channel', async () => {
|
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 repository = new TessDurableSessionRepository(handle.db);
|
||||||
const coordinator = new DurableSessionCoordinator(repository);
|
const coordinator = new DurableSessionCoordinator(repository);
|
||||||
const runtimeProviders = { sendMessage: vi.fn().mockResolvedValue(undefined) };
|
const runtimeProviders = { sendMessage: vi.fn().mockResolvedValue(undefined) };
|
||||||
|
|||||||
@@ -1,14 +1,15 @@
|
|||||||
|
import { createHash } from 'node:crypto';
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import {
|
import {
|
||||||
and,
|
and,
|
||||||
asc,
|
asc,
|
||||||
desc,
|
desc,
|
||||||
eq,
|
eq,
|
||||||
tessCheckpoints,
|
interactionCheckpoints,
|
||||||
tessHandoffs,
|
interactionHandoffs,
|
||||||
tessInbox,
|
interactionInbox,
|
||||||
tessOutbox,
|
interactionOutbox,
|
||||||
tessSessions,
|
interactionSessions,
|
||||||
type Db,
|
type Db,
|
||||||
} from '@mosaicstack/db';
|
} from '@mosaicstack/db';
|
||||||
import { seal, unseal } from '@mosaicstack/auth';
|
import { seal, unseal } from '@mosaicstack/auth';
|
||||||
@@ -37,9 +38,10 @@ export class TessDurableSessionRepository implements DurableSessionStore {
|
|||||||
|
|
||||||
async create(identity: DurableSessionIdentity): Promise<void> {
|
async create(identity: DurableSessionIdentity): Promise<void> {
|
||||||
await this.db
|
await this.db
|
||||||
.insert(tessSessions)
|
.insert(interactionSessions)
|
||||||
.values({
|
.values({
|
||||||
id: identity.sessionId,
|
id: identity.sessionId,
|
||||||
|
agentName: identity.agentName,
|
||||||
tenantId: identity.tenantId,
|
tenantId: identity.tenantId,
|
||||||
ownerId: identity.ownerId,
|
ownerId: identity.ownerId,
|
||||||
providerId: identity.providerId,
|
providerId: identity.providerId,
|
||||||
@@ -60,25 +62,28 @@ export class TessDurableSessionRepository implements DurableSessionStore {
|
|||||||
const [inbox, outbox, checkpoints, handoffs] = await Promise.all([
|
const [inbox, outbox, checkpoints, handoffs] = await Promise.all([
|
||||||
this.db
|
this.db
|
||||||
.select()
|
.select()
|
||||||
.from(tessInbox)
|
.from(interactionInbox)
|
||||||
.where(eq(tessInbox.sessionId, sessionId))
|
.where(eq(interactionInbox.sessionId, sessionId))
|
||||||
.orderBy(asc(tessInbox.createdAt)),
|
.orderBy(asc(interactionInbox.createdAt)),
|
||||||
this.db
|
this.db
|
||||||
.select()
|
.select()
|
||||||
.from(tessOutbox)
|
.from(interactionOutbox)
|
||||||
.where(eq(tessOutbox.sessionId, sessionId))
|
.where(eq(interactionOutbox.sessionId, sessionId))
|
||||||
.orderBy(asc(tessOutbox.createdAt)),
|
.orderBy(asc(interactionOutbox.createdAt)),
|
||||||
this.db
|
this.db
|
||||||
.select()
|
.select()
|
||||||
.from(tessCheckpoints)
|
.from(interactionCheckpoints)
|
||||||
.where(eq(tessCheckpoints.sessionId, sessionId))
|
.where(eq(interactionCheckpoints.sessionId, sessionId))
|
||||||
.orderBy(desc(tessCheckpoints.compactionEpoch), desc(tessCheckpoints.createdAt))
|
.orderBy(
|
||||||
|
desc(interactionCheckpoints.compactionEpoch),
|
||||||
|
desc(interactionCheckpoints.createdAt),
|
||||||
|
)
|
||||||
.limit(1),
|
.limit(1),
|
||||||
this.db
|
this.db
|
||||||
.select()
|
.select()
|
||||||
.from(tessHandoffs)
|
.from(interactionHandoffs)
|
||||||
.where(eq(tessHandoffs.sessionId, sessionId))
|
.where(eq(interactionHandoffs.sessionId, sessionId))
|
||||||
.orderBy(asc(tessHandoffs.createdAt)),
|
.orderBy(asc(interactionHandoffs.createdAt)),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const checkpoint = checkpoints[0];
|
const checkpoint = checkpoints[0];
|
||||||
@@ -92,30 +97,36 @@ export class TessDurableSessionRepository implements DurableSessionStore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async enqueueInbox(input: DurableInboxInput): Promise<DurableEnqueueResult<DurableInboxStatus>> {
|
async enqueueInbox(input: DurableInboxInput): Promise<DurableEnqueueResult<DurableInboxStatus>> {
|
||||||
|
const digest = contentDigest(input.content);
|
||||||
const record: DurableInboxInput = {
|
const record: DurableInboxInput = {
|
||||||
...input,
|
...input,
|
||||||
content: redactSensitiveContent(input.content).content,
|
content: redactSensitiveContent(input.content).content,
|
||||||
};
|
};
|
||||||
const inserted = await this.db
|
const inserted = await this.db
|
||||||
.insert(tessInbox)
|
.insert(interactionInbox)
|
||||||
.values({ ...record, content: seal(record.content), status: 'pending' })
|
.values({
|
||||||
|
...record,
|
||||||
|
content: seal(record.content),
|
||||||
|
contentDigest: digest,
|
||||||
|
status: 'pending',
|
||||||
|
})
|
||||||
.onConflictDoNothing()
|
.onConflictDoNothing()
|
||||||
.returning({ status: tessInbox.status });
|
.returning({ status: interactionInbox.status });
|
||||||
if (inserted[0]) return { accepted: true, status: inserted[0].status };
|
if (inserted[0]) return { accepted: true, status: inserted[0].status };
|
||||||
|
|
||||||
const existing = await this.db
|
const existing = await this.db
|
||||||
.select()
|
.select()
|
||||||
.from(tessInbox)
|
.from(interactionInbox)
|
||||||
.where(
|
.where(
|
||||||
and(
|
and(
|
||||||
eq(tessInbox.sessionId, input.sessionId),
|
eq(interactionInbox.sessionId, input.sessionId),
|
||||||
eq(tessInbox.idempotencyKey, input.idempotencyKey),
|
eq(interactionInbox.idempotencyKey, input.idempotencyKey),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.limit(1);
|
.limit(1);
|
||||||
if (!existing[0]) throw new Error(`Durable Tess inbox enqueue failed: ${input.idempotencyKey}`);
|
if (!existing[0]) throw new Error(`Durable Tess inbox enqueue failed: ${input.idempotencyKey}`);
|
||||||
const entry = toInbox(existing[0]);
|
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}`);
|
throw new Error(`Durable Tess inbox idempotency conflict: ${input.idempotencyKey}`);
|
||||||
}
|
}
|
||||||
return { accepted: false, status: entry.status };
|
return { accepted: false, status: entry.status };
|
||||||
@@ -125,16 +136,18 @@ export class TessDurableSessionRepository implements DurableSessionStore {
|
|||||||
for (let attempt = 0; attempt < 3; attempt += 1) {
|
for (let attempt = 0; attempt < 3; attempt += 1) {
|
||||||
const candidate = await this.db
|
const candidate = await this.db
|
||||||
.select()
|
.select()
|
||||||
.from(tessInbox)
|
.from(interactionInbox)
|
||||||
.where(and(eq(tessInbox.sessionId, sessionId), eq(tessInbox.status, 'pending')))
|
.where(
|
||||||
.orderBy(asc(tessInbox.createdAt))
|
and(eq(interactionInbox.sessionId, sessionId), eq(interactionInbox.status, 'pending')),
|
||||||
|
)
|
||||||
|
.orderBy(asc(interactionInbox.createdAt))
|
||||||
.limit(1);
|
.limit(1);
|
||||||
const entry = candidate[0];
|
const entry = candidate[0];
|
||||||
if (!entry) return null;
|
if (!entry) return null;
|
||||||
const claimed = await this.db
|
const claimed = await this.db
|
||||||
.update(tessInbox)
|
.update(interactionInbox)
|
||||||
.set({ status: 'processing', updatedAt: new Date() })
|
.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();
|
.returning();
|
||||||
if (claimed[0]) return toInbox(claimed[0]);
|
if (claimed[0]) return toInbox(claimed[0]);
|
||||||
}
|
}
|
||||||
@@ -143,26 +156,26 @@ export class TessDurableSessionRepository implements DurableSessionStore {
|
|||||||
|
|
||||||
async completeInbox(sessionId: string, idempotencyKey: string): Promise<void> {
|
async completeInbox(sessionId: string, idempotencyKey: string): Promise<void> {
|
||||||
await this.db
|
await this.db
|
||||||
.update(tessInbox)
|
.update(interactionInbox)
|
||||||
.set({ status: 'processed', updatedAt: new Date() })
|
.set({ status: 'processed', updatedAt: new Date() })
|
||||||
.where(
|
.where(
|
||||||
and(
|
and(
|
||||||
eq(tessInbox.sessionId, sessionId),
|
eq(interactionInbox.sessionId, sessionId),
|
||||||
eq(tessInbox.idempotencyKey, idempotencyKey),
|
eq(interactionInbox.idempotencyKey, idempotencyKey),
|
||||||
eq(tessInbox.status, 'processing'),
|
eq(interactionInbox.status, 'processing'),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async releaseInbox(sessionId: string, idempotencyKey: string): Promise<void> {
|
async releaseInbox(sessionId: string, idempotencyKey: string): Promise<void> {
|
||||||
await this.db
|
await this.db
|
||||||
.update(tessInbox)
|
.update(interactionInbox)
|
||||||
.set({ status: 'pending', updatedAt: new Date() })
|
.set({ status: 'pending', updatedAt: new Date() })
|
||||||
.where(
|
.where(
|
||||||
and(
|
and(
|
||||||
eq(tessInbox.sessionId, sessionId),
|
eq(interactionInbox.sessionId, sessionId),
|
||||||
eq(tessInbox.idempotencyKey, idempotencyKey),
|
eq(interactionInbox.idempotencyKey, idempotencyKey),
|
||||||
eq(tessInbox.status, 'processing'),
|
eq(interactionInbox.status, 'processing'),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -170,31 +183,37 @@ export class TessDurableSessionRepository implements DurableSessionStore {
|
|||||||
async enqueueOutbox(
|
async enqueueOutbox(
|
||||||
input: DurableOutboxInput,
|
input: DurableOutboxInput,
|
||||||
): Promise<DurableEnqueueResult<DurableOutboxStatus>> {
|
): Promise<DurableEnqueueResult<DurableOutboxStatus>> {
|
||||||
|
const digest = contentDigest(input.content);
|
||||||
const record: DurableOutboxInput = {
|
const record: DurableOutboxInput = {
|
||||||
...input,
|
...input,
|
||||||
content: redactSensitiveContent(input.content).content,
|
content: redactSensitiveContent(input.content).content,
|
||||||
};
|
};
|
||||||
const inserted = await this.db
|
const inserted = await this.db
|
||||||
.insert(tessOutbox)
|
.insert(interactionOutbox)
|
||||||
.values({ ...record, content: seal(record.content), status: 'pending' })
|
.values({
|
||||||
|
...record,
|
||||||
|
content: seal(record.content),
|
||||||
|
contentDigest: digest,
|
||||||
|
status: 'pending',
|
||||||
|
})
|
||||||
.onConflictDoNothing()
|
.onConflictDoNothing()
|
||||||
.returning({ status: tessOutbox.status });
|
.returning({ status: interactionOutbox.status });
|
||||||
if (inserted[0]) return { accepted: true, status: inserted[0].status };
|
if (inserted[0]) return { accepted: true, status: inserted[0].status };
|
||||||
|
|
||||||
const existing = await this.db
|
const existing = await this.db
|
||||||
.select()
|
.select()
|
||||||
.from(tessOutbox)
|
.from(interactionOutbox)
|
||||||
.where(
|
.where(
|
||||||
and(
|
and(
|
||||||
eq(tessOutbox.sessionId, input.sessionId),
|
eq(interactionOutbox.sessionId, input.sessionId),
|
||||||
eq(tessOutbox.idempotencyKey, input.idempotencyKey),
|
eq(interactionOutbox.idempotencyKey, input.idempotencyKey),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.limit(1);
|
.limit(1);
|
||||||
if (!existing[0])
|
if (!existing[0])
|
||||||
throw new Error(`Durable Tess outbox enqueue failed: ${input.idempotencyKey}`);
|
throw new Error(`Durable Tess outbox enqueue failed: ${input.idempotencyKey}`);
|
||||||
const entry = toOutbox(existing[0]);
|
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}`);
|
throw new Error(`Durable Tess outbox idempotency conflict: ${input.idempotencyKey}`);
|
||||||
}
|
}
|
||||||
return { accepted: false, status: entry.status };
|
return { accepted: false, status: entry.status };
|
||||||
@@ -204,16 +223,18 @@ export class TessDurableSessionRepository implements DurableSessionStore {
|
|||||||
for (let attempt = 0; attempt < 3; attempt += 1) {
|
for (let attempt = 0; attempt < 3; attempt += 1) {
|
||||||
const candidate = await this.db
|
const candidate = await this.db
|
||||||
.select()
|
.select()
|
||||||
.from(tessOutbox)
|
.from(interactionOutbox)
|
||||||
.where(and(eq(tessOutbox.sessionId, sessionId), eq(tessOutbox.status, 'pending')))
|
.where(
|
||||||
.orderBy(asc(tessOutbox.createdAt))
|
and(eq(interactionOutbox.sessionId, sessionId), eq(interactionOutbox.status, 'pending')),
|
||||||
|
)
|
||||||
|
.orderBy(asc(interactionOutbox.createdAt))
|
||||||
.limit(1);
|
.limit(1);
|
||||||
const entry = candidate[0];
|
const entry = candidate[0];
|
||||||
if (!entry) return null;
|
if (!entry) return null;
|
||||||
const claimed = await this.db
|
const claimed = await this.db
|
||||||
.update(tessOutbox)
|
.update(interactionOutbox)
|
||||||
.set({ status: 'processing', updatedAt: new Date() })
|
.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();
|
.returning();
|
||||||
if (claimed[0]) return toOutbox(claimed[0]);
|
if (claimed[0]) return toOutbox(claimed[0]);
|
||||||
}
|
}
|
||||||
@@ -225,13 +246,13 @@ export class TessDurableSessionRepository implements DurableSessionStore {
|
|||||||
idempotencyKey: string,
|
idempotencyKey: string,
|
||||||
): Promise<DurableOutboxEntry | null> {
|
): Promise<DurableOutboxEntry | null> {
|
||||||
const claimed = await this.db
|
const claimed = await this.db
|
||||||
.update(tessOutbox)
|
.update(interactionOutbox)
|
||||||
.set({ status: 'processing', updatedAt: new Date() })
|
.set({ status: 'processing', updatedAt: new Date() })
|
||||||
.where(
|
.where(
|
||||||
and(
|
and(
|
||||||
eq(tessOutbox.sessionId, sessionId),
|
eq(interactionOutbox.sessionId, sessionId),
|
||||||
eq(tessOutbox.idempotencyKey, idempotencyKey),
|
eq(interactionOutbox.idempotencyKey, idempotencyKey),
|
||||||
eq(tessOutbox.status, 'pending'),
|
eq(interactionOutbox.status, 'pending'),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.returning();
|
.returning();
|
||||||
@@ -240,26 +261,26 @@ export class TessDurableSessionRepository implements DurableSessionStore {
|
|||||||
|
|
||||||
async completeOutbox(sessionId: string, idempotencyKey: string): Promise<void> {
|
async completeOutbox(sessionId: string, idempotencyKey: string): Promise<void> {
|
||||||
await this.db
|
await this.db
|
||||||
.update(tessOutbox)
|
.update(interactionOutbox)
|
||||||
.set({ status: 'delivered', updatedAt: new Date() })
|
.set({ status: 'delivered', updatedAt: new Date() })
|
||||||
.where(
|
.where(
|
||||||
and(
|
and(
|
||||||
eq(tessOutbox.sessionId, sessionId),
|
eq(interactionOutbox.sessionId, sessionId),
|
||||||
eq(tessOutbox.idempotencyKey, idempotencyKey),
|
eq(interactionOutbox.idempotencyKey, idempotencyKey),
|
||||||
eq(tessOutbox.status, 'processing'),
|
eq(interactionOutbox.status, 'processing'),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async releaseOutbox(sessionId: string, idempotencyKey: string): Promise<void> {
|
async releaseOutbox(sessionId: string, idempotencyKey: string): Promise<void> {
|
||||||
await this.db
|
await this.db
|
||||||
.update(tessOutbox)
|
.update(interactionOutbox)
|
||||||
.set({ status: 'pending', updatedAt: new Date() })
|
.set({ status: 'pending', updatedAt: new Date() })
|
||||||
.where(
|
.where(
|
||||||
and(
|
and(
|
||||||
eq(tessOutbox.sessionId, sessionId),
|
eq(interactionOutbox.sessionId, sessionId),
|
||||||
eq(tessOutbox.idempotencyKey, idempotencyKey),
|
eq(interactionOutbox.idempotencyKey, idempotencyKey),
|
||||||
eq(tessOutbox.status, 'processing'),
|
eq(interactionOutbox.status, 'processing'),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -271,14 +292,14 @@ export class TessDurableSessionRepository implements DurableSessionStore {
|
|||||||
summary: redactSensitiveContent(input.summary).content,
|
summary: redactSensitiveContent(input.summary).content,
|
||||||
};
|
};
|
||||||
const inserted = await this.db
|
const inserted = await this.db
|
||||||
.insert(tessCheckpoints)
|
.insert(interactionCheckpoints)
|
||||||
.values({
|
.values({
|
||||||
...checkpoint,
|
...checkpoint,
|
||||||
cursor: seal(checkpoint.cursor),
|
cursor: seal(checkpoint.cursor),
|
||||||
summary: seal(checkpoint.summary),
|
summary: seal(checkpoint.summary),
|
||||||
})
|
})
|
||||||
.onConflictDoNothing()
|
.onConflictDoNothing()
|
||||||
.returning({ checkpointId: tessCheckpoints.checkpointId });
|
.returning({ checkpointId: interactionCheckpoints.checkpointId });
|
||||||
if (inserted[0]) return;
|
if (inserted[0]) return;
|
||||||
|
|
||||||
const existing = await this.findCheckpoint(input.sessionId, input.checkpointId);
|
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<DurableCheckpoint | null> {
|
async findCheckpoint(sessionId: string, checkpointId: string): Promise<DurableCheckpoint | null> {
|
||||||
const checkpoints = await this.db
|
const checkpoints = await this.db
|
||||||
.select()
|
.select()
|
||||||
.from(tessCheckpoints)
|
.from(interactionCheckpoints)
|
||||||
.where(
|
.where(
|
||||||
and(
|
and(
|
||||||
eq(tessCheckpoints.sessionId, sessionId),
|
eq(interactionCheckpoints.sessionId, sessionId),
|
||||||
eq(tessCheckpoints.checkpointId, checkpointId),
|
eq(interactionCheckpoints.checkpointId, checkpointId),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.limit(1);
|
.limit(1);
|
||||||
@@ -308,10 +329,10 @@ export class TessDurableSessionRepository implements DurableSessionStore {
|
|||||||
throw new Error(`Durable Tess handoff checkpoint is unavailable: ${input.checkpointId}`);
|
throw new Error(`Durable Tess handoff checkpoint is unavailable: ${input.checkpointId}`);
|
||||||
}
|
}
|
||||||
const inserted = await this.db
|
const inserted = await this.db
|
||||||
.insert(tessHandoffs)
|
.insert(interactionHandoffs)
|
||||||
.values({ ...input })
|
.values({ ...input })
|
||||||
.onConflictDoNothing()
|
.onConflictDoNothing()
|
||||||
.returning({ handoffId: tessHandoffs.handoffId });
|
.returning({ handoffId: interactionHandoffs.handoffId });
|
||||||
if (inserted[0]) return;
|
if (inserted[0]) return;
|
||||||
|
|
||||||
const existing = await this.findHandoff(input.handoffId);
|
const existing = await this.findHandoff(input.handoffId);
|
||||||
@@ -323,8 +344,8 @@ export class TessDurableSessionRepository implements DurableSessionStore {
|
|||||||
async findHandoff(handoffId: string): Promise<DurableHandoff | null> {
|
async findHandoff(handoffId: string): Promise<DurableHandoff | null> {
|
||||||
const handoffs = await this.db
|
const handoffs = await this.db
|
||||||
.select()
|
.select()
|
||||||
.from(tessHandoffs)
|
.from(interactionHandoffs)
|
||||||
.where(eq(tessHandoffs.handoffId, handoffId))
|
.where(eq(interactionHandoffs.handoffId, handoffId))
|
||||||
.limit(1);
|
.limit(1);
|
||||||
const handoff = handoffs[0];
|
const handoff = handoffs[0];
|
||||||
return handoff ? toHandoff(handoff) : null;
|
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
|
// reached an external target before a crash, so it is deliberately not
|
||||||
// replayed by generic recovery.
|
// replayed by generic recovery.
|
||||||
await this.db
|
await this.db
|
||||||
.update(tessInbox)
|
.update(interactionInbox)
|
||||||
.set({ status: 'pending', updatedAt: new Date() })
|
.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<DurableSessionIdentity | null> {
|
private async session(sessionId: string): Promise<DurableSessionIdentity | null> {
|
||||||
const sessions = await this.db
|
const sessions = await this.db
|
||||||
.select()
|
.select()
|
||||||
.from(tessSessions)
|
.from(interactionSessions)
|
||||||
.where(eq(tessSessions.id, sessionId))
|
.where(eq(interactionSessions.id, sessionId))
|
||||||
.limit(1);
|
.limit(1);
|
||||||
const session = sessions[0];
|
const session = sessions[0];
|
||||||
return session
|
return session
|
||||||
? {
|
? {
|
||||||
|
agentName: session.agentName,
|
||||||
sessionId: session.id,
|
sessionId: session.id,
|
||||||
tenantId: session.tenantId,
|
tenantId: session.tenantId,
|
||||||
ownerId: session.ownerId,
|
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 {
|
function sameIdentity(left: DurableSessionIdentity, right: DurableSessionIdentity): boolean {
|
||||||
return (
|
return (
|
||||||
|
left.agentName === right.agentName &&
|
||||||
left.sessionId === right.sessionId &&
|
left.sessionId === right.sessionId &&
|
||||||
left.tenantId === right.tenantId &&
|
left.tenantId === right.tenantId &&
|
||||||
left.ownerId === right.ownerId &&
|
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 {
|
return {
|
||||||
sessionId: row.sessionId,
|
sessionId: row.sessionId,
|
||||||
idempotencyKey: row.idempotencyKey,
|
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 {
|
return {
|
||||||
sessionId: row.sessionId,
|
sessionId: row.sessionId,
|
||||||
idempotencyKey: row.idempotencyKey,
|
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 {
|
return {
|
||||||
sessionId: row.sessionId,
|
sessionId: row.sessionId,
|
||||||
checkpointId: row.checkpointId,
|
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 {
|
return {
|
||||||
sessionId: row.sessionId,
|
sessionId: row.sessionId,
|
||||||
handoffId: row.handoffId,
|
handoffId: row.handoffId,
|
||||||
|
|||||||
@@ -70,11 +70,12 @@ describe('CommandAuthorizationService', () => {
|
|||||||
tenantId: 'tenant-1',
|
tenantId: 'tenant-1',
|
||||||
channelId: 'discord:operator',
|
channelId: 'discord:operator',
|
||||||
correlationId: 'correlation-malformed-expiry',
|
correlationId: 'correlation-malformed-expiry',
|
||||||
|
agentName: 'Nova',
|
||||||
};
|
};
|
||||||
const service = createService('admin', entries);
|
const service = createService('admin', entries);
|
||||||
const approval = await service.createRuntimeTerminationApproval(action);
|
const approval = await service.createRuntimeTerminationApproval(action);
|
||||||
expect(approval).not.toBeNull();
|
expect(approval).not.toBeNull();
|
||||||
const key = `tess:command-approval:${approval!.approvalId}`;
|
const key = `agent:Nova:command-approval:${approval!.approvalId}`;
|
||||||
const stored = entries.get(key);
|
const stored = entries.get(key);
|
||||||
expect(stored).toBeDefined();
|
expect(stored).toBeDefined();
|
||||||
entries.set(key, JSON.stringify({ ...JSON.parse(stored!), expiresAt: 'not-a-date' }));
|
entries.set(key, JSON.stringify({ ...JSON.parse(stored!), expiresAt: 'not-a-date' }));
|
||||||
@@ -93,6 +94,7 @@ describe('CommandAuthorizationService', () => {
|
|||||||
tenantId: 'tenant-1',
|
tenantId: 'tenant-1',
|
||||||
channelId: 'discord:operator',
|
channelId: 'discord:operator',
|
||||||
correlationId: 'correlation-1',
|
correlationId: 'correlation-1',
|
||||||
|
agentName: 'Nova',
|
||||||
};
|
};
|
||||||
const beforeRestart = createService('admin', entries);
|
const beforeRestart = createService('admin', entries);
|
||||||
const approval = await beforeRestart.createRuntimeTerminationApproval(action);
|
const approval = await beforeRestart.createRuntimeTerminationApproval(action);
|
||||||
|
|||||||
@@ -23,6 +23,8 @@ export interface RuntimeTerminationApprovalAction {
|
|||||||
tenantId: string;
|
tenantId: string;
|
||||||
channelId: string;
|
channelId: string;
|
||||||
correlationId: string;
|
correlationId: string;
|
||||||
|
/** Provisioned roster identity; isolates approvals between interaction agents. */
|
||||||
|
agentName: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RuntimeTerminationApproval extends RuntimeTerminationApprovalAction {
|
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.
|
* command approvals. This deliberately avoids a parallel approval database.
|
||||||
*/
|
*/
|
||||||
async createRuntimeTerminationApproval(
|
async createRuntimeTerminationApproval(
|
||||||
@@ -108,7 +110,12 @@ export class CommandAuthorizationService {
|
|||||||
...action,
|
...action,
|
||||||
expiresAt: new Date(Date.now() + 5 * 60_000).toISOString(),
|
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;
|
return approval;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -116,7 +123,7 @@ export class CommandAuthorizationService {
|
|||||||
approvalId: string,
|
approvalId: string,
|
||||||
action: RuntimeTerminationApprovalAction,
|
action: RuntimeTerminationApprovalAction,
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
const encoded = await this.redis.get(this.key(approvalId));
|
const encoded = await this.redis.get(this.runtimeKey(action.agentName, approvalId));
|
||||||
if (!encoded) return false;
|
if (!encoded) return false;
|
||||||
let approval: unknown;
|
let approval: unknown;
|
||||||
try {
|
try {
|
||||||
@@ -134,7 +141,7 @@ export class CommandAuthorizationService {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if ((await this.resolveRole(approval.actorId)) !== 'admin') 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<CommandRole | null> {
|
private async resolveRole(actorId: string): Promise<CommandRole | null> {
|
||||||
@@ -196,6 +203,7 @@ export class CommandAuthorizationService {
|
|||||||
action.tenantId,
|
action.tenantId,
|
||||||
action.channelId,
|
action.channelId,
|
||||||
action.correlationId,
|
action.correlationId,
|
||||||
|
action.agentName,
|
||||||
].every((value: string): boolean => value.trim().length > 0);
|
].every((value: string): boolean => value.trim().length > 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -209,6 +217,7 @@ export class CommandAuthorizationService {
|
|||||||
tenantId: action.tenantId,
|
tenantId: action.tenantId,
|
||||||
channelId: action.channelId,
|
channelId: action.channelId,
|
||||||
correlationId: action.correlationId,
|
correlationId: action.correlationId,
|
||||||
|
agentName: action.agentName,
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.digest('hex');
|
.digest('hex');
|
||||||
@@ -244,11 +253,16 @@ export class CommandAuthorizationService {
|
|||||||
'sessionId' in value &&
|
'sessionId' in value &&
|
||||||
'channelId' in value &&
|
'channelId' in value &&
|
||||||
'correlationId' in value &&
|
'correlationId' in value &&
|
||||||
|
'agentName' in value &&
|
||||||
'expiresAt' in value
|
'expiresAt' in value
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private key(approvalId: string): string {
|
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}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { CommandAuthorizationService } from './command-authorization.service.js'
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Adapter from the provider registry's exact termination action to the shared,
|
* 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.
|
* persistence or replay semantics.
|
||||||
*/
|
*/
|
||||||
@Injectable()
|
@Injectable()
|
||||||
|
|||||||
@@ -108,7 +108,7 @@ describe('SessionGCService', () => {
|
|||||||
} as never;
|
} as never;
|
||||||
const payload = { command: 'gc', conversationId: 'owned' };
|
const payload = { command: 'gc', conversationId: 'owned' };
|
||||||
const approval = await authorization.createApproval(command, payload, 'admin-1');
|
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);
|
const gc = new SessionGCService(redis as never, mockLogService as unknown as LogService);
|
||||||
|
|
||||||
await gc.collect('owned');
|
await gc.collect('owned');
|
||||||
|
|||||||
@@ -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.
|
`@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
|
## Authority Model
|
||||||
|
|
||||||
@@ -61,7 +61,7 @@ Valkey holds the existing short-lived, one-time command-approval records; Postgr
|
|||||||
### M2 Durable Recovery
|
### M2 Durable Recovery
|
||||||
|
|
||||||
`@mosaicstack/agent` owns a transport-neutral state machine and `apps/gateway` provides its
|
`@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`.
|
per-session unique idempotency key and transition `pending → processing → processed|delivered`.
|
||||||
Checkpoints are immutable history scoped by session and checkpoint ID: the latest checkpoint
|
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.
|
supports compaction recovery, while a handoff always resolves the exact checkpoint it references.
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
} from './tess-durable-session.js';
|
} from './tess-durable-session.js';
|
||||||
|
|
||||||
const IDENTITY: DurableSessionIdentity = {
|
const IDENTITY: DurableSessionIdentity = {
|
||||||
|
agentName: 'Nova',
|
||||||
sessionId: 'tess-session-1',
|
sessionId: 'tess-session-1',
|
||||||
tenantId: 'tenant-1',
|
tenantId: 'tenant-1',
|
||||||
ownerId: 'owner-1',
|
ownerId: 'owner-1',
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
export interface DurableSessionIdentity {
|
export interface DurableSessionIdentity {
|
||||||
|
/** Provisioned roster identity; isolates durable state between named agents. */
|
||||||
|
agentName: string;
|
||||||
sessionId: string;
|
sessionId: string;
|
||||||
tenantId: string;
|
tenantId: string;
|
||||||
ownerId: string;
|
ownerId: string;
|
||||||
@@ -220,7 +222,13 @@ export class DurableSessionCoordinator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private assertIdentity(identity: DurableSessionIdentity): void {
|
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) {
|
if (identity.runtimeSessionId.trim().length === 0) {
|
||||||
throw new Error('Durable runtime session identity is required');
|
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 {
|
function identitiesEqual(left: DurableSessionIdentity, right: DurableSessionIdentity): boolean {
|
||||||
return (
|
return (
|
||||||
|
left.agentName === right.agentName &&
|
||||||
left.sessionId === right.sessionId &&
|
left.sessionId === right.sessionId &&
|
||||||
left.tenantId === right.tenantId &&
|
left.tenantId === right.tenantId &&
|
||||||
left.ownerId === right.ownerId &&
|
left.ownerId === right.ownerId &&
|
||||||
|
|||||||
71
packages/db/drizzle/0012_interaction_durable_state.sql
Normal file
71
packages/db/drizzle/0012_interaction_durable_state.sql
Normal file
@@ -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");
|
||||||
@@ -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");
|
|
||||||
@@ -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");
|
||||||
@@ -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");
|
|
||||||
@@ -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;
|
||||||
@@ -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;
|
|
||||||
@@ -3354,8 +3354,8 @@
|
|||||||
"checkConstraints": {},
|
"checkConstraints": {},
|
||||||
"isRLSEnabled": false
|
"isRLSEnabled": false
|
||||||
},
|
},
|
||||||
"public.tess_checkpoints": {
|
"public.interaction_checkpoints": {
|
||||||
"name": "tess_checkpoints",
|
"name": "interaction_checkpoints",
|
||||||
"schema": "",
|
"schema": "",
|
||||||
"columns": {
|
"columns": {
|
||||||
"session_id": {
|
"session_id": {
|
||||||
@@ -3405,10 +3405,10 @@
|
|||||||
},
|
},
|
||||||
"indexes": {},
|
"indexes": {},
|
||||||
"foreignKeys": {
|
"foreignKeys": {
|
||||||
"tess_checkpoints_session_id_tess_sessions_id_fk": {
|
"interaction_checkpoints_session_id_interaction_sessions_id_fk": {
|
||||||
"name": "tess_checkpoints_session_id_tess_sessions_id_fk",
|
"name": "interaction_checkpoints_session_id_interaction_sessions_id_fk",
|
||||||
"tableFrom": "tess_checkpoints",
|
"tableFrom": "interaction_checkpoints",
|
||||||
"tableTo": "tess_sessions",
|
"tableTo": "interaction_sessions",
|
||||||
"columnsFrom": [
|
"columnsFrom": [
|
||||||
"session_id"
|
"session_id"
|
||||||
],
|
],
|
||||||
@@ -3421,8 +3421,8 @@
|
|||||||
},
|
},
|
||||||
"compositePrimaryKeys": {},
|
"compositePrimaryKeys": {},
|
||||||
"uniqueConstraints": {
|
"uniqueConstraints": {
|
||||||
"tess_checkpoints_checkpoint_id_unique": {
|
"interaction_checkpoints_checkpoint_id_unique": {
|
||||||
"name": "tess_checkpoints_checkpoint_id_unique",
|
"name": "interaction_checkpoints_checkpoint_id_unique",
|
||||||
"nullsNotDistinct": false,
|
"nullsNotDistinct": false,
|
||||||
"columns": [
|
"columns": [
|
||||||
"checkpoint_id"
|
"checkpoint_id"
|
||||||
@@ -3433,8 +3433,8 @@
|
|||||||
"checkConstraints": {},
|
"checkConstraints": {},
|
||||||
"isRLSEnabled": false
|
"isRLSEnabled": false
|
||||||
},
|
},
|
||||||
"public.tess_handoffs": {
|
"public.interaction_handoffs": {
|
||||||
"name": "tess_handoffs",
|
"name": "interaction_handoffs",
|
||||||
"schema": "",
|
"schema": "",
|
||||||
"columns": {
|
"columns": {
|
||||||
"handoff_id": {
|
"handoff_id": {
|
||||||
@@ -3469,7 +3469,7 @@
|
|||||||
},
|
},
|
||||||
"status": {
|
"status": {
|
||||||
"name": "status",
|
"name": "status",
|
||||||
"type": "tess_handoff_status",
|
"type": "interaction_handoff_status",
|
||||||
"typeSchema": "public",
|
"typeSchema": "public",
|
||||||
"primaryKey": false,
|
"primaryKey": false,
|
||||||
"notNull": true,
|
"notNull": true,
|
||||||
@@ -3491,8 +3491,8 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"indexes": {
|
"indexes": {
|
||||||
"tess_handoffs_session_status_idx": {
|
"interaction_handoffs_session_status_idx": {
|
||||||
"name": "tess_handoffs_session_status_idx",
|
"name": "interaction_handoffs_session_status_idx",
|
||||||
"columns": [
|
"columns": [
|
||||||
{
|
{
|
||||||
"expression": "session_id",
|
"expression": "session_id",
|
||||||
@@ -3514,10 +3514,10 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"foreignKeys": {
|
"foreignKeys": {
|
||||||
"tess_handoffs_session_id_tess_sessions_id_fk": {
|
"interaction_handoffs_session_id_interaction_sessions_id_fk": {
|
||||||
"name": "tess_handoffs_session_id_tess_sessions_id_fk",
|
"name": "interaction_handoffs_session_id_interaction_sessions_id_fk",
|
||||||
"tableFrom": "tess_handoffs",
|
"tableFrom": "interaction_handoffs",
|
||||||
"tableTo": "tess_sessions",
|
"tableTo": "interaction_sessions",
|
||||||
"columnsFrom": [
|
"columnsFrom": [
|
||||||
"session_id"
|
"session_id"
|
||||||
],
|
],
|
||||||
@@ -3534,8 +3534,8 @@
|
|||||||
"checkConstraints": {},
|
"checkConstraints": {},
|
||||||
"isRLSEnabled": false
|
"isRLSEnabled": false
|
||||||
},
|
},
|
||||||
"public.tess_inbox": {
|
"public.interaction_inbox": {
|
||||||
"name": "tess_inbox",
|
"name": "interaction_inbox",
|
||||||
"schema": "",
|
"schema": "",
|
||||||
"columns": {
|
"columns": {
|
||||||
"id": {
|
"id": {
|
||||||
@@ -3571,7 +3571,7 @@
|
|||||||
},
|
},
|
||||||
"status": {
|
"status": {
|
||||||
"name": "status",
|
"name": "status",
|
||||||
"type": "tess_inbox_status",
|
"type": "interaction_inbox_status",
|
||||||
"typeSchema": "public",
|
"typeSchema": "public",
|
||||||
"primaryKey": false,
|
"primaryKey": false,
|
||||||
"notNull": true,
|
"notNull": true,
|
||||||
@@ -3593,8 +3593,8 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"indexes": {
|
"indexes": {
|
||||||
"tess_inbox_session_idempotency_idx": {
|
"interaction_inbox_session_idempotency_idx": {
|
||||||
"name": "tess_inbox_session_idempotency_idx",
|
"name": "interaction_inbox_session_idempotency_idx",
|
||||||
"columns": [
|
"columns": [
|
||||||
{
|
{
|
||||||
"expression": "session_id",
|
"expression": "session_id",
|
||||||
@@ -3614,8 +3614,8 @@
|
|||||||
"method": "btree",
|
"method": "btree",
|
||||||
"with": {}
|
"with": {}
|
||||||
},
|
},
|
||||||
"tess_inbox_session_status_created_idx": {
|
"interaction_inbox_session_status_created_idx": {
|
||||||
"name": "tess_inbox_session_status_created_idx",
|
"name": "interaction_inbox_session_status_created_idx",
|
||||||
"columns": [
|
"columns": [
|
||||||
{
|
{
|
||||||
"expression": "session_id",
|
"expression": "session_id",
|
||||||
@@ -3643,10 +3643,10 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"foreignKeys": {
|
"foreignKeys": {
|
||||||
"tess_inbox_session_id_tess_sessions_id_fk": {
|
"interaction_inbox_session_id_interaction_sessions_id_fk": {
|
||||||
"name": "tess_inbox_session_id_tess_sessions_id_fk",
|
"name": "interaction_inbox_session_id_interaction_sessions_id_fk",
|
||||||
"tableFrom": "tess_inbox",
|
"tableFrom": "interaction_inbox",
|
||||||
"tableTo": "tess_sessions",
|
"tableTo": "interaction_sessions",
|
||||||
"columnsFrom": [
|
"columnsFrom": [
|
||||||
"session_id"
|
"session_id"
|
||||||
],
|
],
|
||||||
@@ -3663,8 +3663,8 @@
|
|||||||
"checkConstraints": {},
|
"checkConstraints": {},
|
||||||
"isRLSEnabled": false
|
"isRLSEnabled": false
|
||||||
},
|
},
|
||||||
"public.tess_outbox": {
|
"public.interaction_outbox": {
|
||||||
"name": "tess_outbox",
|
"name": "interaction_outbox",
|
||||||
"schema": "",
|
"schema": "",
|
||||||
"columns": {
|
"columns": {
|
||||||
"id": {
|
"id": {
|
||||||
@@ -3706,7 +3706,7 @@
|
|||||||
},
|
},
|
||||||
"status": {
|
"status": {
|
||||||
"name": "status",
|
"name": "status",
|
||||||
"type": "tess_outbox_status",
|
"type": "interaction_outbox_status",
|
||||||
"typeSchema": "public",
|
"typeSchema": "public",
|
||||||
"primaryKey": false,
|
"primaryKey": false,
|
||||||
"notNull": true,
|
"notNull": true,
|
||||||
@@ -3728,8 +3728,8 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"indexes": {
|
"indexes": {
|
||||||
"tess_outbox_session_idempotency_idx": {
|
"interaction_outbox_session_idempotency_idx": {
|
||||||
"name": "tess_outbox_session_idempotency_idx",
|
"name": "interaction_outbox_session_idempotency_idx",
|
||||||
"columns": [
|
"columns": [
|
||||||
{
|
{
|
||||||
"expression": "session_id",
|
"expression": "session_id",
|
||||||
@@ -3749,8 +3749,8 @@
|
|||||||
"method": "btree",
|
"method": "btree",
|
||||||
"with": {}
|
"with": {}
|
||||||
},
|
},
|
||||||
"tess_outbox_session_status_created_idx": {
|
"interaction_outbox_session_status_created_idx": {
|
||||||
"name": "tess_outbox_session_status_created_idx",
|
"name": "interaction_outbox_session_status_created_idx",
|
||||||
"columns": [
|
"columns": [
|
||||||
{
|
{
|
||||||
"expression": "session_id",
|
"expression": "session_id",
|
||||||
@@ -3778,10 +3778,10 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"foreignKeys": {
|
"foreignKeys": {
|
||||||
"tess_outbox_session_id_tess_sessions_id_fk": {
|
"interaction_outbox_session_id_interaction_sessions_id_fk": {
|
||||||
"name": "tess_outbox_session_id_tess_sessions_id_fk",
|
"name": "interaction_outbox_session_id_interaction_sessions_id_fk",
|
||||||
"tableFrom": "tess_outbox",
|
"tableFrom": "interaction_outbox",
|
||||||
"tableTo": "tess_sessions",
|
"tableTo": "interaction_sessions",
|
||||||
"columnsFrom": [
|
"columnsFrom": [
|
||||||
"session_id"
|
"session_id"
|
||||||
],
|
],
|
||||||
@@ -3798,8 +3798,8 @@
|
|||||||
"checkConstraints": {},
|
"checkConstraints": {},
|
||||||
"isRLSEnabled": false
|
"isRLSEnabled": false
|
||||||
},
|
},
|
||||||
"public.tess_sessions": {
|
"public.interaction_sessions": {
|
||||||
"name": "tess_sessions",
|
"name": "interaction_sessions",
|
||||||
"schema": "",
|
"schema": "",
|
||||||
"columns": {
|
"columns": {
|
||||||
"id": {
|
"id": {
|
||||||
@@ -3849,9 +3849,9 @@
|
|||||||
},
|
},
|
||||||
"indexes": {},
|
"indexes": {},
|
||||||
"foreignKeys": {
|
"foreignKeys": {
|
||||||
"tess_sessions_owner_id_users_id_fk": {
|
"interaction_sessions_owner_id_users_id_fk": {
|
||||||
"name": "tess_sessions_owner_id_users_id_fk",
|
"name": "interaction_sessions_owner_id_users_id_fk",
|
||||||
"tableFrom": "tess_sessions",
|
"tableFrom": "interaction_sessions",
|
||||||
"tableTo": "users",
|
"tableTo": "users",
|
||||||
"columnsFrom": [
|
"columnsFrom": [
|
||||||
"owner_id"
|
"owner_id"
|
||||||
@@ -4132,16 +4132,16 @@
|
|||||||
"revoked"
|
"revoked"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"public.tess_handoff_status": {
|
"public.interaction_handoff_status": {
|
||||||
"name": "tess_handoff_status",
|
"name": "interaction_handoff_status",
|
||||||
"schema": "public",
|
"schema": "public",
|
||||||
"values": [
|
"values": [
|
||||||
"pending",
|
"pending",
|
||||||
"accepted"
|
"accepted"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"public.tess_inbox_status": {
|
"public.interaction_inbox_status": {
|
||||||
"name": "tess_inbox_status",
|
"name": "interaction_inbox_status",
|
||||||
"schema": "public",
|
"schema": "public",
|
||||||
"values": [
|
"values": [
|
||||||
"pending",
|
"pending",
|
||||||
@@ -4149,8 +4149,8 @@
|
|||||||
"processed"
|
"processed"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"public.tess_outbox_status": {
|
"public.interaction_outbox_status": {
|
||||||
"name": "tess_outbox_status",
|
"name": "interaction_outbox_status",
|
||||||
"schema": "public",
|
"schema": "public",
|
||||||
"values": [
|
"values": [
|
||||||
"pending",
|
"pending",
|
||||||
|
|||||||
@@ -3354,8 +3354,8 @@
|
|||||||
"checkConstraints": {},
|
"checkConstraints": {},
|
||||||
"isRLSEnabled": false
|
"isRLSEnabled": false
|
||||||
},
|
},
|
||||||
"public.tess_checkpoints": {
|
"public.interaction_checkpoints": {
|
||||||
"name": "tess_checkpoints",
|
"name": "interaction_checkpoints",
|
||||||
"schema": "",
|
"schema": "",
|
||||||
"columns": {
|
"columns": {
|
||||||
"id": {
|
"id": {
|
||||||
@@ -3411,8 +3411,8 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"indexes": {
|
"indexes": {
|
||||||
"tess_checkpoints_session_idempotency_idx": {
|
"interaction_checkpoints_session_idempotency_idx": {
|
||||||
"name": "tess_checkpoints_session_idempotency_idx",
|
"name": "interaction_checkpoints_session_idempotency_idx",
|
||||||
"columns": [
|
"columns": [
|
||||||
{
|
{
|
||||||
"expression": "session_id",
|
"expression": "session_id",
|
||||||
@@ -3432,8 +3432,8 @@
|
|||||||
"method": "btree",
|
"method": "btree",
|
||||||
"with": {}
|
"with": {}
|
||||||
},
|
},
|
||||||
"tess_checkpoints_session_epoch_idx": {
|
"interaction_checkpoints_session_epoch_idx": {
|
||||||
"name": "tess_checkpoints_session_epoch_idx",
|
"name": "interaction_checkpoints_session_epoch_idx",
|
||||||
"columns": [
|
"columns": [
|
||||||
{
|
{
|
||||||
"expression": "session_id",
|
"expression": "session_id",
|
||||||
@@ -3455,10 +3455,10 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"foreignKeys": {
|
"foreignKeys": {
|
||||||
"tess_checkpoints_session_id_tess_sessions_id_fk": {
|
"interaction_checkpoints_session_id_interaction_sessions_id_fk": {
|
||||||
"name": "tess_checkpoints_session_id_tess_sessions_id_fk",
|
"name": "interaction_checkpoints_session_id_interaction_sessions_id_fk",
|
||||||
"tableFrom": "tess_checkpoints",
|
"tableFrom": "interaction_checkpoints",
|
||||||
"tableTo": "tess_sessions",
|
"tableTo": "interaction_sessions",
|
||||||
"columnsFrom": [
|
"columnsFrom": [
|
||||||
"session_id"
|
"session_id"
|
||||||
],
|
],
|
||||||
@@ -3475,8 +3475,8 @@
|
|||||||
"checkConstraints": {},
|
"checkConstraints": {},
|
||||||
"isRLSEnabled": false
|
"isRLSEnabled": false
|
||||||
},
|
},
|
||||||
"public.tess_handoffs": {
|
"public.interaction_handoffs": {
|
||||||
"name": "tess_handoffs",
|
"name": "interaction_handoffs",
|
||||||
"schema": "",
|
"schema": "",
|
||||||
"columns": {
|
"columns": {
|
||||||
"handoff_id": {
|
"handoff_id": {
|
||||||
@@ -3511,7 +3511,7 @@
|
|||||||
},
|
},
|
||||||
"status": {
|
"status": {
|
||||||
"name": "status",
|
"name": "status",
|
||||||
"type": "tess_handoff_status",
|
"type": "interaction_handoff_status",
|
||||||
"typeSchema": "public",
|
"typeSchema": "public",
|
||||||
"primaryKey": false,
|
"primaryKey": false,
|
||||||
"notNull": true,
|
"notNull": true,
|
||||||
@@ -3533,8 +3533,8 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"indexes": {
|
"indexes": {
|
||||||
"tess_handoffs_session_status_idx": {
|
"interaction_handoffs_session_status_idx": {
|
||||||
"name": "tess_handoffs_session_status_idx",
|
"name": "interaction_handoffs_session_status_idx",
|
||||||
"columns": [
|
"columns": [
|
||||||
{
|
{
|
||||||
"expression": "session_id",
|
"expression": "session_id",
|
||||||
@@ -3556,10 +3556,10 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"foreignKeys": {
|
"foreignKeys": {
|
||||||
"tess_handoffs_session_id_tess_sessions_id_fk": {
|
"interaction_handoffs_session_id_interaction_sessions_id_fk": {
|
||||||
"name": "tess_handoffs_session_id_tess_sessions_id_fk",
|
"name": "interaction_handoffs_session_id_interaction_sessions_id_fk",
|
||||||
"tableFrom": "tess_handoffs",
|
"tableFrom": "interaction_handoffs",
|
||||||
"tableTo": "tess_sessions",
|
"tableTo": "interaction_sessions",
|
||||||
"columnsFrom": [
|
"columnsFrom": [
|
||||||
"session_id"
|
"session_id"
|
||||||
],
|
],
|
||||||
@@ -3576,8 +3576,8 @@
|
|||||||
"checkConstraints": {},
|
"checkConstraints": {},
|
||||||
"isRLSEnabled": false
|
"isRLSEnabled": false
|
||||||
},
|
},
|
||||||
"public.tess_inbox": {
|
"public.interaction_inbox": {
|
||||||
"name": "tess_inbox",
|
"name": "interaction_inbox",
|
||||||
"schema": "",
|
"schema": "",
|
||||||
"columns": {
|
"columns": {
|
||||||
"id": {
|
"id": {
|
||||||
@@ -3613,7 +3613,7 @@
|
|||||||
},
|
},
|
||||||
"status": {
|
"status": {
|
||||||
"name": "status",
|
"name": "status",
|
||||||
"type": "tess_inbox_status",
|
"type": "interaction_inbox_status",
|
||||||
"typeSchema": "public",
|
"typeSchema": "public",
|
||||||
"primaryKey": false,
|
"primaryKey": false,
|
||||||
"notNull": true,
|
"notNull": true,
|
||||||
@@ -3635,8 +3635,8 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"indexes": {
|
"indexes": {
|
||||||
"tess_inbox_session_idempotency_idx": {
|
"interaction_inbox_session_idempotency_idx": {
|
||||||
"name": "tess_inbox_session_idempotency_idx",
|
"name": "interaction_inbox_session_idempotency_idx",
|
||||||
"columns": [
|
"columns": [
|
||||||
{
|
{
|
||||||
"expression": "session_id",
|
"expression": "session_id",
|
||||||
@@ -3656,8 +3656,8 @@
|
|||||||
"method": "btree",
|
"method": "btree",
|
||||||
"with": {}
|
"with": {}
|
||||||
},
|
},
|
||||||
"tess_inbox_session_status_created_idx": {
|
"interaction_inbox_session_status_created_idx": {
|
||||||
"name": "tess_inbox_session_status_created_idx",
|
"name": "interaction_inbox_session_status_created_idx",
|
||||||
"columns": [
|
"columns": [
|
||||||
{
|
{
|
||||||
"expression": "session_id",
|
"expression": "session_id",
|
||||||
@@ -3685,10 +3685,10 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"foreignKeys": {
|
"foreignKeys": {
|
||||||
"tess_inbox_session_id_tess_sessions_id_fk": {
|
"interaction_inbox_session_id_interaction_sessions_id_fk": {
|
||||||
"name": "tess_inbox_session_id_tess_sessions_id_fk",
|
"name": "interaction_inbox_session_id_interaction_sessions_id_fk",
|
||||||
"tableFrom": "tess_inbox",
|
"tableFrom": "interaction_inbox",
|
||||||
"tableTo": "tess_sessions",
|
"tableTo": "interaction_sessions",
|
||||||
"columnsFrom": [
|
"columnsFrom": [
|
||||||
"session_id"
|
"session_id"
|
||||||
],
|
],
|
||||||
@@ -3705,8 +3705,8 @@
|
|||||||
"checkConstraints": {},
|
"checkConstraints": {},
|
||||||
"isRLSEnabled": false
|
"isRLSEnabled": false
|
||||||
},
|
},
|
||||||
"public.tess_outbox": {
|
"public.interaction_outbox": {
|
||||||
"name": "tess_outbox",
|
"name": "interaction_outbox",
|
||||||
"schema": "",
|
"schema": "",
|
||||||
"columns": {
|
"columns": {
|
||||||
"id": {
|
"id": {
|
||||||
@@ -3748,7 +3748,7 @@
|
|||||||
},
|
},
|
||||||
"status": {
|
"status": {
|
||||||
"name": "status",
|
"name": "status",
|
||||||
"type": "tess_outbox_status",
|
"type": "interaction_outbox_status",
|
||||||
"typeSchema": "public",
|
"typeSchema": "public",
|
||||||
"primaryKey": false,
|
"primaryKey": false,
|
||||||
"notNull": true,
|
"notNull": true,
|
||||||
@@ -3770,8 +3770,8 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"indexes": {
|
"indexes": {
|
||||||
"tess_outbox_session_idempotency_idx": {
|
"interaction_outbox_session_idempotency_idx": {
|
||||||
"name": "tess_outbox_session_idempotency_idx",
|
"name": "interaction_outbox_session_idempotency_idx",
|
||||||
"columns": [
|
"columns": [
|
||||||
{
|
{
|
||||||
"expression": "session_id",
|
"expression": "session_id",
|
||||||
@@ -3791,8 +3791,8 @@
|
|||||||
"method": "btree",
|
"method": "btree",
|
||||||
"with": {}
|
"with": {}
|
||||||
},
|
},
|
||||||
"tess_outbox_session_status_created_idx": {
|
"interaction_outbox_session_status_created_idx": {
|
||||||
"name": "tess_outbox_session_status_created_idx",
|
"name": "interaction_outbox_session_status_created_idx",
|
||||||
"columns": [
|
"columns": [
|
||||||
{
|
{
|
||||||
"expression": "session_id",
|
"expression": "session_id",
|
||||||
@@ -3820,10 +3820,10 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"foreignKeys": {
|
"foreignKeys": {
|
||||||
"tess_outbox_session_id_tess_sessions_id_fk": {
|
"interaction_outbox_session_id_interaction_sessions_id_fk": {
|
||||||
"name": "tess_outbox_session_id_tess_sessions_id_fk",
|
"name": "interaction_outbox_session_id_interaction_sessions_id_fk",
|
||||||
"tableFrom": "tess_outbox",
|
"tableFrom": "interaction_outbox",
|
||||||
"tableTo": "tess_sessions",
|
"tableTo": "interaction_sessions",
|
||||||
"columnsFrom": [
|
"columnsFrom": [
|
||||||
"session_id"
|
"session_id"
|
||||||
],
|
],
|
||||||
@@ -3840,8 +3840,8 @@
|
|||||||
"checkConstraints": {},
|
"checkConstraints": {},
|
||||||
"isRLSEnabled": false
|
"isRLSEnabled": false
|
||||||
},
|
},
|
||||||
"public.tess_sessions": {
|
"public.interaction_sessions": {
|
||||||
"name": "tess_sessions",
|
"name": "interaction_sessions",
|
||||||
"schema": "",
|
"schema": "",
|
||||||
"columns": {
|
"columns": {
|
||||||
"id": {
|
"id": {
|
||||||
@@ -3891,9 +3891,9 @@
|
|||||||
},
|
},
|
||||||
"indexes": {},
|
"indexes": {},
|
||||||
"foreignKeys": {
|
"foreignKeys": {
|
||||||
"tess_sessions_owner_id_users_id_fk": {
|
"interaction_sessions_owner_id_users_id_fk": {
|
||||||
"name": "tess_sessions_owner_id_users_id_fk",
|
"name": "interaction_sessions_owner_id_users_id_fk",
|
||||||
"tableFrom": "tess_sessions",
|
"tableFrom": "interaction_sessions",
|
||||||
"tableTo": "users",
|
"tableTo": "users",
|
||||||
"columnsFrom": [
|
"columnsFrom": [
|
||||||
"owner_id"
|
"owner_id"
|
||||||
@@ -4174,16 +4174,16 @@
|
|||||||
"revoked"
|
"revoked"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"public.tess_handoff_status": {
|
"public.interaction_handoff_status": {
|
||||||
"name": "tess_handoff_status",
|
"name": "interaction_handoff_status",
|
||||||
"schema": "public",
|
"schema": "public",
|
||||||
"values": [
|
"values": [
|
||||||
"pending",
|
"pending",
|
||||||
"accepted"
|
"accepted"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"public.tess_inbox_status": {
|
"public.interaction_inbox_status": {
|
||||||
"name": "tess_inbox_status",
|
"name": "interaction_inbox_status",
|
||||||
"schema": "public",
|
"schema": "public",
|
||||||
"values": [
|
"values": [
|
||||||
"pending",
|
"pending",
|
||||||
@@ -4191,8 +4191,8 @@
|
|||||||
"processed"
|
"processed"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"public.tess_outbox_status": {
|
"public.interaction_outbox_status": {
|
||||||
"name": "tess_outbox_status",
|
"name": "interaction_outbox_status",
|
||||||
"schema": "public",
|
"schema": "public",
|
||||||
"values": [
|
"values": [
|
||||||
"pending",
|
"pending",
|
||||||
|
|||||||
@@ -3354,8 +3354,8 @@
|
|||||||
"checkConstraints": {},
|
"checkConstraints": {},
|
||||||
"isRLSEnabled": false
|
"isRLSEnabled": false
|
||||||
},
|
},
|
||||||
"public.tess_checkpoints": {
|
"public.interaction_checkpoints": {
|
||||||
"name": "tess_checkpoints",
|
"name": "interaction_checkpoints",
|
||||||
"schema": "",
|
"schema": "",
|
||||||
"columns": {
|
"columns": {
|
||||||
"id": {
|
"id": {
|
||||||
@@ -3411,8 +3411,8 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"indexes": {
|
"indexes": {
|
||||||
"tess_checkpoints_session_idempotency_idx": {
|
"interaction_checkpoints_session_idempotency_idx": {
|
||||||
"name": "tess_checkpoints_session_idempotency_idx",
|
"name": "interaction_checkpoints_session_idempotency_idx",
|
||||||
"columns": [
|
"columns": [
|
||||||
{
|
{
|
||||||
"expression": "session_id",
|
"expression": "session_id",
|
||||||
@@ -3432,8 +3432,8 @@
|
|||||||
"method": "btree",
|
"method": "btree",
|
||||||
"with": {}
|
"with": {}
|
||||||
},
|
},
|
||||||
"tess_checkpoints_session_epoch_idx": {
|
"interaction_checkpoints_session_epoch_idx": {
|
||||||
"name": "tess_checkpoints_session_epoch_idx",
|
"name": "interaction_checkpoints_session_epoch_idx",
|
||||||
"columns": [
|
"columns": [
|
||||||
{
|
{
|
||||||
"expression": "session_id",
|
"expression": "session_id",
|
||||||
@@ -3455,10 +3455,10 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"foreignKeys": {
|
"foreignKeys": {
|
||||||
"tess_checkpoints_session_id_tess_sessions_id_fk": {
|
"interaction_checkpoints_session_id_interaction_sessions_id_fk": {
|
||||||
"name": "tess_checkpoints_session_id_tess_sessions_id_fk",
|
"name": "interaction_checkpoints_session_id_interaction_sessions_id_fk",
|
||||||
"tableFrom": "tess_checkpoints",
|
"tableFrom": "interaction_checkpoints",
|
||||||
"tableTo": "tess_sessions",
|
"tableTo": "interaction_sessions",
|
||||||
"columnsFrom": [
|
"columnsFrom": [
|
||||||
"session_id"
|
"session_id"
|
||||||
],
|
],
|
||||||
@@ -3475,8 +3475,8 @@
|
|||||||
"checkConstraints": {},
|
"checkConstraints": {},
|
||||||
"isRLSEnabled": false
|
"isRLSEnabled": false
|
||||||
},
|
},
|
||||||
"public.tess_handoffs": {
|
"public.interaction_handoffs": {
|
||||||
"name": "tess_handoffs",
|
"name": "interaction_handoffs",
|
||||||
"schema": "",
|
"schema": "",
|
||||||
"columns": {
|
"columns": {
|
||||||
"handoff_id": {
|
"handoff_id": {
|
||||||
@@ -3511,7 +3511,7 @@
|
|||||||
},
|
},
|
||||||
"status": {
|
"status": {
|
||||||
"name": "status",
|
"name": "status",
|
||||||
"type": "tess_handoff_status",
|
"type": "interaction_handoff_status",
|
||||||
"typeSchema": "public",
|
"typeSchema": "public",
|
||||||
"primaryKey": false,
|
"primaryKey": false,
|
||||||
"notNull": true,
|
"notNull": true,
|
||||||
@@ -3533,8 +3533,8 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"indexes": {
|
"indexes": {
|
||||||
"tess_handoffs_session_status_idx": {
|
"interaction_handoffs_session_status_idx": {
|
||||||
"name": "tess_handoffs_session_status_idx",
|
"name": "interaction_handoffs_session_status_idx",
|
||||||
"columns": [
|
"columns": [
|
||||||
{
|
{
|
||||||
"expression": "session_id",
|
"expression": "session_id",
|
||||||
@@ -3556,10 +3556,10 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"foreignKeys": {
|
"foreignKeys": {
|
||||||
"tess_handoffs_session_id_tess_sessions_id_fk": {
|
"interaction_handoffs_session_id_interaction_sessions_id_fk": {
|
||||||
"name": "tess_handoffs_session_id_tess_sessions_id_fk",
|
"name": "interaction_handoffs_session_id_interaction_sessions_id_fk",
|
||||||
"tableFrom": "tess_handoffs",
|
"tableFrom": "interaction_handoffs",
|
||||||
"tableTo": "tess_sessions",
|
"tableTo": "interaction_sessions",
|
||||||
"columnsFrom": [
|
"columnsFrom": [
|
||||||
"session_id"
|
"session_id"
|
||||||
],
|
],
|
||||||
@@ -3576,8 +3576,8 @@
|
|||||||
"checkConstraints": {},
|
"checkConstraints": {},
|
||||||
"isRLSEnabled": false
|
"isRLSEnabled": false
|
||||||
},
|
},
|
||||||
"public.tess_inbox": {
|
"public.interaction_inbox": {
|
||||||
"name": "tess_inbox",
|
"name": "interaction_inbox",
|
||||||
"schema": "",
|
"schema": "",
|
||||||
"columns": {
|
"columns": {
|
||||||
"id": {
|
"id": {
|
||||||
@@ -3613,7 +3613,7 @@
|
|||||||
},
|
},
|
||||||
"status": {
|
"status": {
|
||||||
"name": "status",
|
"name": "status",
|
||||||
"type": "tess_inbox_status",
|
"type": "interaction_inbox_status",
|
||||||
"typeSchema": "public",
|
"typeSchema": "public",
|
||||||
"primaryKey": false,
|
"primaryKey": false,
|
||||||
"notNull": true,
|
"notNull": true,
|
||||||
@@ -3635,8 +3635,8 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"indexes": {
|
"indexes": {
|
||||||
"tess_inbox_session_idempotency_idx": {
|
"interaction_inbox_session_idempotency_idx": {
|
||||||
"name": "tess_inbox_session_idempotency_idx",
|
"name": "interaction_inbox_session_idempotency_idx",
|
||||||
"columns": [
|
"columns": [
|
||||||
{
|
{
|
||||||
"expression": "session_id",
|
"expression": "session_id",
|
||||||
@@ -3656,8 +3656,8 @@
|
|||||||
"method": "btree",
|
"method": "btree",
|
||||||
"with": {}
|
"with": {}
|
||||||
},
|
},
|
||||||
"tess_inbox_session_status_created_idx": {
|
"interaction_inbox_session_status_created_idx": {
|
||||||
"name": "tess_inbox_session_status_created_idx",
|
"name": "interaction_inbox_session_status_created_idx",
|
||||||
"columns": [
|
"columns": [
|
||||||
{
|
{
|
||||||
"expression": "session_id",
|
"expression": "session_id",
|
||||||
@@ -3685,10 +3685,10 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"foreignKeys": {
|
"foreignKeys": {
|
||||||
"tess_inbox_session_id_tess_sessions_id_fk": {
|
"interaction_inbox_session_id_interaction_sessions_id_fk": {
|
||||||
"name": "tess_inbox_session_id_tess_sessions_id_fk",
|
"name": "interaction_inbox_session_id_interaction_sessions_id_fk",
|
||||||
"tableFrom": "tess_inbox",
|
"tableFrom": "interaction_inbox",
|
||||||
"tableTo": "tess_sessions",
|
"tableTo": "interaction_sessions",
|
||||||
"columnsFrom": [
|
"columnsFrom": [
|
||||||
"session_id"
|
"session_id"
|
||||||
],
|
],
|
||||||
@@ -3705,8 +3705,8 @@
|
|||||||
"checkConstraints": {},
|
"checkConstraints": {},
|
||||||
"isRLSEnabled": false
|
"isRLSEnabled": false
|
||||||
},
|
},
|
||||||
"public.tess_outbox": {
|
"public.interaction_outbox": {
|
||||||
"name": "tess_outbox",
|
"name": "interaction_outbox",
|
||||||
"schema": "",
|
"schema": "",
|
||||||
"columns": {
|
"columns": {
|
||||||
"id": {
|
"id": {
|
||||||
@@ -3754,7 +3754,7 @@
|
|||||||
},
|
},
|
||||||
"status": {
|
"status": {
|
||||||
"name": "status",
|
"name": "status",
|
||||||
"type": "tess_outbox_status",
|
"type": "interaction_outbox_status",
|
||||||
"typeSchema": "public",
|
"typeSchema": "public",
|
||||||
"primaryKey": false,
|
"primaryKey": false,
|
||||||
"notNull": true,
|
"notNull": true,
|
||||||
@@ -3776,8 +3776,8 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"indexes": {
|
"indexes": {
|
||||||
"tess_outbox_session_idempotency_idx": {
|
"interaction_outbox_session_idempotency_idx": {
|
||||||
"name": "tess_outbox_session_idempotency_idx",
|
"name": "interaction_outbox_session_idempotency_idx",
|
||||||
"columns": [
|
"columns": [
|
||||||
{
|
{
|
||||||
"expression": "session_id",
|
"expression": "session_id",
|
||||||
@@ -3797,8 +3797,8 @@
|
|||||||
"method": "btree",
|
"method": "btree",
|
||||||
"with": {}
|
"with": {}
|
||||||
},
|
},
|
||||||
"tess_outbox_session_status_created_idx": {
|
"interaction_outbox_session_status_created_idx": {
|
||||||
"name": "tess_outbox_session_status_created_idx",
|
"name": "interaction_outbox_session_status_created_idx",
|
||||||
"columns": [
|
"columns": [
|
||||||
{
|
{
|
||||||
"expression": "session_id",
|
"expression": "session_id",
|
||||||
@@ -3826,10 +3826,10 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"foreignKeys": {
|
"foreignKeys": {
|
||||||
"tess_outbox_session_id_tess_sessions_id_fk": {
|
"interaction_outbox_session_id_interaction_sessions_id_fk": {
|
||||||
"name": "tess_outbox_session_id_tess_sessions_id_fk",
|
"name": "interaction_outbox_session_id_interaction_sessions_id_fk",
|
||||||
"tableFrom": "tess_outbox",
|
"tableFrom": "interaction_outbox",
|
||||||
"tableTo": "tess_sessions",
|
"tableTo": "interaction_sessions",
|
||||||
"columnsFrom": [
|
"columnsFrom": [
|
||||||
"session_id"
|
"session_id"
|
||||||
],
|
],
|
||||||
@@ -3846,8 +3846,8 @@
|
|||||||
"checkConstraints": {},
|
"checkConstraints": {},
|
||||||
"isRLSEnabled": false
|
"isRLSEnabled": false
|
||||||
},
|
},
|
||||||
"public.tess_sessions": {
|
"public.interaction_sessions": {
|
||||||
"name": "tess_sessions",
|
"name": "interaction_sessions",
|
||||||
"schema": "",
|
"schema": "",
|
||||||
"columns": {
|
"columns": {
|
||||||
"id": {
|
"id": {
|
||||||
@@ -3897,9 +3897,9 @@
|
|||||||
},
|
},
|
||||||
"indexes": {},
|
"indexes": {},
|
||||||
"foreignKeys": {
|
"foreignKeys": {
|
||||||
"tess_sessions_owner_id_users_id_fk": {
|
"interaction_sessions_owner_id_users_id_fk": {
|
||||||
"name": "tess_sessions_owner_id_users_id_fk",
|
"name": "interaction_sessions_owner_id_users_id_fk",
|
||||||
"tableFrom": "tess_sessions",
|
"tableFrom": "interaction_sessions",
|
||||||
"tableTo": "users",
|
"tableTo": "users",
|
||||||
"columnsFrom": [
|
"columnsFrom": [
|
||||||
"owner_id"
|
"owner_id"
|
||||||
@@ -4180,16 +4180,16 @@
|
|||||||
"revoked"
|
"revoked"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"public.tess_handoff_status": {
|
"public.interaction_handoff_status": {
|
||||||
"name": "tess_handoff_status",
|
"name": "interaction_handoff_status",
|
||||||
"schema": "public",
|
"schema": "public",
|
||||||
"values": [
|
"values": [
|
||||||
"pending",
|
"pending",
|
||||||
"accepted"
|
"accepted"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"public.tess_inbox_status": {
|
"public.interaction_inbox_status": {
|
||||||
"name": "tess_inbox_status",
|
"name": "interaction_inbox_status",
|
||||||
"schema": "public",
|
"schema": "public",
|
||||||
"values": [
|
"values": [
|
||||||
"pending",
|
"pending",
|
||||||
@@ -4197,8 +4197,8 @@
|
|||||||
"processed"
|
"processed"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"public.tess_outbox_status": {
|
"public.interaction_outbox_status": {
|
||||||
"name": "tess_outbox_status",
|
"name": "interaction_outbox_status",
|
||||||
"schema": "public",
|
"schema": "public",
|
||||||
"values": [
|
"values": [
|
||||||
"pending",
|
"pending",
|
||||||
|
|||||||
@@ -90,21 +90,21 @@
|
|||||||
"idx": 12,
|
"idx": 12,
|
||||||
"version": "7",
|
"version": "7",
|
||||||
"when": 1783911983447,
|
"when": 1783911983447,
|
||||||
"tag": "0012_tess_durable_state",
|
"tag": "0012_interaction_durable_state",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"idx": 13,
|
"idx": 13,
|
||||||
"version": "7",
|
"version": "7",
|
||||||
"when": 1783913232578,
|
"when": 1783913232578,
|
||||||
"tag": "0013_tess_checkpoint_history",
|
"tag": "0013_interaction_checkpoint_history",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"idx": 14,
|
"idx": 14,
|
||||||
"version": "7",
|
"version": "7",
|
||||||
"when": 1783913398006,
|
"when": 1783913398006,
|
||||||
"tag": "0014_tess_outbox_channel_scope",
|
"tag": "0014_interaction_outbox_channel_scope",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -491,20 +491,24 @@ export const agentLogs = pgTable(
|
|||||||
// PostgreSQL is canonical for restart-safe Tess session recovery. The state
|
// PostgreSQL is canonical for restart-safe Tess session recovery. The state
|
||||||
// machine lives in @mosaicstack/agent; these records are its durable adapter.
|
// 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',
|
'pending',
|
||||||
'processing',
|
'processing',
|
||||||
'processed',
|
'processed',
|
||||||
]);
|
]);
|
||||||
export const tessOutboxStatusEnum = pgEnum('tess_outbox_status', [
|
export const interactionOutboxStatusEnum = pgEnum('interaction_outbox_status', [
|
||||||
'pending',
|
'pending',
|
||||||
'processing',
|
'processing',
|
||||||
'delivered',
|
'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(),
|
id: text('id').primaryKey(),
|
||||||
|
agentName: text('agent_name').notNull(),
|
||||||
tenantId: text('tenant_id').notNull(),
|
tenantId: text('tenant_id').notNull(),
|
||||||
ownerId: text('owner_id')
|
ownerId: text('owner_id')
|
||||||
.notNull()
|
.notNull()
|
||||||
@@ -515,55 +519,57 @@ export const tessSessions = pgTable('tess_sessions', {
|
|||||||
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
|
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const tessInbox = pgTable(
|
export const interactionInbox = pgTable(
|
||||||
'tess_inbox',
|
'interaction_inbox',
|
||||||
{
|
{
|
||||||
id: uuid('id').primaryKey().defaultRandom(),
|
id: uuid('id').primaryKey().defaultRandom(),
|
||||||
sessionId: text('session_id')
|
sessionId: text('session_id')
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => tessSessions.id, { onDelete: 'cascade' }),
|
.references(() => interactionSessions.id, { onDelete: 'cascade' }),
|
||||||
idempotencyKey: text('idempotency_key').notNull(),
|
idempotencyKey: text('idempotency_key').notNull(),
|
||||||
correlationId: text('correlation_id').notNull(),
|
correlationId: text('correlation_id').notNull(),
|
||||||
content: text('content').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(),
|
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
|
||||||
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
|
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
|
||||||
},
|
},
|
||||||
(t) => [
|
(t) => [
|
||||||
uniqueIndex('tess_inbox_session_idempotency_idx').on(t.sessionId, t.idempotencyKey),
|
uniqueIndex('interaction_inbox_session_idempotency_idx').on(t.sessionId, t.idempotencyKey),
|
||||||
index('tess_inbox_session_status_created_idx').on(t.sessionId, t.status, t.createdAt),
|
index('interaction_inbox_session_status_created_idx').on(t.sessionId, t.status, t.createdAt),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
export const tessOutbox = pgTable(
|
export const interactionOutbox = pgTable(
|
||||||
'tess_outbox',
|
'interaction_outbox',
|
||||||
{
|
{
|
||||||
id: uuid('id').primaryKey().defaultRandom(),
|
id: uuid('id').primaryKey().defaultRandom(),
|
||||||
sessionId: text('session_id')
|
sessionId: text('session_id')
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => tessSessions.id, { onDelete: 'cascade' }),
|
.references(() => interactionSessions.id, { onDelete: 'cascade' }),
|
||||||
idempotencyKey: text('idempotency_key').notNull(),
|
idempotencyKey: text('idempotency_key').notNull(),
|
||||||
correlationId: text('correlation_id').notNull(),
|
correlationId: text('correlation_id').notNull(),
|
||||||
channelId: text('channel_id').notNull(),
|
channelId: text('channel_id').notNull(),
|
||||||
kind: text('kind').notNull(),
|
kind: text('kind').notNull(),
|
||||||
content: text('content').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(),
|
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
|
||||||
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
|
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
|
||||||
},
|
},
|
||||||
(t) => [
|
(t) => [
|
||||||
uniqueIndex('tess_outbox_session_idempotency_idx').on(t.sessionId, t.idempotencyKey),
|
uniqueIndex('interaction_outbox_session_idempotency_idx').on(t.sessionId, t.idempotencyKey),
|
||||||
index('tess_outbox_session_status_created_idx').on(t.sessionId, t.status, t.createdAt),
|
index('interaction_outbox_session_status_created_idx').on(t.sessionId, t.status, t.createdAt),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
export const tessCheckpoints = pgTable(
|
export const interactionCheckpoints = pgTable(
|
||||||
'tess_checkpoints',
|
'interaction_checkpoints',
|
||||||
{
|
{
|
||||||
id: uuid('id').primaryKey().defaultRandom(),
|
id: uuid('id').primaryKey().defaultRandom(),
|
||||||
sessionId: text('session_id')
|
sessionId: text('session_id')
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => tessSessions.id, { onDelete: 'cascade' }),
|
.references(() => interactionSessions.id, { onDelete: 'cascade' }),
|
||||||
checkpointId: text('checkpoint_id').notNull(),
|
checkpointId: text('checkpoint_id').notNull(),
|
||||||
cursor: text('cursor').notNull(),
|
cursor: text('cursor').notNull(),
|
||||||
summary: text('summary').notNull(),
|
summary: text('summary').notNull(),
|
||||||
@@ -572,26 +578,26 @@ export const tessCheckpoints = pgTable(
|
|||||||
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
|
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
|
||||||
},
|
},
|
||||||
(t) => [
|
(t) => [
|
||||||
uniqueIndex('tess_checkpoints_session_idempotency_idx').on(t.sessionId, t.checkpointId),
|
uniqueIndex('interaction_checkpoints_session_idempotency_idx').on(t.sessionId, t.checkpointId),
|
||||||
index('tess_checkpoints_session_epoch_idx').on(t.sessionId, t.compactionEpoch),
|
index('interaction_checkpoints_session_epoch_idx').on(t.sessionId, t.compactionEpoch),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
export const tessHandoffs = pgTable(
|
export const interactionHandoffs = pgTable(
|
||||||
'tess_handoffs',
|
'interaction_handoffs',
|
||||||
{
|
{
|
||||||
handoffId: text('handoff_id').primaryKey(),
|
handoffId: text('handoff_id').primaryKey(),
|
||||||
sessionId: text('session_id')
|
sessionId: text('session_id')
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => tessSessions.id, { onDelete: 'cascade' }),
|
.references(() => interactionSessions.id, { onDelete: 'cascade' }),
|
||||||
destination: text('destination').notNull(),
|
destination: text('destination').notNull(),
|
||||||
correlationId: text('correlation_id').notNull(),
|
correlationId: text('correlation_id').notNull(),
|
||||||
checkpointId: text('checkpoint_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(),
|
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
|
||||||
updatedAt: timestamp('updated_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 ─────────────────────────────────────────────────────────────────
|
// ─── Skills ─────────────────────────────────────────────────────────────────
|
||||||
|
|||||||
Reference in New Issue
Block a user