b3-prosaic-harness-postgres 0.3.0__tar.gz → 0.4.1__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 (52) hide show
  1. {b3_prosaic_harness_postgres-0.3.0/src/b3_prosaic_harness_postgres.egg-info → b3_prosaic_harness_postgres-0.4.1}/PKG-INFO +50 -9
  2. {b3_prosaic_harness_postgres-0.3.0 → b3_prosaic_harness_postgres-0.4.1}/README.md +48 -7
  3. {b3_prosaic_harness_postgres-0.3.0 → b3_prosaic_harness_postgres-0.4.1}/pyproject.toml +2 -2
  4. {b3_prosaic_harness_postgres-0.3.0 → b3_prosaic_harness_postgres-0.4.1/src/b3_prosaic_harness_postgres.egg-info}/PKG-INFO +50 -9
  5. {b3_prosaic_harness_postgres-0.3.0 → b3_prosaic_harness_postgres-0.4.1}/src/b3_prosaic_harness_postgres.egg-info/SOURCES.txt +2 -0
  6. {b3_prosaic_harness_postgres-0.3.0 → b3_prosaic_harness_postgres-0.4.1}/src/b3_prosaic_harness_postgres.egg-info/requires.txt +1 -1
  7. {b3_prosaic_harness_postgres-0.3.0 → b3_prosaic_harness_postgres-0.4.1}/src/prosaic_harness_postgres/__init__.py +1 -1
  8. {b3_prosaic_harness_postgres-0.3.0 → b3_prosaic_harness_postgres-0.4.1}/src/prosaic_harness_postgres/cli.py +10 -2
  9. {b3_prosaic_harness_postgres-0.3.0 → b3_prosaic_harness_postgres-0.4.1}/src/prosaic_harness_postgres/session_schema.py +31 -4
  10. {b3_prosaic_harness_postgres-0.3.0 → b3_prosaic_harness_postgres-0.4.1}/src/prosaic_harness_postgres/session_store.py +9 -2
  11. {b3_prosaic_harness_postgres-0.3.0 → b3_prosaic_harness_postgres-0.4.1}/src/prosaic_harness_postgres/store.py +2 -2
  12. {b3_prosaic_harness_postgres-0.3.0 → b3_prosaic_harness_postgres-0.4.1}/src/prosaic_harness_postgres/transport.py +7 -2
  13. {b3_prosaic_harness_postgres-0.3.0 → b3_prosaic_harness_postgres-0.4.1}/tests/support/local_cluster.py +6 -5
  14. {b3_prosaic_harness_postgres-0.3.0 → b3_prosaic_harness_postgres-0.4.1}/tests/support/session_documents.py +26 -0
  15. b3_prosaic_harness_postgres-0.4.1/tests/test_durability_profiles.py +96 -0
  16. {b3_prosaic_harness_postgres-0.3.0 → b3_prosaic_harness_postgres-0.4.1}/tests/test_packaging.py +9 -8
  17. b3_prosaic_harness_postgres-0.4.1/tests/test_session_context.py +112 -0
  18. {b3_prosaic_harness_postgres-0.3.0 → b3_prosaic_harness_postgres-0.4.1}/LICENSE +0 -0
  19. {b3_prosaic_harness_postgres-0.3.0 → b3_prosaic_harness_postgres-0.4.1}/MANIFEST.in +0 -0
  20. {b3_prosaic_harness_postgres-0.3.0 → b3_prosaic_harness_postgres-0.4.1}/NOTICE +0 -0
  21. {b3_prosaic_harness_postgres-0.3.0 → b3_prosaic_harness_postgres-0.4.1}/setup.cfg +0 -0
  22. {b3_prosaic_harness_postgres-0.3.0 → b3_prosaic_harness_postgres-0.4.1}/src/b3_prosaic_harness_postgres.egg-info/dependency_links.txt +0 -0
  23. {b3_prosaic_harness_postgres-0.3.0 → b3_prosaic_harness_postgres-0.4.1}/src/b3_prosaic_harness_postgres.egg-info/entry_points.txt +0 -0
  24. {b3_prosaic_harness_postgres-0.3.0 → b3_prosaic_harness_postgres-0.4.1}/src/b3_prosaic_harness_postgres.egg-info/top_level.txt +0 -0
  25. {b3_prosaic_harness_postgres-0.3.0 → b3_prosaic_harness_postgres-0.4.1}/src/prosaic_harness_postgres/leases.py +0 -0
  26. {b3_prosaic_harness_postgres-0.3.0 → b3_prosaic_harness_postgres-0.4.1}/src/prosaic_harness_postgres/schema.py +0 -0
  27. {b3_prosaic_harness_postgres-0.3.0 → b3_prosaic_harness_postgres-0.4.1}/src/prosaic_harness_postgres/session_leases.py +0 -0
  28. {b3_prosaic_harness_postgres-0.3.0 → b3_prosaic_harness_postgres-0.4.1}/tests/conftest.py +0 -0
  29. {b3_prosaic_harness_postgres-0.3.0 → b3_prosaic_harness_postgres-0.4.1}/tests/support/linux_wheel_smoke.py +0 -0
  30. {b3_prosaic_harness_postgres-0.3.0 → b3_prosaic_harness_postgres-0.4.1}/tests/support/model_server.py +0 -0
  31. {b3_prosaic_harness_postgres-0.3.0 → b3_prosaic_harness_postgres-0.4.1}/tests/support/session_workers.py +0 -0
  32. {b3_prosaic_harness_postgres-0.3.0 → b3_prosaic_harness_postgres-0.4.1}/tests/support/tcp_proxy.py +0 -0
  33. {b3_prosaic_harness_postgres-0.3.0 → b3_prosaic_harness_postgres-0.4.1}/tests/support/workers.py +0 -0
  34. {b3_prosaic_harness_postgres-0.3.0 → b3_prosaic_harness_postgres-0.4.1}/tests/test_cli.py +0 -0
  35. {b3_prosaic_harness_postgres-0.3.0 → b3_prosaic_harness_postgres-0.4.1}/tests/test_durability.py +0 -0
  36. {b3_prosaic_harness_postgres-0.3.0 → b3_prosaic_harness_postgres-0.4.1}/tests/test_engine.py +0 -0
  37. {b3_prosaic_harness_postgres-0.3.0 → b3_prosaic_harness_postgres-0.4.1}/tests/test_examples.py +0 -0
  38. {b3_prosaic_harness_postgres-0.3.0 → b3_prosaic_harness_postgres-0.4.1}/tests/test_leases.py +0 -0
  39. {b3_prosaic_harness_postgres-0.3.0 → b3_prosaic_harness_postgres-0.4.1}/tests/test_local_cluster.py +0 -0
  40. {b3_prosaic_harness_postgres-0.3.0 → b3_prosaic_harness_postgres-0.4.1}/tests/test_native_factory.py +0 -0
  41. {b3_prosaic_harness_postgres-0.3.0 → b3_prosaic_harness_postgres-0.4.1}/tests/test_recovery.py +0 -0
  42. {b3_prosaic_harness_postgres-0.3.0 → b3_prosaic_harness_postgres-0.4.1}/tests/test_registration.py +0 -0
  43. {b3_prosaic_harness_postgres-0.3.0 → b3_prosaic_harness_postgres-0.4.1}/tests/test_routing.py +0 -0
  44. {b3_prosaic_harness_postgres-0.3.0 → b3_prosaic_harness_postgres-0.4.1}/tests/test_schema.py +0 -0
  45. {b3_prosaic_harness_postgres-0.3.0 → b3_prosaic_harness_postgres-0.4.1}/tests/test_session_fencing.py +0 -0
  46. {b3_prosaic_harness_postgres-0.3.0 → b3_prosaic_harness_postgres-0.4.1}/tests/test_session_recovery.py +0 -0
  47. {b3_prosaic_harness_postgres-0.3.0 → b3_prosaic_harness_postgres-0.4.1}/tests/test_session_schema.py +0 -0
  48. {b3_prosaic_harness_postgres-0.3.0 → b3_prosaic_harness_postgres-0.4.1}/tests/test_session_store.py +0 -0
  49. {b3_prosaic_harness_postgres-0.3.0 → b3_prosaic_harness_postgres-0.4.1}/tests/test_session_workers.py +0 -0
  50. {b3_prosaic_harness_postgres-0.3.0 → b3_prosaic_harness_postgres-0.4.1}/tests/test_store.py +0 -0
  51. {b3_prosaic_harness_postgres-0.3.0 → b3_prosaic_harness_postgres-0.4.1}/tests/test_transport.py +0 -0
  52. {b3_prosaic_harness_postgres-0.3.0 → b3_prosaic_harness_postgres-0.4.1}/tests/test_workers.py +0 -0
