Skip to content

fix(gmail): make outcome scans work on the v7 sealed schema - #781

Open
YiFanWangSCU wants to merge 1 commit into
ebarti:mainfrom
YiFanWangSCU:fix/gmail-feedback-v7-job-id
Open

fix(gmail): make outcome scans work on the v7 sealed schema#781
YiFanWangSCU wants to merge 1 commit into
ebarti:mainfrom
YiFanWangSCU:fix/gmail-feedback-v7-job-id

Conversation

@YiFanWangSCU

Copy link
Copy Markdown

Closes #780

Summary

The Gmail outcome scan read application_outcomes.job_key, but the sealed v7 schema keys the feedback tables by the canonical jobs.job_id, so any scan on a canonical v7 database failed with sqlite3.OperationalError: no such column: o.job_key before Gmail auth was ever consulted.

The feedback tables' schema is now probed and each read/write adapts:

  • _outcome_anchors / _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 unchanged so the v6→v7 migration tests keep their v2.0.8/v6-shaped source tables.

Validation

  • New test_outcome_anchors_join_v7_job_id and test_v7_linked_message_writes_canonical_job_id scan a canonical v7 candidate database (create_unstamped_exact_v7_candidate) and both reproduce the reported crash on the pre-fix code (TDD red → green).
  • Ran with 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.py109 passed
  • ruff check on both changed files and git diff --check: clean

Checklist

  • External contributor commits include a DCO Signed-off-by: trailer (git commit -s).
  • I ran the relevant local checks and listed them above. Heavy CI runs automatically for ordinary same-repository pull requests and cumulative stack heads, but not public forks; maintainers run manual workflows or local validation for fork contributions after review.
  • I did not include secrets, private profile data, resumes, generated application materials, raw logs, browser profiles, SQLite databases, or local paths.

AI assistance

Developed with Claude Code (Anthropic's CLI). Every line is explainable; the full diff and all validation output were reviewed before opening.

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>
@ebarti
ebarti self-requested a review August 9, 2026 11:55
@ebarti ebarti self-assigned this Aug 9, 2026

@ebarti ebarti left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:

  1. 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.
  2. 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.
  3. 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"

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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"

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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),

@ebarti ebarti Aug 9, 2026

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

bug(gmail): outcome scan queries application_outcomes.job_key, which does not exist in the v7 schema

2 participants