# Phase 0 — LeadRat API spike: verdict (2026-07-09)

Ran against the **real LeadRat API** with operator approval and credentials
(DECIDE 7). Base `https://connect.leadrat.com`, tenant `silveroakglobal`.
Test data per operator: name `ammar testing`, email
`ammar@silveroakglobal.ae`, phone `971500000000`. **Exactly one lead was
created and no other lead was read, modified, or deleted.**

Test lead: `7d65e5db-0872-477a-9170-f4b8b82bb2ff`. Its notes field now reads
"TEST LEAD from channels-manager Phase-0 API spike (2026-07-09). Safe to
delete." **Operator action: delete it via the LeadRat UI** — the API has no
delete endpoint.

Spike scripts were deleted after the run; the credentials were passed as
environment variables and never written to disk or committed.

## Headline verdict

**Notes OVERWRITE. They do not append.** The production assumption is wrong,
and it is silently losing lead context today.

`LeadRatService.appendNote()` (`apps/api/src/leadrat/leadrat.service.ts:113`)
does a blind `PUT /api/v1/lead/notes/{id}` with only the new text, on the
assumption that LeadRat appends server-side. Proven false:

| step | call | notes field afterwards |
|---|---|---|
| create | `POST /api/v1/lead` with `notes: "NOTE A"` | `NOTE A` |
| append | `PUT /api/v1/lead/notes/{id}` `notes: "NOTE B"` | `NOTE B` |
| append | `PUT /api/v1/lead/notes/{id}` `notes: "NOTE C"` | `NOTE C` |

Each write replaced the last. Since the `crm_lead` tool description tells the
model to call again for every new detail, a qualified lead's notes in LeadRat
currently contain **only whatever the final call happened to say** — the
`[HOT]` grading, the earlier slots, and the conversation context from every
prior call are gone. Nothing surfaces this: the API returns
`{"succeeded": true, "message": "Lead notes updated successfully."}` every
time.

This is a real, live data-loss bug. It belongs in the Phase 1 fix-now batch
alongside the code-review findings.

## Mitigation (proven, not theorised)

Read-modify-write works, and the read half exists even though the client never
knew it:

- **`GET /api/v1/lead/{id}` exists and returns the full lead**, including
  `notes`. The current client has no single-lead read at all.
- Fetching `notes`, concatenating, and PUTting back preserved history:
  final value contained both `NOTE C` and the newly appended `NOTE D`.

So `appendNote` should become: GET the lead, prepend/append the timestamped
new note to the existing text, PUT the merged string. The obvious hazard is a
lost update if two turns append concurrently; the agent already serialises
turns per contact, and the same fix that closes review finding 9 (deliver
inside the per-contact queue) covers this.

## Second finding: `updateLead` would 400 on any partial patch

`PUT /api/v1/lead/{id}` is a **full replace**, not a patch. Sending
`{id, rating: "Hot"}` returns:

```
400 {"errors":{"Name":["The Name field is required."],
                "ContactNo":["The ContactNo field is required."]}}
```

`LeadRatService.updateLead(crmId, fields)` (line 101) sends `{id, ...fields}`,
so it fails for any caller that omits name and contactNo. It is currently
**unused** by the agent adapter (which only creates, appends notes, and
reassigns), so this is latent rather than live — but it is a trap for the
first person who calls it. Any real update must send name and contactNo too.

## Third finding: temperature has a real field, but it is not reachable yet

The lead object has a top-level **`rating`** field (currently `null`), which is
the natural home for the hot/warm/cold grading that today is smuggled into a
note prefix (`[HOT] …`). This answers the roadmap's long-standing open
question (`plans/agent-capabilities-roadmap.md:44`) with a "yes, a field
exists". It could not be set during the spike because the partial PUT is
rejected (finding 2) and the accepted enum for `rating` is undocumented.

Resolving it needs one more short probe with a full-replace payload, trying
the plausible values. Deferred to Phase 1, and it is a small win: a real
field beats a note prefix, and it frees the notes for actual context.

## Other observations

- Auth is as the code assumes: `POST /api/v1/authentication/token` with
  `{apiKey, secretKey}` plus a `tenant` header; token at `data.accessToken`.
- Lead create returns the crmId as a bare string in `data`.
- `GET /api/v1/lead?searchText=…` returns
  `"Page size must be less than or equal to 100"` with zero items — the list
  endpoint needs an explicit `PageSize`. Not needed now that the by-id read
  is known to work.
- `GET /api/v1/lead/notes/{id}` returns **405**; notes are read from the lead
  object, not a sub-resource.
- The lead has a `communications` collection (null here) that may be the
  proper activity log. Worth a look if notes-as-a-blob keeps hurting.
- There is **no delete endpoint** on leads. Test leads need manual cleanup.

## What this changes

1. **Notes are NOT append-safe.** The bot-owned-field question in target
   picture §9.10 is answered: keep writing notes, but only via
   read-modify-write. A bot-owned custom field is not needed for notes.
2. **Add a `getLead(crmId)` to the client** — it unlocks both the fix and any
   future read-back or reconciliation.
3. **Phase 1 fix-now** gains: `appendNote` read-modify-write (high, live data
   loss), and a guard or signature fix on `updateLead` (low, latent).
4. **Phase 1 nice-to-have**: probe `rating`'s enum with a full-replace payload
   and move temperature off the note prefix.
