Phase 8 follow-up: resolve the child named in the question

The Hub forwards `person` equal to the asker (`PersonOrDefault`), so the spoken
question "what does Harry have on today at school?" arrives with person "sam"
and returned "no data for that person" (found by the Hub agent on AUD-116).

resolveChild now falls back to the question text when the requested person is
not one of this project's children: whole-word, case-insensitive match against
the child names this service already publishes in /capabilities. An explicit
person still wins, two named children still return answer:null, and the
permission check still runs on the resolved child. No model, no free text.

Live end-to-end through the Hub, with no person field:

  POST http://192.168.20.13:8788/ask
  {"text":"what does Harry have on today at school?","asker":"sam","source":"console"}
  200
  {"status":"ok",
   "answer":"Harry has 8 school events today from 8:55am to 6:00pm and 6 overdue tasks.",
   "route":{"kind":"project","project":"compass_nhs"}}

Image 0.6.1-phase8; docs/phase-8-hub-notes.md records the fallback and the
end-to-end transcript.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
2026-10-15 19:54:26 +11:00
parent 75e4ff12bf
commit 06f759c37b
7 changed files with 230 additions and 31 deletions

View File

@@ -89,6 +89,29 @@ Jo and Sam) may ask about any child, and anyone else may ask about themselves
only. An unknown person returns `answer: null` with a reason; no data is leaked
between the children.
### Which child the question is about
The contract makes `person` optional and says it defaults to `asker`. The Hub
(as of AUD-116) forwards `person = asker`, so the spoken question *"what does
Harry have on today at school?"* arrives with `person: "sam"`. Because this
project is the expert on its own data, `resolveChild` falls back to the question
text when the requested person is not one of its children:
- The requested `person` wins when it names a child (so an explicit `person` is
never overridden).
- Otherwise the question is split into whole words and matched,
case-insensitively, against the child names this service already publishes in
`/capabilities`.
- Exactly one match resolves the child; two matches ("what do Harry and Finn
have on?") return `answer: null` + a reason, because the contract answers for
one person at most.
- The permission check still runs on the resolved child.
No model and no free-text guessing are involved, and a name is never matched
inside a longer word. This makes the Hub's current call shape answer correctly;
[AUD-117](/AUD/issues/AUD-117) (the Hub deriving `person` itself) remains a
useful Hub-side improvement, not a dependency of this phase.
## `/notify` — the interpretation
The contract defines `POST /notify` as project-to-Hub (the *project* posts to the
@@ -118,7 +141,31 @@ make verify-phase8
deploy/verify-phase8.sh --send
```
## Evidence (2026-10-15, 08:46–08:47 UTC / 19:46–19:47 AEDT)
## Evidence (2026-10-15)
### End-to-end through the live Hub (08:56 UTC / 19:56 AEDT)
The literal phase question, sent to the **Hub** on its inbound API with no
`person` field, after [AUD-116](/AUD/issues/AUD-116) registered `compass_nhs`:
```
POST http://192.168.20.13:8788/ask
{"text":"what does Harry have on today at school?","asker":"sam","source":"console"}
200
{
"status": "ok",
"request_id": "01M4ZC9G4TRXJ3MFSMJ66JSVQ1",
"answer": "Harry has 8 school events today from 8:55am to 6:00pm and 6 overdue tasks.",
"shape": "text",
"route": {"kind": "project", "project": "compass_nhs"},
"expires": "2026-10-15T13:00:00Z"
}
```
This is the Phase 8 definition of done: the Hub asks, and gets a correct answer.
### Against the service directly
`deploy/verify-phase8.sh` on `.13` against `http://127.0.0.1:8792`:
@@ -164,30 +211,20 @@ hub notify delivered from=verify-phase8 level=info recipient=Sam channel=email
hub notify delivered from=verify-phase8 level=info recipient=Sam channel=whatsapp status=200
```
## The Hub wiring (remaining)
## The Hub wiring (done)
The Hub container (`central-ai-hub:local`, port 8788) currently runs with
`HUB_PROJECTS` empty, so it has no data project to route to. Registering this
service and restarting the Hub is a change to a running, family-used service and
a change to `~/.config/environment.d/10-secrets.conf`, so it is done only with a
recorded approval.
The Hub container (`central-ai-hub:local`, port 8788) now has this service
registered. The change was made by the Hub agent on
[AUD-116](/AUD/issues/AUD-116):
The change is exactly:
1. `~/.config/environment.d/10-secrets.conf` gained
`HUB_PROJECT_COMPASS_NHS_TOKEN` and `compass_nhs=http://192.168.20.13:8792`
in `HUB_PROJECTS` (alongside `email_agent` and `internet_control`).
2. The `hub` container was restarted once, bounded.
3. The transcript above is the result.
1. Add to `~/.config/environment.d/10-secrets.conf`:
`HUB_PROJECT_COMPASS_NHS_TOKEN=<the value of SCHOOL_HUB_TOKEN in this project's environment.d/school-service.env>`.
2. Set `HUB_PROJECTS` there to include
`compass_nhs=http://192.168.20.13:8792` (keep the existing `email_agent` entry).
3. Restart the `hub` container: `docker compose -f /home/sam/paseo/projects/central_ai_llm_master/docker-compose.yml up -d hub`.
Then the end-to-end check is:
```bash
curl -sS -H "Authorization: Bearer $HUB_TOKEN_CONSOLE" -H 'Content-Type: application/json' \
-d '{"text":"what does Harry have on today at school?","asker":"sam","source":"console"}' \
http://192.168.20.13:8788/ask
# expect route.kind = project, route.project = compass_nhs, status ok
```
`routing.md` was not changed: the `compass_nhs` row already existed and matches
this service's `/capabilities` name.
## Decisions and notes