@retinue/agentkit 0.3.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/server/bin.d.ts +53 -0
- package/dist/server/bin.js +91 -13
- package/dist/server/cli-worker.js +6 -3
- package/dist/server/cli.js +6 -4
- package/dist/server/config.d.ts +11 -1
- package/dist/server/config.js +18 -0
- package/dist/server/doctor.d.ts +13 -2
- package/dist/server/doctor.js +1 -1
- package/dist/server/pool.d.ts +47 -0
- package/dist/server/pool.js +73 -0
- package/package.json +1 -1
package/dist/server/bin.d.ts
CHANGED
|
@@ -15,5 +15,58 @@
|
|
|
15
15
|
* `migrate` and `doctor` deliberately need **no** app module: a database is provisioned before an application
|
|
16
16
|
* exists, and a diagnostic that cannot run until everything else is configured is a diagnostic nobody can use.
|
|
17
17
|
*/
|
|
18
|
+
/**
|
|
19
|
+
* The advisory-lock key comes from `schema.ts` — #252's AC-2, and #266's AC-4.
|
|
20
|
+
*
|
|
21
|
+
* It used to be defined here. That made "the CLI and `auto` mode use the same key" a property of two constants
|
|
22
|
+
* happening to be equal, and a copy that drifted would produce two locks, no serialisation, and the original
|
|
23
|
+
* crash returning with the fix apparently in place. Two constants that must be equal are one constant.
|
|
24
|
+
*/
|
|
25
|
+
/**
|
|
26
|
+
* Create the configured schema if it is not there, before anything tries to use it.
|
|
27
|
+
*
|
|
28
|
+
* `migrate` is the command that owns provisioning, so the namespace it was told to provision into is
|
|
29
|
+
* its job too — and the reason is worse than a missing-schema error. **Postgres does not error.**
|
|
30
|
+
* `SET search_path TO retinue, public` succeeds when `retinue` does not exist: a missing entry is
|
|
31
|
+
* skipped, not rejected. `CREATE TABLE conversations` then lands in the first schema that *does*
|
|
32
|
+
* exist, which is `public` — so all 34 platform migrations would silently be created alongside the
|
|
33
|
+
* product's tables, reporting success the whole way. Verified against a real Postgres 17: the SET
|
|
34
|
+
* returns, the CREATE returns, and `information_schema` says `public`.
|
|
35
|
+
*
|
|
36
|
+
* That is the failure this function exists to prevent, and it is why it runs before the pool rather
|
|
37
|
+
* than relying on a connection error that never comes.
|
|
38
|
+
*
|
|
39
|
+
* On its own connection with the **default** search path, which is the part that is easy to get wrong.
|
|
40
|
+
* A connection configured for `retinue` cannot be the one that creates `retinue` — `openPostgres`
|
|
41
|
+
* destroys it during setup, before a statement of ours runs.
|
|
42
|
+
*
|
|
43
|
+
* `IF NOT EXISTS` and nothing else: no owner, no grants, no drop. Provisioning a namespace is additive;
|
|
44
|
+
* deciding who may use it is a deployment's decision and not a migration's.
|
|
45
|
+
*/
|
|
46
|
+
/**
|
|
47
|
+
* Which `migrate` invocations must change nothing.
|
|
48
|
+
*
|
|
49
|
+
* Its own function because it is the link between a flag and a side effect, and that link is invisible
|
|
50
|
+
* to a test of either end: sabotaging it to `false` — so `--dry-run` provisions a schema — broke
|
|
51
|
+
* nothing, while every assertion about `ensureSchema` itself stayed green. The list is also the same
|
|
52
|
+
* one the read-only branch below uses, so a third flag added to one and not the other cannot silently
|
|
53
|
+
* become a writing dry run.
|
|
54
|
+
*/
|
|
55
|
+
export declare const READ_ONLY_FLAGS: readonly ["--status", "--dry-run"];
|
|
56
|
+
export declare const isReadOnly: (flags: ReadonlySet<string>) => boolean;
|
|
57
|
+
export declare const ensureSchema: (config: {
|
|
58
|
+
readonly databaseUrl: string;
|
|
59
|
+
readonly databaseSchema?: string;
|
|
60
|
+
}, { create, connect, }: {
|
|
61
|
+
readonly create: boolean;
|
|
62
|
+
readonly connect?: (settings: {
|
|
63
|
+
readonly databaseUrl: string;
|
|
64
|
+
}) => Promise<{
|
|
65
|
+
readonly sql: {
|
|
66
|
+
query<Row>(text: string, params?: readonly unknown[]): Promise<Row[]>;
|
|
67
|
+
};
|
|
68
|
+
readonly end: () => Promise<void>;
|
|
69
|
+
}>;
|
|
70
|
+
}) => Promise<boolean>;
|
|
18
71
|
export declare const main: (argv: readonly string[], env?: NodeJS.ProcessEnv) => Promise<number>;
|
|
19
72
|
//# sourceMappingURL=bin.d.ts.map
|
package/dist/server/bin.js
CHANGED
|
@@ -29,15 +29,20 @@ const USAGE = `retinue <command>
|
|
|
29
29
|
doctor Check configuration, database, schema and Redis. Reports every failure.
|
|
30
30
|
|
|
31
31
|
Configuration comes from the environment; see .env.example.`;
|
|
32
|
-
/**
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
32
|
+
/**
|
|
33
|
+
* Opened lazily and per command, so `doctor` and `migrate` never load a driver they do not use.
|
|
34
|
+
*
|
|
35
|
+
* The pool itself comes from `pool.ts`, which is also what the API host and the worker use — the schema
|
|
36
|
+
* a deployment configures has to be the same one migrations run against, and that is only true while
|
|
37
|
+
* there is one place that decides it.
|
|
38
|
+
*
|
|
39
|
+
* A connect timeout, because the default is *none*: `doctor` against a refused port sat silently instead
|
|
40
|
+
* of reporting the failure it exists to report. Short, since every command here either connects
|
|
41
|
+
* immediately or is misconfigured.
|
|
42
|
+
*/
|
|
43
|
+
const postgres = async (config) => {
|
|
44
|
+
const { openPostgres } = await import("./pool.js");
|
|
45
|
+
return openPostgres({ ...config, connectionTimeoutMillis: 5_000 });
|
|
41
46
|
};
|
|
42
47
|
/**
|
|
43
48
|
* The advisory-lock key comes from `schema.ts` — #252's AC-2, and #266's AC-4.
|
|
@@ -46,12 +51,85 @@ const postgres = async (url) => {
|
|
|
46
51
|
* happening to be equal, and a copy that drifted would produce two locks, no serialisation, and the original
|
|
47
52
|
* crash returning with the fix apparently in place. Two constants that must be equal are one constant.
|
|
48
53
|
*/
|
|
54
|
+
/**
|
|
55
|
+
* Create the configured schema if it is not there, before anything tries to use it.
|
|
56
|
+
*
|
|
57
|
+
* `migrate` is the command that owns provisioning, so the namespace it was told to provision into is
|
|
58
|
+
* its job too — and the reason is worse than a missing-schema error. **Postgres does not error.**
|
|
59
|
+
* `SET search_path TO retinue, public` succeeds when `retinue` does not exist: a missing entry is
|
|
60
|
+
* skipped, not rejected. `CREATE TABLE conversations` then lands in the first schema that *does*
|
|
61
|
+
* exist, which is `public` — so all 34 platform migrations would silently be created alongside the
|
|
62
|
+
* product's tables, reporting success the whole way. Verified against a real Postgres 17: the SET
|
|
63
|
+
* returns, the CREATE returns, and `information_schema` says `public`.
|
|
64
|
+
*
|
|
65
|
+
* That is the failure this function exists to prevent, and it is why it runs before the pool rather
|
|
66
|
+
* than relying on a connection error that never comes.
|
|
67
|
+
*
|
|
68
|
+
* On its own connection with the **default** search path, which is the part that is easy to get wrong.
|
|
69
|
+
* A connection configured for `retinue` cannot be the one that creates `retinue` — `openPostgres`
|
|
70
|
+
* destroys it during setup, before a statement of ours runs.
|
|
71
|
+
*
|
|
72
|
+
* `IF NOT EXISTS` and nothing else: no owner, no grants, no drop. Provisioning a namespace is additive;
|
|
73
|
+
* deciding who may use it is a deployment's decision and not a migration's.
|
|
74
|
+
*/
|
|
75
|
+
/**
|
|
76
|
+
* Which `migrate` invocations must change nothing.
|
|
77
|
+
*
|
|
78
|
+
* Its own function because it is the link between a flag and a side effect, and that link is invisible
|
|
79
|
+
* to a test of either end: sabotaging it to `false` — so `--dry-run` provisions a schema — broke
|
|
80
|
+
* nothing, while every assertion about `ensureSchema` itself stayed green. The list is also the same
|
|
81
|
+
* one the read-only branch below uses, so a third flag added to one and not the other cannot silently
|
|
82
|
+
* become a writing dry run.
|
|
83
|
+
*/
|
|
84
|
+
export const READ_ONLY_FLAGS = ["--status", "--dry-run"];
|
|
85
|
+
export const isReadOnly = (flags) => READ_ONLY_FLAGS.some((flag) => flags.has(flag));
|
|
86
|
+
export const ensureSchema = async (config, { create,
|
|
87
|
+
// Injectable so a test can run this against a real Postgres — PGlite — without a live server. The
|
|
88
|
+
// default is the same `postgres` every command here uses.
|
|
89
|
+
connect = (settings) => postgres(settings), }) => {
|
|
90
|
+
if (config.databaseSchema === undefined)
|
|
91
|
+
return true;
|
|
92
|
+
const { sql, end } = await connect({ databaseUrl: config.databaseUrl });
|
|
93
|
+
try {
|
|
94
|
+
if (create) {
|
|
95
|
+
// Validated as an unquoted identifier by `loadConfig` — the only reason this concatenation is safe,
|
|
96
|
+
// and the reason that check refuses anything Postgres would need quoted.
|
|
97
|
+
await sql.query(`CREATE SCHEMA IF NOT EXISTS ${config.databaseSchema}`);
|
|
98
|
+
console.log(`schema: ${config.databaseSchema} ready`);
|
|
99
|
+
return true;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* The read-only paths report instead of creating, and that is not fussiness.
|
|
103
|
+
*
|
|
104
|
+
* `--dry-run` and `--status` are documented a few lines below as side-effect free — the note says
|
|
105
|
+
* `public` has 0 tables after a dry run against a fresh database — and a reader deciding whether to
|
|
106
|
+
* trust a dry run is exactly the reader who must not find it provisioned a schema. Creating one is
|
|
107
|
+
* a small side effect and a large broken promise.
|
|
108
|
+
*/
|
|
109
|
+
const rows = await sql.query(`select exists (select 1 from pg_namespace where nspname = $1) as exists`, [config.databaseSchema]);
|
|
110
|
+
if (rows[0]?.exists === true)
|
|
111
|
+
return true;
|
|
112
|
+
console.error(`schema: ${config.databaseSchema} does not exist. Postgres will not complain — it skips a missing ` +
|
|
113
|
+
`entry in search_path — so tables would be created in the next schema on the path instead, ` +
|
|
114
|
+
`silently. Run \`retinue migrate\`, which creates it, or unset RETINUE_DATABASE_SCHEMA to use the ` +
|
|
115
|
+
`connection's own schema deliberately.`);
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
118
|
+
finally {
|
|
119
|
+
await end();
|
|
120
|
+
}
|
|
121
|
+
};
|
|
49
122
|
const migrate = async (flags, env = process.env) => {
|
|
50
123
|
const config = loadConfig(env);
|
|
51
|
-
|
|
124
|
+
// Before the pool, because a missing schema does not fail: `SET search_path` skips an entry that does
|
|
125
|
+
// not exist and every CREATE lands in `public` instead, silently. The read-only flags report rather
|
|
126
|
+
// than create — see `ensureSchema`.
|
|
127
|
+
if (!(await ensureSchema(config, { create: !isReadOnly(flags) })))
|
|
128
|
+
return 1;
|
|
129
|
+
const { sql, open, end } = await postgres(config);
|
|
52
130
|
try {
|
|
53
131
|
const { createSchemaManager, MIGRATION_LOCK } = await import("../entries/adapters-postgres.js");
|
|
54
|
-
if (
|
|
132
|
+
if (isReadOnly(flags)) {
|
|
55
133
|
// Read-only paths take no lock. `plan()` and `currentVersion()` are documented as side-effect free — only
|
|
56
134
|
// `apply()` creates the ledger table — which is what makes a dry run honest rather than a dry run that
|
|
57
135
|
// provisions one table. Verified: after `--dry-run` against a fresh database, `public` has 0 tables.
|
|
@@ -116,8 +194,8 @@ const migrate = async (flags, env = process.env) => {
|
|
|
116
194
|
const doctor = async (env = process.env) => {
|
|
117
195
|
const results = await runChecks({
|
|
118
196
|
env,
|
|
119
|
-
connectPostgres: async (
|
|
120
|
-
const { sql, end } = await postgres(
|
|
197
|
+
connectPostgres: async (settings) => {
|
|
198
|
+
const { sql, end } = await postgres(settings);
|
|
121
199
|
return { query: (text, params) => sql.query(text, params), end };
|
|
122
200
|
},
|
|
123
201
|
connectRedis: async (url) => {
|
|
@@ -25,9 +25,12 @@ export const runWorker = async (env = process.env) => {
|
|
|
25
25
|
const { config, sql } = await boot({
|
|
26
26
|
env,
|
|
27
27
|
connect: async (loaded) => {
|
|
28
|
-
const {
|
|
29
|
-
|
|
30
|
-
|
|
28
|
+
const { openPostgres } = await import("./pool.js");
|
|
29
|
+
// Through the shared pool for the schema, which matters most here: the worker is the process whose
|
|
30
|
+
// writes nobody watches, so a worker in `public` while the host is in `retinue` is a split brain
|
|
31
|
+
// that shows up as runs that vanish rather than as an error.
|
|
32
|
+
const { sql } = await openPostgres(loaded);
|
|
33
|
+
return { sql };
|
|
31
34
|
},
|
|
32
35
|
});
|
|
33
36
|
/**
|
package/dist/server/cli.js
CHANGED
|
@@ -38,12 +38,14 @@ export const runApiHost = async (env = process.env) => {
|
|
|
38
38
|
const { config, sql, runner } = await boot({
|
|
39
39
|
env,
|
|
40
40
|
connect: async (loaded) => {
|
|
41
|
-
const {
|
|
42
|
-
const { createPgExecutor, createPoolOpener } = await import("../entries/adapters-postgres.js");
|
|
43
|
-
const pool = new Pool({ connectionString: loaded.databaseUrl });
|
|
41
|
+
const { openPostgres } = await import("./pool.js");
|
|
44
42
|
// `open` is what lets `boot` build a transaction scope. Without it the API host had no runner and
|
|
45
43
|
// `sendMessage` could not claim a conversation — #254.
|
|
46
|
-
|
|
44
|
+
//
|
|
45
|
+
// The whole config is passed, not just the URL: `databaseSchema` has to reach the pool or the host
|
|
46
|
+
// reads and writes `public` while migrations ran somewhere else.
|
|
47
|
+
const { sql, open } = await openPostgres(loaded);
|
|
48
|
+
return { sql, open };
|
|
47
49
|
},
|
|
48
50
|
});
|
|
49
51
|
const deps = await app.deps({ config, sql, ...(runner === undefined ? {} : { runner }) });
|
package/dist/server/config.d.ts
CHANGED
|
@@ -4,6 +4,16 @@ export type RetinueConfig = {
|
|
|
4
4
|
readonly redisUrl: string;
|
|
5
5
|
/** How the schema is provisioned at boot. `off` in production, so managed migrations stay in control. */
|
|
6
6
|
readonly schemaMode: SchemaMode;
|
|
7
|
+
/**
|
|
8
|
+
* The Postgres schema the platform's own tables live in, or `undefined` for the connection's default.
|
|
9
|
+
*
|
|
10
|
+
* Exists because a deployment may be sharing a database with a product that owns `public` — which is
|
|
11
|
+
* exactly the ShareFlow case: its adapters qualify every one of their queries as `public.`, so with this
|
|
12
|
+
* set to `retinue` one pool serves both, platform tables in one schema and product tables in the other.
|
|
13
|
+
* Without it the platform's 35 migrations land in `public` alongside the product's, and the first name
|
|
14
|
+
* they share is a migration that fails or, worse, one that succeeds against the wrong table.
|
|
15
|
+
*/
|
|
16
|
+
readonly databaseSchema?: string;
|
|
7
17
|
readonly port: number;
|
|
8
18
|
readonly workerConcurrency: number;
|
|
9
19
|
readonly logLevel: "debug" | "info" | "warn" | "error";
|
|
@@ -38,5 +48,5 @@ export declare const loadConfig: (env: Env) => RetinueConfig;
|
|
|
38
48
|
* test that asserts the error names every missing variable is what found it.
|
|
39
49
|
*/
|
|
40
50
|
export declare const REQUIRED_VARIABLES: readonly ["RETINUE_DATABASE_URL", "RETINUE_REDIS_URL"];
|
|
41
|
-
export declare const OPTIONAL_VARIABLES: readonly ["SCHEMA_MODE", "WORKER_CONCURRENCY", "LOG_LEVEL", "PORT"];
|
|
51
|
+
export declare const OPTIONAL_VARIABLES: readonly ["SCHEMA_MODE", "DATABASE_SCHEMA", "WORKER_CONCURRENCY", "LOG_LEVEL", "PORT"];
|
|
42
52
|
//# sourceMappingURL=config.d.ts.map
|
package/dist/server/config.js
CHANGED
|
@@ -76,6 +76,20 @@ export const loadConfig = (env) => {
|
|
|
76
76
|
if (!SCHEMA_MODES.includes(rawSchemaMode)) {
|
|
77
77
|
fail(named("SCHEMA_MODE"), `must be one of ${SCHEMA_MODES.join(", ")}, got "${rawSchemaMode}"`);
|
|
78
78
|
}
|
|
79
|
+
/**
|
|
80
|
+
* A schema name, validated as an identifier rather than quoted.
|
|
81
|
+
*
|
|
82
|
+
* `SET search_path` takes no parameters — it is not a value position, so there is no placeholder to bind
|
|
83
|
+
* and the name is concatenated into SQL. That makes this the one config value where a lax check is a SQL
|
|
84
|
+
* injection with the deployment's own credentials, so the pattern is deliberately narrow: what Postgres
|
|
85
|
+
* accepts unquoted, and nothing else. A rejected name fails boot, where it is one line to fix.
|
|
86
|
+
*/
|
|
87
|
+
const rawSchema = lookup("DATABASE_SCHEMA");
|
|
88
|
+
const databaseSchema = rawSchema === undefined || rawSchema.trim() === "" ? undefined : rawSchema.trim();
|
|
89
|
+
if (databaseSchema !== undefined && !/^[A-Za-z_][A-Za-z0-9_$]*$/.test(databaseSchema)) {
|
|
90
|
+
fail(named("DATABASE_SCHEMA"), `must be an unquoted Postgres identifier — letters, digits, underscore and $, not starting with a ` +
|
|
91
|
+
`digit — got "${databaseSchema.slice(0, 24)}"`);
|
|
92
|
+
}
|
|
79
93
|
const positiveInt = (suffix, fallback) => {
|
|
80
94
|
// `PORT` has no prefix — it is the conventional name and always has been, so it is read directly.
|
|
81
95
|
const variable = suffix === "PORT" ? "PORT" : named(suffix);
|
|
@@ -103,6 +117,9 @@ export const loadConfig = (env) => {
|
|
|
103
117
|
databaseUrl,
|
|
104
118
|
redisUrl,
|
|
105
119
|
schemaMode: rawSchemaMode,
|
|
120
|
+
// Spread, not `databaseSchema: undefined` — `exactOptionalPropertyTypes` is on, so an explicit
|
|
121
|
+
// undefined is a different type from an absent key.
|
|
122
|
+
...(databaseSchema === undefined ? {} : { databaseSchema }),
|
|
106
123
|
port,
|
|
107
124
|
workerConcurrency,
|
|
108
125
|
logLevel: rawLogLevel,
|
|
@@ -120,6 +137,7 @@ export const loadConfig = (env) => {
|
|
|
120
137
|
export const REQUIRED_VARIABLES = ["RETINUE_DATABASE_URL", "RETINUE_REDIS_URL"];
|
|
121
138
|
export const OPTIONAL_VARIABLES = [
|
|
122
139
|
"SCHEMA_MODE",
|
|
140
|
+
"DATABASE_SCHEMA",
|
|
123
141
|
"WORKER_CONCURRENCY",
|
|
124
142
|
"LOG_LEVEL",
|
|
125
143
|
"PORT",
|
package/dist/server/doctor.d.ts
CHANGED
|
@@ -39,8 +39,19 @@ export declare const describeUrl: (value: string) => string;
|
|
|
39
39
|
export declare const scrub: (message: string) => string;
|
|
40
40
|
export type DoctorDeps = {
|
|
41
41
|
readonly env?: Readonly<Record<string, string | undefined>>;
|
|
42
|
-
/**
|
|
43
|
-
|
|
42
|
+
/**
|
|
43
|
+
* Injected so the checks are testable without a database or a Redis.
|
|
44
|
+
*
|
|
45
|
+
* Takes the whole connection setting rather than a URL, because `databaseSchema` changes the answer:
|
|
46
|
+
* the schema probe counts applied migrations, and reading `public` when the deployment configured
|
|
47
|
+
* `retinue` reports "0 of 35 applied → run migrate" about a schema that is fully migrated. The
|
|
48
|
+
* comment below already names that class of bug — a diagnostic sending an operator to fix the wrong
|
|
49
|
+
* thing — and a URL-only signature is how this one would have got in.
|
|
50
|
+
*/
|
|
51
|
+
readonly connectPostgres?: (settings: {
|
|
52
|
+
readonly databaseUrl: string;
|
|
53
|
+
readonly databaseSchema?: string;
|
|
54
|
+
}) => Promise<{
|
|
44
55
|
query(text: string, params?: readonly unknown[]): Promise<unknown>;
|
|
45
56
|
end(): Promise<void>;
|
|
46
57
|
}>;
|
package/dist/server/doctor.js
CHANGED
|
@@ -179,7 +179,7 @@ export const runChecks = async (deps = {}) => {
|
|
|
179
179
|
*/
|
|
180
180
|
let reachable = false;
|
|
181
181
|
try {
|
|
182
|
-
sql = await withTimeout("postgres", connectPostgres(config
|
|
182
|
+
sql = await withTimeout("postgres", connectPostgres(config));
|
|
183
183
|
await withTimeout("postgres", sql.query("select 1"));
|
|
184
184
|
reachable = true;
|
|
185
185
|
results.push({ name: "postgres", ok: true, detail: `reachable at ${describeUrl(config.databaseUrl)}` });
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one place a `pg.Pool` is built for this host — REQ-041 (#190).
|
|
3
|
+
*
|
|
4
|
+
* There were four: `bin.ts` for `migrate`/`doctor`, `cli.ts` for the API host, `cli-worker.ts` for the
|
|
5
|
+
* worker, and the driver `doctor` is handed. Four copies of "how this deployment connects" is three
|
|
6
|
+
* copies too many, and the one that drifts is the one nobody exercises — the worker, whose writes are
|
|
7
|
+
* the ones that must land in the same schema as everything else.
|
|
8
|
+
*
|
|
9
|
+
* **What it adds over `new Pool`.** `RETINUE_DATABASE_SCHEMA` names the schema the platform's tables
|
|
10
|
+
* live in, and honouring it takes two things that must agree:
|
|
11
|
+
*
|
|
12
|
+
* - `pool.on("connect")` sets `search_path` on **every** connection, because `createPgExecutor` runs
|
|
13
|
+
* each query through `pool.query`, which takes a different connection per call. A pooled connection
|
|
14
|
+
* carries whatever `search_path` its last user left, so setting it once at startup means the first
|
|
15
|
+
* few statements land in the right schema and the rest land wherever. That failure is not loud: it
|
|
16
|
+
* is a table created in `public` by a migration that reported success.
|
|
17
|
+
* - `createPoolOpener(pool, …)` sets it again per checkout, which is what the transaction scope uses.
|
|
18
|
+
* Redundant on paper and deliberately kept: the opener is the path that holds `SELECT … FOR UPDATE`
|
|
19
|
+
* across statements, and it must not depend on a listener having fired.
|
|
20
|
+
*
|
|
21
|
+
* **`public` stays on the path** after the named schema. Two things need it: the `vector` type is
|
|
22
|
+
* pinned to `public` so it resolves from any schema (see the note in `migrations.ts`), and ShareFlow's
|
|
23
|
+
* adapters qualify all 70 of their queries as `public.`, which is what lets one pool serve a platform
|
|
24
|
+
* schema and a product schema at once.
|
|
25
|
+
*/
|
|
26
|
+
import type { SqlExecutor } from "../adapters/postgres/sql.js";
|
|
27
|
+
import type { ConnectionOpener } from "../adapters/postgres/transaction.js";
|
|
28
|
+
export type PostgresConnection = {
|
|
29
|
+
readonly sql: SqlExecutor;
|
|
30
|
+
readonly open: ConnectionOpener;
|
|
31
|
+
readonly end: () => Promise<void>;
|
|
32
|
+
};
|
|
33
|
+
export type PoolSettings = {
|
|
34
|
+
readonly databaseUrl: string;
|
|
35
|
+
readonly databaseSchema?: string;
|
|
36
|
+
/** Left unset by the host and the worker, which is node-postgres's default of no timeout. */
|
|
37
|
+
readonly connectionTimeoutMillis?: number;
|
|
38
|
+
};
|
|
39
|
+
/**
|
|
40
|
+
* The `search_path` for a named schema, or `undefined` to leave the connection's own.
|
|
41
|
+
*
|
|
42
|
+
* Exported because it is the only string in this file that ends up in SQL, and a test that pins it is
|
|
43
|
+
* cheaper than reading two call sites to find out what a deployment actually gets.
|
|
44
|
+
*/
|
|
45
|
+
export declare const searchPathFor: (schema: string | undefined) => string | undefined;
|
|
46
|
+
export declare const openPostgres: (settings: PoolSettings) => Promise<PostgresConnection>;
|
|
47
|
+
//# sourceMappingURL=pool.d.ts.map
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one place a `pg.Pool` is built for this host — REQ-041 (#190).
|
|
3
|
+
*
|
|
4
|
+
* There were four: `bin.ts` for `migrate`/`doctor`, `cli.ts` for the API host, `cli-worker.ts` for the
|
|
5
|
+
* worker, and the driver `doctor` is handed. Four copies of "how this deployment connects" is three
|
|
6
|
+
* copies too many, and the one that drifts is the one nobody exercises — the worker, whose writes are
|
|
7
|
+
* the ones that must land in the same schema as everything else.
|
|
8
|
+
*
|
|
9
|
+
* **What it adds over `new Pool`.** `RETINUE_DATABASE_SCHEMA` names the schema the platform's tables
|
|
10
|
+
* live in, and honouring it takes two things that must agree:
|
|
11
|
+
*
|
|
12
|
+
* - `pool.on("connect")` sets `search_path` on **every** connection, because `createPgExecutor` runs
|
|
13
|
+
* each query through `pool.query`, which takes a different connection per call. A pooled connection
|
|
14
|
+
* carries whatever `search_path` its last user left, so setting it once at startup means the first
|
|
15
|
+
* few statements land in the right schema and the rest land wherever. That failure is not loud: it
|
|
16
|
+
* is a table created in `public` by a migration that reported success.
|
|
17
|
+
* - `createPoolOpener(pool, …)` sets it again per checkout, which is what the transaction scope uses.
|
|
18
|
+
* Redundant on paper and deliberately kept: the opener is the path that holds `SELECT … FOR UPDATE`
|
|
19
|
+
* across statements, and it must not depend on a listener having fired.
|
|
20
|
+
*
|
|
21
|
+
* **`public` stays on the path** after the named schema. Two things need it: the `vector` type is
|
|
22
|
+
* pinned to `public` so it resolves from any schema (see the note in `migrations.ts`), and ShareFlow's
|
|
23
|
+
* adapters qualify all 70 of their queries as `public.`, which is what lets one pool serve a platform
|
|
24
|
+
* schema and a product schema at once.
|
|
25
|
+
*/
|
|
26
|
+
/**
|
|
27
|
+
* The `search_path` for a named schema, or `undefined` to leave the connection's own.
|
|
28
|
+
*
|
|
29
|
+
* Exported because it is the only string in this file that ends up in SQL, and a test that pins it is
|
|
30
|
+
* cheaper than reading two call sites to find out what a deployment actually gets.
|
|
31
|
+
*/
|
|
32
|
+
export const searchPathFor = (schema) => schema === undefined || schema === "" ? undefined : `${schema}, public`;
|
|
33
|
+
export const openPostgres = async (settings) => {
|
|
34
|
+
const { Pool } = await import("pg");
|
|
35
|
+
const { createPgExecutor, createPoolOpener } = await import("../entries/adapters-postgres.js");
|
|
36
|
+
const searchPath = searchPathFor(settings.databaseSchema);
|
|
37
|
+
const pool = new Pool({
|
|
38
|
+
connectionString: settings.databaseUrl,
|
|
39
|
+
...(settings.connectionTimeoutMillis === undefined
|
|
40
|
+
? {}
|
|
41
|
+
: { connectionTimeoutMillis: settings.connectionTimeoutMillis }),
|
|
42
|
+
});
|
|
43
|
+
if (searchPath !== undefined) {
|
|
44
|
+
/**
|
|
45
|
+
* Queued on the client, not awaited — which is what makes it correct rather than racy.
|
|
46
|
+
*
|
|
47
|
+
* node-postgres queues queries per client in order, so this `SET` is ahead of whatever the borrower
|
|
48
|
+
* runs next on that same connection. An `await` here would have nothing to attach to: `connect` is
|
|
49
|
+
* an event, and the pool hands the client out regardless of what a listener is still doing.
|
|
50
|
+
*
|
|
51
|
+
* A failure is surfaced rather than swallowed, and it is worth being precise about what it can and
|
|
52
|
+
* cannot catch. It means the role may not *use* the schema. It does **not** mean the schema is
|
|
53
|
+
* missing: `SET search_path TO retinue, public` succeeds when `retinue` does not exist, because a
|
|
54
|
+
* missing entry is skipped rather than rejected, and every write then lands in `public` with no
|
|
55
|
+
* error anywhere. Nothing at this layer can see that — which is why `retinue migrate` creates the
|
|
56
|
+
* schema before anything connects, and why this listener is not the guard against it.
|
|
57
|
+
*/
|
|
58
|
+
pool.on("connect", (client) => {
|
|
59
|
+
void client.query(`SET search_path TO ${searchPath}`).catch((error) => {
|
|
60
|
+
client.end();
|
|
61
|
+
pool.emit("error", error instanceof Error
|
|
62
|
+
? new Error(`could not SET search_path TO ${searchPath}: ${error.message}`, { cause: error })
|
|
63
|
+
: new Error(`could not SET search_path TO ${searchPath}`), client);
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
return {
|
|
68
|
+
sql: createPgExecutor(pool),
|
|
69
|
+
open: createPoolOpener(pool, searchPath),
|
|
70
|
+
end: () => pool.end(),
|
|
71
|
+
};
|
|
72
|
+
};
|
|
73
|
+
//# sourceMappingURL=pool.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@retinue/agentkit",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"description": "A provider-neutral, durable AI agent runtime for TypeScript: agents, tools, approvals, context, knowledge and persistence behind ports.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|