Skip to content

Refuse a job code call with no step to put the result in - #676

Merged
hanna-paasivirta merged 9 commits into
mainfrom
fix/job-key-miss
Sep 9, 2026
Merged

hanna-paasivirta merged 9 commits into
mainfrom
fix/job-key-miss

Conversation

@elias-ba

@elias-ba elias-ba commented Sep 8, 2026

Copy link
Copy Markdown
Collaborator

Short Description

Two ways a code change vanished with no way for the caller to say so: a job-code call with no job_key, and the router's direct route discarding whether the edits landed.

Fixes #677

Implementation Details

The job-code tool allowed a call with no job_key, so nothing matched, the subagent still ran, and the code it wrote was dropped. It is required now, both execution paths refuse before spending the call, and a key matching nothing is answered with the workflow's real step keys so the planner can correct itself in the same turn.

Required rather than an enum of those keys because the tools block sits in the cached prefix: a per-workflow schema would re-cache it and the system prompt behind it every turn.

The router's direct route discarded job_chat's diff. It is the shortcut for single-step edits, so it was the case where a client could least tell a reply that changed something from one that changed nothing. It now reports it in the planner's shape; OpenFn/lightning#5143 surfaces it. The YAML helpers also no longer raise on a workflow that is not a mapping of jobs.

pytest services/global_chat/tests/unit/ -q — 124 pass. The acceptance suite needs a live key and is too flaky to read from.

AI Usage

Please disclose whether you've used AI in this work (it's cool, we just want to know!):

  • Yes, I have used AI
  • No, I have not used AI

You can read more details in our Responsible AI Policy

Ask the global assistant to change a step's code and sometimes nothing changes,
with a reply that talks as though something did.

A job_key that names no step already errors before any code is written. The
silent case is a call with no job_key at all, which the schema allowed: the
description says to set it, but required listed only message and attachments.
Nothing matches, the subagent still runs, writes code, and the planner drops it
because there is nowhere to stitch it.

job_key is now required, so the model cannot omit it, and both execution paths
refuse before spending the subagent call rather than after. The parallel path
had the same hole as the single one.

The errors now name the workflow's actual step keys. "Retry with the exact job
key" asks the planner to fix the thing it just got wrong with no more
information than it had the first time; naming them lets it correct itself in
the same turn.

Made job_key required rather than an enum of the real keys on purpose. An enum
would make the mistake impossible, but the tools block sits in the cached
prefix alongside a system prompt, so a schema that varies per workflow would
re-cache both every time.

One existing test asserted the old behaviour, that a keyless call generates
code and reports it was not stitched. That documents the outcome rather than
arguing for it, and it is the outcome OpenFn/lightning#5133 calls a bug, so it
now asserts the subagent is never called.
elias-ba added a commit to OpenFn/lightning that referenced this pull request Sep 8, 2026
The 5133 half only covered the planner. Apollo's router is the shortcut for a
single-step code edit, which is the common case, and it discarded job_chat's
diff — so the reply this was written for still said nothing. Fixed upstream in
OpenFn/apollo#676, which reports it in the same shape the planner does. The
changelog now names the Apollo version this needs rather than claiming it
outright.

Two bugs of mine. The Try again on a failed edit was gated on being in the last
two messages, but a retry appends one reply, so [prompt, flagged reply, new
reply] brought the stale notice's button back and every click started another
job. It is gated on being the last message now. And the flag was set whenever
any subagent call applied no patches, so a turn where one edit failed and
another landed put "couldn't apply the change" on a reply carrying the change;
it now also requires that nothing came back to apply.

The join reply put "validation_error" in `reason`, which the client shows to
the reader as it stands, so the fix for a crash would have displayed a code.
The field errors become a sentence. The comment claiming this matched
new_message's shape was wrong: those replies use `type` and `errors`, and a
join reply uses `reason`.

The counter was replacing the AI disclaimer rather than sitting beside it, so
the disclaimer vanished from 9,500 characters on.

Four test gaps, three of which let the above through. Nothing covered the
channel serializer, so hardcoding the flag to false broke the feature end to
end with the suite green. Nothing covered a turn with one failed and one
applied edit. Nothing covered exactly 10,000 characters, which the server
accepts and an off-by-one would have blocked. And the retry test used a shape a
retry never produces.
… malformed YAML raising

The router is the shortcut Apollo takes for a single-step code edit, so it is
the common case, and it threw job_chat's diff away. A client could not tell a
reply that changed nothing from one that changed something on the route most
edits take. It now reports it in the same shape the planner does, so there is
one place to look.

