Skip to content

UP-5022: link tasks against pre-consolidation platforms - #2272

Draft
claude[bot] wants to merge 4 commits into
devfrom
claude/up-5022-engine-platform-dataset-compat
Draft

claude[bot] wants to merge 4 commits into
devfrom
claude/up-5022-engine-platform-dataset-compat

Conversation

@claude

@claude claude Bot commented Sep 11, 2026

Copy link
Copy Markdown
Contributor

Requested by Tal Erez · Slack thread

Before: with the latest engine against a pre-consolidation platform, linking or creating a task fails. The engine creates two datasets for the task (traces and guardrails) and then posts one model linked to both; the older platform requires an Arthur shield model to be linked to exactly one dataset, so it rejects the model create call with a 400. The engine rolls both datasets back and the job errors out. The practical consequence is that a customer cannot take an engine upgrade without upgrading the platform at the same time.

After: the engine always attempts the consolidated two-dataset shape and, when the platform rejects it with the one-dataset constraint, creates the single-dataset form that platform accepts. Task creation and linking succeed against both old and new platforms. There is no change in behaviour against a consolidated platform: the two-dataset attempt is the only attempt, with the same requests, in the same order, with the same payloads.

How: try-then-fallback, with no version probe. The engine posts the two datasets and the model linked to both. If that model create comes back as a 400 carrying the platform's must be linked to exactly one dataset constraint, the attempt's own rollback deletes the datasets it created, and the engine retries with the single-dataset shape: one dataset, named after the task with no suffix the way it was pre-consolidation, and a model linked only to that. On the create path the raw job spec still carries task_type, which says exactly which dataset the task used to get, so that path reproduces the old shape rather than guessing. Neither the rejection path nor a failed retry leaves orphaned datasets. The retry is logged with the platform's own rejection text and status, so the reason is visible in the job log — on an old platform this fallback is the normal route, not an exception. A 400 that is not the one-dataset constraint is not retried. The cost of dropping the probe is one rejected request on genuinely old platforms.

Open decisions

  • Which dataset the link path keeps. On the link path the original task kind cannot be inferred — CreateModelLinkTaskJobSpec carries only job_type, task_id, connector_id and onboarding_identifier, with no field for the task kind, and TaskResponse.is_agentic is now always True — so the fallback dataset type sits behind the module-level constant LEGACY_SINGLE_DATASET_FALLBACK_PROBLEM_TYPE in task_management_job_executors.py, defaulting to the guardrails dataset (ARTHUR_SHIELD). Guardrails or traces is a product decision and is still open. Flipping it to AGENTIC_TRACE is a one-line change to that constant. Review evidence for the decision: linking an agentic task with 724 traces against an old platform made it guardrails-only, so none of those traces were visible until the platform is upgraded (the traces themselves are retained in GenAI). Review also established that on pre-consolidation platforms the UI's link-existing flow asks the user to choose a kind, but that choice is not carried on the link request and so never reaches the engine — which makes this a three-way choice (default to traces, infer the kind from the task, or keep guardrails and accept the gap) rather than a simple default flip.

Resolved during review

  • The numeric cutoff was removed. The boundary was first written as 1.4.2592, then corrected in review to 1.4.2594 — the earliest release tag shipping the consolidation commit 18896423, since scope/app_plane/app/operators/models_operator.py is byte-for-byte identical at 1.4.2591-release, 1.4.2592-release and 1.4.2593-release, all three still carrying the must be linked to exactly one dataset guard at lines 390-395. Review then showed that no numeric cutoff can be sound at all. arthur-scope CI sets RELEASE_TAG from the build's git tag stripping only a trailing -release (.gitlab-ci.yml, set-release-tag), and that value is what /api/health reports (scope/app_plane/Dockerfilescope/app_plane/app/api_models/health.py), so a deploy build reports a string like 1.4.2592-ab71a845-deploy-gcp-united, which the parser reduced to 1.4.2592. Deploy tags are cut off the same version number as the release preceding them: 1.4.2592-ab71a845-deploy-gcp-united and 1.4.2592-283bf583-deploy-gcp-united both contain 18896423, while 1.4.2592-release does not. One version number therefore names platforms of both kinds. The customer's own deploy-gcp-united builds are exactly that case, and under the cutoff would have been sent the single-dataset shape despite accepting two, leaving the task with one of its two datasets until the platform's own migration ran. The probe, the version parser, the cutoff constant and the boundary tests that pinned 2593/2594 are all removed in favour of try-then-fallback.

  • Retry log line. The fallback's warning carries the platform's status and its own rejection text, read from the response body only (never str(exc), which also renders response headers), degrading through data.detail → JSON detail → raw body → a fixed message, whitespace-collapsed and truncated at 500 characters. Confirmed working in review against a real pre-consolidation platform.

  • Build numbers. App-plane 75039 is 1.4.2594-release and 75411 is 1.4.2603-release (from scope-version-manifest.json at those tags), answered in review. Informational — no longer load-bearing now that no version number is compared.

