Skip to content

Commit c5c4394

Browse files
markmhendricksonateles-agentclaude
authored
fix(cli): align observation_source enum with API + document v0.17→v0.18 breaking change (#1841) (#1851)
* fix(cli): align observation_source enum with API + document v0.17→v0.18 breaking change (#1841) `observation_source` was tightened from arbitrary strings (v0.17) to a closed enum in v0.18.0, but the change shipped undocumented and the CLI enum drifted from the API. - CLI/API parity: add the missing `sync` value to OBSERVATION_SOURCE_CLI_VALUES so the CLI accepts exactly what the API and openapi.yaml accept (sensor, llm_summary, workflow_state, human, import, sync). - Improve the "Invalid --observation-source" error to list the valid values and tell users to put custom v0.17 labels in the free-form `data_source` field. - Update all three `--observation-source` CLI help strings (store, ingest, snapshots export) to include `sync` + a migration note. - Docs: CLI reference enum + migration link; new "Migrating observation_source from v0.17 to v0.18" subsection in fleet_onboarding.md with a custom-string → enum + data_source mapping table. - Release: document the retroactive breaking change in the v0.18.3 release supplement (Breaking changes + Fixes #1841). - Mark the enum as an intentional v0.17 breaking change in action_schemas.ts. - Test: tests/unit/cli_observation_source_flag.test.ts asserts CLI↔API enum parity, that `sync` is accepted, and that an invalid value's error carries the data_source migration hint. No back-compat shim (operator verdict P2: document, don't widen). Closes #1841 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * chore(catalog): regenerate automated test catalog for #1841 enum/migration tests Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: ateles-agent <agent@ateles.ai> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 1289cc1 commit c5c4394

7 files changed

Lines changed: 127 additions & 10 deletions

File tree

docs/developer/cli_reference.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -693,6 +693,7 @@ Cross-instance peer sync. Backed by `src/services/sync/` and the HTTP `/peers` s
693693
- Use `--json=` (equals, no space) so the payload is parsed as entities input.
694694
- Bare `--json` (without `=`) remains the global output-format flag.
695695
- `--file <path>`: Path to JSON file containing entity array. Use for long payloads.
696+
- `--observation-source <kind>`: Classify the *kind* of write. Closed enum — `sensor | llm_summary | workflow_state | human | import | sync` (default `llm_summary`). **Breaking change in v0.18.x (was open in v0.17):** arbitrary strings are rejected. Put custom v0.17 labels (e.g. `cboe_live`, `stale_cache`) in the free-form `data_source` field and map the closest enum value here. See [Migrating from v0.17 to v0.18](fleet_onboarding.md#migrating-observation_source-from-v017-to-v018).
696697
- `--interpretation-source-id <id>` / `--interpretation-source-ref <structured|unstructured>` plus `--interpretation-config <json>`: Opt into Source -> Interpretation -> Observation provenance for source-derived extraction. Omit for ordinary already-structured/chat-native facts.
697698
- Silent-failure guard (v0.5.1+): when a commit-mode store returns `entities_created=0`, no resolved entities, and is not an idempotency replay (`replayed: true`), the CLI emits a non-fatal stderr warning so agents/humans don't mistake an empty result for success. Typical root cause: fields nested under `attributes` (see v0.5.0 breaking change) or mismatched `user_id` scope.
698699
- Idempotency replays: `POST /store` returns `replayed: true` when a request is a deterministic re-commit of a previously committed `(user_id, idempotency_key)` tuple. Use this to distinguish "same call, no new work" from "call produced zero entities."
@@ -951,7 +952,7 @@ Fleet-general write-integrity tooling built on the canonical snapshot layer. `sn
951952
- `--entity-types <csv>`: Restrict to comma-separated entity types (e.g. `agent_task,agent_attempt`).
952953
- `--agent-sub <sub>`: Restrict to a single AAuth `agent_sub`.
953954
- `--attribution-tier <tier>`: Restrict to one tier — `hardware | software | unverified_client | anonymous`.
954-
- `--observation-source <kind>`: Restrict to one write kind — `sensor | llm_summary | workflow_state | human | import`.
955+
- `--observation-source <kind>`: Restrict to one write kind — `sensor | llm_summary | workflow_state | human | import | sync`.
955956
- `--since <iso>`: ISO-8601 lower bound on `last_observation_at`.
956957
- `--limit <n>`: Cap on returned entities (default: 500).
957958
- `--out <path>`: Write the export to this file instead of stdout.

docs/developer/fleet_onboarding.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,9 @@ Set `observation_source` on every structured write. The value is orthogonal to A
4141
| `llm_summary` | LLM-authored summaries / extractions. **Default** when `observation_source` is unspecified. |
4242
| `human` | Direct human edits, acceptances, corrections. |
4343
| `import` | Batch / ETL ingestion. Ranked last by default because provenance is typically second-hand. |
44+
| `sync` | Writes replayed from a peer during fleet sync (Phase 5 loop prevention). Set automatically by the sync path; rarely supplied by hand. |
45+
46+
`sensor`, `llm_summary`, `workflow_state`, `human`, `import`, and `sync` are the **only** accepted values — this is a closed enum (see the v0.17→v0.18 migration note below).
4447

4548
Reducer tie-breaking proceeds `source_priority → observation_source_priority → observed_at`. Per-schema overrides (e.g. `agent_sensor_signal` locks `observation_source_priority` so sensor rows always win) live on `SchemaDefinition.reducer_config.observation_source_priority`.
4649

@@ -69,6 +72,26 @@ neotoma store \
6972

7073
See the applied rules in [`.cursor/rules/neotoma_cli.mdc`](../../.cursor/rules/neotoma_cli.mdc) (rule 14a) or [`docs/developer/mcp/instructions.md`](./mcp/instructions.md) for the agent-instruction wording.
7174

75+
### Migrating `observation_source` from v0.17 to v0.18
76+
77+
**Breaking change (v0.18.x, [#1841](https://github.com/markmhendrickson/neotoma/issues/1841)).** In v0.17, `observation_source` (and the `--observation-source` CLI flag) accepted arbitrary strings — e.g. `cboe_live`, `stale_cache`, `computed_greeks`. v0.18 restricts it to the closed enum above. Stores and `--observation-source` flags carrying a custom string now fail with:
78+
79+
```
80+
Invalid --observation-source "cboe_live". Expected one of: sensor, llm_summary, workflow_state, human, import, sync. Custom v0.17 observation_source labels are no longer accepted — put them in the free-form "data_source" field instead.
81+
```
82+
83+
`observation_source` is a *fixed classification* of the kind of write (it drives reducer tie-breaking); the descriptive label of *where* a value came from belongs in the free-form `data_source` field, which remains unconstrained. To migrate, map each custom string to the closest enum value and move the original label into `data_source`:
84+
85+
| v0.17 custom string (examples) | v0.18 `observation_source` | `data_source` (free-form) |
86+
| ------------------------------ | -------------------------- | ------------------------- |
87+
| `cboe_live`, `*_live`, market/exchange feeds, webhooks, telemetry | `sensor` | `cboe_live` |
88+
| `computed_greeks`, derived/calculated state, state-machine output | `workflow_state` | `computed_greeks` |
89+
| `gpt_summary`, model-authored extractions / summaries | `llm_summary` | `gpt_summary` |
90+
| `analyst_note`, manual edits / acceptances | `human` | `analyst_note` |
91+
| `stale_cache`, `nightly_etl`, batch / backfill ingestion | `import` | `stale_cache` |
92+
93+
If no enum value fits the *kind* of write, default to `import` (second-hand provenance) or `llm_summary` (the system default) and preserve the original string in `data_source`. There is no back-compat shim — the enum is enforced at the parse boundary on both the CLI and the API.
94+
7295
## 3. Use the fleet-general `agent_*` schemas
7396

7497
Neotoma ships a v0.1 "lowest common denominator" schema set in the `agent_runtime` category. Store runtime data against these types so your drift reports, timeline views, and Inspector filters work without fleet-specific migrations:

docs/releases/in_progress/v0.18.3/github_release_supplement.md

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ No OpenAPI changes. `source_storage` is an existing `StoreRequest` field; this r
3737
- **#1826**`source_storage: "reference"` was unreachable over HTTP `POST /store`. The OpenAPI contract accepted it (v0.18.1) but `handleStorePost` still stored inline. The REST handler now honors reference mode end to end.
3838
- **#1827 (REST extension)** — the combined `entities[] + file + source_storage:reference` call returned a 500 on `observations.id` UNIQUE collision because the REST structured leg lacked the existing-observation guard that the MCP path has. Brought to parity.
3939
- **#1837** — the Inspector graph viewer fired `retrieve_graph_neighborhood` in an unbounded loop and never painted. Root cause was a global `refetchInterval: 5000`, not unbounded traversal. Polling disabled on the graph hooks; canvas height fixed.
40+
- **#1841** — the v0.18.0 `observation_source` enum restriction was undocumented, and the CLI enum had drifted from the API (missing `sync`). Documented the breaking change + migration mapping, aligned the CLI enum and error message to the API, no back-compat shim. See *Breaking changes* above.
4041

4142
## Tests and validation
4243

@@ -52,4 +53,16 @@ Security review artifact: [docs/releases/in_progress/v0.18.3/security_review.md]
5253

5354
## Breaking changes
5455

55-
No breaking changes. No OpenAPI tightening, no request-shape changes, no field promotions or removals. Both behavior changes (reference stores honored, duplicate observations replayed) are strict improvements aligning REST with MCP. Patch bump is correct per SemVer.
56+
This patch introduces no *new* breaking changes. No OpenAPI tightening, no request-shape changes, no field promotions or removals. Both behavior changes (reference stores honored, duplicate observations replayed) are strict improvements aligning REST with MCP. Patch bump is correct per SemVer.
57+
58+
### Retroactively documented: `observation_source` closed enum (since v0.18.0)
59+
60+
[#1841](https://github.com/markmhendrickson/neotoma/issues/1841) surfaced a v0.18.0 breaking change that shipped **undocumented**: `observation_source` was tightened from arbitrary strings (v0.17) to a closed enum — `sensor`, `llm_summary`, `workflow_state`, `human`, `import`, `sync`. Stores carrying a custom string (e.g. `cboe_live`, `stale_cache`) now hard-fail at the parse boundary on both the CLI and the API. There is no back-compat shim (operator verdict P2: document, don't widen).
61+
62+
This release does **not** change that enforcement; it documents and de-drifts it:
63+
64+
- The CLI `--observation-source` enum now matches the API/OpenAPI exactly (added the missing `sync`); previously the CLI rejected a value the API accepts.
65+
- The invalid-value error message now tells users to put custom v0.17 labels in the free-form `data_source` field.
66+
- A migration guide (custom v0.17 strings → closest enum + `data_source`) was added to [`docs/developer/fleet_onboarding.md`](../../developer/fleet_onboarding.md#migrating-observation_source-from-v017-to-v018) and the [CLI reference](../../developer/cli_reference.md).
67+
68+
**If you upgraded from v0.17 and used custom `observation_source` strings:** map each to the closest enum value and move the original label to `data_source`. See the migration guide for the mapping table.

docs/testing/automated_test_catalog.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ flowchart TD
6969
### Suite counts
7070
| Suite | Files |
7171
|---|---:|
72-
| Vitest unit tests | 145 |
72+
| Vitest unit tests | 146 |
7373
| Vitest service tests | 35 |
7474
| Source-adjacent tests | 64 |
7575
| Vitest integration tests | 148 |
@@ -109,7 +109,7 @@ flowchart TD
109109
**Runner:** `vitest`
110110
**Command:** `npm test -- tests/unit`
111111
**Requirements:** Basic `.env` if required by the module under test.
112-
**Files (145):**
112+
**Files (146):**
113113
- `tests/unit/aauth_admission.test.ts`
114114
- `tests/unit/aauth_attestation_apple_se.test.ts`
115115
- `tests/unit/aauth_attestation_revocation.test.ts`
@@ -142,6 +142,7 @@ flowchart TD
142142
- `tests/unit/cli_aauth_tpm2_attestation.test.ts`
143143
- `tests/unit/cli_aauth_yubikey_attestation.test.ts`
144144
- `tests/unit/cli_bug_fixes.test.ts`
145+
- `tests/unit/cli_observation_source_flag.test.ts`
145146
- `tests/unit/cli_schema_register_reducer_config.test.ts`
146147
- `tests/unit/client_diagnose.test.ts`
147148
- `tests/unit/client_helpers.test.ts`

src/cli/index.ts

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -132,21 +132,29 @@ function inferMimeTypeForPath(filePath: string): string {
132132
return getMimeTypeFromExtension(ext) ?? "application/octet-stream";
133133
}
134134

135-
const OBSERVATION_SOURCE_CLI_VALUES = [
135+
// Must stay in lockstep with OBSERVATION_SOURCE_VALUES in
136+
// src/shared/action_schemas.ts and the `observation_source` enum in
137+
// openapi.yaml. `sync` is API-accepted (Phase 5 peer-sync loop prevention),
138+
// so the CLI must accept it too. This enum was tightened from arbitrary
139+
// strings in v0.17 — an intentional breaking change (see issue #1841);
140+
// custom v0.17 labels now belong in the free-form `data_source` field.
141+
export const OBSERVATION_SOURCE_CLI_VALUES = [
136142
"sensor",
137143
"llm_summary",
138144
"workflow_state",
139145
"human",
140146
"import",
147+
"sync",
141148
] as const;
142149

143150
type ObservationSourceFlag = (typeof OBSERVATION_SOURCE_CLI_VALUES)[number];
144151

145-
function normalizeObservationSourceFlag(raw: string): ObservationSourceFlag {
152+
export function normalizeObservationSourceFlag(raw: string): ObservationSourceFlag {
146153
const normalized = raw.trim().toLowerCase().replace(/-/g, "_");
147154
if (!(OBSERVATION_SOURCE_CLI_VALUES as readonly string[]).includes(normalized)) {
148155
throw new Error(
149-
`Invalid --observation-source "${raw}". Expected one of: ${OBSERVATION_SOURCE_CLI_VALUES.join(", ")}.`
156+
`Invalid --observation-source "${raw}". Expected one of: ${OBSERVATION_SOURCE_CLI_VALUES.join(", ")}. ` +
157+
`Custom v0.17 observation_source labels are no longer accepted — put them in the free-form "data_source" field instead.`
150158
);
151159
}
152160
return normalized as ObservationSourceFlag;
@@ -14281,7 +14289,7 @@ program
1428114289
)
1428214290
.option(
1428314291
"--observation-source <kind>",
14284-
"Kind of write: sensor, llm_summary (default), workflow_state, human, or import"
14292+
"Kind of write: sensor, llm_summary (default), workflow_state, human, import, or sync (custom v0.17 values → use data_source)"
1428514293
)
1428614294
.option("--idempotency-key <key>", "Idempotency key to prevent duplicate stores")
1428714295
.option(
@@ -14534,7 +14542,7 @@ program
1453414542
)
1453514543
.option(
1453614544
"--observation-source <kind>",
14537-
"Kind of write: sensor, llm_summary (default), workflow_state, human, or import"
14545+
"Kind of write: sensor, llm_summary (default), workflow_state, human, import, or sync (custom v0.17 values → use data_source)"
1453814546
)
1453914547
.option(
1454014548
"--source-upload",
@@ -15758,7 +15766,7 @@ snapshotsCommand
1575815766
)
1575915767
.option(
1576015768
"--observation-source <kind>",
15761-
"Restrict to one write kind: sensor | llm_summary | workflow_state | human | import"
15769+
"Restrict to one write kind: sensor | llm_summary | workflow_state | human | import | sync (custom v0.17 values → use data_source)"
1576215770
)
1576315771
.option("--since <iso>", "ISO-8601 lower bound on last_observation_at")
1576415772
.option("--limit <n>", "Cap on returned entities (default: 500)")

src/shared/action_schemas.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -439,6 +439,12 @@ export const ObservationsQueryRequestSchema = z.object({
439439
* is applied by the write path, not at parse time, so MCP callers that
440440
* omit the field remain LLM-driven by construction without breaking
441441
* idempotency hashes or contract-test fixtures.
442+
*
443+
* NOTE (v0.17 → v0.18 breaking change, issue #1841): this is a CLOSED enum.
444+
* v0.17 accepted arbitrary `observation_source` strings; v0.18 restricts them
445+
* to the values below — an intentional breaking change, not a regression.
446+
* Custom v0.17 labels (e.g. `cboe_live`, `stale_cache`) now belong in the
447+
* free-form `data_source` field; map the closest enum value here.
442448
*/
443449
export const OBSERVATION_SOURCE_VALUES = [
444450
"sensor",
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
/**
2+
* Unit tests for the CLI `--observation-source` flag validator
3+
* (`normalizeObservationSourceFlag` in src/cli/index.ts).
4+
*
5+
* Issue #1841: `observation_source` was tightened from arbitrary strings
6+
* (v0.17) to a closed enum (v0.18.x). This test pins:
7+
* 1. The CLI enum matches the API/OpenAPI enum EXACTLY, including `sync`
8+
* (the value that previously drifted — CLI rejected it, API accepted it).
9+
* 2. An invalid value's error message lists the valid values AND tells the
10+
* user to put custom v0.17 labels in the `data_source` field.
11+
*/
12+
13+
import { describe, expect, it } from "vitest";
14+
15+
import {
16+
OBSERVATION_SOURCE_CLI_VALUES,
17+
normalizeObservationSourceFlag,
18+
} from "../../src/cli/index.ts";
19+
import { OBSERVATION_SOURCE_VALUES } from "../../src/shared/action_schemas.ts";
20+
21+
describe("#1841 CLI --observation-source enum", () => {
22+
it("CLI enum matches the canonical API/schema enum exactly", () => {
23+
// The canonical enum lives in action_schemas.ts and openapi.yaml.
24+
// CLI parity is the whole point of the fix — drift here re-breaks #1841.
25+
expect([...OBSERVATION_SOURCE_CLI_VALUES].sort()).toEqual(
26+
[...OBSERVATION_SOURCE_VALUES].sort()
27+
);
28+
});
29+
30+
it.each([...OBSERVATION_SOURCE_VALUES])(
31+
"accepts the API-accepted value %s",
32+
(value) => {
33+
expect(normalizeObservationSourceFlag(value)).toBe(value);
34+
}
35+
);
36+
37+
it("now accepts `sync` (was the CLI/API drift in #1841)", () => {
38+
expect(OBSERVATION_SOURCE_CLI_VALUES).toContain("sync");
39+
expect(normalizeObservationSourceFlag("sync")).toBe("sync");
40+
});
41+
42+
it("normalizes case and hyphens to the canonical underscore form", () => {
43+
expect(normalizeObservationSourceFlag("LLM-SUMMARY")).toBe("llm_summary");
44+
expect(normalizeObservationSourceFlag(" workflow-state ")).toBe(
45+
"workflow_state"
46+
);
47+
});
48+
49+
it("rejects a custom v0.17 string with a data_source migration hint", () => {
50+
let caught: Error | undefined;
51+
try {
52+
normalizeObservationSourceFlag("cboe_live");
53+
} catch (err) {
54+
caught = err as Error;
55+
}
56+
expect(caught).toBeDefined();
57+
const message = caught!.message;
58+
// Lists the value that was rejected and the valid values.
59+
expect(message).toContain('"cboe_live"');
60+
expect(message).toContain("sync");
61+
expect(message).toContain("llm_summary");
62+
// Tells the user where custom v0.17 labels now belong.
63+
expect(message).toContain("data_source");
64+
});
65+
});

0 commit comments

Comments
 (0)