@@ -1,13 +1,13 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: b3-prosaic-harness-postgres
3
- Version: 0.3.0
3
+ Version: 0.4.1
4
4
  Summary: Explicit PostgreSQL run and conversation storage for Prosaic Harness
5
5
  License-Expression: Apache-2.0
6
6
  Requires-Python: >=3.11
7
7
  Description-Content-Type: text/markdown
8
8
  License-File: LICENSE
9
9
  License-File: NOTICE
10
- Requires-Dist: b3-prosaic-harness<0.9,>=0.8
10
+ Requires-Dist: b3-prosaic-harness<0.10,>=0.9
11
11
  Requires-Dist: psycopg[binary]<4,>=3.2.3
12
12
  Requires-Dist: psycopg-pool<4,>=3.2
13
13
  Provides-Extra: test
@@ -24,13 +24,35 @@ Run on worker A, inspect/resume on worker B, without a shared writable run
24
24
  directory. Core remains database-free; install this separate adapter only when
25
25
  you need shared durable state. The API is synchronous; a private I/O thread
26
26
  bounds network waits. Model execution holds no database connection/transaction.
27
- The conversation candidate is `b3-prosaic-harness-postgres` **0.3.0**, selecting
28
- `b3-prosaic-harness>=0.8,<0.9`. The adapter is maintained in this repository but
29
- installed and versioned separately, keeping PostgreSQL drivers optional for
30
- Harness users. Imports and CLI names remain unchanged. Final qualification and
31
- publication of this conversation release train are pending. The historical
32
- [Harness v0.6.1 release](https://github.com/B3Cognition/prosaic-harness/releases/tag/v0.6.1)
33
- retains its original 0.6.1/0.1.1 package names and receipts.
27
+ The adapter is independently versioned: `b3-prosaic-harness-postgres` **0.4.1**
28
+ requires `b3-prosaic-harness>=0.9,<0.10`. PostgreSQL drivers remain optional for
29
+ core Harness users. Imports and CLI names are unchanged.
30
+
31
+ ## Durability profiles
32
+
33
+ The default `durability_profile="postgresql"` requires `full_page_writes=on`.
34
+ An operator who has independently verified an Amazon Aurora PostgreSQL cluster
35
+ may explicitly select `durability_profile="aurora-postgresql"` on both
36
+ `PostgresRunStore` and `PostgresSessionStore` (including `from_env`). This accepts
37
+ `full_page_writes=off` under Aurora's log-based storage contract. It does not
38
+ weaken fsync, synchronous commit, writable-primary, logged-table, lease or
39
+ unknown-commit checks. The guard runs for every operation and connection reuse.
40
+ Invalid profiles fail before connecting. This profile is an operator assertion,
41
+ not engine autodetection or proof that arbitrary storage is safe. NEVER select
42
+ it for ordinary PostgreSQL just to suppress a readiness failure.
43
+
44
+ For every CLI command, pass `--durability-profile aurora-postgresql` explicitly.
45
+ Migration and runtime must select the same profile. Verify the actual engine and
46
+ version with AWS RDS metadata/DBA evidence; AWS also documents `aurora_version()`.
47
+ Capture a failed SQL probe's actual error and inspect its schema/search path;
48
+ NEVER convert an unavailable probe into permission to bypass durability checks.
49
+ No AWS credentials or model calls are needed by the adapter.
50
+
51
+ AWS describes the storage contract in
52
+ [its architecture guide](https://aws.amazon.com/blogs/database/is-amazon-rds-for-postgresql-or-amazon-aurora-postgresql-a-better-choice-for-me/).
53
+ Native PostgreSQL tests qualify the policy and all existing recovery behavior;
54
+ they do not emulate Aurora storage. An actual Aurora deployment needs synthetic
55
+ persistence/restart/failover qualification before enabling production work.
34
56
 
35
57
  ## 1. Install and supply an existing database
36
58
 
@@ -316,3 +338,22 @@ conversation backend. Run the owned PostgreSQL 16 and 18 fixtures shown above
316
338
  with `--require-postgres`. Missing infrastructure or any skipped required case
317
339
  fails qualification. Installed Linux AMD64 and ARM64 acceptance is required
318
340
  before claiming the conversation candidate is ready for production.
341
+
342
+ ## Optional context storage contract
343
+
344
+ Conversation storage contract 2 supports `context_assembly_v1`, reads both v1
345
+ and v2 records, and preserves existing documents. New database initialization
346
+ creates contract 2. Existing contract-1 databases require an explicit migration;
347
+ runtime readiness never performs it. Drain old writers, deploy upgraded workers,
348
+ then use migration-owner credentials:
349
+
350
+ ```sh
351
+ prosaic-harness-postgres migrate-conversations --dsn-env HARNESS_MIGRATION_DSN
352
+ ```
353
+
354
+ `PostgresSessionStore.migrate_context_storage()` is the equivalent operator API.
355
+ The migration is locked, transactional and repeatable; restricted runtime roles
356
+ cannot update schema metadata. Old workers reject the upgraded storage contract.
357
+ Keep migration-owner credentials separate from runtime credentials. Enable host
358
+ context policies only after Runtime, Harness and store advertise the capability.
359
+ No production token counter is bundled or certified by this storage adapter.
@@ -4,13 +4,35 @@ Run on worker A, inspect/resume on worker B, without a shared writable run
4
4
  directory. Core remains database-free; install this separate adapter only when
5
5
  you need shared durable state. The API is synchronous; a private I/O thread
6
6
  bounds network waits. Model execution holds no database connection/transaction.
7
- The conversation candidate is `b3-prosaic-harness-postgres` **0.3.0**, selecting
8
- `b3-prosaic-harness>=0.8,<0.9`. The adapter is maintained in this repository but
9
- installed and versioned separately, keeping PostgreSQL drivers optional for
10
- Harness users. Imports and CLI names remain unchanged. Final qualification and
11
- publication of this conversation release train are pending. The historical
12
- [Harness v0.6.1 release](https://github.com/B3Cognition/prosaic-harness/releases/tag/v0.6.1)
13
- retains its original 0.6.1/0.1.1 package names and receipts.
7
+ The adapter is independently versioned: `b3-prosaic-harness-postgres` **0.4.1**
8
+ requires `b3-prosaic-harness>=0.9,<0.10`. PostgreSQL drivers remain optional for
9
+ core Harness users. Imports and CLI names are unchanged.
10
+
11
+ ## Durability profiles
12
+
13
+ The default `durability_profile="postgresql"` requires `full_page_writes=on`.
14
+ An operator who has independently verified an Amazon Aurora PostgreSQL cluster
15
+ may explicitly select `durability_profile="aurora-postgresql"` on both
16
+ `PostgresRunStore` and `PostgresSessionStore` (including `from_env`). This accepts
17
+ `full_page_writes=off` under Aurora's log-based storage contract. It does not
18
+ weaken fsync, synchronous commit, writable-primary, logged-table, lease or
19
+ unknown-commit checks. The guard runs for every operation and connection reuse.
20
+ Invalid profiles fail before connecting. This profile is an operator assertion,
21
+ not engine autodetection or proof that arbitrary storage is safe. NEVER select
22
+ it for ordinary PostgreSQL just to suppress a readiness failure.
23
+
24
+ For every CLI command, pass `--durability-profile aurora-postgresql` explicitly.
25
+ Migration and runtime must select the same profile. Verify the actual engine and
26
+ version with AWS RDS metadata/DBA evidence; AWS also documents `aurora_version()`.
27
+ Capture a failed SQL probe's actual error and inspect its schema/search path;
28
+ NEVER convert an unavailable probe into permission to bypass durability checks.
29
+ No AWS credentials or model calls are needed by the adapter.
30
+
31
+ AWS describes the storage contract in
32
+ [its architecture guide](https://aws.amazon.com/blogs/database/is-amazon-rds-for-postgresql-or-amazon-aurora-postgresql-a-better-choice-for-me/).
33
+ Native PostgreSQL tests qualify the policy and all existing recovery behavior;
34
+ they do not emulate Aurora storage. An actual Aurora deployment needs synthetic
35
+ persistence/restart/failover qualification before enabling production work.
14
36
 
15
37
  ## 1. Install and supply an existing database
16
38
 
@@ -296,3 +318,22 @@ conversation backend. Run the owned PostgreSQL 16 and 18 fixtures shown above
296
318
  with `--require-postgres`. Missing infrastructure or any skipped required case
297
319
  fails qualification. Installed Linux AMD64 and ARM64 acceptance is required
298
320
  before claiming the conversation candidate is ready for production.
321
+
322
+ ## Optional context storage contract
323
+
324
+ Conversation storage contract 2 supports `context_assembly_v1`, reads both v1
325
+ and v2 records, and preserves existing documents. New database initialization
326
+ creates contract 2. Existing contract-1 databases require an explicit migration;
327
+ runtime readiness never performs it. Drain old writers, deploy upgraded workers,
328
+ then use migration-owner credentials:
329
+
330
+ ```sh
331
+ prosaic-harness-postgres migrate-conversations --dsn-env HARNESS_MIGRATION_DSN
332
+ ```
333
+
334
+ `PostgresSessionStore.migrate_context_storage()` is the equivalent operator API.
335
+ The migration is locked, transactional and repeatable; restricted runtime roles
336
+ cannot update schema metadata. Old workers reject the upgraded storage contract.
337
+ Keep migration-owner credentials separate from runtime credentials. Enable host
338
+ context policies only after Runtime, Harness and store advertise the capability.
339
+ No production token counter is bundled or certified by this storage adapter.
@@ -4,13 +4,13 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "b3-prosaic-harness-postgres"
7
- version = "0.3.0"
7
+ version = "0.4.1"
8
8
  description = "Explicit PostgreSQL run and conversation storage for Prosaic Harness"
9
9
  requires-python = ">=3.11"
10
10
  readme = "README.md"
11
11
  license = "Apache-2.0"
12
12
  license-files = ["LICENSE", "NOTICE"]
13
- dependencies = ["b3-prosaic-harness>=0.8,<0.9", "psycopg[binary]>=3.2.3,<4", "psycopg-pool>=3.2,<4"]
13
+ dependencies = ["b3-prosaic-harness>=0.9,<0.10", "psycopg[binary]>=3.2.3,<4", "psycopg-pool>=3.2,<4"]
14
14
 
15
15
  [project.optional-dependencies]
16
16
  test = ["pytest>=8", "build>=1", "setuptools>=77", "wheel", "packaging"]
@@ -1,13 +1,13 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: b3-prosaic-harness-postgres
3
- Version: 0.3.0
3
+ Version: 0.4.1
4
4
  Summary: Explicit PostgreSQL run and conversation storage for Prosaic Harness
5
5
  License-Expression: Apache-2.0
6
6
  Requires-Python: >=3.11
7
7
  Description-Content-Type: text/markdown
8
8
  License-File: LICENSE
9
9
  License-File: NOTICE
10
- Requires-Dist: b3-prosaic-harness<0.9,>=0.8
10
+ Requires-Dist: b3-prosaic-harness<0.10,>=0.9
11
11
  Requires-Dist: psycopg[binary]<4,>=3.2.3
12
12
  Requires-Dist: psycopg-pool<4,>=3.2
13
13
  Provides-Extra: test
@@ -24,13 +24,35 @@ Run on worker A, inspect/resume on worker B, without a shared writable run
24
24
  directory. Core remains database-free; install this separate adapter only when
25
25
  you need shared durable state. The API is synchronous; a private I/O thread
26
26
  bounds network waits. Model execution holds no database connection/transaction.
27
- The conversation candidate is `b3-prosaic-harness-postgres` **0.3.0**, selecting
28
- `b3-prosaic-harness>=0.8,<0.9`. The adapter is maintained in this repository but
29
- installed and versioned separately, keeping PostgreSQL drivers optional for
30
- Harness users. Imports and CLI names remain unchanged. Final qualification and
31
- publication of this conversation release train are pending. The historical
32
- [Harness v0.6.1 release](https://github.com/B3Cognition/prosaic-harness/releases/tag/v0.6.1)
33
- retains its original 0.6.1/0.1.1 package names and receipts.
27
+ The adapter is independently versioned: `b3-prosaic-harness-postgres` **0.4.1**
28
+ requires `b3-prosaic-harness>=0.9,<0.10`. PostgreSQL drivers remain optional for
29
+ core Harness users. Imports and CLI names are unchanged.
30
+
31
+ ## Durability profiles
32
+
33
+ The default `durability_profile="postgresql"` requires `full_page_writes=on`.
34
+ An operator who has independently verified an Amazon Aurora PostgreSQL cluster
35
+ may explicitly select `durability_profile="aurora-postgresql"` on both
36
+ `PostgresRunStore` and `PostgresSessionStore` (including `from_env`). This accepts
37
+ `full_page_writes=off` under Aurora's log-based storage contract. It does not
38
+ weaken fsync, synchronous commit, writable-primary, logged-table, lease or
39
+ unknown-commit checks. The guard runs for every operation and connection reuse.
40
+ Invalid profiles fail before connecting. This profile is an operator assertion,
41
+ not engine autodetection or proof that arbitrary storage is safe. NEVER select
42
+ it for ordinary PostgreSQL just to suppress a readiness failure.
43
+
44
+ For every CLI command, pass `--durability-profile aurora-postgresql` explicitly.
45
+ Migration and runtime must select the same profile. Verify the actual engine and
46
+ version with AWS RDS metadata/DBA evidence; AWS also documents `aurora_version()`.
47
+ Capture a failed SQL probe's actual error and inspect its schema/search path;
48
+ NEVER convert an unavailable probe into permission to bypass durability checks.
49
+ No AWS credentials or model calls are needed by the adapter.
50
+
51
+ AWS describes the storage contract in
52
+ [its architecture guide](https://aws.amazon.com/blogs/database/is-amazon-rds-for-postgresql-or-amazon-aurora-postgresql-a-better-choice-for-me/).
53
+ Native PostgreSQL tests qualify the policy and all existing recovery behavior;
54
+ they do not emulate Aurora storage. An actual Aurora deployment needs synthetic
55
+ persistence/restart/failover qualification before enabling production work.
34
56
 
35
57
  ## 1. Install and supply an existing database
36
58
 
@@ -316,3 +338,22 @@ conversation backend. Run the owned PostgreSQL 16 and 18 fixtures shown above
316
338
  with `--require-postgres`. Missing infrastructure or any skipped required case
317
339
  fails qualification. Installed Linux AMD64 and ARM64 acceptance is required
318
340
  before claiming the conversation candidate is ready for production.
341
+
342
+ ## Optional context storage contract
343
+
344
+ Conversation storage contract 2 supports `context_assembly_v1`, reads both v1
345
+ and v2 records, and preserves existing documents. New database initialization
346
+ creates contract 2. Existing contract-1 databases require an explicit migration;
347
+ runtime readiness never performs it. Drain old writers, deploy upgraded workers,
348
+ then use migration-owner credentials:
349
+
350
+ ```sh
351
+ prosaic-harness-postgres migrate-conversations --dsn-env HARNESS_MIGRATION_DSN
352
+ ```
353
+
354
+ `PostgresSessionStore.migrate_context_storage()` is the equivalent operator API.
355
+ The migration is locked, transactional and repeatable; restricted runtime roles
356
+ cannot update schema metadata. Old workers reject the upgraded storage contract.
357
+ Keep migration-owner credentials separate from runtime credentials. Enable host
358
+ context policies only after Runtime, Harness and store advertise the capability.
359
+ No production token counter is bundled or certified by this storage adapter.
@@ -21,6 +21,7 @@ src/prosaic_harness_postgres/transport.py
21
21
  tests/conftest.py
22
22
  tests/test_cli.py
23
23
  tests/test_durability.py
24
+ tests/test_durability_profiles.py
24
25
  tests/test_engine.py
25
26
  tests/test_examples.py
26
27
  tests/test_leases.py
@@ -31,6 +32,7 @@ tests/test_recovery.py
31
32
  tests/test_registration.py
32
33
  tests/test_routing.py
33
34
  tests/test_schema.py
35
+ tests/test_session_context.py
34
36
  tests/test_session_fencing.py
35
37
  tests/test_session_recovery.py
36
38
  tests/test_session_schema.py
@@ -1,4 +1,4 @@
1
- b3-prosaic-harness<0.9,>=0.8
1
+ b3-prosaic-harness<0.10,>=0.9
2
2
  psycopg[binary]<4,>=3.2.3
3
3
  psycopg-pool<4,>=3.2
4
4
 
@@ -1,5 +1,5 @@
1
1
  """Separate PostgreSQL adapter for Prosaic Harness."""
2
- __version__ = '0.3.0'
2
+ __version__ = '0.4.1'
3
3
 
4
4
  from .store import PostgresRunStore
5
5
  from .session_store import PostgresSessionStore
@@ -44,15 +44,23 @@ def check_write(store):
44
44
  def main(argv=None):
45
45
  parser = argparse.ArgumentParser(description='Initialize/check an existing Harness database')
46
46
  commands = parser.add_subparsers(dest='command', required=True)
47
- for name in ('init', 'doctor'):
47
+ for name in ('init', 'doctor', 'migrate-conversations'):
48
48
  command = commands.add_parser(name)
49
49
  command.add_argument('--dsn-env', required=True, help='name of the DSN environment variable, never its value')
50
+ command.add_argument('--durability-profile', choices=['postgresql', 'aurora-postgresql'],
51
+ default='postgresql', help='explicit operator-verified database storage contract')
50
52
  if name == 'doctor':
51
53
  command.add_argument('--check-write', action='store_true', help='explicit disposable write test; DELETE credentials required')
52
54
  args = parser.parse_args(argv)
53
55
  try:
56
+ if args.command == 'migrate-conversations':
57
+ from .session_store import PostgresSessionStore
58
+ with PostgresSessionStore.from_env(args.dsn_env, namespace='conversation-migration', durability_profile=args.durability_profile) as store:
59
+ store.migrate_context_storage()
60
+ print(json.dumps({'migrated': True, 'conversation_storage_contract': 2}))
61
+ return 0
54
62
  namespace = 'prosaic-diagnostic-' + uuid.uuid4().hex
55
- with PostgresRunStore.from_env(args.dsn_env, namespace=namespace) as store:
63
+ with PostgresRunStore.from_env(args.dsn_env, namespace=namespace, durability_profile=args.durability_profile) as store:
56
64
  if args.command == 'init':
57
65
  store.initialize()
58
66
  result = {'initialized': True, 'schema_version': 1}
@@ -1,7 +1,7 @@
1
1
  """Independent, explicit version-1 conversation storage and readiness."""
2
2
  from prosaic_harness import StoreUninitialized, StoreIncompatible
3
3
 
4
- SCHEMA_VERSION = STORAGE_CONTRACT_VERSION = 1
4
+ SCHEMA_VERSION = STORAGE_CONTRACT_VERSION = 2
5
5
  MIGRATION_LOCK = 0x50524348415401
6
6
  SCHEMA = 'prosaic_harness_conversations'
7
7
 
@@ -33,7 +33,7 @@ CREATE TABLE prosaic_harness_conversations.receipts (
33
33
  PRIMARY KEY(namespace, session_id, invocation_id),
34
34
  FOREIGN KEY(namespace, session_id, scope_identity) REFERENCES
35
35
  prosaic_harness_conversations.sessions(namespace, session_id, scope_identity));
36
- INSERT INTO prosaic_harness_conversations.schema_version VALUES(TRUE, 1, 1);
36
+ INSERT INTO prosaic_harness_conversations.schema_version VALUES(TRUE, 2, 2);
37
37
  """
38
38
 
39
39
  COLUMNS = {
@@ -69,7 +69,7 @@ CONSTRAINTS = {
69
69
  }
70
70
 
71
71
 
72
- async def ready_on_connection(conn):
72
+ async def ready_on_connection(conn, *, expected_versions=None):
73
73
  namespace = await (await conn.execute("SELECT oid,pg_catalog.has_schema_privilege(oid,'USAGE'), "
74
74
  "pg_catalog.pg_has_role(nspowner,'USAGE') "
75
75
  "FROM pg_catalog.pg_namespace WHERE nspname=%s", (SCHEMA,))).fetchone()
@@ -154,7 +154,7 @@ async def ready_on_connection(conn):
154
154
  raise StoreIncompatible('use least-privilege conversation runtime grants')
155
155
  versions = await (await conn.execute('SELECT schema_version,storage_contract_version '
156
156
  'FROM prosaic_harness_conversations.schema_version')).fetchall()
157
- if versions != [(SCHEMA_VERSION, STORAGE_CONTRACT_VERSION)]:
157
+ if versions != [expected_versions or (SCHEMA_VERSION, STORAGE_CONTRACT_VERSION)]:
158
158
  raise StoreIncompatible('conversation schema version is incompatible; no automatic migration')
159
159
 
160
160
 
@@ -183,3 +183,30 @@ def initialize(transport):
183
183
  await conn.execute(DDL)
184
184
  await ready_on_connection(conn)
185
185
  transport.call(migrate, timeout_s=30)
186
+
187
+
188
+ def migrate_context_storage(transport):
189
+ async def migrate(conn):
190
+ await conn.execute("SELECT pg_catalog.set_config('lock_timeout','25000ms',true), "
191
+ "pg_catalog.set_config('statement_timeout','28000ms',true)")
192
+ await conn.execute('SELECT pg_catalog.pg_advisory_xact_lock(%s)', (MIGRATION_LOCK,))
193
+ namespace = await (await conn.execute("SELECT pg_catalog.pg_has_role(nspowner,'USAGE') "
194
+ 'FROM pg_catalog.pg_namespace WHERE nspname=%s', (SCHEMA,))).fetchone()
195
+ if namespace is None:
196
+ raise StoreUninitialized('initialize conversation storage explicitly first')
197
+ if not namespace[0]:
198
+ raise StoreIncompatible('migration role must own conversation schema')
199
+ versions = await (await conn.execute('SELECT schema_version,storage_contract_version '
200
+ 'FROM prosaic_harness_conversations.schema_version')).fetchall()
201
+ if versions == [(2, 2)]:
202
+ await ready_on_connection(conn)
203
+ return
204
+ if versions != [(1, 1)]:
205
+ raise StoreIncompatible('conversation migration requires intact version-1 storage')
206
+ await ready_on_connection(conn, expected_versions=(1, 1))
207
+ cursor = await conn.execute('UPDATE prosaic_harness_conversations.schema_version '
208
+ 'SET schema_version=2,storage_contract_version=2 WHERE singleton IS TRUE')
209
+ if cursor.rowcount != 1:
210
+ raise StoreIncompatible('conversation version metadata is incompatible')
211
+ await ready_on_connection(conn)
212
+ transport.call(migrate, timeout_s=30)
@@ -33,7 +33,7 @@ class PostgresSessionStore:
33
33
 
34
34
  def __init__(self, dsn, *, namespace, lease_s=60, renew_s=15,
35
35
  operation_timeout_s=10, pool_timeout_s=5, connect_timeout_s=5,
36
- lock_timeout_s=1, statement_timeout_s=5, pool_size=8, max_active_leases=4):
36
+ lock_timeout_s=1, statement_timeout_s=5, pool_size=8, max_active_leases=4, durability_profile="postgresql"):
37
37
  self._namespace = validate_namespace(namespace)
38
38
  positive(lease_s, 'lease_s')
39
39
  positive(renew_s, 'renew_s')
@@ -49,7 +49,7 @@ class PostgresSessionStore:
49
49
  self._transport = PostgresTransport(dsn, operation_timeout_s=operation_timeout_s,
50
50
  pool_timeout_s=pool_timeout_s, connect_timeout_s=connect_timeout_s,
51
51
  lock_timeout_s=lock_timeout_s, statement_timeout_s=statement_timeout_s,
52
- pool_size=pool_size, max_active_leases=max_active_leases)
52
+ pool_size=pool_size, max_active_leases=max_active_leases, durability_profile=durability_profile)
53
53
 
54
54
  @property
55
55
  def namespace(self):
@@ -60,6 +60,13 @@ class PostgresSessionStore:
60
60
 
61
61
  def check_ready(self):
62
62
  session_schema.check_ready(self._transport)
63
+ @property
64
+ def capabilities(self):
65
+ self.check_ready()
66
+ return frozenset({'context_assembly_v1'})
67
+
68
+ def migrate_context_storage(self):
69
+ session_schema.migrate_context_storage(self._transport)
63
70
 
64
71
  def _scope(self, scope):
65
72
  if type(scope) is not ConversationScope or scope.namespace != self.namespace:
@@ -29,7 +29,7 @@ class PostgresRunStore:
29
29
 
30
30
  def __init__(self, dsn, *, namespace, lease_s=60, renew_s=15,
31
31
  operation_timeout_s=10, pool_timeout_s=5, connect_timeout_s=5,
32
- lock_timeout_s=1, statement_timeout_s=5, pool_size=8, max_active_leases=4):
32
+ lock_timeout_s=1, statement_timeout_s=5, pool_size=8, max_active_leases=4, durability_profile="postgresql"):
33
33
  self.namespace = validate_namespace(namespace)
34
34
  positive(lease_s, 'lease_s')
35
35
  positive(renew_s, 'renew_s')
@@ -46,7 +46,7 @@ class PostgresRunStore:
46
46
  self._transport = PostgresTransport(dsn, operation_timeout_s=operation_timeout_s,
47
47
  pool_timeout_s=pool_timeout_s, connect_timeout_s=connect_timeout_s,
48
48
  lock_timeout_s=lock_timeout_s, statement_timeout_s=statement_timeout_s,
49
- pool_size=pool_size, max_active_leases=max_active_leases)
49
+ pool_size=pool_size, max_active_leases=max_active_leases, durability_profile=durability_profile)
50
50
 
51
51
  def initialize(self):
52
52
  schema.initialize(self._transport)
@@ -47,7 +47,10 @@ class _Operation:
47
47
  class PostgresTransport:
48
48
  def __init__(self, dsn, *, operation_timeout_s=10, pool_timeout_s=5,
49
49
  connect_timeout_s=5, lock_timeout_s=1, statement_timeout_s=5,
50
- pool_size=8, max_active_leases=4):
50
+ pool_size=8, max_active_leases=4, durability_profile="postgresql"):
51
+ if type(durability_profile) is not str or durability_profile not in {'postgresql', 'aurora-postgresql'}:
52
+ raise ValueError('durability_profile must be postgresql or aurora-postgresql')
53
+ self.durability_profile = durability_profile
51
54
  for name, value in locals().copy().items():
52
55
  if name.endswith('_s'):
53
56
  positive(value, name)
@@ -138,7 +141,9 @@ class PostgresTransport:
138
141
  pg_catalog.current_setting('transaction_read_only'),
139
142
  pg_catalog.current_setting('fsync'), pg_catalog.current_setting('full_page_writes'),
140
143
  pg_catalog.current_setting('synchronous_commit')""")).fetchone()
141
- if not row or row[0] or row[1:4] != ('off', 'on', 'on') or row[4] not in {'on', 'remote_apply'}:
144
+ full_page_values = {'on', 'off'} if self.durability_profile == 'aurora-postgresql' else {'on'}
145
+ if (not row or row[0] or row[1:3] != ('off', 'on')
146
+ or row[3] not in full_page_values or row[4] not in {'on', 'remote_apply'}):
142
147
  raise StoreIncompatible('require writable primary, logged durability and safe WAL settings')
143
148
 
144
149
  async def _sql(self, operation, callback, renewal, budget):
@@ -40,7 +40,7 @@ def candidate_wheels(root):
40
40
  spec.loader.exec_module(release)
41
41
  release.verify_upstream(Path(receipt_directory).resolve(strict=True), wheelhouse)
42
42
  return [wheelhouse / 'b3_prosaic-0.4.1-py3-none-any.whl',
43
- wheelhouse / 'b3_prosaic_runtime-0.9.0-py3-none-any.whl']
43
+ wheelhouse / 'b3_prosaic_runtime-0.9.7-py3-none-any.whl']
44
44
 
45
45
 
46
46
  class LocalCluster:
@@ -177,12 +177,12 @@ class LocalCluster:
177
177
  set -eu
178
178
  apt-get update -qq
179
179
  python -m venv /tmp/core
180
- /tmp/core/bin/python -m pip install /core-wheels/b3_prosaic_harness-0.8.0-py3-none-any.whl >/dev/null
180
+ /tmp/core/bin/python -m pip install /core-wheels/b3_prosaic_harness-0.9.4-py3-none-any.whl >/dev/null
181
181
  /tmp/core/bin/python -I -c 'import importlib.util,prosaic_harness; assert importlib.util.find_spec("psycopg") is None; assert importlib.util.find_spec("prosaic_harness_postgres") is None'
182
182
  /tmp/core/bin/python -I /fixture/workflow_factory_smoke.py
183
183
  /tmp/core/bin/python -I /fixture/conversation_smoke.py
184
184
  python -m venv /tmp/both
185
- /tmp/both/bin/python -m pip install /core-wheels/b3_prosaic_harness-0.8.0-py3-none-any.whl /adapter-wheels/b3_prosaic_harness_postgres-0.3.0-py3-none-any.whl >/dev/null
185
+ /tmp/both/bin/python -m pip install /core-wheels/b3_prosaic_harness-0.9.4-py3-none-any.whl /adapter-wheels/b3_prosaic_harness_postgres-0.4.1-py3-none-any.whl >/dev/null
186
186
  /tmp/both/bin/python -I /fixture/linux_wheel_smoke.py
187
187
  '''
188
188
  if dependencies:
@@ -266,12 +266,13 @@ def main():
266
266
  cluster.stop()
267
267
  else:
268
268
  env = os.environ | {'HARNESS_TEST_DATABASE_URL': cluster.dsn,
269
- 'HARNESS_TEST_CONTAINER_ENGINE': cluster.engine, 'HARNESS_TEST_POSTGRES_VERSION': cluster.version}
269
+ 'HARNESS_TEST_CONTAINER_ENGINE': cluster.engine, 'HARNESS_TEST_POSTGRES_VERSION': cluster.version,
270
+ 'PROSAIC_LIFECYCLE_TEST_DSN': cluster.dsn}
270
271
  root = Path(__file__).resolve().parents[4]
271
272
  report = (['--junitxml=' + env['HARNESS_TEST_JUNIT_XML']]
272
273
  if env.get('HARNESS_TEST_JUNIT_XML') else [])
273
274
  raise SystemExit(subprocess.run([sys.executable, '-m', 'pytest', '-q',
274
- 'adapters/postgres/tests', '--require-postgres', *report],
275
+ 'adapters/postgres/tests', 'tests/test_conversation_postgres_lifecycle.py', '--require-postgres', *report],
275
276
  cwd=root, env=env, timeout=600).returncode)
276
277
 
277
278
 
@@ -38,3 +38,29 @@ def receipt(state, *, invocation_id='b' * 32, failure=None, uncertain=False):
38
38
  result = TurnResult(Result(1 if failure else 0, '' if failure else 'Done', failure or ''),
39
39
  completed, evidence)
40
40
  return make_receipt(state, invocation_id, result)
41
+
42
+
43
+ def context_document(scope, *, session_id='a'*32):
44
+ from prosaic_runtime import (ContextPolicy, TokenCounterDescriptor,
45
+ TokenCounterRegistry, TokenCounterRegistration)
46
+ base = bindings()
47
+ class Counter:
48
+ def count_tokens(self, request): return 10
49
+ descriptor = TokenCounterDescriptor('fixture-v1', 'openai-compatible',
50
+ 'synthetic', 'openai-chat-v1', 4096, '1', '0'*64, '1'*64,
51
+ 'exact', 'Synthetic fixed fixture grammar only.')
52
+ counters = TokenCounterRegistry({'fixture-v1': TokenCounterRegistration(descriptor, Counter())})
53
+ policy = ContextPolicy('fullHistory', 4096, 128, 0, 'fixture-v1')
54
+ binding = ConversationBindings(base.agent, version='context-v1', config=base.config,
55
+ policy=base.policy, run_policy=base.run_policy,
56
+ context_policy=policy, token_counters=counters)
57
+ state = new_session(session_id, scope, binding)
58
+ prepared = binding.runtime().prepare_turn(binding.agent, message='Hello ž',
59
+ policy=binding.run_policy, limits=binding.turn_limits,
60
+ context_policy=policy, token_counters=counters)
61
+ state = reserve_attempt(state, message_id='message', message='Hello ž',
62
+ invocation_id='b'*32, context_identity=prepared.context_identity,
63
+ encoding_version=prepared.encoding_version, input_bytes=prepared.input_bytes,
64
+ provider_allowance=2, tool_allowance=0, context_selection=prepared.context_selection,
65
+ context_report=prepared.context_report, context_work=prepared.context_work)
66
+ return state, prepared
@@ -0,0 +1,96 @@
1
+ """An explicit storage contract changes only the full-page-write guard."""
2
+ import asyncio
3
+ from unittest.mock import AsyncMock
4
+
5
+ import pytest
6
+ from prosaic_harness import StoreIncompatible
7
+ from prosaic_harness_postgres.transport import PostgresTransport
8
+
9
+
10
+ @pytest.mark.parametrize('profile', ['postgresql', 'aurora-postgresql'])
11
+ @pytest.mark.parametrize('row,accepted', [
12
+ ((False, 'off', 'on', 'on', 'on'), True),
13
+ ((False, 'off', 'on', 'off', 'on'), 'aurora'),
14
+ ((False, 'off', 'off', 'off', 'on'), False),
15
+ ((False, 'off', 'on', 'off', 'off'), False),
16
+ ((True, 'off', 'on', 'off', 'on'), False),
17
+ ((False, 'on', 'on', 'off', 'on'), False),
18
+ ((False, 'off', 'on', 'on', 'remote_apply'), True),
19
+ ((False, 'off', 'on', 'on', 'local'), False),
20
+ ((False, 'off', 'on', 'invalid', 'on'), False),
21
+ (None, False),
22
+ ])
23
+ def test_profile_changes_only_full_page_requirement(profile, row, accepted):
24
+ transport = object.__new__(PostgresTransport)
25
+ transport.durability_profile = profile
26
+ cursor = AsyncMock()
27
+ cursor.fetchone.return_value = row
28
+ connection = AsyncMock()
29
+ connection.execute.return_value = cursor
30
+ allowed = accepted is True or (accepted == 'aurora' and profile == 'aurora-postgresql')
31
+ if allowed:
32
+ asyncio.run(transport._guard(connection))
33
+ else:
34
+ with pytest.raises(StoreIncompatible):
35
+ asyncio.run(transport._guard(connection))
36
+
37
+
38
+ @pytest.mark.parametrize('profile', ['', 'storage', 'Aurora', None, True])
39
+ def test_unknown_profile_rejected_before_connection(profile):
40
+ with pytest.raises(ValueError, match='durability_profile'):
41
+ PostgresTransport('unused', durability_profile=profile)
42
+
43
+
44
+ @pytest.mark.parametrize('module_name,class_name', [('store', 'PostgresRunStore'), ('session_store', 'PostgresSessionStore')])
45
+ def test_public_store_and_from_env_forward_profile(monkeypatch, module_name, class_name):
46
+ import importlib
47
+ module = importlib.import_module('prosaic_harness_postgres.' + module_name)
48
+ captured = []
49
+ monkeypatch.setattr(module, 'PostgresTransport', lambda dsn, **kw: captured.append(kw))
50
+ monkeypatch.setenv('SYNTHETIC_DSN', 'synthetic')
51
+ cls = getattr(module, class_name)
52
+ cls('synthetic', namespace='fixture')
53
+ cls.from_env('SYNTHETIC_DSN', namespace='fixture', durability_profile='aurora-postgresql')
54
+ assert [kw['durability_profile'] for kw in captured] == ['postgresql', 'aurora-postgresql']
55
+
56
+
57
+ @pytest.mark.parametrize('command', ['init', 'doctor', 'migrate-conversations'])
58
+ def test_cli_forwards_explicit_profile(monkeypatch, command):
59
+ from prosaic_harness_postgres import cli, session_store
60
+ calls = []
61
+ class Store:
62
+ def __enter__(self): return self
63
+ def __exit__(self, *args): pass
64
+ def initialize(self): pass
65
+ def check_ready(self): pass
66
+ def migrate_context_storage(self): pass
67
+ def create(name, **kwargs):
68
+ calls.append(kwargs['durability_profile'])
69
+ return Store()
70
+ monkeypatch.setattr(cli.PostgresRunStore, 'from_env', create)
71
+ monkeypatch.setattr(session_store.PostgresSessionStore, 'from_env', create)
72
+ assert cli.main([command, '--dsn-env', 'SYNTHETIC_DSN', '--durability-profile', 'aurora-postgresql']) == 0
73
+ assert calls == ['aurora-postgresql']
74
+
75
+
76
+ @pytest.mark.parametrize('class_name', ['PostgresRunStore', 'PostgresSessionStore'])
77
+ def test_explicit_profile_retains_real_connection_guards(db_admin_dsn, class_name):
78
+ import prosaic_harness_postgres
79
+ cls = getattr(prosaic_harness_postgres, class_name)
80
+ with cls(db_admin_dsn, namespace='fixture', durability_profile='aurora-postgresql', pool_size=1) as store:
81
+ store.initialize()
82
+ store.check_ready()
83
+ store._transport.call(lambda conn: conn.execute('SET synchronous_commit=off'))
84
+ with pytest.raises(StoreIncompatible):
85
+ store.check_ready()
86
+
87
+
88
+ def test_public_version_matches_distribution():
89
+ import ast
90
+ from pathlib import Path
91
+ import tomllib
92
+ root = Path(__file__).resolve().parents[1]
93
+ version = tomllib.loads((root/'pyproject.toml').read_text())['project']['version']
94
+ module = ast.parse((root/'src/prosaic_harness_postgres/__init__.py').read_text())
95
+ assert next(ast.literal_eval(n.value) for n in module.body if isinstance(n, ast.Assign)
96
+ and any(isinstance(t, ast.Name) and t.id=='__version__' for t in n.targets)) == version
@@ -13,8 +13,8 @@ import pytest
13
13
  import yaml
14
14
 
15
15
  ROOT = Path(__file__).resolve().parents[3]
16
- CORE = ROOT / 'dist/b3_prosaic_harness-0.8.0-py3-none-any.whl'
17
- ADAPTER = ROOT / 'adapters/postgres/dist/b3_prosaic_harness_postgres-0.3.0-py3-none-any.whl'
16
+ CORE = ROOT / 'dist/b3_prosaic_harness-0.9.4-py3-none-any.whl'
17
+ ADAPTER = ROOT / 'adapters/postgres/dist/b3_prosaic_harness_postgres-0.4.1-py3-none-any.whl'
18
18
 
19
19
  def test_owned_fixture_uses_free_explicit_subnet_when_default_pool_is_exhausted():
20
20
  from support.local_cluster import owned_subnet
@@ -73,13 +73,13 @@ def metadata(path):
73
73
  def test_wheel_contents_and_dependency_boundaries():
74
74
  core, names = metadata(CORE)
75
75
  assert core['Name'] == 'b3-prosaic-harness'
76
- assert core['Version'] == '0.8.0'
76
+ assert core['Version'] == '0.9.4'
77
77
  assert not any(name.startswith('prosaic_harness_postgres/') for name in names)
78
78
  assert not any('psycopg' in value.lower() for value in core.get_all('Requires-Dist'))
79
79
  adapter, names = metadata(ADAPTER)
80
80
  assert adapter['Name'] == 'b3-prosaic-harness-postgres'
81
- assert adapter['Version'] == '0.3.0'
82
- assert any(value.startswith('b3-prosaic-harness<0.9,>=0.8') for value in adapter.get_all('Requires-Dist'))
81
+ assert adapter['Version'] == '0.4.1'
82
+ assert any(value.startswith('b3-prosaic-harness<0.10,>=0.9') for value in adapter.get_all('Requires-Dist'))
83
83
  assert 'prosaic_harness_postgres/cli.py' in names
84
84
  assert any(value.startswith('psycopg[binary]<4,>=3.2.3')
85
85
  for value in adapter.get_all('Requires-Dist'))
@@ -134,7 +134,8 @@ def test_installed_memory_file_conversations_and_saved_receipt_recovery(wheel_en
134
134
  report = json.loads(result.stdout)
135
135
  assert set(report['stores']) == {'memory', 'file'}
136
136
  for evidence in report['stores'].values():
137
- assert evidence['providerRequests'] == 4
137
+ assert evidence['providerRequests'] == 6
138
+ assert evidence['sameMessageFailedRetry'] is True
138
139
  assert evidence['savedReceiptAdoptedWithoutDispatch'] is True
139
140
  assert evidence['failedCorrectionRetained'] is True
140
141
 
@@ -184,7 +185,7 @@ def curated_candidates(release_tool, tmp_path):
184
185
  candidates = []
185
186
  for repo, expected in (
186
187
  ('B3Cognition/prosaic', {'b3-prosaic': '0.4.1'}),
187
- ('B3Cognition/prosaic-runtime', {'b3-prosaic-runtime': '0.9.0',
188
+ ('B3Cognition/prosaic-runtime', {'b3-prosaic-runtime': '0.9.7',
188
189
  'b3-prosaic-runtime-postgres': '0.3.0'})):
189
190
  artifacts = []
190
191
  for name, version in expected.items():
@@ -252,4 +253,4 @@ def test_container_mounts_verified_dependency_bytes_read_only(
252
253
  create = calls[0]
253
254
  assert str(wheelhouse) + ':/candidate-wheels:ro' in create
254
255
  assert 'PIP_FIND_LINKS=/candidate-wheels' in create[-1]
255
- assert '/candidate-wheels/b3_prosaic_runtime-0.9.0-py3-none-any.whl' in create[-1]
256
+ assert '/candidate-wheels/b3_prosaic_runtime-0.9.7-py3-none-any.whl' in create[-1]
@@ -0,0 +1,112 @@
1
+ """Owner-only explicit v1->v2 migration; real PostgreSQL transactional evidence."""
2
+ import pytest
3
+ import psycopg
4
+ from prosaic_harness import StoreIncompatible, ConversationScope
5
+ from prosaic_harness.session_contracts import new_session
6
+ import prosaic_harness_postgres as pg
7
+ from prosaic_harness_postgres import session_schema
8
+ from test_session_schema import grant_conversations
9
+ from support.session_documents import bindings
10
+ from dataclasses import replace
11
+
12
+
13
+ def legacy_storage(db_admin_dsn):
14
+ with pg.PostgresSessionStore(db_admin_dsn, namespace='context-test') as owner:
15
+ owner.initialize()
16
+ # Table/role contract is identical in v1; only explicit contract metadata changes.
17
+ with psycopg.connect(db_admin_dsn) as connection:
18
+ connection.execute('UPDATE prosaic_harness_conversations.schema_version '
19
+ 'SET schema_version=1,storage_contract_version=1')
20
+
21
+
22
+ def test_owner_migration_is_explicit_repeatable_and_keeps_v1_records(db_admin_dsn):
23
+ scope = ConversationScope('context-test', 'sandbox', 'app', 'tenant', 'actor')
24
+ state = new_session('a'*32, scope, bindings())
25
+ with pg.PostgresSessionStore(db_admin_dsn, namespace='context-test') as owner:
26
+ owner.initialize()
27
+ with owner.lease('a'*32, scope=scope) as lease:
28
+ owner.create_session('a'*32, state, lease, scope=scope)
29
+ before = owner.load_session('a'*32, scope=scope)
30
+ with psycopg.connect(db_admin_dsn) as connection:
31
+ connection.execute('UPDATE prosaic_harness_conversations.schema_version '
32
+ 'SET schema_version=1,storage_contract_version=1')
33
+ with pg.PostgresSessionStore(db_admin_dsn, namespace='context-test') as owner:
34
+ with pytest.raises(StoreIncompatible): owner.check_ready()
35
+ with pytest.raises(StoreIncompatible): owner.initialize()
36
+ owner.migrate_context_storage()
37
+ owner.migrate_context_storage()
38
+ assert 'context_assembly_v1' in owner.capabilities
39
+ assert owner.load_session('a'*32, scope=scope) == before
40
+ with pytest.raises(StoreIncompatible):
41
+ owner._transport.call(lambda connection:
42
+ session_schema.ready_on_connection(connection, expected_versions=(1, 1)))
43
+
44
+
45
+ def test_runtime_role_cannot_migrate_or_gain_owner_privileges(db_admin_dsn, runtime_dsn):
46
+ legacy_storage(db_admin_dsn)
47
+ grant_conversations(db_admin_dsn, runtime_dsn)
48
+ with pg.PostgresSessionStore(runtime_dsn, namespace='context-test') as runtime:
49
+ with pytest.raises(StoreIncompatible): runtime.migrate_context_storage()
50
+ with psycopg.connect(db_admin_dsn) as connection:
51
+ assert connection.execute('SELECT schema_version,storage_contract_version '
52
+ 'FROM prosaic_harness_conversations.schema_version').fetchone() == (1, 1)
53
+ with pg.PostgresSessionStore(db_admin_dsn, namespace='context-test') as owner:
54
+ owner.migrate_context_storage()
55
+ with pg.PostgresSessionStore(runtime_dsn, namespace='context-test') as runtime:
56
+ assert 'context_assembly_v1' in runtime.capabilities
57
+
58
+
59
+ def test_v2_checkpoint_is_fenced_and_owner_filtered(db_admin_dsn, runtime_dsn):
60
+ from prosaic_runtime import ContextWorkState
61
+ from prosaic_harness import LeaseLost, SessionNotFound
62
+ from prosaic_harness.session_context import checkpoint_context_work
63
+ from support.session_documents import context_document
64
+ scope = ConversationScope('context-test', 'sandbox', 'app', 'tenant', 'actor')
65
+ with pg.PostgresSessionStore(db_admin_dsn, namespace='context-test') as owner:
66
+ owner.initialize()
67
+ grant_conversations(db_admin_dsn, runtime_dsn)
68
+ state, prepared = context_document(scope)
69
+ with pg.PostgresSessionStore(runtime_dsn, namespace='context-test') as first, \
70
+ pg.PostgresSessionStore(runtime_dsn, namespace='context-test') as replica:
71
+ with first.lease('a'*32, scope=scope) as lease:
72
+ revision = first.create_session('a'*32, state, lease, scope=scope)
73
+ before = prepared.context_work.to_dict()
74
+ work = ContextWorkState.from_dict({**before, 'checkpointSequence': 1,
75
+ 'reservedActiveMicros': 2_000_000 - before['activeAssemblyMicros']})
76
+ changed = checkpoint_context_work(state, 'b'*32, work)
77
+ current = first.save_session('a'*32, changed, revision, lease, scope=scope)
78
+ assert replica.load_session('a'*32, scope=scope).state == changed
79
+ with pytest.raises(SessionNotFound):
80
+ replica.load_session('a'*32, scope=replace(scope, actor='foreign'))
81
+ with psycopg.connect(db_admin_dsn) as admin:
82
+ admin.execute('UPDATE prosaic_harness_conversations.leases '
83
+ 'SET generation=generation+1 WHERE namespace=%s AND session_id=%s',
84
+ ('context-test', 'a'*32))
85
+ with pytest.raises(LeaseLost):
86
+ first.save_session('a'*32, changed, current, lease, scope=scope)
87
+ def test_failed_postupdate_validation_rolls_back_metadata(db_admin_dsn, monkeypatch):
88
+ legacy_storage(db_admin_dsn)
89
+ original = session_schema.ready_on_connection
90
+ async def reject_v2(connection, **kwargs):
91
+ row = await (await connection.execute('SELECT schema_version FROM '
92
+ 'prosaic_harness_conversations.schema_version')).fetchone()
93
+ if row == (2,): raise StoreIncompatible('synthetic postupdate validation failure')
94
+ return await original(connection, **kwargs)
95
+ monkeypatch.setattr(session_schema, 'ready_on_connection', reject_v2)
96
+ with pg.PostgresSessionStore(db_admin_dsn, namespace='context-test') as owner:
97
+ with pytest.raises(StoreIncompatible): owner.migrate_context_storage()
98
+ with psycopg.connect(db_admin_dsn) as connection:
99
+ assert connection.execute('SELECT schema_version,storage_contract_version '
100
+ 'FROM prosaic_harness_conversations.schema_version').fetchone() == (1, 1)
101
+
102
+
103
+ def test_operator_cli_migrates_without_exposing_connection_details(db_admin_dsn, monkeypatch, capsys):
104
+ import json
105
+ from prosaic_harness_postgres.cli import main
106
+ legacy_storage(db_admin_dsn)
107
+ monkeypatch.setenv('CONTEXT_TEST_DSN', db_admin_dsn)
108
+ assert main(['migrate-conversations', '--dsn-env', 'CONTEXT_TEST_DSN']) == 0
109
+ output = capsys.readouterr()
110
+ assert json.loads(output.out) == {'migrated': True, 'conversation_storage_contract': 2}
111
+ assert output.err == ''
112
+ assert db_admin_dsn not in output.out