The YAML helpers assumed a mapping of jobs with string keys. The workflow is a
client payload, so a root that parses to a list or a string, a `jobs:` that is a
list, or a numeric job key each raised — and the only handler above turns that
into a 500 for the whole turn. Both helpers now fail closed to "not found".

Also: the "not found" branch keyed off a falsy job body rather than a missing
key, so a step that exists with no value was refused and then named in the
error as though it did not exist.
elias-ba added a commit to OpenFn/lightning that referenced this pull request Sep 9, 2026
* Answer a too-long first message instead of dropping the connection

Starting a chat with a message over the limit put a raw changeset in the join
reply. A changeset has no JSON encoder, so encoding raised and the socket died
before any reply went out: from the panel the assistant simply did nothing. The
reply now carries the same formatted errors new_message has always returned.

The trigger is a long paste, so the box also shows the limit before you hit it.
Nothing until you are within five hundred characters, then a counter, then send
is refused with the count in red. A counter that is always on reads as a warning
about a limit almost nobody meets.

Refusing is still the wrong answer; the right one is to take the paste as an
attachment the way Claude and Slack do, which is #5138.

Closes #4883

* Say when the assistant could not apply the code change

Ask for a code change, get a reply with nothing to apply and no explanation.
It reads as the assistant deciding not to help.

Apollo's job-code subagent reports how many of its edits landed, and zero means
it tried and could not. That count travels to us already: the subagent's whole
result goes into the planner's subagent_results, which global chat spreads into
meta, which build_global_message already reads. We were ignoring it.

The reply itself succeeded, so this is not a failure of the message and does not
belong in the failure columns. It is a flag on the message meta, alongside
from_global and apply_failed, and the panel renders the same notice a failure
gets.

Try again is offered, and only on the newest exchange. Every way a patch fails
is a property of what the model generated that turn - an anchor that is not in
the code, an ambiguous one, a missing field - and Apollo has already tried once
to correct itself, so another go can genuinely work. But a retry appends a new
reply and leaves this one saying the same thing for good, so an older exchange
would keep a button that starts a fresh job.

Apollo's own warning stays in the log. One of its two sources is
`f"Failed to apply edit: {str(e)}"`, which carries container paths.

One case this does not cover: code generated for a job key that matched
nothing, so it never reached the workflow. Apollo knows, and writes it into the
model's tool result, but does not put it in the response.

Closes #5133

* Act on the review of both fixes

The 5133 half only covered the planner. Apollo's router is the shortcut for a
single-step code edit, which is the common case, and it discarded job_chat's
diff — so the reply this was written for still said nothing. Fixed upstream in
OpenFn/apollo#676, which reports it in the same shape the planner does. The
changelog now names the Apollo version this needs rather than claiming it
outright.

Two bugs of mine. The Try again on a failed edit was gated on being in the last
two messages, but a retry appends one reply, so [prompt, flagged reply, new
reply] brought the stale notice's button back and every click started another
job. It is gated on being the last message now. And the flag was set whenever
any subagent call applied no patches, so a turn where one edit failed and
another landed put "couldn't apply the change" on a reply carrying the change;
it now also requires that nothing came back to apply.

The join reply put "validation_error" in `reason`, which the client shows to
the reader as it stands, so the fix for a crash would have displayed a code.
The field errors become a sentence. The comment claiming this matched
new_message's shape was wrong: those replies use `type` and `errors`, and a
join reply uses `reason`.

The counter was replacing the AI disclaimer rather than sitting beside it, so
the disclaimer vanished from 9,500 characters on.

Four test gaps, three of which let the above through. Nothing covered the
channel serializer, so hardcoding the flag to false broke the feature end to
end with the suite green. Nothing covered a turn with one failed and one
applied edit. Nothing covered exactly 10,000 characters, which the server
accepts and an off-by-one would have blocked. And the retry test used a shape a
retry never produces.

* Cut the commentary back

53 added comment lines to 18. Two of them said the same thing about the retry
gate, seven lines apart, because a later edit stacked on an earlier one. The
test comments repeated the production ones almost verbatim.

@hanna-paasivirta hanna-paasivirta left a comment

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.

This is great, thank you Elias! I removed some dead code that the tests were using, and verified the tests still pass. I also fixed the count shared with Lightning on applied changes to be 0 when changes fail.

@hanna-paasivirta
hanna-paasivirta merged commit 01d7028 into main Sep 9, 2026
3 checks passed
@elias-ba

elias-ba commented Sep 9, 2026

Copy link
Copy Markdown
Collaborator Author

Thank you very much Hanna

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.

Global chat: a code change can vanish without anyone able to say so

2 participants