Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 11 additions & 6 deletions adapters/chatgpt/metadata/chatgpt-adapter.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"adapter_path": "adapters/chatgpt/selective-intelligence",
"transformation": "runtime_only_with_nested_role_entrypoints_as_role_references",
"single_skill_entrypoint": "SKILL.md",
"runtime_file_count": 59,
"runtime_file_count": 61,
"role_path_map": {
"subskills/si-aligner/SKILL.md": "subskills/si-aligner/ROLE.md",
"subskills/si-intake/SKILL.md": "subskills/si-intake/ROLE.md",
Expand All @@ -18,7 +18,7 @@
"subskills/si-worker/SKILL.md": "subskills/si-worker/ROLE.md"
},
"behavioral_contract": "source_preserved_not_installed_behavior_proof",
"source_manifest_sha256": "e1a569d7f2c6cccae1d652b81011100db01790ae3168da9d43a5d2a23e05abc7",
"source_manifest_sha256": "09aac47c8c11717defd8f837391ec4126e7e62696813dd428e7eeb14180f89bf",
"source_file_sha256": {
"SKILL.md": "afb4c6e096aa83683105c45b5525ac34ee436654be4dacc9c2a44f01bf2033b1",
"VERSION": "326dd5d8641f399990de42def50ac29eb4c9222601c0a0a3fc3c947dd3bec65d",
Expand All @@ -31,7 +31,8 @@
"references/activation-and-adoption.md": "04df2423ec610be344366dd646b427e3e9062f90244b0e4143a6fb02218b5f74",
"references/actual-intent-alignment.md": "3bc1fac79f7fed56c098c5dd01ce7c5e49a973c9a2455606f1ff7c495373a9ad",
"references/architecture-reuse.md": "5d1c4d09ca8e7407d19c7fc9d68587b2ea0b61be61e5c3448890af2c8766138d",
"references/continuity-and-impact.md": "78f93ccaaed14e58544c5a0c78b6bf933922b4c576a597b13d63ac74d0de53e0",
"references/continuity-and-impact.md": "ffd9f6b96147ba77d251854f4b3fd2ffbb3cd4b9deccf98b8fcfac849fd109f7",
"references/postgres-session-storage.md": "db0d16851060a6563366b85e37657554b4108475d78f30b8215bb570b02c8dce",
"references/durable-progress-and-recovery.md": "1457eed72572aa6e0f34ab6773b81735e3f1ee22e5427e44a238619b034a609d",
"references/evidence-and-completion.md": "f05424d0e1b8c68624aab05d3e03361e312a08a92bc3b3338b86e29a1971569f",
"references/failure-patterns-and-gates.md": "90b4b2fc15069a33cdef3fc39d653b718f4ef4e27345c0db5dab3d7fca5408b5",
Expand Down Expand Up @@ -65,8 +66,9 @@
"scripts/feedback.py": "90bfdc574d03b630ea5e68f22185c848d37ab93e3edca768b0d2b501f7924bbb",
"scripts/intent_contract.py": "a3bcb84336179e5dc9e18f55c7f02c27bbe777a116aa0942bee807ea82eb58a6",
"scripts/lane_registry.py": "f9417ae2ac0a3911220c75b9f83cbe5204a30142f948b54611df6e4712846e35",
"scripts/lane_session.py": "d20f57cc7c00d84b4e91399d2c2051b130f6f5144331907206b01efd085b17a8",
"scripts/lane_session.py": "1119e62d04825221ef8c823bdc9e6ac8ac351458715a5e4e40932fb899bbf89d",
"scripts/policy_guard.py": "bc8be4706a053114973371ce9bfeb94a13881e3fa42779ac4abea550a1229bbe",
"scripts/postgres_sessions.py": "e19ea9cffa6872b62d2870c59e4e08a10f231c2ecfab77018bf0e46341c3b297",
"scripts/progress_checkpoint.py": "3660538c309ff0c01f87e615037e83a7cb8bd5fc651f66f22569da4cf599e0aa",
"scripts/project_index.py": "496eb8e316fa7baf85c3c0b35ead76053d41c842846367163a5eadb7e94b755c",
"scripts/prompt_queue.py": "1f5423659c07bcf842454931e28a0547c3020e19d5afeb58fee5adf6fa1c4f33",
Expand All @@ -92,7 +94,8 @@
"references/activation-and-adoption.md": "04df2423ec610be344366dd646b427e3e9062f90244b0e4143a6fb02218b5f74",
"references/actual-intent-alignment.md": "3bc1fac79f7fed56c098c5dd01ce7c5e49a973c9a2455606f1ff7c495373a9ad",
"references/architecture-reuse.md": "5d1c4d09ca8e7407d19c7fc9d68587b2ea0b61be61e5c3448890af2c8766138d",
"references/continuity-and-impact.md": "78f93ccaaed14e58544c5a0c78b6bf933922b4c576a597b13d63ac74d0de53e0",
"references/continuity-and-impact.md": "ffd9f6b96147ba77d251854f4b3fd2ffbb3cd4b9deccf98b8fcfac849fd109f7",
"references/postgres-session-storage.md": "db0d16851060a6563366b85e37657554b4108475d78f30b8215bb570b02c8dce",
"references/durable-progress-and-recovery.md": "1457eed72572aa6e0f34ab6773b81735e3f1ee22e5427e44a238619b034a609d",
"references/evidence-and-completion.md": "f05424d0e1b8c68624aab05d3e03361e312a08a92bc3b3338b86e29a1971569f",
"references/failure-patterns-and-gates.md": "90b4b2fc15069a33cdef3fc39d653b718f4ef4e27345c0db5dab3d7fca5408b5",
Expand Down Expand Up @@ -126,8 +129,9 @@
"scripts/feedback.py": "90bfdc574d03b630ea5e68f22185c848d37ab93e3edca768b0d2b501f7924bbb",
"scripts/intent_contract.py": "a3bcb84336179e5dc9e18f55c7f02c27bbe777a116aa0942bee807ea82eb58a6",
"scripts/lane_registry.py": "f9417ae2ac0a3911220c75b9f83cbe5204a30142f948b54611df6e4712846e35",
"scripts/lane_session.py": "d20f57cc7c00d84b4e91399d2c2051b130f6f5144331907206b01efd085b17a8",
"scripts/lane_session.py": "1119e62d04825221ef8c823bdc9e6ac8ac351458715a5e4e40932fb899bbf89d",
"scripts/policy_guard.py": "bc8be4706a053114973371ce9bfeb94a13881e3fa42779ac4abea550a1229bbe",
"scripts/postgres_sessions.py": "e19ea9cffa6872b62d2870c59e4e08a10f231c2ecfab77018bf0e46341c3b297",
"scripts/progress_checkpoint.py": "3660538c309ff0c01f87e615037e83a7cb8bd5fc651f66f22569da4cf599e0aa",
"scripts/project_index.py": "496eb8e316fa7baf85c3c0b35ead76053d41c842846367163a5eadb7e94b755c",
"scripts/prompt_queue.py": "1f5423659c07bcf842454931e28a0547c3020e19d5afeb58fee5adf6fa1c4f33",
Expand Down Expand Up @@ -155,6 +159,7 @@
"scripts/lane_registry.py",
"scripts/lane_session.py",
"scripts/policy_guard.py",
"scripts/postgres_sessions.py",
"scripts/text_gate.py"
]
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

