Libra CodeHub

CedarCopilot/cedar-mail

Branch: staging

fix(aop): resolve field ownership from subagent docs, with one canonical agent-id precedence

merged#2565CedarCopilot

CedarCopilot wants to merge 2 commits into staging from fix/field-ownership-subagent-docs

Live on prod, no production signal yetTimeline and evidence
  1. Opened
    Aug 24, 2026, 10:27 AM
  2. Aug 24, 2026, 2:17 PM
  3. Merged
    Aug 24, 2026, 2:56 PM
  4. Live on prod
    Aug 24, 2026, 2:56 PM
  5. Observed 27 hours
    Aug 24, 2026, 2:56 PM
  6. Pipelines steady after this deploy
    Aug 24, 2026, 2:56 PM
  7. Unobserved

    Live on prod, no production signal yet

    Aug 25, 2026, 6:23 PM

Behaviors Libra is checking

CRM field ownership now recognizes enabled subagent documents as live owners, so their claimed fields remain protected and are editable by the owning running subagent.Inconclusivelow confidence

OTEL fallback found no prod spans matching getFieldOwnership / buildFieldPermissions since deploy.

prod, checked Aug 25, 2026, 2:55 PM
The CRM updater is dispatched with the canonical resolved subagent agent ID rather than a frontmatter-only ID.Inconclusivelow confidence

OTEL fallback found no prod spans matching runCrmUpdaterTool since deploy.

prod, checked Aug 25, 2026, 3:55 PM
The next-steps task aggregator uses the canonical resolved subagent agent ID when loading its instructions.Inconclusivelow confidence

Verification cited only 10 matching post-deploy events, below the 20-event floor for calling a change verified. Nothing is failing; there is not yet enough traffic to confirm it.

prod, checked Aug 25, 2026, 4:55 PM
Direct playbook section subagent execution resolves agent IDs with the same metadata-first precedence as other subagent paths.Inconclusivelow confidence

OTEL fallback found no prod spans matching runPlaybookSectionExecution since deploy.

prod, checked Aug 25, 2026, 5:06 PM
Subagent execution and callers consistently use a document's metadata.agent_id as the canonical identity, falling back to frontmatter.agent_id only when metadata is absent.Inconclusivelow confidence

OTEL fallback found no prod spans matching readSubagentFull / run-subagent / runSingleSubagent since deploy.

prod, checked Aug 25, 2026, 5:06 PM
The healing job explicitly identifies empty subagent documents as requiring instruction content before an agent ID can be repaired, instead of misclassifying them as malformed frontmatter.Inconclusivelow confidence

Verification cited only 10 matching post-deploy events, below the 20-event floor for calling a change verified. Nothing is failing; there is not yet enough traffic to confirm it.

prod, checked Aug 25, 2026, 5:23 PM

Libra has verdicts on 0 of 8 tracked behaviors on prod; 8 are still being checked. Libra checks hourly for 3 days after each deploy.

Two commits. The first fixes the ownership read surface; the second removes the id-resolution divergence that made the first only conditionally correct.

1. Field ownership read from aop_agents (a table nothing writes)

getFieldOwnership resolved a field's ownerAgentId by looking it up in the legacy aop_agents table. Subagent documents replaced that table , the header of services/aop/aop-agents.ts states outright that "this file performs no aop_agents table reads" , so no doc-backed agent has a row there.

Every stamped owner therefore resolved as "deleted", and both consumers silently no-op'd: the CRM updater's exclusion set (live-editable-field.ts:25) came back empty, and write-time forbidding (updateConversationFieldsTool.ts:247) never added the field.

owner row exists in aop_agentsfieldsAOPs
no1212101
yes, enabled131

All 1225 are owned by strategist.md. So the CRM updater has been writing the_play, how_we_win, risks, on_track and the 8 MEDDPICC fields that stampStrategistOwnership explicitly handed to the Strategist.

Fix: resolve owners from getAllSubagentsForAop, the canonical listing for the document system. It merges user- and org-level docs (user shadowing org on filename) and drops any doc whose frontmatter sets enabled: false, so what it returns is the live-and-enabled set and the auto-reversion contract is preserved rather than reimplemented. The pure resolveFieldOwnership core is untouched, so its tests still apply.

Behavior change: the CRM updater stops writing those 12 strategic fields on the 102 affected AOPs, leaving the Strategist as their sole writer. That is what stampStrategistOwnership's docstring always described; it just never took effect. Nothing is orphaned , all 1225 stamped owners match a subagent doc's metadata.agent_id for the same user and AOP, including the 13 that previously resolved via aop_agents:

owned_fields | match_metadata_agent_id | match_doc_row_id | no_doc_match
        1225 |                    1225 |                0 |            0

2. Five readers, five different agent-id precedences

documents.metadata.agent_id is the canonical identity of a subagent doc: it is what playbook <ref> resolution matches on, what findSubagentByAgentId looks up, and what verify-subagent-doc.ts:87 requires to be present and equal to the frontmatter id. The readers disagreed anyway:

readerprecedence
subagents.ts, aop-agents.tsmetadata → frontmatter → document id
runSubagentTool.tsfrontmatter → sha256(userId:aopId:name)
run-single-subagent.tsfrontmatter only, hard error when absent
orchestrator-dispatch-tools.tsfrontmatter only
updateNextStepsAndTasksTool.tsfrontmatter only

So the id a field's ownerAgentId was stamped with could differ from the id its owning subagent executed under. Because buildFieldPermissions forbids a field to everyone except its owner and the CRM updater excludes owned fields, such a field ends up writable by nobody. That made commit 1 correct only for docs where both ids happen to agree.

Fix: resolveSubagentAgentId(metadata, frontmatter) is now the single place the precedence lives. readSubagentFull / readSubagentByDocId return the resolved agentId and documentId through a named SubagentDocRead, and every call site reads that instead of reaching into frontmatter. Each caller keeps its own last resort for a doc carrying neither id, so no existing execution identity moves: run-subagent keeps its stable hash, the doc listings keep the document id.

Prod impact: 3 docs, all carrying a metadata id with no matching frontmatter one; 0 docs have ids that disagree.

next-steps.md       be91650e-017f-4364-93e9-34d06707f78d
crm-updater.md      2b57f89d-1461-4fbd-a08b-c221963296dd
post-call-coach.md  post-call-coach

be91650e… already has 402 executions reco

Show production surfaces and changed-file mapping

Production surfaces

Libra has not measured any production surfaces for this change yet.

Changed files → surfaces

  • apps/server/src/db/migrations/scripts/playbook-health/heal-subagent-agent-ids.tsno production surface mapped
  • apps/server/src/mastra/tools/event-execution/orchestrator-dispatch-tools.tsno production surface mapped
  • apps/server/src/mastra/tools/event-execution/runSubagentTool.tsno production surface mapped
  • apps/server/src/mastra/tools/task/updateNextStepsAndTasksTool.tsno production surface mapped
  • apps/server/src/services/aop/aop-agents.tsno production surface mapped
  • apps/server/src/services/aop/field-ownership.tsno production surface mapped
  • apps/server/src/services/playbook/playbook-execution-triggers.tsno production surface mapped
  • apps/server/src/services/playbook/reference-resolver.tsno production surface mapped
  • apps/server/src/services/playbook/run-single-subagent.tsno production surface mapped
  • apps/server/src/services/playbook/subagents.tsno production surface mapped