🤖 Generated with Claude Code

https://claude.ai/code/session_019EPaWvFdzEc4AejyuJV3yv


Generated by Claude Code

The engine creates two datasets per task (traces + guardrails) and posts one
model linked to both. A platform older than 1.4.2592 requires an Arthur shield
model to be linked to exactly one dataset and rejects that with a 400, so both
create_task and link_task fail against it.

Probe the platform's release version via GET /api/health once per job and emit
the legacy single-dataset shape below the boundary, named after the task with
no suffix the way it was pre-consolidation. Because release_version is
environment-sourced and can be "unknown" or unreachable, an unreadable version
still attempts the current shape and falls back when the platform answers with
the one-dataset constraint; the rejected attempt's rollback clears its own
datasets, so neither the retry nor a failed retry leaves orphans.

On the create path the raw job spec still carries task_type, which says which
dataset the task used to get. The link path has no such signal and the task's
original kind is no longer recoverable, so that fallback sits behind
LEGACY_SINGLE_DATASET_FALLBACK_PROBLEM_TYPE pending a product decision.

Against 1.4.2592+ the two-dataset path is unchanged.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019EPaWvFdzEc4AejyuJV3yv
Review follow-ups on the compatibility shim. No change to the behaviour
against 1.4.2592 and later.

- _parse_platform_release_version accepted digits that int() then rejects.
  str.isdigit() is true for characters like superscript digits, so a version
  string containing one passed the guard and raised ValueError out of the
  probe instead of reading as unknown. isdecimal() is the predicate that
  matches what int() accepts.
- The legacy spec lookup raised a bare StopIteration if
  LEGACY_SINGLE_DATASET_FALLBACK_PROBLEM_TYPE named a problem type no task
  dataset uses. That constant is meant to be flipped, so say what is wrong.
- The probe-says-old path logged nothing. It is the path that creates a task
  without its complementary dataset, and the job log is the only place that
  is visible, so warn with the chosen dataset type.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019EPaWvFdzEc4AejyuJV3yv
Comment thread ml-engine/src/ml_engine/job_executors/task_management_job_executors.py Outdated
Comment thread ml-engine/tests/unit/job_executors/test_task_management_job_executors.py Outdated
# the task's original kind is unrecoverable. PENDING PRODUCT DECISION (UP-5022) —
# ARTHUR_SHIELD keeps guardrails, AGENTIC_TRACE would keep traces and evals instead.
# Flip this one value to change it.
LEGACY_SINGLE_DATASET_FALLBACK_PROBLEM_TYPE = ModelProblemType.ARTHUR_SHIELD

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

When I linked an agentic task with 724 traces against old Scope, this default made it guardrails-only, so none of its traces showed up. The traces are still in GenAI, and a platform upgrade to 1.4.2594 adds the traces dataset back. Is that OK for United until then, or should linking default to traces?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This is useful evidence — a concrete task with 724 traces going guardrails-only is exactly what the open decision needed, and I've recorded it in the PR body under "Which dataset the link path keeps".

I'm not changing the default here. It sits behind the single documented constant LEGACY_SINGLE_DATASET_FALLBACK_PROBLEM_TYPE, so switching to AGENTIC_TRACE is a one-line change whenever the call is made. The decision is the PR author's and has been put to them with your evidence attached.

Leaving this thread open until they answer.


Generated by Claude Code

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

More context for this decision. On pre-consolidation platforms, the link-existing flow in the platform UI lists each task twice, as " - traces" and " - guardrails", so the user explicitly chooses a kind. But the link request carries only task_id (unify-frontend useCreateGenAiModel.ts, calling projectCreateModelLinkTask), and CreateModelLinkTaskJobSpec has no field for it. So the user's choice never reaches the engine, and this constant overrides it every time.

In practice: a user who links an agent task and picks "traces" gets a guardrails-only model. Every later fetch or metrics job reads only inferences, so the task's traces never appear on that platform until it's upgraded.

Options:
(a) default to AGENTIC_TRACE, since most tasks linked after consolidation are agent tasks;
(b) infer the kind from the task itself, e.g. no guardrail rules but traces present → traces;
(c) keep guardrails and accept the gap until the platform upgrades.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This changes the shape of the decision, and I've checked the part of it that sits in this repo.

Confirmed: CreateModelLinkTaskJobSpec has no field for the chosen kind. In arthur_common/models/task_job_specs.py it is exactly job_type, task_id, connector_id, onboarding_identifier, and the generated binding agrees — arthur_client/api_bindings/models/create_model_link_task_job_spec.py declares __properties = ["job_type", "task_id", "connector_id", "onboarding_identifier"]. Compare CreateModelTaskJobSpec on the create path, which does still carry the legacy task_type the engine reads. So the engine has no channel through which the user's selection could arrive, and LEGACY_SINGLE_DATASET_FALLBACK_PROBLEM_TYPE is necessarily what decides it. I didn't go into the frontend repo, so the UI half of your description — the duplicated traces/guardrails entries and the hook sending only the task id — I'm taking as reported rather than confirming.

