amfs-adapter-postgres 0.2.43__tar.gz → 0.2.44__tar.gz

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.
Files changed (30) hide show
  1. {amfs_adapter_postgres-0.2.43 → amfs_adapter_postgres-0.2.44}/PKG-INFO +2 -2
  2. {amfs_adapter_postgres-0.2.43 → amfs_adapter_postgres-0.2.44}/pyproject.toml +2 -2
  3. {amfs_adapter_postgres-0.2.43 → amfs_adapter_postgres-0.2.44}/src/amfs_postgres/adapter.py +35 -8
  4. {amfs_adapter_postgres-0.2.43 → amfs_adapter_postgres-0.2.44}/src/amfs_postgres/async_adapter.py +16 -3
  5. {amfs_adapter_postgres-0.2.43 → amfs_adapter_postgres-0.2.44}/.gitignore +0 -0
  6. {amfs_adapter_postgres-0.2.43 → amfs_adapter_postgres-0.2.44}/src/amfs_postgres/__init__.py +0 -0
  7. {amfs_adapter_postgres-0.2.43 → amfs_adapter_postgres-0.2.44}/src/amfs_postgres/_fts.py +0 -0
  8. {amfs_adapter_postgres-0.2.43 → amfs_adapter_postgres-0.2.44}/src/amfs_postgres/evidence_sql.py +0 -0
  9. {amfs_adapter_postgres-0.2.43 → amfs_adapter_postgres-0.2.44}/src/amfs_postgres/knn.py +0 -0
  10. {amfs_adapter_postgres-0.2.43 → amfs_adapter_postgres-0.2.44}/src/amfs_postgres/migrations/001_initial.sql +0 -0
  11. {amfs_adapter_postgres-0.2.43 → amfs_adapter_postgres-0.2.44}/src/amfs_postgres/migrations/002_artifact_refs.sql +0 -0
  12. {amfs_adapter_postgres-0.2.43 → amfs_adapter_postgres-0.2.44}/src/amfs_postgres/migrations/002_search_indexes.sql +0 -0
  13. {amfs_adapter_postgres-0.2.43 → amfs_adapter_postgres-0.2.44}/src/amfs_postgres/migrations/003_knowledge_graph.sql +0 -0
  14. {amfs_adapter_postgres-0.2.43 → amfs_adapter_postgres-0.2.44}/src/amfs_postgres/migrations/004_agent_groups.sql +0 -0
  15. {amfs_adapter_postgres-0.2.43 → amfs_adapter_postgres-0.2.44}/src/amfs_postgres/migrations/005_configurable_embedding_dim.sql +0 -0
  16. {amfs_adapter_postgres-0.2.43 → amfs_adapter_postgres-0.2.44}/src/amfs_postgres/migrations/006_is_artifact.sql +0 -0
  17. {amfs_adapter_postgres-0.2.43 → amfs_adapter_postgres-0.2.44}/src/amfs_postgres/migrations/006_outcome_confidence_semantics.sql +0 -0
  18. {amfs_adapter_postgres-0.2.43 → amfs_adapter_postgres-0.2.44}/src/amfs_postgres/migrations/007_outcome_propagation_row_copy.sql +0 -0
  19. {amfs_adapter_postgres-0.2.43 → amfs_adapter_postgres-0.2.44}/src/amfs_postgres/migrations/008_outcome_evidence.sql +0 -0
  20. {amfs_adapter_postgres-0.2.43 → amfs_adapter_postgres-0.2.44}/src/amfs_postgres/migrations/009_action_priors.sql +0 -0
  21. {amfs_adapter_postgres-0.2.43 → amfs_adapter_postgres-0.2.44}/src/amfs_postgres/migrations/010_local_evidence.sql +0 -0
  22. {amfs_adapter_postgres-0.2.43 → amfs_adapter_postgres-0.2.44}/src/amfs_postgres/migrations/011_outcome_task_text.sql +0 -0
  23. {amfs_adapter_postgres-0.2.43 → amfs_adapter_postgres-0.2.44}/src/amfs_postgres/migrations/012_evidence_from_model.sql +0 -0
  24. {amfs_adapter_postgres-0.2.43 → amfs_adapter_postgres-0.2.44}/src/amfs_postgres/migrations/013_outcome_verified_by_backfill.sql +0 -0
  25. {amfs_adapter_postgres-0.2.43 → amfs_adapter_postgres-0.2.44}/src/amfs_postgres/migrations/014_outcome_covariates.sql +0 -0
  26. {amfs_adapter_postgres-0.2.43 → amfs_adapter_postgres-0.2.44}/src/amfs_postgres/retention.py +0 -0
  27. {amfs_adapter_postgres-0.2.43 → amfs_adapter_postgres-0.2.44}/src/amfs_postgres/schema.sql +0 -0
  28. {amfs_adapter_postgres-0.2.43 → amfs_adapter_postgres-0.2.44}/src/amfs_postgres/tenant_context.py +0 -0
  29. {amfs_adapter_postgres-0.2.43 → amfs_adapter_postgres-0.2.44}/src/amfs_postgres/tenant_gucs.py +0 -0
  30. {amfs_adapter_postgres-0.2.43 → amfs_adapter_postgres-0.2.44}/src/amfs_postgres/trace_partitions.py +0 -0
