Tools are materialized per connection (the tool rows are keyed by owner/integration/connection); an integration has no tools, it has the config that derives them. So the integration-side staleness marker is about the config that changed, not the tools — name it for the cause. The connection keeps tools_synced_at, which is accurate (connections do have tools). The comparison now reads tools_synced_at < config_revised_at: 'this connection's tools predate the current config'. Pre-release column, so the cloud migration + snapshot are edited in place; the libSQL/D1 hosts pick up the new name via boot schema-ensure.
Unified · Split
| @@ -1,4 +1,4 @@ | ||
| 1 | 1 | ALTER TABLE "connection" ADD COLUMN "description" text;--> statement-breakpoint |
| 2 | 2 | ALTER TABLE "connection" ADD COLUMN "tools_synced_at" bigint;--> statement-breakpoint |
| 3 | 3 | ALTER TABLE "integration" ADD COLUMN "name" text;--> statement-breakpoint |
| 4 | −ALTER TABLE "integration" ADD COLUMN "tools_revised_at" bigint; | |
| 4 | +ALTER TABLE "integration" ADD COLUMN "config_revised_at" bigint; | |
| @@ -497,8 +497,8 @@ · 496 unmodified lines | ||
| 497 | 497 | "primaryKey": false, |
| 498 | 498 | "notNull": true |
| 499 | 499 | }, |
| 500 | − "tools_revised_at": { | |
| 501 | − "name": "tools_revised_at", | |
| 500 | + "config_revised_at": { | |
| 501 | + "name": "config_revised_at", | |
| 502 | 502 | "type": "bigint", |
| 503 | 503 | "primaryKey": false, |
| 504 | 504 | "notNull": false |
| @@ -17,7 +17,7 @@ export const integration = pgTable( · 16 unmodified lines | ||
| 17 | 17 | plugin_id: text("plugin_id").notNull(), |
| 18 | 18 | name: text("name"), |
| 19 | 19 | description: text("description").notNull(), |
| 20 | − tools_revised_at: bigint("tools_revised_at", { mode: "bigint" }), | |
| 20 | + config_revised_at: bigint("config_revised_at", { mode: "bigint" }), | |
| 21 | 21 | config: json("config"), |
| 22 | 22 | can_remove: boolean("can_remove").notNull().default(true), |
| 23 | 23 | can_refresh: boolean("can_refresh").notNull().default(false), |
| @@ -3,7 +3,7 @@ · 2 unmodified lines | ||
| 3 | 3 | // the new tool catalog on that member's next read — not just the editor's. |
| 4 | 4 | // |
| 5 | 5 | // This is the lazy-convergence design. Tools are stored per connection. The |
| 6 | −// editor stamps the integration's `tools_revised_at`, but the owner policy lets | |
| 6 | +// editor stamps the integration's `config_revised_at`, but the owner policy lets | |
| 7 | 7 | // them rebuild catalogs only in their own partition — they cannot write a |
| 8 | 8 | // co-worker's personal connection rows. So each member's connection carries a |
| 9 | 9 | // `tools_synced_at` and rebuilds itself the next time THAT member lists tools. |
| @@ -23,13 +23,13 @@ const v1Tables = { · 22 unmodified lines | ||
| 23 | 23 | }; |
| 24 | 24 | |
| 25 | 25 | // The running schema adds two nullable columns the old file predates — the |
| 26 | −// shape of the integration-descriptions feature (name + tools_revised_at). | |
| 26 | +// shape of the integration-descriptions feature (name + config_revised_at). | |
| 27 | 27 | const v2Tables = { |
| 28 | 28 | integration: table("integration", { |
| 29 | 29 | id: idColumn("id", "varchar(255)"), |
| 30 | 30 | slug: column("slug", "varchar(255)"), |
| 31 | 31 | name: column("name", "string").nullable(), |
| 32 | − tools_revised_at: column("tools_revised_at", "bigint").nullable(), | |
| 32 | + config_revised_at: column("config_revised_at", "bigint").nullable(), | |
| 33 | 33 | }), |
| 34 | 34 | }; |
| 35 | 35 | |
| @@ -69,7 +69,7 @@ test("ensure evolves an existing table with the schema's new nullable columns", · 33 unmodified lines | ||
| 69 | 69 | }); |
| 70 | 70 | const evolved = await columnNames(client); |
| 71 | 71 | expect(evolved).toContain("name"); |
| 72 | − expect(evolved).toContain("tools_revised_at"); | |
| 72 | + expect(evolved).toContain("config_revised_at"); | |
| 73 | 73 | |
| 74 | 74 | // Idempotent: a second boot tolerates the now-duplicate columns. |
| 75 | 75 | await ensureDrizzleRuntimeSchemaFromTables(db, { |
| @@ -129,7 +129,7 @@ export const coreTables = defineTables({ · 128 unmodified lines | ||
| 129 | 129 | // template edit). Compared against each connection's `tools_synced_at` |
| 130 | 130 | // so OTHER subjects' connections — whose tool rows the updater cannot |
| 131 | 131 | // write under the owner policy — lazily rebuild on their next read. |
| 132 | − tools_revised_at: nullableBigintColumn("tools_revised_at"), | |
| 132 | + config_revised_at: nullableBigintColumn("config_revised_at"), | |
| 133 | 133 | can_remove: boolColumn("can_remove", true), |
| 134 | 134 | can_refresh: boolColumn("can_refresh", false), |
| 135 | 135 | created_at: dateColumn("created_at"), |
| @@ -156,7 +156,7 @@ export const coreTables = defineTables({ · 20 unmodified lines | ||
| 156 | 156 | // create, editable after; never reset by OAuth re-mints. |
| 157 | 157 | description: nullableTextColumn("description"), |
| 158 | 158 | // Epoch ms of the last tool (re)production for this connection. Stale |
| 159 | − // vs the integration's `tools_revised_at` → re-produced on next read. | |
| 159 | + // vs the integration's `config_revised_at` → re-produced on next read. | |
| 160 | 160 | tools_synced_at: nullableBigintColumn("tools_synced_at"), |
| 161 | 161 | oauth_client: nullableTextColumn("oauth_client"), |
| 162 | 162 | // The OWNER of `oauth_client` (a Personal connection may be minted through |
| @@ -1759,7 +1759,7 @@ export const createExecutor = <const TPlugins extends readonly AnyPlugin[] = rea · 1758 unmodified lines | ||
| 1759 | 1759 | // rebuild catalogs in its own partition (owner policy), so revise |
| 1760 | 1760 | // the integration: other subjects' connections compare this stamp |
| 1761 | 1761 | // against their `tools_synced_at` and lazily rebuild on next read. |
| 1762 | − set.tools_revised_at = now.getTime(); | |
| 1762 | + set.config_revised_at = now.getTime(); | |
| 1763 | 1763 | } |
| 1764 | 1764 | yield* core.updateMany("integration", { |
| 1765 | 1765 | where: (b: AnyCb) => b("slug", "=", String(slug)), |
| @@ -1837,7 +1837,7 @@ export const createExecutor = <const TPlugins extends readonly AnyPlugin[] = rea · 71 unmodified lines | ||
| 1837 | 1837 | b("connection", "=", String(ref.name)), |
| 1838 | 1838 | ); |
| 1839 | 1839 | // Every exit stamps the sync time — including the cleanup paths that |
| 1840 | − // produce zero tools — so the stale-catalog check (`tools_revised_at` | |
| 1840 | + // produce zero tools — so the stale-catalog check (`config_revised_at` | |
| 1841 | 1841 | // vs `tools_synced_at`) doesn't re-attempt this connection per read. |
| 1842 | 1842 | const stampSynced = core.updateMany("connection", { |
| 1843 | 1843 | where: (b: AnyCb) => |
| @@ -2403,11 +2403,11 @@ export const createExecutor = <const TPlugins extends readonly AnyPlugin[] = rea · 559 unmodified lines | ||
| 2403 | 2403 | // catalog in place and retries on the next read. |
| 2404 | 2404 | const syncStaleConnectionTools = Effect.gen(function* () { |
| 2405 | 2405 | const revised = yield* core.findMany("integration", { |
| 2406 | − where: (b: AnyCb) => b.isNotNull("tools_revised_at"), | |
| 2406 | + where: (b: AnyCb) => b.isNotNull("config_revised_at"), | |
| 2407 | 2407 | }); |
| 2408 | 2408 | if (revised.length === 0) return; |
| 2409 | 2409 | const revisedAt = new Map( |
| 2410 | − revised.map((row) => [row.slug, Number(row.tools_revised_at)] as const), | |
| 2410 | + revised.map((row) => [row.slug, Number(row.config_revised_at)] as const), | |
| 2411 | 2411 | ); |
| 2412 | 2412 | const connections = yield* core.findMany("connection", { |
| 2413 | 2413 | where: (b: AnyCb) => b.or(...revised.map((row) => b("integration", "=", row.slug))), |
| @@ -1 +1 @@ | ||
| 1 | −Subproject commit 894206f09681d77f1d9d708a8d49f1c7dc72fa1f | |
| 1 | +Subproject commit 36638da8c104e92cb3522e0dca336618cdfd8205 | |