fix(gmail): make outcome scans work on the v7 sealed schema - #781
fix(gmail): make outcome scans work on the v7 sealed schema#781YiFanWangSCU wants to merge 1 commit into
Conversation
The v7 sealed schema keys the feedback tables (application_outcomes, application_email_evidence, application_outcome_suggestions) by the canonical jobs.job_id instead of the legacy job_key (application URL), so any Gmail outcome scan on a canonical v7 database failed with "no such column: o.job_key" before Gmail auth was consulted. Probe the feedback and jobs tables and adapt each read/write: - _outcome_anchors and _apply_run_anchors join jobs by job_id when the v7 column is present (falling back to the legacy URL join) and expose job_key as jobs.url. - The evidence/suggestion INSERTs write the canonical job id (resolved through jobs.url) when job_id is present, else the legacy job_key. - _record_safe_event writes the v7 job_events columns (tenant_id, job_id, identity_version) when they exist. ensure_application_feedback_tables is left untouched so the v6 to v7 migration tests keep their v2.0.8/v6-shaped source tables. Regression coverage: scan a canonical v7 candidate database (outcome anchor join and linked-message writes) plus the existing v6 path. Signed-off-by: Riceff <103807108+YiFanWangSCU@users.noreply.github.com>
There was a problem hiding this comment.
Thanks for this, it is a genuinely strong first contribution. You found the exact root cause of #780, and the tests are precisely how I like regressions pinned here: seeded against the exact v7 candidate schema, asserting the canonical writes end-to-end.
The three inline comments converge on one simplification that makes your diff smaller than what you wrote: this runtime only ever opens exact-v7 databases, so instead of probing schemas and branching, the code can assume v7 unconditionally:
- Carry the canonical job_id on ApplicationAnchor (your anchor queries already join jobs, so it's one more selected column), keep the URL as a separate field used only as the Gmail-search hint, and drop _job_id_for_url entirely. Besides the identity-contract point in the inline comment, the helper hides a crash: it returns None when the URL doesn't resolve (e.g. the anchor's job was deleted mid-scan), and both application_email_evidence.job_id and application_outcome_suggestions.job_id are NOT NULL on v, so the scan would die on an IntegrityError, which is the same class of failure you're fixing.
- Scope every join and filter by (tenant_id, job_id) per the inline cross-tenant comment, that's the composite key everywhere in this schema.
- Emit the canonical event payload (jobId, no legacy jobKey alias) per the inline SSE comment, so the scan's invalidation event actually reaches open clients.
Two small things while you're in there: identity_version is hardcoded 1 with a commen, import the constant if it's exported rather than mirroring it; and don't worry about this module's pre-existing column probing (column_expr etc.) or its legacy-shaped ensure*/test, that's older debt, we'll track a full v7 cleanup of this module as a follow-up issue, it's not on you.
I'll approve the CI run on your branch after the fixes. Happy to merge once the v7-only shape lands. Thanks again; this fixes a real breakage and the test discipline made it easy to review.
| company_expr = _first_column_expr(columns, ["company", "site", "employer"], prefix="j") | ||
| application_url_expr = _column_expr(columns, "application_url", prefix="j") | ||
| job_join_expr = ( | ||
| "j.job_id = o.job_id" if outcome_job_column == "job_id" else "j.url = o.job_key" |
There was a problem hiding this comment.
Scope these v7 anchors by the full tenant identity. job_id is unique only with tenant_id, but this join uses only job_id, while _apply_run_anchors also omits an a.tenant_id filter and tenant join. With the same UUID in local and another tenant, a local outcome produced anchors for both tenants, and an other-tenant apply run did the same. Please join on both tenant_id and job_id, filter every anchor source to TENANT_ID, and add a shared-JobId cross-tenant regression.
| thread_id = _nullable_text(full_message.get("threadId") or metadata.get("threadId")) | ||
| linked_at_text = _iso(linked_at) | ||
| evidence_columns = _columns(conn, "application_email_evidence") | ||
| evidence_job_column = "job_id" if "job_id" in evidence_columns else "job_key" |
There was a problem hiding this comment.
Keep this runtime on the exact-v7 identity contract. Probing for job_id and falling back to job_key reintroduces runtime table-shape detection and internal URL resolution, both explicitly prohibited after the v7 cutover. It also leaves ApplicationAnchor.job_key as jobs.url, so the scan response emits URL-shaped jobKey values rather than canonical JobIds. Please carry canonical job_id in the anchor, keep URL as a separate Gmail-search hint, and read/write/return job_id unconditionally; v6 handling belongs only in migration fixtures.
| values: dict[str, Any] = { | ||
| "tenant_id": TENANT_ID, | ||
| "job_url": job_key, | ||
| "job_id": _job_id_for_url(conn, job_key), |
There was a problem hiding this comment.
Emit a canonical v7 event payload here. Although this adds the job_id column, the payload still contains legacy jobKey: <URL>; canonicalSseEnvelope rejects any payload with that alias, so this event is silently dropped and open clients do not invalidate after the scan. Emit canonical `jobId, preferably through the canonical event helper, and add a scanner-to-SSE regression.
Closes #780
Summary
The Gmail outcome scan read
application_outcomes.job_key, but the sealed v7 schema keys the feedback tables by the canonicaljobs.job_id, so any scan on a canonical v7 database failed withsqlite3.OperationalError: no such column: o.job_keybefore Gmail auth was ever consulted.The feedback tables' schema is now probed and each read/write adapts:
_outcome_anchors/_apply_run_anchorsjoinjobsbyjob_idwhen the v7 column is present (falling back to the legacy URL join) and exposejob_keyasjobs.url.jobs.url) whenjob_idis present, else the legacyjob_key._record_safe_eventwrites the v7job_eventscolumns (tenant_id,job_id,identity_version) when they exist.ensure_application_feedback_tablesis left unchanged so the v6→v7 migration tests keep their v2.0.8/v6-shaped source tables.Validation
test_outcome_anchors_join_v7_job_idandtest_v7_linked_message_writes_canonical_job_idscan a canonical v7 candidate database (create_unstamped_exact_v7_candidate) and both reproduce the reported crash on the pre-fix code (TDD red → green).PYTHONPATH=workers/automation/src workers/automation/.venv/bin/python -m pytest:test_gmail_feedback.py test_gmail_connector.py test_gmail_mcp_config.py test_doctor_gmail_mcp.py test_feedback_signal_reader.py test_dashboard_projection.py test_v6_to_v7_feedback_copy.py test_v6_to_v7_preflight.py test_apply_run_aggregate.py test_apply_runs_projection_from_events.py— 109 passedruff checkon both changed files andgit diff --check: cleanChecklist
Signed-off-by:trailer (git commit -s).AI assistance
Developed with Claude Code (Anthropic's CLI). Every line is explainable; the full diff and all validation output were reviewed before opening.