feat(tess): persist durable session state
Some checks failed
ci/woodpecker/pr/ci Pipeline failed

This commit is contained in:
Jarvis
2026-07-12 23:10:21 -05:00
parent e3b5113be2
commit 102a7b606b
22 changed files with 14785 additions and 13 deletions

View File

@@ -0,0 +1,68 @@
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");

View File

@@ -0,0 +1,5 @@
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");

View File

@@ -0,0 +1,5 @@
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;

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -85,6 +85,27 @@
"when": 1782310438919,
"tag": "0011_bitter_gateway",
"breakpoints": true
},
{
"idx": 12,
"version": "7",
"when": 1783911983447,
"tag": "0012_tess_durable_state",
"breakpoints": true
},
{
"idx": 13,
"version": "7",
"when": 1783913232578,
"tag": "0013_tess_checkpoint_history",
"breakpoints": true
},
{
"idx": 14,
"version": "7",
"when": 1783913398006,
"tag": "0014_tess_outbox_channel_scope",
"breakpoints": true
}
]
}

View File

@@ -487,6 +487,113 @@ export const agentLogs = pgTable(
],
);
// ─── Tess durable session state ─────────────────────────────────────────────
// PostgreSQL is canonical for restart-safe Tess session recovery. The state
// machine lives in @mosaicstack/agent; these records are its durable adapter.
export const tessInboxStatusEnum = pgEnum('tess_inbox_status', [
'pending',
'processing',
'processed',
]);
export const tessOutboxStatusEnum = pgEnum('tess_outbox_status', [
'pending',
'processing',
'delivered',
]);
export const tessHandoffStatusEnum = pgEnum('tess_handoff_status', ['pending', 'accepted']);
export const tessSessions = pgTable('tess_sessions', {
id: text('id').primaryKey(),
tenantId: text('tenant_id').notNull(),
ownerId: text('owner_id')
.notNull()
.references(() => users.id, { onDelete: 'cascade' }),
providerId: text('provider_id').notNull(),
runtimeSessionId: text('runtime_session_id').notNull(),
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
});
export const tessInbox = pgTable(
'tess_inbox',
{
id: uuid('id').primaryKey().defaultRandom(),
sessionId: text('session_id')
.notNull()
.references(() => tessSessions.id, { onDelete: 'cascade' }),
idempotencyKey: text('idempotency_key').notNull(),
correlationId: text('correlation_id').notNull(),
content: text('content').notNull(),
status: tessInboxStatusEnum('status').notNull().default('pending'),
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
},
(t) => [
uniqueIndex('tess_inbox_session_idempotency_idx').on(t.sessionId, t.idempotencyKey),
index('tess_inbox_session_status_created_idx').on(t.sessionId, t.status, t.createdAt),
],
);
export const tessOutbox = pgTable(
'tess_outbox',
{
id: uuid('id').primaryKey().defaultRandom(),
sessionId: text('session_id')
.notNull()
.references(() => tessSessions.id, { onDelete: 'cascade' }),
idempotencyKey: text('idempotency_key').notNull(),
correlationId: text('correlation_id').notNull(),
channelId: text('channel_id').notNull(),
kind: text('kind').notNull(),
content: text('content').notNull(),
status: tessOutboxStatusEnum('status').notNull().default('pending'),
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
},
(t) => [
uniqueIndex('tess_outbox_session_idempotency_idx').on(t.sessionId, t.idempotencyKey),
index('tess_outbox_session_status_created_idx').on(t.sessionId, t.status, t.createdAt),
],
);
export const tessCheckpoints = pgTable(
'tess_checkpoints',
{
id: uuid('id').primaryKey().defaultRandom(),
sessionId: text('session_id')
.notNull()
.references(() => tessSessions.id, { onDelete: 'cascade' }),
checkpointId: text('checkpoint_id').notNull(),
cursor: text('cursor').notNull(),
summary: text('summary').notNull(),
compactionEpoch: integer('compaction_epoch').notNull(),
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
},
(t) => [
uniqueIndex('tess_checkpoints_session_idempotency_idx').on(t.sessionId, t.checkpointId),
index('tess_checkpoints_session_epoch_idx').on(t.sessionId, t.compactionEpoch),
],
);
export const tessHandoffs = pgTable(
'tess_handoffs',
{
handoffId: text('handoff_id').primaryKey(),
sessionId: text('session_id')
.notNull()
.references(() => tessSessions.id, { onDelete: 'cascade' }),
destination: text('destination').notNull(),
correlationId: text('correlation_id').notNull(),
checkpointId: text('checkpoint_id').notNull(),
status: tessHandoffStatusEnum('status').notNull().default('pending'),
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
},
(t) => [index('tess_handoffs_session_status_idx').on(t.sessionId, t.status)],
);
// ─── Skills ─────────────────────────────────────────────────────────────────
export const skills = pgTable(