popoto.integrations.config¶
popoto.integrations.config
¶
Environment-driven configuration for the harness integration.
Everything the hook process and the MCP server need is resolved from the
environment plus the current working directory, because neither has a
config file of its own: a hook is a bare command string in the harness's
settings, and an MCP server is a command plus an env map. There is no
place to pass Python arguments, so the environment is the whole interface.
Every variable is optional. The zero-configuration path is "local Redis or Valkey on the default port, memories scoped to this project".
====================================== ============================= ====
Variable Default Note
====================================== ============================= ====
POPOTO_MEMORY_URL REDIS_URL or
redis://localhost:6379/0 Valkey URLs are identical
POPOTO_MEMORY_AGENT_ID basename of the cwd Per-project scoping
POPOTO_MEMORY_MAX_ITEMS 5 Diverges from the benchmark; see below
POPOTO_MEMORY_MAX_TOKENS 800 Under Codex's 2500-token cap
POPOTO_MEMORY_INGEST raw raw | heuristic
POPOTO_MEMORY_ENABLED 1 Kill switch, no config edit needed
POPOTO_MEMORY_LOG ~/.popoto/memory.log Where swallowed errors land
POPOTO_MEMORY_TURN_KEYED 1 0 restores the pre-#574 session FIFO handoff
====================================== ============================= ====
The max_items / max_tokens divergence is deliberate and is the
only place this package departs from the benchmarked configuration. The
retrieval benchmark ran at max_items=20, which is a reasonable budget
for a question-answering evaluation and the wrong budget for a coding
harness, where a turn fires every few seconds and context is contested by
file contents, tool output, and the system prompt. Codex additionally caps
injected context at 2500 tokens (additionalContextLimit). Scoring is
not changed: score_weights stays at the benchmarked
{"relevance": 1.0} and retrieval stays on the lexical/BM25 path that
:class:popoto.recipes.DefaultMemory selects.
DEFAULT_URL = 'redis://localhost:6379/0'
module-attribute
¶
Connection URL used when neither POPOTO_MEMORY_URL nor REDIS_URL
is set. Valkey uses the same scheme, so this default covers both.
ALLOW_DB0_ENV = 'POPOTO_MEMORY_ALLOW_DB0'
module-attribute
¶
Deploy-level opt-in for writing agent memory to Redis database 0. An
environment variable rather than a constructor argument on purpose: a PyPI
adopter running the hook cannot edit model code, and a hook is a bare
command string with no place to pass Python arguments. Accepts the same
truthy set as POPOTO_MEMORY_ENABLED (1/true/yes/on).
HOOK_SOCKET_TIMEOUT_SECONDS = 1.0
module-attribute
¶
Socket connect and read timeout applied when the integration binds its
own connection (POPOTO_MEMORY_URL or REDIS_URL). The read hook sits
on the user's prompt path, so a hung server must cost about a second, not
the library default of five per attempt. Retries are disabled for the same
reason: the harness will run the hook again next turn. Lives here rather
than in popoto.fields.constants.Defaults because it is integration
transport config, not a retrieval tuning constant; see that docstring for
the convention.
DEFAULT_MAX_ITEMS = 5
module-attribute
¶
Records injected per turn. See the module docstring for why this is not the benchmark's 20.
DEFAULT_MAX_TOKENS = 800
module-attribute
¶
Soft token budget for the injected block, chosen to sit well under
Codex's 2500-token additionalContextLimit.
DEFAULT_INGEST = 'raw'
module-attribute
¶
Write path. raw is :class:~popoto.extraction.RawTurnExtractionProvider,
the arm issue #489 measured ahead of the heuristic (0.3636 vs 0.2078 judged
accuracy).
VALID_INGEST_MODES = ('raw', 'heuristic')
module-attribute
¶
Accepted POPOTO_MEMORY_INGEST values. llm is deliberately absent:
it requires an API key, and the zero-key promise is the point of this
integration. Use the library path with ClaudeExtractionProvider for
that.
DEFAULT_LOG_PATH = '~/.popoto/memory.log'
module-attribute
¶
Where the hook writes the errors it swallows. A hook has no console, so
a file is the only observable surface besides popoto-memory doctor.
PENDING_TTL_SECONDS = 3600
module-attribute
¶
Lifetime of a turn's pending-record handoff between the read hook and the write hook. An hour is long enough for a slow turn and short enough that an abandoned session does not leak keys.
MemoryConfig
dataclass
¶
Resolved configuration for one :class:~popoto.integrations.service.MemoryService.
Attributes:
| Name | Type | Description |
|---|---|---|
url |
str
|
Redis or Valkey connection URL. |
agent_id |
str
|
Partition key for every read and write. Defaults to the basename of the working directory, so two projects on one Redis do not read each other's memories. |
max_items |
int
|
Maximum records injected per turn. |
max_tokens |
int
|
Soft token budget for the injected block. |
ingest |
str
|
|
enabled |
bool
|
When |
log_path |
Path
|
File that swallowed exceptions are appended to. |
url_is_explicit |
bool
|
|
turn_keyed |
bool
|
When |
Source code in src/popoto/integrations/config.py
111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 | |
from_env(env=None, cwd=None)
classmethod
¶
Resolve configuration from environment variables and a directory.
Unparseable numeric values fall back to their defaults rather than raising: a typo in a harness config must not break the user's turn.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
env
|
Optional[Mapping[str, str]]
|
Mapping to read variables from. Defaults to |
None
|
cwd
|
Optional[str]
|
Directory whose basename becomes the default |
None
|
Returns:
| Type | Description |
|---|---|
MemoryConfig
|
A frozen :class: |
Source code in src/popoto/integrations/config.py
Db0RefusedError
¶
Bases: ValueError
Raised when agent memory would be written to Redis database 0.
Subclasses ValueError so the existing handlers keep working: the
hook's blanket catch, the MCP dispatcher's, and doctor's explicit
except ValueError all predate this error and all do the right thing
with it.
Source code in src/popoto/integrations/config.py
derive_agent_id(cwd=None)
¶
Derive the default agent_id from a directory path.
The basename of the working directory. Coarse on purpose: it is
understandable at a glance, stable across sessions in the same
checkout, and overridable with POPOTO_MEMORY_AGENT_ID. Two
same-named directories in different parents do collide; a project
identity scheme is a separate design problem.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
cwd
|
Optional[str]
|
Directory to derive from. Defaults to the process working
directory. An unreadable working directory yields
|
None
|
Returns:
| Type | Description |
|---|---|
str
|
A non-empty agent id string. |
Source code in src/popoto/integrations/config.py
redact_url(url)
¶
Return url with any password replaced by ***.
status() feeds the MCP memory_status tool and doctor, both
of which land in transcripts, so the connection URL must never carry
the credential through.
Source code in src/popoto/integrations/config.py
effective_db(config)
¶
The database number this service will actually write to.
Two cases, and conflating them is the trap:
url_is_explicit-- the caller named a URL, so itsdbis the answer. A URL with nodbat all raises the "no database number"ValueErrorhere.- otherwise -- Popoto's live connection is the answer, not
config.url. With noPOPOTO_MEMORY_URL,config.urlisDEFAULT_URL(database 0) even when the process is on database 15: the pytest plugin swapsPOPOTO_REDIS_DB's pool in place and never touchesMemoryConfig. Readingconfig.urlhere would refuse on every test in the suite and on every host application that configured its own connection.
Source code in src/popoto/integrations/config.py
suggest_free_db()
¶
Lowest database in 1..15 that currently holds no keys, or None.
Best effort and advisory only. Reads INFO keyspace (a core command
on both Redis and Valkey), which reports only non-empty databases, so
anything in 1..15 absent from that report is empty. Any exception
yields None; a diagnostic must never be the thing that fails. This
function never rebinds anything.
Source code in src/popoto/integrations/config.py
bind_connection(config)
¶
Point Popoto's shared connection at config.url.
Only acts when POPOTO_MEMORY_URL was set explicitly. Without it,
Popoto's own import-time resolution (REDIS_URL, else
localhost:6379/0) already produced the same target, and leaving the
live connection alone is the safe behavior for an in-process caller --
a test running under the Popoto pytest plugin on an isolated database,
or the Hermes plugin (plugins/hermes/__init__.py) inside a
long-lived harness -- which has already chosen its connection. Rebinding
those to a default would move
writes to database 0 behind the caller's back.
The rebind swaps the pool on the existing client object rather than
replacing the client. Most of Popoto imports POPOTO_REDIS_DB at
module load for speed, so those references are bound to one object for
the life of the process; assigning a new client to the module global
would leave every already-imported module writing to the old target.
This is the same in-place technique popoto.pytest_plugin._swap_db
uses, and for the same reason.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
config
|
MemoryConfig
|
The resolved configuration whose |
required |
Returns:
| Type | Description |
|---|---|
bool
|
|
bool
|
already correct or no explicit URL was given. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
Db0RefusedError
|
If the effective database is 0 and neither
|
Source code in src/popoto/integrations/config.py
375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 | |