Skip to content
Merged
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
16 changes: 16 additions & 0 deletions docs/alpha/headless-ubuntu-install.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ Default paths:

The installer renders [packaging/ubuntu/alicebot.env.example](../../packaging/ubuntu/alicebot.env.example) into `~/.config/alicebot/.env` if the file does not already exist. Existing config is preserved.
For local Postgres installs, it detects the server major version, installs the matching `postgresql-<major>-pgvector` package, and creates the `vector` extension before migrations.
After migrations, it seeds the configured local Alice user row when missing so CLI, scheduler, MCP, and headless doctor checks have a valid continuity owner even when `/vnext` has not been opened yet.

Important config keys:

Expand All @@ -86,6 +87,7 @@ ALICE_API_HOST=127.0.0.1
ALICE_API_PORT=8000
ALICE_WEB_HOST=127.0.0.1
ALICE_WEB_PORT=3000
ALICEBOT_AUTH_USER_ID=00000000-0000-0000-0000-000000000001
ALICE_SECRET_PROVIDER=encrypted_local
MODEL_PROVIDER=deterministic_local
CORS_ALLOWED_ORIGINS=http://127.0.0.1:3000,http://localhost:3000
Expand Down Expand Up @@ -119,6 +121,20 @@ scripts/validate_env.sh ~/.config/alicebot/.env .env.lite apps/web/.env.local
# Create the alicebot database/roles to match ~/.config/alicebot/.env before this step.
sudo -u postgres psql -d alicebot -c 'CREATE EXTENSION IF NOT EXISTS vector;'
./.venv/bin/python -m alembic -c apps/api/alembic.ini upgrade head
set -a
. ~/.config/alicebot/.env
set +a
psql "$DATABASE_ADMIN_URL" <<'SQL'
INSERT INTO users (id, email, display_name)
VALUES (
'00000000-0000-0000-0000-000000000001',
'local-alpha-00000000-0000-0000-0000-000000000001@alicebot.local',
'Local Alpha User'
)
ON CONFLICT (id) DO UPDATE
SET email = EXCLUDED.email,
display_name = EXCLUDED.display_name;
SQL
./.venv/bin/alicebot vnext doctor --fix-safe --ci
./.venv/bin/alicebot vnext alpha check --headless --skip-smokes
```
Expand Down
19 changes: 19 additions & 0 deletions docs/alpha/hermes-dogfood-ubuntu.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,25 @@ project_scope:

Use `trusted_local_agent` only for a local operator-owned Hermes runtime on the same host. For narrower project work, use `project_scoped_agent`.

## Memory Provider Capture Config

For the Alice Hermes memory provider, `bridge_mode: auto` or `bridge_mode: assist`
now enables post-turn `sync_turn` capture when `sync_turn_capture_enabled` is
omitted. Keep the flag explicit in production dogfood configs so the posture is
obvious:

```json
{
"base_url": "http://127.0.0.1:8000",
"user_id": "00000000-0000-0000-0000-000000000001",
"bridge_mode": "auto",
"sync_turn_capture_enabled": true
}
```

Set `sync_turn_capture_enabled` to `false` when Hermes should use Alice recall
and prefetch without committing post-turn capture candidates.

## MCP Config Example

```yaml
Expand Down
4 changes: 3 additions & 1 deletion docs/integrations/hermes-memory-provider.md
Original file line number Diff line number Diff line change
Expand Up @@ -160,11 +160,13 @@ Practical default:
- `prefetch_max_recent_changes` (int)
- `prefetch_max_open_loops` (int)
- `prefetch_include_non_promotable_facts` (bool)
- `sync_turn_capture_enabled` (bool, default `false`)
- `sync_turn_capture_enabled` (bool, default `false`; when omitted, an explicitly configured `bridge_mode: assist` or `bridge_mode: auto` enables `sync_turn` capture)
- `memory_write_capture_enabled` (bool, default `false`)
- `bridge_mode` (string enum: `manual`, `assist`, `auto`; default `assist`)
- `session_end_flush_timeout_seconds` (float, default `5.0`)

`sync_turn_capture_enabled: false` always wins. Use that when you want bridge recall/prefetch behavior without post-turn capture, even if `bridge_mode` is `assist` or `auto`.

Legacy compatibility keys still accepted for shipped configs:

- `prefetch_limit`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,14 @@ The provider reads and writes:
- `max_recent_changes` (int, default `5`)
- `max_open_loops` (int, default `5`)
- `include_non_promotable_facts` (bool, default `false`)
- `auto_capture` (bool, default `false`)
- `sync_turn_capture_enabled` (bool, default `false`; when omitted, an explicit `bridge_mode` of `assist` or `auto` enables turn capture)
- `bridge_mode` (string enum: `manual`, `assist`, `auto`; default `assist`)
- `mirror_memory_writes` (bool, default `false`)