@@ -1,9 +1,9 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: amfs-adapter-postgres
3
- Version: 0.2.43
3
+ Version: 0.2.44
4
4
  Summary: AMFS Postgres adapter with back-propagation triggers
5
5
  License-Expression: Apache-2.0
6
6
  Requires-Python: >=3.11
7
- Requires-Dist: amfs-core>=0.3.54
7
+ Requires-Dist: amfs-core>=0.3.56
8
8
  Requires-Dist: psycopg-pool>=3.1
9
9
  Requires-Dist: psycopg[binary]<4,>=3.1
@@ -1,13 +1,13 @@
1
1
  [project]
2
2
  name = "amfs-adapter-postgres"
3
- version = "0.2.43"
3
+ version = "0.2.44"
4
4
  description = "AMFS Postgres adapter with back-propagation triggers"
5
5
  requires-python = ">=3.11"
6
6
  license = "Apache-2.0"
7
7
  dependencies = [
8
8
  # 0.3.14 adds the capture fields this adapter now persists and selects back,
9
9
  # tool_calls among them.
10
- "amfs-core>=0.3.54",
10
+ "amfs-core>=0.3.56",
11
11
  "psycopg[binary]>=3.1,<4",
12
12
  "psycopg-pool>=3.1",
13
13
  ]