Use this reference when work spans sessions, agents, branches, worktrees, repositories, interrupted runs, or more than one build. The purpose is to preserve one product truth while allowing safe progress.

For native SI workers that need a shared durable session store, see [optional PostgreSQL session persistence](postgres-session-storage.md). This is an opt-in persistence adapter; it does not replace SI governance or move workspace artifacts.

## Contents

- [Resume from evidence](#resume-from-evidence)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
# Optional shared session persistence

Use this for shared checkpoints with **one active executor per workspace and
session**. A replacement process may recover only after the previous executor
is confirmed terminated. The existing file backend remains the default and
requires no database, API key, paid service, or Python dependency.

SI retains interpretation, approval, task, receipt, and verification ownership.
PostgreSQL stores the same validated JSON session and monotonic revision. This
does not turn another product, gateway, or model into an SI engine.

## Configure a native SI worker

Install the optional `psycopg[binary]>=3.2,<4` dependency in that worker's Python
environment. Set these server-side variables using the deployment secret store:

| Variable | Value |
| --- | --- |
| `SI_SESSION_BACKEND` | `postgres` |
| `SI_SESSION_EXECUTION_MODE` | `single-active`; explicitly acknowledge the execution prerequisite below |
| `SI_SESSION_DATABASE_URL` | Direct PostgreSQL connection URI for the approved database |
| `SI_SESSION_NAMESPACE` | Explicit project identifier, such as `si-worker-preview` |

Use a direct Neon endpoint, **not** its `-pooler` transaction-pooling endpoint.
The engine holds a session advisory lock while a governed operation runs. Each
checkpoint commits independently so it survives a killed worker. After confirming
that worker has terminated, a replacement acquires the lock and reads the
persisted revision. Remote
connections require `sslmode=verify-full`; system trust roots are the default.

Initialize the table explicitly on the intended isolated database:

```bash
python scripts/lane_session.py initialize-store
```

This creates `public.si_runtime_sessions`. Normal session reads/writes never
provision a database or create a table. Give the runtime role only the database
and table privileges it needs; use a separate migration identity if appropriate.
Namespaces prevent accidental record collisions, **not** access by another
holder of the same database credentials. Separate product trust boundaries need
separate roles/databases or an independently enforced authorization layer.

The existing engine, CLI and MCP callers continue through `lane_session`; no
second checkpoint API or duplicated orchestration engine is introduced. Keep
workspaces and artifact evidence accessible at the paths recorded in the session.
Shared session storage alone does not move local files between machines.

## Recovery contract

- A database advisory lock serializes cooperative checkpoint updates while the
connection is alive. It is not a worker lease or a scheduler.
- Stale revisions cannot overwrite newer approval, correction, or evidence.
- A missing store, lost connection, invalid configuration, or database error
fails closed; it never creates a local replacement session.
- Process death releases its database lock. Completed evidence remains durable.
- A network failure can release that lock while the old executor still runs.
PostgreSQL revision checks do not fence workspace writes, rollback, or external
tools. **Do not automatically launch a replacement on lock release, connection
loss, or timeout. Confirm the old executor has terminated first.** The explicit
`single-active` setting acknowledges this prerequisite; it does not enforce
process termination or make active-active execution safe.
- Database durability is not exactly-once execution of arbitrary external tools.
Preserve existing pending/unknown receipts, reconciliation and idempotency
rules before retrying an interrupted external action.
- Selecting `file` again does not migrate sessions back. Do not change a live
worker's backend or namespace as a recovery shortcut.

## Verify before adopting

Set `SI_TEST_POSTGRES_URL` to a **disposable local or approved isolated test**
database and run the PostgreSQL session tests. They create the session table and
use temporary unique namespaces. Never supply a customer production database.

```bash
python -B -m unittest discover -s tests -p test_postgres_sessions.py
```

Those tests cover process restart, lock release after process death, concurrent
writers, stale revisions, namespace separation and recovery of real engine
verification evidence. Local Postgres proof does not establish a Neon
deployment, installed client adoption, cross-machine workspace availability or
provider-side spending limits. Record those states separately.
60 changes: 47 additions & 13 deletions adapters/chatgpt/selective-intelligence/scripts/lane_session.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

from intent_contract import classify_intent, concept_tokens, merge_active_contract
import checkpoint as CP
from postgres_sessions import configured_store

try:
import lane_registry as reg # type: ignore
Expand Down Expand Up @@ -72,9 +73,13 @@ def sessions_dir() -> Path:
return directory.resolve()


def session_path(session_id: str) -> Path:
def _validate_session_id(session_id: str) -> None:
if not session_id or any(ch not in "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_" for ch in session_id):
raise ValueError("invalid session id")


def session_path(session_id: str) -> Path:
_validate_session_id(session_id)
return sessions_dir() / f"{session_id}.session.json"


Expand All @@ -89,11 +94,17 @@ class SessionConflictError(RuntimeError):

@contextmanager
def session_lock(session_id: str):
"""Serialize cooperative SI writers across threads and local processes.
"""Serialize cooperative SI writers through the configured storage lock.

Reentrant in one thread. Locks are advisory and do not authenticate hostile
filesystem writers or provide distributed/network-filesystem transactions.
filesystem writers or fence external actions after a lost database lock.
"""
_validate_session_id(session_id)
store = configured_store()
if store is not None:
with store.lock(session_id):
yield
return
path = session_path(session_id).with_suffix(".lock")
key = str(path)
with _THREAD_LOCKS_GUARD:
Expand Down Expand Up @@ -140,12 +151,13 @@ def _revision(session: dict[str, Any]) -> int:


def save_session(session: dict[str, Any]) -> None:
"""Compare revisions under the session lock, then atomically replace JSON.
"""Compare revisions under the session lock, then atomically persist JSON.

Missing revisions on historical records start at zero; saving upgrades
storage only and never restores checkpoint or task execution authority.
"""
target = session_path(session["sessionId"])
_validate_session_id(session["sessionId"])
store = configured_store()
expected = _revision(session)
with session_lock(session["sessionId"]):
current = load_session(session["sessionId"])
Expand All @@ -158,6 +170,13 @@ def save_session(session: dict[str, Any]) -> None:
staged["updatedAt"] = _now()
staged["persistenceRevision"] = actual + 1
encoded = json.dumps(staged, indent=2, sort_keys=True, allow_nan=False)
if store is not None:
if not store.save(session["sessionId"], expected, staged):
raise SessionConflictError("session changed since it was loaded; reload before saving")
session["updatedAt"] = staged["updatedAt"]
session["persistenceRevision"] = staged["persistenceRevision"]
return
target = session_path(session["sessionId"])
temp = target.with_suffix(f".{uuid.uuid4().hex}.tmp")
try:
with temp.open("x", encoding="utf-8") as handle:
Expand All @@ -174,19 +193,26 @@ def save_session(session: dict[str, Any]) -> None:

def load_session(session_id: str) -> dict[str, Any] | None:
try:
path = session_path(session_id)
_validate_session_id(session_id)
except ValueError:
return None
if path.is_symlink():
raise ValueError("session file must not be a symlink")
try:
data = json.loads(path.read_text(encoding="utf-8"))
except FileNotFoundError:
return None
store = configured_store()
if store is not None:
data = store.load(session_id)
if data is None:
return None
else:
path = session_path(session_id)
if path.is_symlink():
raise ValueError("session file must not be a symlink")
try:
data = json.loads(path.read_text(encoding="utf-8"))
except FileNotFoundError:
return None
if not isinstance(data, dict) or data.get("schemaVersion") != SCHEMA_VERSION:
raise ValueError("unsupported session schema")
if data.get("sessionId") != session_id:
raise ValueError("saved session identity differs from its file")
raise ValueError("saved session identity differs from its storage key")
_revision(data)
return data

Expand Down Expand Up @@ -688,7 +714,15 @@ def main() -> int:
show.add_argument("--session", required=True)
ready = sub.add_parser("ready")
ready.add_argument("--session", required=True)
sub.add_parser("initialize-store", help="Create the optional Postgres session table on the configured database")
args = parser.parse_args()
if args.command == "initialize-store":
store = configured_store()
if store is None:
parser.error("initialize-store requires SI_SESSION_BACKEND=postgres")
store.initialize()
print(json.dumps({"initialized": True, "backend": "postgres", "namespace": store.namespace}))
return 0
session = load_session(args.session)
if not session:
print(json.dumps({"error": "session not found"}))
Expand Down
Loading
Loading