Legacy `auto_capture` and `capture_mode` keys are still accepted. Explicit
`sync_turn_capture_enabled: false` disables turn capture even when `bridge_mode`
is `assist` or `auto`.

## Transport and Identity Safety

- non-loopback `base_url` values must use `https://`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,12 @@ def _parse_bridge_mode(value: Any, *, default: str) -> str:
return default


def _bridge_mode_implies_sync_capture(_bridge_mode: str, raw_bridge_mode: Any) -> bool:
if not isinstance(raw_bridge_mode, str):
return False
return raw_bridge_mode.strip().lower() in {"assist", "auto"}


def _normalize_base_url(value: str) -> str:
normalized = value.strip().rstrip("/")
if normalized.endswith("/v0"):
Expand Down Expand Up @@ -251,7 +257,15 @@ def _resolve_config_value(values: Dict[str, Any], key: str) -> tuple[Any, str]:


def _default_config() -> Dict[str, Any]:
return {
bridge_mode_value = _first_non_empty(
os.environ.get("ALICE_MEMORY_BRIDGE_MODE"),
os.environ.get("ALICE_MEMORY_CAPTURE_MODE"),
)
sync_turn_capture_value = _first_non_empty(
os.environ.get("ALICE_MEMORY_SYNC_TURN_CAPTURE_ENABLED"),
os.environ.get("ALICE_MEMORY_AUTO_CAPTURE"),
)
config = {
"base_url": os.environ.get("ALICE_API_BASE_URL", _DEFAULT_BASE_URL),
"user_id": os.environ.get("ALICE_MEMORY_USER_ID", os.environ.get("ALICEBOT_AUTH_USER_ID", "")),
"timeout_seconds": _parse_float(
Expand Down Expand Up @@ -294,34 +308,28 @@ def _default_config() -> Dict[str, Any]:
),
default=False,
),
"sync_turn_capture_enabled": _parse_bool(
_first_non_empty(
os.environ.get("ALICE_MEMORY_SYNC_TURN_CAPTURE_ENABLED"),
os.environ.get("ALICE_MEMORY_AUTO_CAPTURE"),
),
default=False,
),
"memory_write_capture_enabled": _parse_bool(
_first_non_empty(
os.environ.get("ALICE_MEMORY_MEMORY_WRITE_CAPTURE_ENABLED"),
os.environ.get("ALICE_MEMORY_MIRROR_WRITES"),
),
default=False,
),
"bridge_mode": _parse_bridge_mode(
_first_non_empty(
os.environ.get("ALICE_MEMORY_BRIDGE_MODE"),
os.environ.get("ALICE_MEMORY_CAPTURE_MODE"),
),
default=_DEFAULT_BRIDGE_MODE,
),
"session_end_flush_timeout_seconds": _parse_float(
os.environ.get("ALICE_MEMORY_SESSION_END_FLUSH_TIMEOUT_SECONDS"),
default=_DEFAULT_SESSION_END_FLUSH_TIMEOUT_SECONDS,
min_value=0.5,
max_value=30.0,
),
}
if sync_turn_capture_value is not None:
config["sync_turn_capture_enabled"] = _parse_bool(
sync_turn_capture_value,
default=False,
)
if bridge_mode_value is not None:
config["bridge_mode"] = bridge_mode_value
return config


def _load_config_with_status(hermes_home: str) -> Dict[str, Any]:
Expand Down Expand Up @@ -490,12 +498,21 @@ def get_config_schema(self) -> List[Dict[str, Any]]:
]

def save_config(self, values: Dict[str, Any], hermes_home: str) -> None:
cfg = _load_config(hermes_home)
cfg = _default_config()
path = _config_path(hermes_home)
if path.exists():
try:
raw = json.loads(path.read_text(encoding="utf-8"))
if isinstance(raw, dict):
for key, value in raw.items():
if value is not None and value != "":
cfg[key] = value
except Exception:
logger.debug("Failed to parse %s before saving config", path, exc_info=True)
cfg.update(values)
cfg, errors, _ = _load_config_dict_from_values(cfg)
if errors:
raise ValueError("; ".join(errors))
path = _config_path(hermes_home)
path.write_text(json.dumps(cfg, indent=2, sort_keys=True) + "\n", encoding="utf-8")

def system_prompt_block(self) -> str:
Expand Down Expand Up @@ -1021,12 +1038,20 @@ def _load_config_dict_from_values(values: Dict[str, Any]) -> tuple[Dict[str, Any
default=False,
)

bridge_mode_value, bridge_mode_source = _resolve_config_value(values, "bridge_mode")
if bridge_mode_source != "bridge_mode":
legacy_config_keys.append(bridge_mode_source)
config["bridge_mode"] = _parse_bridge_mode(
bridge_mode_value,
default=_DEFAULT_BRIDGE_MODE,
)

sync_turn_capture_value, sync_turn_capture_source = _resolve_config_value(values, "sync_turn_capture_enabled")
if sync_turn_capture_source != "sync_turn_capture_enabled":
legacy_config_keys.append(sync_turn_capture_source)
config["sync_turn_capture_enabled"] = _parse_bool(
sync_turn_capture_value,
default=False,
default=_bridge_mode_implies_sync_capture(config["bridge_mode"], bridge_mode_value),
)

memory_write_capture_value, memory_write_capture_source = _resolve_config_value(
Expand All @@ -1040,14 +1065,6 @@ def _load_config_dict_from_values(values: Dict[str, Any]) -> tuple[Dict[str, Any
default=False,
)

bridge_mode_value, bridge_mode_source = _resolve_config_value(values, "bridge_mode")
if bridge_mode_source != "bridge_mode":
legacy_config_keys.append(bridge_mode_source)
config["bridge_mode"] = _parse_bridge_mode(
bridge_mode_value,
default=_DEFAULT_BRIDGE_MODE,
)

config["session_end_flush_timeout_seconds"] = _parse_float(
values.get("session_end_flush_timeout_seconds"),
default=_DEFAULT_SESSION_END_FLUSH_TIMEOUT_SECONDS,
Expand Down
44 changes: 44 additions & 0 deletions scripts/install-ubuntu.sh
Original file line number Diff line number Diff line change
Expand Up @@ -366,6 +366,49 @@ SQL
sudo -u postgres psql -d alicebot -c "CREATE EXTENSION IF NOT EXISTS vector;" >/dev/null
}

seed_default_user_from_env() {
if [ "${DRY_RUN}" -eq 1 ]; then
log "+ seed configured local Alice user row if missing"
return
fi
"${INSTALL_DIR}/.venv/bin/python" - <<'PY'
import os
from uuid import UUID

import psycopg


user_id_raw = os.environ.get("ALICEBOT_AUTH_USER_ID", "").strip()
if not user_id_raw:
raise SystemExit("ALICEBOT_AUTH_USER_ID is required before seeding the local Alice user")

try:
user_id = UUID(user_id_raw)
except ValueError as exc:
raise SystemExit("ALICEBOT_AUTH_USER_ID must be a valid UUID") from exc

conninfo = os.environ.get("DATABASE_ADMIN_URL") or os.environ.get("DATABASE_URL")
if not conninfo:
raise SystemExit("DATABASE_ADMIN_URL or DATABASE_URL is required before seeding the local Alice user")

email = f"local-alpha-{user_id}@alicebot.local"
display_name = "Local Alpha User"

with psycopg.connect(conninfo) as conn:
with conn.cursor() as cur:
cur.execute(
"""
INSERT INTO users (id, email, display_name)
VALUES (%s, %s, %s)
ON CONFLICT (id) DO UPDATE
SET email = EXCLUDED.email,
display_name = EXCLUDED.display_name
""",
(user_id, email, display_name),
)
PY
}

install_project_dependencies() {
run python3 -m venv "${INSTALL_DIR}/.venv"
run "${INSTALL_DIR}/.venv/bin/python" -m pip install --upgrade pip
Expand All @@ -382,6 +425,7 @@ run_migrations_and_checks() {
set +a
fi
run_in_install_dir "${INSTALL_DIR}/.venv/bin/python" -m alembic -c apps/api/alembic.ini upgrade head
seed_default_user_from_env
run "${INSTALL_DIR}/.venv/bin/alicebot" vnext doctor --fix-safe --ci
if [ "${RUN_ALPHA_CHECK}" -eq 1 ]; then
run "${INSTALL_DIR}/.venv/bin/alicebot" vnext alpha check --headless --skip-smokes
Expand Down
5 changes: 3 additions & 2 deletions scripts/install_hermes_alice_memory_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,16 +105,17 @@ def main() -> int:
print(" 3) hermes config set memory.provider alice")
print(" 4) ./.venv/bin/python scripts/run_hermes_memory_provider_smoke.py")
print()
print("Bridge B1 config keys:")
print("Bridge B2 config keys:")
print(" - prefetch_recall_limit")
print(" - prefetch_max_recent_changes")
print(" - prefetch_max_open_loops")
print(" - prefetch_include_non_promotable_facts")
print(" - sync_turn_capture_enabled")
print(" - memory_write_capture_enabled")
print(" - bridge_mode")
print(" - session_end_flush_timeout_seconds")
print("Legacy compatibility keys still accepted: prefetch_limit, max_recent_changes,")
print("max_open_loops, include_non_promotable_facts, auto_capture, mirror_memory_writes")
print("max_open_loops, include_non_promotable_facts, auto_capture, mirror_memory_writes, capture_mode")
return 0


Expand Down
Loading