@@ -222,6 +222,14 @@ def priority_score_sql(now: datetime, config: Any | None = None) -> tuple[str, l
222
222
  return expr, params
223
223
 
224
224
 
225
+ #: A row whose TTL has not passed: the predicate every read path that
226
+ #: surfaces current entries carries (``list``, ``search``, ``semantic_search``,
227
+ #: the agent listing). ``NOW()`` rather than a parameter because the fragment
228
+ #: sits in WHERE lists whose parameters are positional and spliced around by
229
+ #: their callers; with nothing to bind it cannot shift any of them.
230
+ _NOT_EXPIRED = "(ttl_at IS NULL OR ttl_at > NOW())"
231
+
232
+
225
233
  def list_conditions(
226
234
  namespace: str,
227
235
  entity_path: str | None,
@@ -231,6 +239,7 @@ def list_conditions(
231
239
  scope: SqlScope | None,
232
240
  agent_id: str | None,
233
241
  branch_scope: tuple[str, list[Any]] | None = None,
242
+ include_expired: bool = False,
234
243
  ) -> tuple[list[str], list[Any]]:
235
244
  """The WHERE behind ``list()`` and ``count_entries()`` on both adapters.
236
245
 
@@ -238,6 +247,13 @@ def list_conditions(
238
247
  about which rows exist, and so the shared-namespace guard is applied to
239
248
  the unscoped read in one place (``tests/test_shared_path_scoping.py``).
240
249
 
250
+ The rows are the live ones: current and not past their ``ttl_at``, which
251
+ is the rule ``read`` and ``search`` apply, so a TTL takes effect on every
252
+ listing at once instead of when the sweep next archives the row. Two
253
+ callers see expired rows all the same. *include_superseded* is the
254
+ history, which hides nothing; *include_expired* is for the sweep, whose
255
+ whole purpose is to find them (``list_expired``).
256
+
241
257
  *branch_scope* replaces the plain ``branch = %s`` predicate with the
242
258
  overlay fragment from :func:`branch_scope_sql` (a branch's own live rows
243
259
  plus the parent's for every key it has not touched) — the adapters pass
@@ -258,6 +274,8 @@ def list_conditions(
258
274
 
259
275
  if not include_superseded:
260
276
  conditions.append("superseded_at IS NULL")
277
+ if not include_expired:
278
+ conditions.append(_NOT_EXPIRED)
261
279
  if agent_id is not None:
262
280
  conditions.append("agent_id = %s")
263
281
  params.append(agent_id)
@@ -2453,13 +2471,15 @@ class PostgresAdapter(AdapterABC):
2453
2471
 
2454
2472
  What a TTL sweep needs: the rows to archive, found through the
2455
2473
  partial index on ``ttl_at`` (``idx_entries_ttl``) rather than by
2456
- listing the tenant and checking each entry's clock in Python. Same
2457
- row set as ``list()`` — current versions, this branch, shared paths
2458
- excluded — narrowed to the expired ones. *limit* bounds one sweep;
2459
- the next sweep picks up the rest.
2474
+ listing the tenant and checking each entry's clock in Python. The
2475
+ scope is ``list()``'s — current versions, this branch, shared paths
2476
+ excluded — and the rows are the ones ``list()`` leaves out: those past
2477
+ their TTL and not yet archived. *limit* bounds one sweep; the next
2478
+ sweep picks up the rest.
2460
2479
  """
2461
2480
  conditions, params = list_conditions(
2462
2481
  self._namespace, None, include_superseded=False, branch=branch, scope=None, agent_id=None,
2482
+ include_expired=True,
2463
2483
  )
2464
2484
  conditions.append("ttl_at IS NOT NULL AND ttl_at <= %s")
2465
2485
  params.append(now or datetime.now(timezone.utc))
@@ -2684,7 +2704,7 @@ class PostgresAdapter(AdapterABC):
2684
2704
 
2685
2705
  # The branch scope is resolved on the connection below (it may need the
2686
2706
  # branch's parent); a placeholder keeps its position in the WHERE.
2687
- conditions = ["namespace = %s", _BRANCH_SCOPE, "superseded_at IS NULL"]
2707
+ conditions = ["namespace = %s", _BRANCH_SCOPE, "superseded_at IS NULL", _NOT_EXPIRED]
2688
2708
  params: list[Any] = [self._namespace]
2689
2709
 
2690
2710
  if query.depth < 3:
@@ -2821,7 +2841,7 @@ class PostgresAdapter(AdapterABC):
2821
2841
 
2822
2842
  query_vec = query.embedding if query.embedding is not None else embedder.embed(query.text)
2823
2843
 
2824
- conditions = ["namespace = %s", "superseded_at IS NULL", "embedding IS NOT NULL"]
2844
+ conditions = ["namespace = %s", "superseded_at IS NULL", "embedding IS NOT NULL", _NOT_EXPIRED]
2825
2845
  params: list[Any] = [self._namespace]
2826
2846
 
2827
2847
  if query.entity_path is not None:
@@ -2928,15 +2948,20 @@ class PostgresAdapter(AdapterABC):
2928
2948
  belonging to another agent turns up in ``keys``. A NULL *agent_id*
2929
2949
  compares as unknown rather than true, so passing nothing counts shared
2930
2950
  entries only — the safe direction.
2951
+
2952
+ The TTL predicate is here for the same reason: ``list()`` leaves out an
2953
+ entry past its ``ttl_at``, so the key of one must not turn up in
2954
+ ``keys`` either.
2931
2955
  """
2932
2956
  with self._pool.connection() as conn:
2933
2957
  row = conn.execute(
2934
- """SELECT count(*) AS total,
2958
+ f"""SELECT count(*) AS total,
2935
2959
  count(*) FILTER (WHERE coalesce(recall_count, 0) = 0)
2936
2960
  AS never_read
2937
2961
  FROM amfs_memory_entries
2938
2962
  WHERE namespace = %s AND branch = %s AND entity_path = %s
2939
2963
  AND superseded_at IS NULL
2964
+ AND {_NOT_EXPIRED}
2940
2965
  AND (%s::text IS NULL OR key <> %s)
2941
2966
  AND (shared OR agent_id = %s::text)""",
2942
2967
  (self._namespace, branch, entity_path, exclude_key, exclude_key,
@@ -2944,9 +2969,10 @@ class PostgresAdapter(AdapterABC):
2944
2969
  ).fetchone()
2945
2970
  keys = [
2946
2971
  r["key"] for r in conn.execute(
2947
- """SELECT key FROM amfs_memory_entries
2972
+ f"""SELECT key FROM amfs_memory_entries
2948
2973
  WHERE namespace = %s AND branch = %s AND entity_path = %s
2949
2974
  AND superseded_at IS NULL
2975
+ AND {_NOT_EXPIRED}
2950
2976
  AND (%s::text IS NULL OR key <> %s)
2951
2977
  AND (shared OR agent_id = %s::text)
2952
2978
  ORDER BY key LIMIT %s""",
@@ -5086,6 +5112,7 @@ class PostgresAdapter(AdapterABC):
5086
5112
  "agent_id = %s",
5087
5113
  "superseded_at IS NULL",
5088
5114
  "branch = 'main'",
5115
+ _NOT_EXPIRED,
5089
5116
  ]
5090
5117
  params: list[Any] = [namespace, agent_id]
5091
5118
  for prefix in exclude_prefixes:
@@ -42,6 +42,7 @@ from amfs_postgres.adapter import (
42
42
  _EVIDENCE_COLUMNS,
43
43
  _EVIDENCE_SELECT,
44
44
  _EXCLUDE_SHARED_PATHS,
45
+ _NOT_EXPIRED,
45
46
  _REUSE_EVENT_INSERT_SQL,
46
47
  PostgresAdapter,
47
48
  _evidence_params,
@@ -353,6 +354,12 @@ class AsyncPostgresAdapter:
353
354
  entry = self._row_to_entry(row)
354
355
  if entry.confidence < min_confidence:
355
356
  return None
357
+ # As ``PostgresAdapter.read`` does: a TTL takes effect on read, without
358
+ # waiting for the sweep. Missing here, the HTTP read route — which
359
+ # tries this adapter first — served an expired entry that the sync
360
+ # adapter behind the same route would have withheld.
361
+ if entry.is_expired():
362
+ return None
356
363
  return entry
357
364
 
358
365
  # ──────────────────────────────────────────────────────────────
@@ -580,7 +587,7 @@ class AsyncPostgresAdapter:
580
587
 
581
588
  # ``_BRANCH_SCOPE`` is swapped for the real branch condition once a
582
589
  # connection is open; its params are spliced in after the namespace.
583
- conditions = ["namespace = %s", _BRANCH_SCOPE, "superseded_at IS NULL"]
590
+ conditions = ["namespace = %s", _BRANCH_SCOPE, "superseded_at IS NULL", _NOT_EXPIRED]
584
591
  params: list[Any] = [self._namespace]
585
592
 
586
593
  if query.depth < 3:
@@ -713,6 +720,7 @@ class AsyncPostgresAdapter:
713
720
  _BRANCH_SCOPE,
714
721
  "superseded_at IS NULL",
715
722
  "embedding IS NOT NULL",
723
+ _NOT_EXPIRED,
716
724
  ]
717
725
  params: list[Any] = [self._namespace]
718
726
  if query.entity_path is not None:
@@ -1145,15 +1153,19 @@ class AsyncPostgresAdapter:
1145
1153
  ``list()`` applied, and this block's ``keys`` land in a tool result. A
1146
1154
  NULL *agent_id* compares as unknown, so passing nothing counts shared
1147
1155
  entries only.
1156
+
1157
+ The TTL predicate is carried for the same reason: ``list()`` leaves out
1158
+ an entry past its ``ttl_at``, so its key must not turn up here either.
1148
1159
  """
1149
1160
  async with self._pool.connection() as conn:
1150
1161
  cur = await conn.execute(
1151
- """SELECT count(*) AS total,
1162
+ f"""SELECT count(*) AS total,
1152
1163
  count(*) FILTER (WHERE coalesce(recall_count, 0) = 0)
1153
1164
  AS never_read
1154
1165
  FROM amfs_memory_entries
1155
1166
  WHERE namespace = %s AND branch = %s AND entity_path = %s
1156
1167
  AND superseded_at IS NULL
1168
+ AND {_NOT_EXPIRED}
1157
1169
  AND (%s::text IS NULL OR key <> %s)
1158
1170
  AND (shared OR agent_id = %s::text)""",
1159
1171
  (self._namespace, branch, entity_path, exclude_key, exclude_key,
@@ -1161,9 +1173,10 @@ class AsyncPostgresAdapter:
1161
1173
  )
1162
1174
  row = await cur.fetchone()
1163
1175
  cur = await conn.execute(
1164
- """SELECT key FROM amfs_memory_entries
1176
+ f"""SELECT key FROM amfs_memory_entries
1165
1177
  WHERE namespace = %s AND branch = %s AND entity_path = %s
1166
1178
  AND superseded_at IS NULL
1179
+ AND {_NOT_EXPIRED}
1167
1180
  AND (%s::text IS NULL OR key <> %s)
1168
1181
  AND (shared OR agent_id = %s::text)
1169
1182
  ORDER BY key LIMIT %s""",