That makes this a three-way choice rather than a default to flip. Your (a) and (c) are one-line changes to the constant; (b) is a different thing — a behaviour change, needing a signal the engine can actually read (task rules and traces off the task itself, not the job spec) and its own correctness discussion about what happens when the signal is ambiguous. Worth separating from (a)/(c) when the call is made.

Still not mine to decide, and unchanged in this push. The option set and your worked example (link an agent task, pick traces, get a guardrails-only model whose traces stay invisible until upgrade) are recorded in the PR body and have been relayed to the PR author. Leaving this thread open.


Generated by Claude Code

Comment thread ml-engine/src/ml_engine/job_executors/task_management_job_executors.py Outdated
@kc961163

Copy link
Copy Markdown
Contributor

Tested against old Scope (1.4.2593, and with the reported version set to 1.4.2582, 1.4.2591 and "unknown"). Creating and linking apps works and leaves no leftover datasets. Not tested: creating a new agentic app, because the current UI no longer sends the agentic flag. Main asks: change the cutoff to 1.4.2594, add a boundary test, and decide guardrails vs traces for linking.

The consolidation commit 18896423 first appears in version tag 1.4.2592, but
the earliest release tag that ships it is 1.4.2594-release. Verified against
arthur-scope: scope/app_plane/app/operators/models_operator.py is byte-for-byte
identical at 1.4.2591-release, 1.4.2592-release and 1.4.2593-release, all three
still carrying the "must be linked to exactly one dataset" guard at lines
390-395, and the guard is first absent at 1.4.2594-release.

At the old 2592 cutoff a real 1.4.2593 platform read as consolidated, so the
engine sent two datasets, the platform rejected them and the task was only
created via the 400 fallback's rollback and retry.

- cutoff constant moved to (1, 4, 2594), with the release-vs-version-tag
  distinction recorded in its comment
- new parametrized test pins the exact boundary one patch apart: 1.4.2593 must
  take the single-dataset path straight from the version probe, with no
  rejection and no retry, and 1.4.2594 must keep the two-dataset path. Moving
  the constant in either direction fails it
- the fallback's retry log line now carries the platform's own rejection text
  and status, read from the response body only, degrading gracefully when the
  body is absent, not JSON or not a mapping

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019EPaWvFdzEc4AejyuJV3yv
@coderabbitai

coderabbitai Bot commented Sep 22, 2026

Copy link
Copy Markdown

Important

Draft PR not reviewed

Draft PRs are not automatically reviewed by default.

  • Trigger a manual review

To automatically review draft PRs, update your CodeRabbit configuration:

reviews:
  auto_review:
    drafts: true

Comment @coderabbitai help to get the list of available commands.

@kc961163

Copy link
Copy Markdown
Contributor

Re-tested 855a507 end to end against a pre-consolidation Scope release (1.4.2593), driven from that release's own UI:

Create, agentic: a single traces dataset via the version probe, no retry. Traces are ingested; metrics and alert jobs complete; the dashboard shows the data.
Create, GenAI (guardrails): a single guardrails dataset. Inferences are ingested and shown.
Link an existing task: onboards with a single guardrails dataset (see the open decision on the default).
Unreadable version: two datasets tried, rejected, rolled back, retried with one; the log states the platform's reason.
No leftover datasets in any case.

Open items:

Deploy-tag version strings make the numeric cutoff ambiguous around 1.4.2592–1.4.2593 (details in the cutoff thread).
The link-path default overrides the user's explicit traces/guardrails choice (details in the default thread).

The /api/health version cutoff cannot be made correct. arthur-scope CI
sets RELEASE_TAG from the build's git tag with only a trailing "-release"
stripped (.gitlab-ci.yml, set-release-tag), and that value is what
/api/health reports (scope/app_plane/Dockerfile -> app/api_models/health.py),
so a deploy build reports e.g. "1.4.2592-ab71a845-deploy-gcp-united".
Deploy tags are cut off the same version number as the release preceding
them: 1.4.2592-ab71a845-deploy-gcp-united contains the consolidation
commit 18896423, while 1.4.2592-release does not. One version number
therefore names platforms of both kinds and no numeric cutoff can be
right for both - the customer's own deploy-gcp-united builds are exactly
this case, and would have been sent the single-dataset shape despite
accepting two, leaving the task half-configured until the platform's own
migration ran.

Always attempt the consolidated two-dataset shape and let the platform's
400 select the legacy shape. Against a current platform the requests are
unchanged; against a pre-consolidation platform the cost is one rejected
attempt, whose datasets are rolled back before the retry.

Removes the health probe, the version parser, the cutoff constant and the
boundary tests that pinned 2593/2594. The fallback dataset-type constant
and its pending product decision are unchanged.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019EPaWvFdzEc4AejyuJV3yv

This branch has not been deployed

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

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants