Skip to content

Object-valued properties are silently dropped on non-task types #2430

Closed

Description

@malibio

What happens

Setting an object-valued property on any type other than task stores it where every read path's flattener will never look, so the field vanishes from output. Not mangled, not null — absent, with no error.

nodespace node create --type venue --property address='{"city":"Berlin"}'
nodespace node get --json <id>   # address is gone

references/cli.md advertises object values as supported input ("Values are parsed as JSON when possible (numbers, booleans, arrays, objects)"), so this is a documented input shape that silently loses data.

Mechanism

create_node (packages/core/src/services/node_service/crud.rs:99) resolves schema fields only for task; every other type normalizes with schema_fields: None.

With an empty schema_field_names, normalize_flat_properties_to_namespace (crud.rs:1704) classifies any object value as a namespace rather than a field:

if value.is_object() && !schema_field_names.contains(key.as_str()) {
    // treated as a (dormant) namespace and hoisted out of the type key

The property lands beside properties[node_type] instead of inside it. flatten_namespaced_properties then reads only the type's own namespace, so the field is dropped.

The codebase already documents this hazard verbatim at packages/agent/src/skill_pipeline.rs:482:

the normalizer misclassifies parameter_schema (an object) as a dormant namespace, hoisting it out of the "tool" key so flatten_properties_for_api later silently drops it.

Seed templates work around it by pre-namespacing. A user typing --property has no such escape hatch.

Scope

No built-in type loses data today: tool pre-namespaces, and schema's fields is an array (which survives the !is_object() filter). The exposure is latent, firing on any user-defined object field.

Why this needs a design decision, not a one-line fix

The obvious fix — resolve schema fields for all types — is explicitly rejected by a measured performance constraint at crud.rs:91:

Only fetch schema from DB for types with meaningful schema fields. Currently only "task" has schema-defined fields; text, date, etc. have no fields. This avoids a ~760ms database lookup for every node creation.

That comment's premise is now stale (user-defined schemas have fields too), but simply removing the gate would reintroduce a ~760ms regression on every node create. A real fix needs one of:

  • a schema cache, so the lookup isn't per-create;
  • narrowing the lookup to creates that actually carry an object-valued property;
  • or distinguishing field-vs-namespace without a schema at all (e.g. an explicit namespace marker at rest).

Each needs fresh measurement against the 760ms claim, which predates the current store.

Acceptance criteria

  • An object-valued property set on a non-task type round-trips through node get, query, and relationship get
  • No per-create schema lookup regression — measure against the ~760ms figure in crud.rs:91 and record the result (that number predates the current store; re-measure rather than trusting it)
  • skill_pipeline.rs's pre-namespacing workaround is removed if the fix makes it unnecessary, or its comment updated to point here if not
  • The cli.md caveat added by the flat-CLI-output change is removed once object values round-trip
  • Covers a user-defined type with an object field end-to-end, not just a unit test of the normalizer

Context

Found during code review of the PR that made CLI read output flat. That change is what makes this user-visible: the nesting used to be observable in raw output, so a dropped field was at least inspectable. Now output is authoritative and the loss is invisible, which is why that PR added an explicit docs caveat rather than claiming completeness.

Activity

  1. malibio commented on Sep 7, 2026

    @malibio
    CollaboratorAuthor

    Correction to the scope above: the gap is wider than filed. It affects node update for every type, task included — not just create on non-task types.

    Both update call sites pass None for schema fields regardless of node type:

    // crud.rs:649 and again at :779
    let normalized_properties = Self::normalize_flat_properties_to_namespace(
        &updated.node_type,
        &properties,
        None, // Schema fields are fetched later if needed
    );

    With None, schema_field_names is empty, so the classifier at crud.rs:1704 hoists any object value out of the type namespace for task as well. It is CLI-reachable: parse_property JSON-parses the value, so nodespace node update <task-id> --property billing='{"city":"Berlin"}' hits it.

    Two consequences for the acceptance criteria:

    • The fix needs to cover update_node (both sites), not only create_node. The 760ms performance constraint documented at crud.rs:91 is about create; whether it applies to update needs its own measurement.
    • Add: an object-valued property set via node update on a task round-trips.

    Found during re-review of the flat-CLI-output PR — the docs caveat there had incorrectly named task as safe and has been corrected to match the real scope.

  2. self-assigned this
    on Sep 7, 2026
  3. malibio commented on Sep 7, 2026

    @malibio
    CollaboratorAuthor

    Baseline — Frontend: 233 test files passed, 4900 tests passed (Duration 83.60s)

  4. malibio commented on Sep 7, 2026

    @malibio
    CollaboratorAuthor

    Measured the schema lookup against the current store before designing around the ~760ms figure in crud.rs:91. get_schema is get_node(node_type) — one primary-key read. 500 iterations after warmup, 500 filler rows present: release 0.011ms/call (hit) and 0.003ms (miss); debug 0.019ms (hit) and 0.010ms (miss). That is 40,000-230,000x below the number in the comment, so the gate had nothing to protect. Fix removes the ambiguity rather than paying the lookup: classification now keys off the property name's underscore prefix, not the value's JSON type, so normalization needs no schema at all. Final: frontend 233 files / 4900 tests (unchanged vs baseline), full test:all green.

  5. added a commit that references this issue on Sep 7, 2026
    2223bb1
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

backendbugSomething isn't working

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions