log_planning_mistake writes TENANT lessons only — a global lesson is created by migration, never by a planning run
Decided (Yue, 2026-08-21): the tool stays, narrowed to tenant. The global library is curated and arrives only by migration.
Today the planner chooses the scope:
name: 'log_planning_mistake',
parameters: { title, body, scope: enum ['global','tenant'], promptHint }
required: ['title', 'body', 'scope']
global means aiProjectId: null — read by every tenant. So a planning run inside one customer's project can write a row that is injected into another customer's plans, un-deduped (the near-dup guard is tenant-only) and unreviewed, with a sanitization prompt as the only control on what crosses the boundary.
A tenant write has none of those properties. The row is aiProjectId = <that project>, and listForInjection scopes reads to scope = 'global' OR aiProjectId = <caller>, so it never leaves the project that produced it. The near-duplicate guard already covers it. That loop is worth keeping: a customer's own recurring mistakes reinforcing their own store is exactly what the live arm was built for, and a migration in our repository can never add a row to one customer's project.
Remove scope from the tool rather than validating it. A required enum whose only legal value is tenant is an invitation to re-add the other one; a tool with no scope parameter cannot express the global write at all. createFromBase stays — reachable from the seed and migration path, unreachable from a planning run.
⚠️ WRITE is not REINFORCE — do not close both doors
"Global lessons always added by a migration script" governs creation. It must not be implemented as "a planning run never touches a global row."
A runtime recurrence must still be able to bump a global lesson's lastOccurredAt and re-enable it — that is MOTIR-3323, and without it the entire curated base corpus goes invisible 90 days after seeding with no path back. Creation and reinforcement are different operations on the same row, and only the first is restricted.
And a MISS on a project-agnostic pattern does not become a tenant lesson. Writing a general lesson into one customer's store to route around the restriction is the failure mode here — it helps one project and hides the pattern from every other. The bug records that a global lesson is owed, and the curated path ships it.
Acceptance criteria
- The tool no longer accepts a scope; a planning run can create only a
tenantlesson, asserted. createFromBaseis unreachable from any planning-run path, asserted; it remains reachable from the seed/migration path.- A runtime recurrence can still reinforce a GLOBAL lesson — clock bumped,
enabledre-asserted, no row created — asserted explicitly so the narrowing does not take the revival with it. - A tenant lesson is readable only by its own project, asserted against the scope clause.
- The instruction that names the tool is updated in the same change — no instruction may still describe a
globaloption. - When the mistake is project-agnostic, the planning bug records that a global lesson is owed; it is not written as a tenant lesson instead.
- Existing global rows are untouched; this changes what can be created, not what exists.
Context refs
motir-aisrc/llm/treeGeneration.ts— the tool definition, its executor, and the instruction namingscope.motir-aisrc/services/lessonService.ts—captureMistake's scope branch,createFromBase,createFromCorrection, the tenant near-dup guard.motir-aisrc/repositories/lessonRepository.ts— thescope = 'global' OR aiProjectId = …read clause that makes a tenant write safe.- MOTIR-3323 — global REINFORCEMENT, which this must not break.
- MOTIR-3326 — the MISS that records a global lesson as owed.
- MOTIR-3307 — the generator that is the only global write path.