@withone/cli 1.49.0 → 1.51.0
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/README.md +50 -4
- package/dist/{chunk-2TWFL3CS.js → chunk-D6W756DN.js} +3 -2
- package/dist/{chunk-4WWK3GO5.js → chunk-UV4YYDF3.js} +1 -1
- package/dist/{chunk-7RV7T5PX.js → chunk-Z4KAQGIG.js} +139 -15
- package/dist/{chunk-GXOIR3GF.js → chunk-ZOJW6IMU.js} +278 -13
- package/dist/{flow-runner-IBB42ED2.js → flow-runner-YDKAYGZW.js} +1 -1
- package/dist/index.js +539 -262
- package/dist/{migrate-TRUZVAT5.js → migrate-LXDIZMXH.js} +2 -2
- package/dist/{runtime-2PRVL2NO.js → runtime-HDZCBTWH.js} +1 -1
- package/dist/{schema-LCRWSFD5.js → schema-SYMG6SRC.js} +1 -1
- package/dist/{sql-HG7R2JML.js → sql-AZDCMY7G.js} +2 -2
- package/package.json +2 -1
- package/profiles/fathom/meetings.json +4 -0
- package/profiles/gmail/gmailThreads.json +5 -0
- package/profiles/google-calendar/events.json +5 -1
- package/skills/one/SKILL.md +63 -6
package/README.md
CHANGED
|
@@ -179,7 +179,7 @@ Opens your browser, you authorize, done. The CLI polls until the connection is l
|
|
|
179
179
|
|
|
180
180
|
### `one list`
|
|
181
181
|
|
|
182
|
-
List your active connections with their status and
|
|
182
|
+
List your active connections with their status, connection keys, and what your access config lets you run on each.
|
|
183
183
|
|
|
184
184
|
```bash
|
|
185
185
|
one list
|
|
@@ -193,6 +193,37 @@ one list
|
|
|
193
193
|
|
|
194
194
|
You need the connection key (rightmost column) when executing actions.
|
|
195
195
|
|
|
196
|
+
**Access reporting.** Every connection carries an `access` field describing what the current [access control](#one-config) config permits — so an agent knows its reach up front instead of discovering it as a failure mid-workflow. Three policies:
|
|
197
|
+
|
|
198
|
+
| Policy | Means | Shown when |
|
|
199
|
+
|--------|-------|-----------|
|
|
200
|
+
| `{"policy": "full"}` | Every action on the connection | Default (admin, no action allowlist) |
|
|
201
|
+
| `{"policy": "methods", "methods": ["GET"]}` | Only actions with those HTTP methods | Permission level is `read` or `write` |
|
|
202
|
+
| `{"policy": "actions", "actions": [{"actionId": "...", "title": "...", "method": "..."}]}` | Only these specific actions | An action allowlist is configured |
|
|
203
|
+
|
|
204
|
+
An action allowlist wins over the permission level (and is then method-filtered by it). When the policy is `actions`, those actions are exactly what may run — no `actions search` needed.
|
|
205
|
+
|
|
206
|
+
```jsonc
|
|
207
|
+
// one --agent list
|
|
208
|
+
{
|
|
209
|
+
"total": 2,
|
|
210
|
+
"showing": 2,
|
|
211
|
+
"connections": [
|
|
212
|
+
{
|
|
213
|
+
"platform": "gmail",
|
|
214
|
+
"state": "operational",
|
|
215
|
+
"key": "live::gmail::default::abc123",
|
|
216
|
+
"access": { "policy": "methods", "methods": ["GET"] }
|
|
217
|
+
}
|
|
218
|
+
],
|
|
219
|
+
"accessHint": "Permission level \"read\": only GET actions will execute."
|
|
220
|
+
}
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
`knowledgeOnly: true` appears when knowledge-only mode is on (execution disabled), and `unresolvedActionIds` lists any allowlisted action ids that could not be looked up. In human output, an `Access` column and a summary note appear only when access is actually scoped.
|
|
224
|
+
|
|
225
|
+
This mirrors the `access` field on the One MCP server's `list_one_integrations` tool, so both surfaces report access the same way.
|
|
226
|
+
|
|
196
227
|
### `one connection delete <connection-key>`
|
|
197
228
|
|
|
198
229
|
Remove a connection by its key.
|
|
@@ -333,10 +364,17 @@ one mem update <id> '{"status":"done"}' # merges into data; refresh
|
|
|
333
364
|
one mem search "design review" # hybrid FTS + semantic (if key set)
|
|
334
365
|
one mem list note --limit 20
|
|
335
366
|
|
|
336
|
-
#
|
|
337
|
-
# frees them). Manage them after creation with `mem key` — NOT
|
|
367
|
+
# Merge keys (`keys[]`) are a first-class column (unique across ACTIVE records;
|
|
368
|
+
# archiving frees them). Manage them after creation with `mem key` — NOT `mem update`.
|
|
338
369
|
one mem key <id> --add email:x@y.com # also --remove / --set <csv>
|
|
339
370
|
|
|
371
|
+
# Find every record tied to a person/company, grouped by type. Spans BOTH columns:
|
|
372
|
+
# `keys[]` (this record IS the entity — drives merge) and `identity_keys[]` (this
|
|
373
|
+
# record INVOLVES the entity — Gmail thread participants, calendar attendees; never merges).
|
|
374
|
+
one mem find-by-key email:jane@acme.com # every record carrying the key
|
|
375
|
+
one mem find-by-key email:jane@acme.com --type gmail/gmailThreads
|
|
376
|
+
one mem find-by-key email:jane@acme.com email:bob@acme.com # intersection — records with BOTH
|
|
377
|
+
|
|
340
378
|
# Backfill searchable_text for rows that landed NULL or with UUID-noise-heavy text
|
|
341
379
|
# (drops ids/timestamps, leads with name/title/email). No embedding provider needed.
|
|
342
380
|
one mem reindex --searchable --type attio/attioPeople
|
|
@@ -358,7 +396,9 @@ one mem status # backend, provider, _upgra
|
|
|
358
396
|
one mem doctor # 7-check health report
|
|
359
397
|
```
|
|
360
398
|
|
|
361
|
-
Key surfaces: `add`, `get`, `update`, `archive`, `list`, `search` (`--deep` forces semantic), `context`, `link`/`linked`/`unlink`, `sources`, `find-by-source`, `export`, `import`, `migrate`, `vacuum`, `reindex`. Run `one guide memory` for the full reference.
|
|
399
|
+
Key surfaces: `add`, `get`, `update`, `archive`, `list`, `search` (`--deep` forces semantic), `context`, `link`/`linked`/`unlink`, `key`, `sources`, `find-by-source`, `find-by-key`, `export`, `import`, `migrate`, `vacuum`, `reindex`. Run `one guide memory` for the full reference.
|
|
400
|
+
|
|
401
|
+
`find-by-key` is the cross-platform join: keys are matched **case-insensitively** (they are lowercased on write, and the query is lowercased to match), `--type` narrows to one record type, and `--limit` (default 10) caps how many records are *shown per type* while the reported per-type `count` and overall `total` stay full match counts. Above 2000 matches the response flags `truncated` and the counts become floors — re-run with `--type` for an exact answer. Populate `identity_keys[]` from a sync profile's `identityKeys` field (see `one guide sync`), which ships pre-wired for `gmail/gmailThreads`, `google-calendar/events`, and `fathom/meetings`.
|
|
362
402
|
|
|
363
403
|
### `one sync` (and `one mem sync` alias)
|
|
364
404
|
|
|
@@ -381,6 +421,10 @@ one sync test stripe/balanceTransactions --show-searchable
|
|
|
381
421
|
# Run — every row lands in memory (SQLite also written for enrich-phase compat)
|
|
382
422
|
one sync run stripe --since 90d
|
|
383
423
|
one mem sync run stripe # identical (alias)
|
|
424
|
+
# Profiles with `enrich` run a second detail pass. It enriches each record ONCE;
|
|
425
|
+
# the list pass thereafter merges rather than replaces, so the enriched payload
|
|
426
|
+
# survives (reported as `memPreserved`). `--full-refresh` does not re-enrich —
|
|
427
|
+
# delete .one/sync/data/<platform>.db to force that.
|
|
384
428
|
|
|
385
429
|
# Query + search (reads from memory)
|
|
386
430
|
one sync query stripe/balanceTransactions --where "status=available" --limit 20
|
|
@@ -576,6 +620,8 @@ one config
|
|
|
576
620
|
|
|
577
621
|
Settings propagate automatically to all installed agent configs.
|
|
578
622
|
|
|
623
|
+
Run [`one list`](#one-list) to see the effect: each connection reports an `access` field describing exactly what these settings let you run on it.
|
|
624
|
+
|
|
579
625
|
#### `one config skills status` / `one config skills sync`
|
|
580
626
|
|
|
581
627
|
`one init` copies the packaged skill files (`SKILL.md`, `references/`) into `~/.agents/skills/one/` and symlinks per-agent paths to that canonical directory. When the CLI self-updates, the skill files in the canonical dir would normally stay frozen at the version that was installed. To prevent stale docs, every CLI command checks a `.one-cli-version` marker in the canonical dir and silently refreshes the skill files if they don't match the running CLI version. No user action required.
|
|
@@ -2319,7 +2319,7 @@ async function executeSubflowStep(step, context, api, permissions, allowedAction
|
|
|
2319
2319
|
if (flowStack.includes(resolvedKey)) {
|
|
2320
2320
|
throw new Error(`Circular flow detected: ${[...flowStack, resolvedKey].join(" \u2192 ")}`);
|
|
2321
2321
|
}
|
|
2322
|
-
const { loadFlowWithMeta: loadFlowWithMeta2 } = await import("./flow-runner-
|
|
2322
|
+
const { loadFlowWithMeta: loadFlowWithMeta2 } = await import("./flow-runner-YDKAYGZW.js");
|
|
2323
2323
|
const { flow: subFlow, rootDir: subRootDir } = loadFlowWithMeta2(resolvedKey);
|
|
2324
2324
|
const subContext = await executeFlow(
|
|
2325
2325
|
subFlow,
|
|
@@ -3238,11 +3238,11 @@ export {
|
|
|
3238
3238
|
ApiError,
|
|
3239
3239
|
OneApi,
|
|
3240
3240
|
TimeoutError,
|
|
3241
|
+
PERMISSION_METHODS,
|
|
3241
3242
|
filterByPermissions,
|
|
3242
3243
|
isMethodAllowed,
|
|
3243
3244
|
isActionAllowed,
|
|
3244
3245
|
buildActionKnowledgeWithGuidance,
|
|
3245
|
-
validateActionInput,
|
|
3246
3246
|
knowledgeCachePath,
|
|
3247
3247
|
searchCachePath,
|
|
3248
3248
|
readCache,
|
|
@@ -3256,6 +3256,7 @@ export {
|
|
|
3256
3256
|
clearEntry,
|
|
3257
3257
|
makeCacheEntry,
|
|
3258
3258
|
resolveActionDetails,
|
|
3259
|
+
validateActionInput,
|
|
3259
3260
|
FLOW_SCHEMA,
|
|
3260
3261
|
getStepTypeDescriptor,
|
|
3261
3262
|
getNestedStepsKeys,
|
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
} from "./chunk-SO323PZP.js";
|
|
12
12
|
|
|
13
13
|
// src/lib/memory/schema.ts
|
|
14
|
-
var SCHEMA_VERSION = "2.
|
|
14
|
+
var SCHEMA_VERSION = "2.3.1";
|
|
15
15
|
var EXTENSIONS_SQL = ``;
|
|
16
16
|
var VECTOR_EXTENSION_SQL = `
|
|
17
17
|
CREATE EXTENSION IF NOT EXISTS vector;
|
|
@@ -24,6 +24,14 @@ CREATE TABLE IF NOT EXISTS mem_records (
|
|
|
24
24
|
data JSONB NOT NULL,
|
|
25
25
|
tags TEXT[],
|
|
26
26
|
keys TEXT[],
|
|
27
|
+
-- Cross-platform identity keys (#128): queryable associations like
|
|
28
|
+
-- email:jane@acme.com for every participant of a record. UNLIKE keys[],
|
|
29
|
+
-- these are NOT merge identifiers \u2014 they are exempt from the
|
|
30
|
+
-- key-uniqueness trigger and the upsert overlap-merge, so a Gmail thread
|
|
31
|
+
-- carrying many participant emails stays its own record instead of
|
|
32
|
+
-- collapsing into a contact (or into every other thread that shares a
|
|
33
|
+
-- participant). Queried by "mem find-by-key".
|
|
34
|
+
identity_keys TEXT[],
|
|
27
35
|
|
|
28
36
|
sources JSONB NOT NULL DEFAULT '{}',
|
|
29
37
|
|
|
@@ -72,6 +80,12 @@ CREATE TABLE IF NOT EXISTS mem_meta (
|
|
|
72
80
|
value TEXT NOT NULL,
|
|
73
81
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
74
82
|
);
|
|
83
|
+
|
|
84
|
+
-- Additive migration for stores created before identity_keys existed (#128).
|
|
85
|
+
-- Safe to re-run; existing rows get identity_keys populated lazily on their
|
|
86
|
+
-- next sync. CREATE TABLE IF NOT EXISTS above won't add columns to a table
|
|
87
|
+
-- that already exists, so the ALTER is required for upgrades.
|
|
88
|
+
ALTER TABLE mem_records ADD COLUMN IF NOT EXISTS identity_keys TEXT[];
|
|
75
89
|
`;
|
|
76
90
|
var VECTOR_COLUMNS_SQL = `
|
|
77
91
|
ALTER TABLE mem_records ADD COLUMN IF NOT EXISTS embedding vector(1536);
|
|
@@ -82,6 +96,7 @@ var INDEXES_SQL = `
|
|
|
82
96
|
CREATE INDEX IF NOT EXISTS idx_records_type ON mem_records(type);
|
|
83
97
|
CREATE INDEX IF NOT EXISTS idx_records_status ON mem_records(status);
|
|
84
98
|
CREATE INDEX IF NOT EXISTS idx_records_keys ON mem_records USING GIN(keys);
|
|
99
|
+
CREATE INDEX IF NOT EXISTS idx_records_identity_keys ON mem_records USING GIN(identity_keys);
|
|
85
100
|
CREATE INDEX IF NOT EXISTS idx_records_tags ON mem_records USING GIN(tags);
|
|
86
101
|
CREATE INDEX IF NOT EXISTS idx_records_data ON mem_records USING GIN(data jsonb_path_ops);
|
|
87
102
|
CREATE INDEX IF NOT EXISTS idx_records_sources ON mem_records USING GIN(sources jsonb_path_ops);
|
|
@@ -105,6 +120,25 @@ BEGIN
|
|
|
105
120
|
END $$;
|
|
106
121
|
`;
|
|
107
122
|
var FUNCTIONS_SQL = `
|
|
123
|
+
-- Drop the pre-#128 11-argument mem_upsert_by_keys (no p_identity_keys).
|
|
124
|
+
--
|
|
125
|
+
-- CREATE OR REPLACE cannot change a function's argument list \u2014 adding
|
|
126
|
+
-- \`p_identity_keys TEXT[] DEFAULT NULL\` defines a SECOND function rather
|
|
127
|
+
-- than replacing the old one. An upgraded store would then hold both
|
|
128
|
+
-- overloads in pg_proc, and because the 12-arg version's trailing param is
|
|
129
|
+
-- defaultable, an 11-arg call matches BOTH and fails with 42725
|
|
130
|
+
-- "function mem_upsert_by_keys(...) is not unique". Verified on PGlite.
|
|
131
|
+
--
|
|
132
|
+
-- The explicit argument list makes this unambiguous (no 42725 on the DROP
|
|
133
|
+
-- itself) and leaves the 12-arg version untouched, so re-running the block
|
|
134
|
+
-- is a no-op. Only needed here, not in VECTOR_FUNCTIONS_SQL: the vector
|
|
135
|
+
-- variant is a same-signature CREATE OR REPLACE that always runs AFTER this
|
|
136
|
+
-- block (ensureSchema and getFullSchemaSQL both order core functions before
|
|
137
|
+
-- vector functions), so by the time it runs the stale 11-arg entry \u2014 whether
|
|
138
|
+
-- it was last written by the core or the vector block \u2014 is already gone.
|
|
139
|
+
DROP FUNCTION IF EXISTS mem_upsert_by_keys(
|
|
140
|
+
TEXT, JSONB, TEXT[], TEXT[], JSONB, TEXT, TEXT, INTEGER, TEXT, TEXT, BOOLEAN);
|
|
141
|
+
|
|
108
142
|
-- Enforce uniqueness of the "keys" array across ACTIVE records only.
|
|
109
143
|
--
|
|
110
144
|
-- Scoping to active is deliberate: an archived record must not squat on a
|
|
@@ -227,7 +261,8 @@ CREATE OR REPLACE FUNCTION mem_upsert_by_keys(
|
|
|
227
261
|
p_weight INTEGER DEFAULT NULL,
|
|
228
262
|
p_embedding TEXT DEFAULT NULL,
|
|
229
263
|
p_embedding_model TEXT DEFAULT NULL,
|
|
230
|
-
p_replace BOOLEAN DEFAULT FALSE
|
|
264
|
+
p_replace BOOLEAN DEFAULT FALSE,
|
|
265
|
+
p_identity_keys TEXT[] DEFAULT NULL
|
|
231
266
|
) RETURNS TABLE (id UUID, action TEXT) LANGUAGE plpgsql AS $$
|
|
232
267
|
DECLARE
|
|
233
268
|
existing_id UUID;
|
|
@@ -262,6 +297,23 @@ BEGIN
|
|
|
262
297
|
ELSE (SELECT ARRAY(SELECT DISTINCT unnest FROM unnest(COALESCE(r.tags, '{}') || COALESCE(p_tags, '{}'))))
|
|
263
298
|
END,
|
|
264
299
|
keys = (SELECT ARRAY(SELECT DISTINCT unnest FROM unnest(COALESCE(r.keys, '{}') || COALESCE(p_keys, '{}')))),
|
|
300
|
+
-- identity_keys: replace on a replace-sync, otherwise union (never
|
|
301
|
+
-- drop an association). NOT part of the overlap-merge above.
|
|
302
|
+
-- Three states, and NULL is NOT the same as '{}' here:
|
|
303
|
+
-- NULL \u2192 caller has no opinion (profile declares no
|
|
304
|
+
-- identityKeys, or the record is still the pre-enrich
|
|
305
|
+
-- shape whose participant fields haven't been fetched
|
|
306
|
+
-- yet) \u2192 KEEP what's stored. Clearing on NULL made
|
|
307
|
+
-- every sync after the first wipe the column, because
|
|
308
|
+
-- the unconditional list-phase write resolves nothing
|
|
309
|
+
-- and enrich only revisits _enriched_at IS NULL rows.
|
|
310
|
+
-- '{}' \u2192 caller authoritatively resolved zero participants
|
|
311
|
+
-- \u2192 clear, so a removed attendee really disappears.
|
|
312
|
+
-- {a,b} \u2192 replace with exactly these.
|
|
313
|
+
identity_keys = CASE
|
|
314
|
+
WHEN p_replace THEN COALESCE(p_identity_keys, r.identity_keys)
|
|
315
|
+
ELSE (SELECT ARRAY(SELECT DISTINCT unnest FROM unnest(COALESCE(r.identity_keys, '{}') || COALESCE(p_identity_keys, '{}'))))
|
|
316
|
+
END,
|
|
265
317
|
sources = r.sources || COALESCE(p_sources, '{}'::jsonb),
|
|
266
318
|
searchable_text = CASE
|
|
267
319
|
WHEN p_replace THEN p_searchable_text
|
|
@@ -280,12 +332,13 @@ BEGIN
|
|
|
280
332
|
result_action := 'updated';
|
|
281
333
|
ELSE
|
|
282
334
|
INSERT INTO mem_records (
|
|
283
|
-
type, data, tags, keys, sources, searchable_text, content_hash, weight
|
|
335
|
+
type, data, tags, keys, identity_keys, sources, searchable_text, content_hash, weight
|
|
284
336
|
) VALUES (
|
|
285
337
|
p_type,
|
|
286
338
|
p_data,
|
|
287
339
|
p_tags,
|
|
288
340
|
p_keys,
|
|
341
|
+
p_identity_keys,
|
|
289
342
|
COALESCE(p_sources, '{}'::jsonb),
|
|
290
343
|
p_searchable_text,
|
|
291
344
|
p_content_hash,
|
|
@@ -311,7 +364,8 @@ CREATE OR REPLACE FUNCTION mem_upsert_by_keys(
|
|
|
311
364
|
p_weight INTEGER DEFAULT NULL,
|
|
312
365
|
p_embedding TEXT DEFAULT NULL,
|
|
313
366
|
p_embedding_model TEXT DEFAULT NULL,
|
|
314
|
-
p_replace BOOLEAN DEFAULT FALSE
|
|
367
|
+
p_replace BOOLEAN DEFAULT FALSE,
|
|
368
|
+
p_identity_keys TEXT[] DEFAULT NULL
|
|
315
369
|
) RETURNS TABLE (id UUID, action TEXT) LANGUAGE plpgsql AS $$
|
|
316
370
|
DECLARE
|
|
317
371
|
existing_id UUID;
|
|
@@ -340,6 +394,13 @@ BEGIN
|
|
|
340
394
|
ELSE (SELECT ARRAY(SELECT DISTINCT unnest FROM unnest(COALESCE(r.tags, '{}') || COALESCE(p_tags, '{}'))))
|
|
341
395
|
END,
|
|
342
396
|
keys = (SELECT ARRAY(SELECT DISTINCT unnest FROM unnest(COALESCE(r.keys, '{}') || COALESCE(p_keys, '{}')))),
|
|
397
|
+
-- identity_keys: replace on replace-sync, else union. Not part of
|
|
398
|
+
-- the overlap-merge. NULL keeps, '{}' clears, non-empty replaces \u2014
|
|
399
|
+
-- see the no-vector variant for why NULL must not clear.
|
|
400
|
+
identity_keys = CASE
|
|
401
|
+
WHEN p_replace THEN COALESCE(p_identity_keys, r.identity_keys)
|
|
402
|
+
ELSE (SELECT ARRAY(SELECT DISTINCT unnest FROM unnest(COALESCE(r.identity_keys, '{}') || COALESCE(p_identity_keys, '{}'))))
|
|
403
|
+
END,
|
|
343
404
|
sources = r.sources || COALESCE(p_sources, '{}'::jsonb),
|
|
344
405
|
searchable_text = CASE
|
|
345
406
|
WHEN p_replace THEN p_searchable_text
|
|
@@ -361,13 +422,14 @@ BEGIN
|
|
|
361
422
|
result_action := 'updated';
|
|
362
423
|
ELSE
|
|
363
424
|
INSERT INTO mem_records (
|
|
364
|
-
type, data, tags, keys, sources, searchable_text, content_hash,
|
|
425
|
+
type, data, tags, keys, identity_keys, sources, searchable_text, content_hash,
|
|
365
426
|
weight, embedding, embedded_at, embedding_model
|
|
366
427
|
) VALUES (
|
|
367
428
|
p_type,
|
|
368
429
|
p_data,
|
|
369
430
|
p_tags,
|
|
370
431
|
p_keys,
|
|
432
|
+
p_identity_keys,
|
|
371
433
|
COALESCE(p_sources, '{}'::jsonb),
|
|
372
434
|
p_searchable_text,
|
|
373
435
|
p_content_hash,
|
|
@@ -505,6 +567,7 @@ function toRecord(row) {
|
|
|
505
567
|
data: row.data,
|
|
506
568
|
tags: row.tags ?? void 0,
|
|
507
569
|
keys: row.keys ?? void 0,
|
|
570
|
+
identity_keys: row.identity_keys ?? void 0,
|
|
508
571
|
sources: row.sources ?? {},
|
|
509
572
|
searchable_text: row.searchable_text,
|
|
510
573
|
embedded_at: row.embedded_at,
|
|
@@ -533,7 +596,17 @@ var CoreBackend = class {
|
|
|
533
596
|
async ensureSchema() {
|
|
534
597
|
const caps = this.caps;
|
|
535
598
|
try {
|
|
536
|
-
|
|
599
|
+
const probe = await this.client.query(
|
|
600
|
+
`SELECT (SELECT value FROM mem_meta WHERE key = 'version') AS version,
|
|
601
|
+
EXISTS (
|
|
602
|
+
SELECT 1 FROM pg_attribute
|
|
603
|
+
WHERE attrelid = to_regclass('mem_records')
|
|
604
|
+
AND attname = 'identity_keys'
|
|
605
|
+
AND NOT attisdropped
|
|
606
|
+
) AS has_sentinel`
|
|
607
|
+
);
|
|
608
|
+
const probed = probe.rows[0];
|
|
609
|
+
if (probed?.version === SCHEMA_VERSION && probed.has_sentinel) return;
|
|
537
610
|
} catch {
|
|
538
611
|
}
|
|
539
612
|
await this.client.transaction(async (tx) => {
|
|
@@ -565,19 +638,20 @@ var CoreBackend = class {
|
|
|
565
638
|
async insert(row) {
|
|
566
639
|
const embedding = vectorLiteral(row.embedding ?? null);
|
|
567
640
|
const sql = this.caps.vectorSearch ? `INSERT INTO mem_records
|
|
568
|
-
(type, data, tags, keys, sources, searchable_text, content_hash, weight,
|
|
641
|
+
(type, data, tags, keys, identity_keys, sources, searchable_text, content_hash, weight,
|
|
569
642
|
embedding, embedded_at, embedding_model)
|
|
570
|
-
VALUES ($1, $2::jsonb, $3::text[], $4::text[], $5::
|
|
571
|
-
$
|
|
643
|
+
VALUES ($1, $2::jsonb, $3::text[], $4::text[], $5::text[], $6::jsonb, $7, $8, $9,
|
|
644
|
+
$10::vector, CASE WHEN $10 IS NOT NULL THEN NOW() ELSE NULL END, $11)
|
|
572
645
|
RETURNING *` : `INSERT INTO mem_records
|
|
573
|
-
(type, data, tags, keys, sources, searchable_text, content_hash, weight)
|
|
574
|
-
VALUES ($1, $2::jsonb, $3::text[], $4::text[], $5::
|
|
646
|
+
(type, data, tags, keys, identity_keys, sources, searchable_text, content_hash, weight)
|
|
647
|
+
VALUES ($1, $2::jsonb, $3::text[], $4::text[], $5::text[], $6::jsonb, $7, $8, $9)
|
|
575
648
|
RETURNING *`;
|
|
576
649
|
const params = this.caps.vectorSearch ? [
|
|
577
650
|
row.type,
|
|
578
651
|
JSON.stringify(row.data),
|
|
579
652
|
row.tags ?? null,
|
|
580
653
|
row.keys ?? null,
|
|
654
|
+
row.identity_keys ?? null,
|
|
581
655
|
JSON.stringify(row.sources ?? {}),
|
|
582
656
|
row.searchable_text ?? null,
|
|
583
657
|
row.content_hash ?? null,
|
|
@@ -589,6 +663,7 @@ var CoreBackend = class {
|
|
|
589
663
|
JSON.stringify(row.data),
|
|
590
664
|
row.tags ?? null,
|
|
591
665
|
row.keys ?? null,
|
|
666
|
+
row.identity_keys ?? null,
|
|
592
667
|
JSON.stringify(row.sources ?? {}),
|
|
593
668
|
row.searchable_text ?? null,
|
|
594
669
|
row.content_hash ?? null,
|
|
@@ -603,7 +678,7 @@ var CoreBackend = class {
|
|
|
603
678
|
const res = await this.client.query(
|
|
604
679
|
`SELECT id, action FROM mem_upsert_by_keys(
|
|
605
680
|
$1::text, $2::jsonb, $3::text[], $4::text[], $5::jsonb, $6::text, $7::text,
|
|
606
|
-
$8::integer, $9::text, $10::text, $11::boolean
|
|
681
|
+
$8::integer, $9::text, $10::text, $11::boolean, $12::text[]
|
|
607
682
|
)`,
|
|
608
683
|
[
|
|
609
684
|
row.type,
|
|
@@ -616,7 +691,8 @@ var CoreBackend = class {
|
|
|
616
691
|
row.weight ?? null,
|
|
617
692
|
embedding,
|
|
618
693
|
embeddingModel,
|
|
619
|
-
opts.replace ?? false
|
|
694
|
+
opts.replace ?? false,
|
|
695
|
+
row.identity_keys ?? null
|
|
620
696
|
]
|
|
621
697
|
);
|
|
622
698
|
const { id, action } = res.rows[0];
|
|
@@ -999,6 +1075,48 @@ var CoreBackend = class {
|
|
|
999
1075
|
);
|
|
1000
1076
|
return res.rows[0] ? toRecord(res.rows[0]) : null;
|
|
1001
1077
|
}
|
|
1078
|
+
async findByKeys(keys, opts = {}) {
|
|
1079
|
+
if (!keys.length) return [];
|
|
1080
|
+
const params = [keys];
|
|
1081
|
+
const where = [
|
|
1082
|
+
// Index-usable prefilter. The exact predicate below concatenates two
|
|
1083
|
+
// columns, which is an expression neither GIN index can answer — on its
|
|
1084
|
+
// own it plans as a Filter over a full scan, making
|
|
1085
|
+
// idx_records_identity_keys pure write cost. Overlap (`&&`) on the bare
|
|
1086
|
+
// columns IS index-usable and is a valid necessary condition here:
|
|
1087
|
+
// containment of a NON-EMPTY $1 implies every element is present, so at
|
|
1088
|
+
// least one element must overlap one of the two columns ($1 is guarded
|
|
1089
|
+
// non-empty by the early return above). The planner ORs the two bitmap
|
|
1090
|
+
// index scans, then the exact recheck drops rows that only partially
|
|
1091
|
+
// match.
|
|
1092
|
+
//
|
|
1093
|
+
// The columns MUST stay bare. Wrapping either in COALESCE turns it back
|
|
1094
|
+
// into an expression over a non-indexed value and the plan collapses to
|
|
1095
|
+
// a full scan again (measured) — do not "tidy" this into COALESCE for
|
|
1096
|
+
// symmetry with the line below. NULL && anything is NULL → not true,
|
|
1097
|
+
// which is exactly right: a record with no keys can't contain any.
|
|
1098
|
+
`(keys && $1::text[] OR identity_keys && $1::text[])`,
|
|
1099
|
+
`(COALESCE(keys, '{}'::text[]) || COALESCE(identity_keys, '{}'::text[])) @> $1::text[]`
|
|
1100
|
+
];
|
|
1101
|
+
const status = opts.status ?? "active";
|
|
1102
|
+
if (status !== "all") {
|
|
1103
|
+
params.push(status);
|
|
1104
|
+
where.push(`status = $${params.length}`);
|
|
1105
|
+
}
|
|
1106
|
+
if (opts.type) {
|
|
1107
|
+
params.push(opts.type);
|
|
1108
|
+
where.push(`type = $${params.length}`);
|
|
1109
|
+
}
|
|
1110
|
+
params.push(Math.min(opts.limit ?? 2e3, 5e3));
|
|
1111
|
+
const res = await this.client.query(
|
|
1112
|
+
`SELECT * FROM mem_records
|
|
1113
|
+
WHERE ${where.join(" AND ")}
|
|
1114
|
+
ORDER BY type ASC, updated_at DESC NULLS LAST
|
|
1115
|
+
LIMIT $${params.length}`,
|
|
1116
|
+
params
|
|
1117
|
+
);
|
|
1118
|
+
return res.rows.map(toRecord);
|
|
1119
|
+
}
|
|
1002
1120
|
async listSources(recordId) {
|
|
1003
1121
|
const res = await this.client.query(
|
|
1004
1122
|
`SELECT sources FROM mem_records WHERE id = $1`,
|
|
@@ -1293,6 +1411,9 @@ var LazyPostgresBackend = class {
|
|
|
1293
1411
|
async findBySource(...a) {
|
|
1294
1412
|
return (await this.ensure()).findBySource(...a);
|
|
1295
1413
|
}
|
|
1414
|
+
async findByKeys(...a) {
|
|
1415
|
+
return (await this.ensure()).findByKeys(...a);
|
|
1416
|
+
}
|
|
1296
1417
|
async listSources(...a) {
|
|
1297
1418
|
return (await this.ensure()).listSources(...a);
|
|
1298
1419
|
}
|
|
@@ -1648,6 +1769,9 @@ var LazyEmbeddedPostgresBackend = class {
|
|
|
1648
1769
|
async findBySource(...a) {
|
|
1649
1770
|
return (await this.ensure()).findBySource(...a);
|
|
1650
1771
|
}
|
|
1772
|
+
async findByKeys(...a) {
|
|
1773
|
+
return (await this.ensure()).findByKeys(...a);
|
|
1774
|
+
}
|
|
1651
1775
|
async listSources(...a) {
|
|
1652
1776
|
return (await this.ensure()).listSources(...a);
|
|
1653
1777
|
}
|
|
@@ -1832,8 +1956,8 @@ async function upsertRecord(input, opts = {}) {
|
|
|
1832
1956
|
const { searchable_text, content_hash, embedding, embedding_model } = await prepareRecord(input, opts, "sync");
|
|
1833
1957
|
const prepared = {
|
|
1834
1958
|
...input,
|
|
1835
|
-
searchable_text: input.searchable_text ?? searchable_text,
|
|
1836
|
-
content_hash: input.content_hash ?? content_hash,
|
|
1959
|
+
searchable_text: opts.preserveDerived ? void 0 : input.searchable_text ?? searchable_text,
|
|
1960
|
+
content_hash: opts.preserveDerived ? void 0 : input.content_hash ?? content_hash,
|
|
1837
1961
|
embedding,
|
|
1838
1962
|
embedding_model
|
|
1839
1963
|
};
|