feat: contract-fix + multi-server fan-out#41
Conversation
Fix a load-bearing config-contract violation and add multi-server, per-session
FAN-OUT in the context-intelligence hook. Folded at the user's direction: the
contract fix is the foundation; fan-out is built on it.
CONTRACT FIX
The hook was the only hook of 8 that read ~/.amplifier/settings.yaml (via
SETTINGS_PATH/_parse_settings_yaml) and os.environ directly in config_resolver.py,
violating MOUNT_PLAN_SPECIFICATION.md / HOOK_CONTRACT.md (the app resolves config
sources and passes a resolved dict to mount(); the module consumes it). The
reference hook hooks-logging is a pure mount-config consumer. config_resolver.py
now has zero ambient reads; all config arrives via the mount config dict (with
${VAR} pre-expanded by the app) plus coordinator capabilities. Net code deletion.
MULTI-SERVER FAN-OUT
Dict-keyed `destinations`, each {url, api_key, include, exclude}. Each session's
events fan out to ALL destinations whose pathspec include/exclude patterns match
the session.working_dir capability -- A only / both / neither. There is NO
target/selector value. Per-destination failure isolation (own client, queue,
circuit breaker); local JSONL always written; fail-fast validation at mount;
legacy scalar config -> one synthesized `default` destination. Selection logic
lives in fanout.py.
VALIDATION
355 unit tests pass (fold-discipline gate runs first). Proven end-to-end in a
Digital Twin with real `amplifier` sessions against two live servers, configured
only via settings.yaml overrides.hook-context-intelligence.config.destinations:
plain -> A only, work -> both, secret -> neither (local-only).
Out of scope: #283 (skill force-load tax).
Generated with [Amplifier](https://github.com/microsoft/amplifier)
Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
3cee885 to
167ae6e
Compare
Configuring multiple servers (reviewer/user note)Multi-server fan-out is driven entirely by overrides:
hook-context-intelligence:
config:
destinations:
personal:
url: "http://192.168.1.6:8000"
api_key: "${PERSONAL_CI_KEY}"
include: ["**"] # everything…
exclude: ["**/client-*/**"] # …except client work
team:
url: "https://team-node.tailnet.ts.net"
api_key: "${TEAM_CI_KEY}"
include: ["**/client-x", "**/client-x/**"] # the client-x dir AND everything under it
A project's |
|
Edited: trimmed my earlier comment down to the items that are genuinely actionable — a couple were overstated, and one (HTTPS enforcement) was me imposing transport policy on your own config, which isn't the hook's call. Removed. Nice work on this — the contract fix reads clean, the Two things I think are worth a decision before merge, one genuine question, and a few minor notes. I verified the first two against the code (traced the diff; ran 1. Legacy
|
Addresses @colombod's review of PR #41: - Correct destinations example patterns. A gitignore-style `**/name/**` pattern matches only INSIDE the directory, so a session started with `cd name && amplifier` (working_dir = .../name) did not match. Examples now use the `["**/name", "**/name/**"]` idiom (directory + contents), with a comment documenting the gotcha. Added tests proving the old pattern misses the directory and the idiom covers both. - Legacy `url`-without-`api_key` degrades gracefully instead of failing to mount. The synthesized `default` destination is created only when both url and api_key are present; url-only now logs a WARNING and runs local-JSONL-only (restores pre-PR behavior). Explicit `destinations.<name>` entries with a missing url/api_key still fail fast (genuine misconfig). - Promote dropped-destination state transitions (queue-full, breaker-open) from DEBUG to WARNING so a configured destination going dark is visible. - Bound the pathspec dependency: >=0.12,<1.0. 360 tests pass (+5 new). 🤖 Generated with [Amplifier](https://github.com/microsoft/amplifier) Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
…mented config; demote legacy scalars The README still presented the legacy single-server scalars (context_intelligence_server_url/api_key) as the primary configuration story across the intro, the "What it does" table, Quick Start, the app-cli override pattern, and the configuration reference — none of which documented the `destinations` multi-server fan-out shape introduced in PR #41. Restructured so the current `destinations` shape is canonical: - Intro + "What it does" describe per-session fan-out by working directory. - Quick Start step 2 configures a `destinations` block (not legacy scalars). - The app-cli override section leads with `destinations`; legacy is a labeled back-compat shortcut. - Configuration reference documents the `destinations` sub-keys, routing/validation semantics, and a Source column; legacy scalars moved to a "Deprecated" subsection noting the url-without-key local-only degradation. 🤖 Generated with [Amplifier](https://github.com/microsoft/amplifier) Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
…e => no-match Follow-on hardening on top of this PR's multi-server fan-out, folding in the remaining item from the now-superseded #41 plus two review decisions: - Promote dropped-destination state transitions (queue-full, circuit-breaker open) from DEBUG to WARNING -- a configured destination going dark is now visible at the default log level. - set_dispatchers() now closes the previously-set dispatchers before swapping in the new list, removing a latent worker/httpx-client leak if it is ever called more than once per handler. - Flip the destination default: a destination with NO `include` now matches NOTHING (previously catch-all `**`). Prevents accidentally fanning every session out to a newly-added server. The legacy single-server synthesis still sets include=("**",) explicitly, so existing single-server users are unaffected. 367 tests pass. Generated with [Amplifier](https://github.com/microsoft/amplifier) Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
|
Superseded by #42, which carries the original fan-out commit (167ae6e) plus all the hardening — gitignore matching, mount-regression graceful-degrade, working_dir-absent degrade-to-local, pathspec bound — and now the follow-on fixes (WARNING on dropped destinations, set_dispatchers leak, no-include⇒no-match default). Everything from this PR is represented in #42. Closing in favor of #42. |
Summary
Enable context-intelligence to fan out each session's events to multiple servers — sending to all destinations whose include/exclude patterns match the session's working directory (A only / both / neither) — while fixing a load-bearing contract violation in the hook's config resolution.
Two changes, folded into one PR per the user's decision: (1) contract fix — the hook becomes a pure mount-config consumer; (2) multi-server fan-out — sessions fan out to matching servers via per-destination include/exclude patterns. There is no
target/selector config value — destination selection is purely per-destination pattern matching, and a session can match (and be sent to) more than one.The Contract Fix (load-bearing)
The context-intelligence hook was the only hook of 8 in the ecosystem that read
~/.amplifier/settings.yamlandos.environdirectly insideconfig_resolver.py— violating MOUNT_PLAN_SPECIFICATION.md / HOOK_CONTRACT.md: "The app layer reads various config sources… Hooks receive configuration via Mount Plan."hooks-logging): pure mount-config consumer — reads only theconfigdict passed tomount()+ one coordinator capability (session.working_dir). Zeroos.environ, zerosettings.yaml.import os,SETTINGS_PATH/_parse_settings_yaml,_ENV_PREFIX,_env(), and the settings.yaml/env fallbacks fromconfig_resolver.py. All config now arrives via the mountconfigdict (with${VAR}already expanded by the app) + coordinator capabilities. Grep confirmsconfig_resolver.pyhas zero ambient reads.The app layer (amplifier-app-cli) already resolves config: user→project settings deep-merge,
${VAR}expansion, and per-module overrides viaoverrides.hook-context-intelligence.config.*. The module consumes that; it no longer re-solves it.The Feature: Multi-Server Fan-Out
Config shape (mount plan, populated by app-cli's
overrides.hook-context-intelligence.config):pathspecgitwildmatch (gitignore semantics), against thesession.working_dircapability (fail-loud if absent — no slug fallback). Exclude wins, per-destination.destinationsis a dict keyed by name, app-cli's user→project settings deep-merge lets a project.amplifier/settings.yamloverride one destination'sinclude/exclude(e.g.destinations.team.include) without restating the others.urlorapi_keyafter expansion → error at mount (not a silent per-event drop). No destinations configured → local-only (valid).Back-compat: legacy scalar
context_intelligence_server_url/api_keyis synthesized into a singledefaultdestination (include: ["**"]). Existing single-server configs load and route unchanged. The servedcontext-intelligence-graph-queryskill binds to the first active destination.Validation
Unit tests — 355 pass
test_aa_contract_fix.py, sorts first): hook reads only mount config (no ambient files/env); existing single-server config still works — verified before any fan-out test runs.test_destinations_schema.py/test_destinations_validation.py(dict parsing, defaults, legacy synthesis, fail-fast),test_fanout_select.py(pathspec, exclude-wins, A/both/neither),test_dispatcher.py(per-destination isolation),test_on_session_ready_routing.py(working_dir capability, migration warning, fail-loud on absent),test_logging_handler_fanout.py(JSONL always written, fan-out to all dispatchers).End-to-end in a Digital Twin — real
amplifiersessions, two live serversValidated via amplifier-tester in an isolated DTU running real Amplifier with this branch installed, configured only through
~/.amplifier/settings.yamloverrides.hook-context-intelligence.config.destinations(the patterns above). Three realamplifiersessions:plain-projwork/appsecret/appThis proves the full real path: settings.yaml override → app-cli resolution → mount plan → hook fan-out by
working_dir→ correct servers — with the hook reading zero ambient config (the contract fix). The fact that fan-out happens at all from a settings.yaml override proves app-cli resolveddestinationsinto the mount plan and the hook consumed it.Out of Scope
context-intelligence-graph-queryskill every session start even when unused — overhead in pipelines and single-command-series workflows.Files Changed
Added:
amplifier_module_hook_context_intelligence/fanout.py(pure selection logic) + 8 test files.Modified:
config_resolver.py(contract fix +Destination/destinations),handlers/logging_handler.py(per-destination dispatchers),__init__.py(mount integration + fan-out),behaviors/context-intelligence.yaml(config examples + deprecation notes),pyproject.toml(+pathspec>=0.12).