Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
913ec20
fix(ai): bind terminal safety-stop authority to adapter provenance
Aug 20, 2026
0199656
fix(ai): close provider safety-stop minting boundary
Aug 20, 2026
b04dd0c
fix(agent): keep safety provenance runtime-private
Aug 20, 2026
1b59203
fix(ai): block the safety-stop mint module from wildcard package exports
Aug 21, 2026
cc0e4b3
fix(ai): restore safety-stop provenance across the pi-native gateway hop
Aug 21, 2026
83c0bec
fix(ai): satisfy type and format contracts in the pi-native restore
Aug 21, 2026
d973c2c
fix(agent): expire safety-stop authority at dispatch and harden the s…
Aug 21, 2026
3ed1d98
docs(changelog): record the #4782 fix-forward hardening entries
Aug 21, 2026
d7cf5ae
fix(ai): fail closed on pi-native safety-stop transport
Aug 21, 2026
d2e9cc0
fix(agent): close remaining safety provenance seams
Aug 21, 2026
88ccaa7
fix(ai): close injected adapter transport authority
Aug 21, 2026
0cee020
fix(ai): close selected Google fetch provenance seam
Aug 21, 2026
57126a8
fix(ai): bind safety minting to runtime dispatch
Aug 21, 2026
8b1fe4e
Merge remote-tracking branch 'origin/pr/4782/head' into owner/pr-4782…
Aug 22, 2026
3fd666b
test(coding-agent): satisfy safety-stop import contract
Aug 22, 2026
2b2a3e0
Merge remote-tracking branch 'origin/dev' into owner/pr-4782-ox-v1
Aug 22, 2026
a5b93bf
fix(ai): bind safety minting to bundled model provenance
Aug 22, 2026
8be3dd8
docs(ai): record model provenance hardening
Aug 22, 2026
6ffe885
fix(ai): preserve safety provenance through provider wrappers
Aug 22, 2026
1e1138f
fix(ai-agent): close remaining safety-stop review seams
Aug 22, 2026
c9474a1
Merge remote-tracking branch 'origin/dev' into owner/pr-4782-ox-v1
Aug 22, 2026
893f3ba
docs(ai): keep safety fixes under unreleased
Aug 22, 2026
20c6bde
test(ai): isolate safety mint gate legs
Aug 22, 2026
05e47bc
fix(ai): stamp direct GitLab dispatch provenance
Aug 22, 2026
294e52c
Merge remote-tracking branch 'origin/dev' into owner/pr-4782-ox-v1
Aug 22, 2026
a360e7a
Merge remote-tracking branch 'origin/dev' into owner/pr-4782-ox-v1
Aug 22, 2026
b6f34f6
Merge remote-tracking branch 'origin/dev' into owner/pr-4782-ox-v1
Aug 22, 2026
e68e37b
Merge remote-tracking branch 'origin/dev' into owner/pr-4782-ox-v1
Aug 22, 2026
c1943f1
Merge remote-tracking branch 'origin/dev' into owner/pr-4782-ox-v1
Aug 22, 2026
8df98d2
Merge remote-tracking branch 'origin/dev' into owner/pr-4782-ox-v1
Aug 22, 2026
73b1176
Merge remote-tracking branch 'origin/dev' into owner/pr-4782-ox-v1
Aug 22, 2026
4557101
fix(ai): preserve safety-stop authority through provider seams
Aug 22, 2026
feae24a
Merge remote-tracking branch 'origin/dev' into owner/pr-4782-ox-v1
Aug 22, 2026
d71c778
Merge remote-tracking branch 'origin/dev' into owner/pr-4782-ox-v1
Aug 22, 2026
5bc5252
fix(ai): close direct safety-stop trust gaps
Aug 22, 2026
52ae9d4
Merge remote-tracking branch 'origin/dev' into owner/pr-4782-ox-v1
Aug 22, 2026
095f195
fix(ai): keep safety-stop revocation private
Aug 22, 2026
5c785d5
fix(ai): preserve sensitive stop provenance
Aug 22, 2026
7c64331
Merge remote-tracking branch 'origin/dev' into owner/pr-4782-ox-v1
Aug 22, 2026
dd93b43
fix(ai): fail closed public provider adapters
Aug 22, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions packages/agent/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,18 @@
# Changelog

## [Unreleased]
- Managed fallback now transfers safety-stop authority only to the adjudicated final assistant shell; intermediate partial snapshots and hostile accessor-backed final messages cannot retain or bypass the provenance boundary (#4777 review).

- Managed assistant reconstruction now copies provider metadata through guarded property reads instead of an unguarded spread, so accessor-trapped metadata degrades without aborting the attempt or creating managed retry authority (#4777 review).
- Hostile Proxy-wrapped final messages no longer reintroduce a forged `provider_safety_stop` label through the sanitizer fallback shell; discarded failure outcomes are now label-free before session policy can suppress provider fallback (#4777 review).


### Fixed
- Staged-payload sizing no longer materializes what it is bounding (#4602 fix-forward of the exact-head 078e22c0 review). All staging measurements now walk the JSON surface directly: exact byte counts come from a code-point walk (quotes, escapes, separators, delimiters, nulls, array holes, and keys all charged) instead of building the full `JSON.stringify` string plus its UTF-8 encoding, and lone surrogates are charged as the six-byte `\udXXX` escape JSON emits rather than their three-byte UTF-8 form, closing a ~2x undercount on surrogate-heavy strings. `structuredClone` is additionally preflighted by a clone-surface walk that never dispatches `toJSON`, accessors, or proxy traps: a live payload class whose compact `toJSON` hides an oversized own payload is rejected as the typed `local_buffer_overflow` at `overflow.preMeasure` — before the duplicate is allocated — instead of being cloned first and rejected at `overflow.staged`. Accessors are no longer invoked at all while sizing (a staged witness getter is read zero times), `undefined`-valued record properties are skipped exactly as `JSON.stringify` omits them, an unmeasurable assistant pair now fails closed like its `#stage` twin instead of being retained with a zero-byte charge, the `overflow.preMeasure` diagnostic reports the incoming event's real bounded size instead of a constant fabricated after `discard()`, and above-ceiling clamp warnings are logged once per distinct knob value with a bounded digest. |
- Provider safety-stop messages now retain their explicitly allowlisted `errorKind: "provider_safety_stop"` through managed assistant snapshots and remain terminal even when transport facts are present on a multi-model fallback chain, while provider payloads still cannot forge the runtime-owned local diagnostic kinds (#4777).
- Terminal safety-stop authority is now provenance-bound instead of data-bound: a provider or custom stream payload that self-labels `errorKind: "provider_safety_stop"` without the adapter-minted mark is stripped at the stream exit before any retry/discard gate or the managed snapshot shell reads it, so a compromised provider can no longer force refusal by naming the field (#4777 review). Authenticated first-party envelopes (structured refusal signals parsed by the anthropic, openai-completions, and google adapters) keep terminal treatment, and only the agent loop's module-private rebuild set carries that authority onto its own destination — clones, JSON/persistence round-trips, and re-emitted payloads are all unauthenticated.
- Safety-stop minting is now limited to the package-private adapter capability, and public AI consumers cannot transfer authority from a genuine marked source to an arbitrary destination. The trailing stream-completion path also sanitizes provenance before rebuilding managed assistant messages, covering streams that end without a `done` or `error` event and keeping forged labels fallback-eligible (#4777 review).
- Safety-stop authority now expires at every stream dispatch entry: committed assistant messages (including a previously adjudicated stop) are handed to the next — possibly custom — stream through `convertToLlm`, and the dispatch-entry expiry guarantees no live authority mark is ever exposed to a stream, so re-use of a committed stop object cannot forge a terminal failure (#4777 review). The provenance strip is also stopReason-independent and rebuilds frozen or Proxy-trapped final messages as plain mutable copies, so a forged label on a nominally successful response cannot survive into the committed message (where it could skip session compaction checks) and cannot abort the run through a rejection trap.
- A foreign error that self-declares a local failure kind no longer gets one either (#4618). `errorKind` and the structured `bufferOverflow` shape now come from a single identity-checked extractor (`managedLocalErrorDiagnostic`) used by both terminal-message producers — `managedFailureMessage` and the `Agent` run catch. Previously the shape was identity-gated but the label was not, so a provider or custom-stream failure carrying `errorKind: "local_buffer_overflow"` reached the parent receipt preview as `Local staging-buffer overflow; structured diagnostic unavailable.` and pointed whoever read it at the wrong subsystem.
- Local diagnostic authority fields are no longer foreign-settable through the managed snapshot shell (#4618). `managedAssistantShell` spreads the provider/stream message snapshot into the rebuilt assistant message; a payload that smuggled a local `errorKind` or `bufferOverflow` through that spread could masquerade as the runtime's own identity-checked diagnostic at the parent boundary. Local kinds and `bufferOverflow` remain stripped from the snapshot spread, while the provider-owned safety-stop kind is copied only through its explicit closed-literal guard.

Expand Down
143 changes: 130 additions & 13 deletions packages/agent/src/agent-loop.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
classifyFallbackTrigger,
EMPTY_RESPONSE_PROVIDER_CODE,
EventStream,
isProviderSafetyStopAuthenticated,
isZodSchema,
streamSimple,
type ToolChoice,
Expand All @@ -34,6 +35,7 @@ import {
} from "@gajae-code/ai/utils";
import { isCursorExecResolved } from "@gajae-code/ai/utils/block-symbols";
import { $credentialEnv, logger, sanitizeText } from "@gajae-code/utils";
import { revokeProviderSafetyStop } from "../../ai/src/adapter-internals/provider-safety-stop";
import type { AttemptScope } from "./attempt-scope";
import {
createHarmonyAuditEvent,
Expand Down Expand Up @@ -491,23 +493,53 @@ function managedContextOverflow(message: AssistantMessage, config: AgentLoopConf
}

/** Managed fallback owns retry policy; only attached typed transport facts may discard an attempt. */
function managedProperty(value: unknown, key: string): unknown {
if (!value || typeof value !== "object") return undefined;
function managedPropertyRead(value: unknown, key: string): { ok: boolean; value: unknown } {
if (!value || typeof value !== "object") return { ok: true, value: undefined };
try {
return Reflect.get(value, key);
return { ok: true, value: Reflect.get(value, key) };
} catch {
return undefined;
return { ok: false, value: undefined };
}
}

function managedProperty(value: unknown, key: string): unknown {
return managedPropertyRead(value, key).value;
}

function managedTransportFailure(failure: unknown) {
const facts = managedProperty(failure, "transportFailure");
return facts && typeof facts === "object" ? transportFailureFacts(facts) : undefined;
}

// AI owns provider-originated authority. The agent loop owns authority for
// the rebuilt message objects it creates; this second WeakSet is deliberately
// module-private so a public AI consumer cannot transfer authority to an
// arbitrary destination. A destination is marked only while this managed
// runtime is rebuilding a source that AI authenticated.
const managedProviderSafetyStops = new WeakSet<object>();

function isManagedProviderSafetyStopAuthenticated(value: unknown): boolean {
return (
isProviderSafetyStopAuthenticated(value) ||
(typeof value === "object" && value !== null && managedProviderSafetyStops.has(value))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Expire safety authority before exposing messages to custom streams

When a session continues after a genuine safety stop, the default convertToLlm passes that committed assistant object unchanged to the next custom StreamFn; the stream can mutate and return that same object as an arbitrary error, and this identity-only WeakSet check still authenticates it, allowing the forged failure to suppress the fallback chain. Fresh evidence at this head is that the public transfer helper was removed, but direct reuse of the authenticated source object still transfers its authority; revoke marks after adjudication or bind them to an immutable, single-invocation envelope.

Useful? React with 👍 / 👎.

);
}

function managedRetryableFailure(failure: unknown): boolean {
const facts = managedTransportFailure(failure);
if (!facts) return false;
// A typed provider safety stop is terminal evidence ahead of any transport
// class, but only with adapter-minted provenance: unauthenticated labels
// are stripped at the stream exit (`sanitizeProviderSafetyStopProvenance`)
// and must fall through to ordinary transport classification so the chain
// can still advance (#4777).
if (
managedProperty(failure, "stopReason") === "error" &&
managedProperty(failure, "errorKind") === "provider_safety_stop" &&
isManagedProviderSafetyStopAuthenticated(failure)
) {
return false;
}
const trigger = classifyFallbackTrigger(facts);
// A plain `forbidden` is terminal: retrying it just re-sends a request the
// caller is not authorized to make, and the credential-mutating consumers
Expand All @@ -532,6 +564,62 @@ function promoteTypedEmptyResponseStop(message: AssistantMessage): void {
message.stopReason = "error";
message.errorMessage = "Provider returned an empty response with zero token usage";
}
/**
* Terminal safety-stop authority is provenance-bound, not data-bound: a
* provider or custom stream payload that self-labels
* `errorKind: "provider_safety_stop"` without the adapter-minted mark must not
* terminalize the failure, because terminal treatment suppresses the user's
* configured fallback chain (#4777 review follow-up). Strip the unauthenticated
* field from the live final message at the single stream-exit point, before
* the managed snapshot shell clones it and before any retry/discard policy
* reads it, so a forged label degrades to an ordinary (fallback-eligible)
* error everywhere downstream — loop gates, session policy, and persistence.
*
* The label is stripped regardless of stopReason: the field is reserved for
* adapter-minted terminal stops, and downstream consumers (session compaction
* checks among them) read it without re-checking the error state, so a forged
* label on a nominally successful response must not survive either. A frozen
* or Proxy-trapped final message is rebuilt as a plain mutable copy instead of
* letting the strip abort the run.
*/

function sanitizeProviderSafetyStopProvenance(
message: AssistantMessage,
model: AgentLoopConfig["model"],
): AssistantMessage {
const errorKindRead = managedPropertyRead(message, "errorKind");

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Authenticate replayed terminals at the external-event boundary

When an SDK host bridge or restart recovery re-emits a terminal message_end, this sanitizer is bypassed because Agent.emitExternalEvent appends and publishes the message directly; AgentSession then persists it and trusts errorKind in retry, fallback, and compaction policy. Since a JSON/restart replay cannot retain the new WeakSet authority, a forged or altered replay carrying errorKind: "provider_safety_stop" is still treated as terminal and can suppress recovery, while stripping every replayed label would lose genuine stops. Add a provenance-aware external admission/recovery token so replayed terminals can be re-authenticated rather than trusted by field value alone.

Useful? React with 👍 / 👎.

if (
errorKindRead.ok &&
(errorKindRead.value !== "provider_safety_stop" || isManagedProviderSafetyStopAuthenticated(message))
) {
return message;
Comment on lines +593 to +595

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Snapshot the safety label before accepting it

When a custom stream returns a Proxy or accessor-backed final message whose errorKind read changes over time, it can return undefined here and "provider_safety_stop" during the subsequent managedAssistantShell rebuild. This early return therefore lets an unauthenticated label survive into the committed message; downstream session logic trusts that field without rechecking provenance, potentially skipping compaction or displaying a false safety-stop hint. Rebuild Proxy/accessor-backed messages from one guarded snapshot rather than accepting the live object after a single read.

Useful? React with 👍 / 👎.

}
const detached = managedAttemptSnapshotDetailed(message).snapshot;
if (isManagedPlainRecord(detached)) {
const rebuilt = { ...detached } as AssistantMessage;
delete rebuilt.errorKind;
return rebuilt;
}
const rebuilt = managedAssistantShell(message, model);
delete rebuilt.errorKind;
return rebuilt;
}

/**
* Expire residual terminal safety-stop authority before a dispatch exposes
* committed history to a stream. Once a stop has been adjudicated, its
* committed assistant message may be handed unchanged to a later — possibly
* custom — stream through `convertToLlm`; a live mark would let that stream
* re-use the authenticated object (or a mutation of it) to forge a terminal
* failure and suppress the fallback chain (#4777 review follow-up).
*/
function expireProviderSafetyStopAuthority(messages: AgentMessage[]): void {
for (const message of messages) {
if (message.role !== "assistant") continue;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Guard role reads during authority expiry

When history contains a Proxy or accessor-backed message whose role getter throws, this new pre-transform scan aborts the request before transformContext can replace or sanitize that entry. Previously a configured transform could discard such extension-supplied history without touching the hostile getter, so this also regresses unmanaged runs even though authority expiry is only needed for marked objects; use a guarded read or revoke by identity without consulting caller-controlled message fields.

Useful? React with 👍 / 👎.

revokeProviderSafetyStop(message);
managedProviderSafetyStops.delete(message);
Comment on lines +618 to +620

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Revoke safety authority regardless of the mutable role

When code mutates a committed authenticated message's live role before the next prompt, this guard skips both revocation sets. Fresh evidence at this head is that Agent.state exposes the live state and its message objects, so a custom-stream integration can change the marked message to role: "user" between prompts; the default converter preserves it, and the subsequent stream can restore role: "assistant" and return the still-marked identity as a forged retry-suppressing failure. Revoke authority from every object in history without using caller-mutable message data as the eligibility predicate.

Useful? React with 👍 / 👎.

}
}

/**
* Neutralize leaked reserved control tokens in-place across the outgoing
Expand Down Expand Up @@ -1693,6 +1781,7 @@ function managedAssistantShell(
value: unknown,
model: AgentLoopConfig["model"],
degradedFieldDiagnostics: Set<string> = new Set<string>(),
transferSafetyStopAuthority = false,
): AssistantMessage {
const detailed = managedAttemptSnapshotDetailed(value);
const snapshotRecord = isManagedPlainRecord(detailed.snapshot) ? detailed.snapshot : undefined;
Expand Down Expand Up @@ -1758,9 +1847,13 @@ function managedAssistantShell(
stopReason === "error" && managedProperty(source, "errorKind") === "provider_safety_stop"
? ("provider_safety_stop" as const)
: undefined;
const safeMetadata: Record<string, unknown> = isManagedPlainRecord(detailed.snapshot)
? { ...detailed.snapshot }
: {};
const safeMetadata: Record<string, unknown> = {};
if (isManagedPlainRecord(detailed.snapshot)) {
for (const key of Object.keys(detailed.snapshot)) {
const metadata = managedProperty(detailed.snapshot, key);
if (metadata !== undefined) safeMetadata[key] = metadata;
}
}
delete safeMetadata.errorMessage;
delete safeMetadata.errorStatus;
delete safeMetadata.transportFailure;
Expand All @@ -1770,7 +1863,7 @@ function managedAssistantShell(
// runtime failure in the executor's parent-facing summary (#4618).
delete safeMetadata.errorKind;
delete safeMetadata.bufferOverflow;
return {
const rebuilt: AssistantMessage = {
...safeMetadata,
role: "assistant",
content,
Expand All @@ -1785,6 +1878,16 @@ function managedAssistantShell(
...(errorKind ? { errorKind } : {}),
...(typeof errorStatus === "number" && Number.isFinite(errorStatus) ? { errorStatus } : {}),
};
// The closed-literal copy above is fed by the stream-exit provenance
// sanitize, so an unauthenticated label never reaches here. Mark the
// runtime-owned destination only when this source is already authenticated;
// no public AI API can perform this transfer (#4777 review).
if (transferSafetyStopAuthority && errorKind && isManagedProviderSafetyStopAuthenticated(value)) {
managedProviderSafetyStops.add(rebuilt);
revokeProviderSafetyStop(value);
if (typeof value === "object" && value !== null) managedProviderSafetyStops.delete(value);
Comment on lines +1885 to +1888

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Revoke authenticated sources outside the transfer guard

When a custom StreamFn retains a genuine first-party safety-stop result but clears errorKind or changes stopReason before returning it during a managed-fallback run, the shell derives no errorKind, so this conditional neither transfers nor revokes the source's WeakSet authority. On a later invocation the stream can restore stopReason: "error", errorKind: "provider_safety_stop", and retryable transport facts on that same object; the sanitizer then accepts it as authenticated and suppresses the configured fallback chain. Fresh evidence relative to the prior retained-source report is that the current fix placed revocation only inside the successful-transfer branch; revoke every authenticated source adjudicated with transferSafetyStopAuthority, even when its mutable fields no longer qualify for transfer.

Useful? React with 👍 / 👎.

}
return rebuilt;
}

function managedContentBlock(block: unknown): AssistantMessage["content"] {
Expand Down Expand Up @@ -3479,7 +3582,7 @@ async function runLoopBody(
return;
}
if (attemptTransaction) {
message = managedAssistantShell(message, config.model);
message = managedAssistantShell(message, config.model, new Set<string>(), true);
const index = currentContext.messages.length - 1;
if (index >= 0 && currentContext.messages[index]?.role === "assistant") {
currentContext.messages[index] = message;
Expand Down Expand Up @@ -3785,10 +3888,22 @@ async function streamAssistantResponse(
const managedDegradedFieldDiagnostics = new Set<string>();
// Apply context transform if configured (AgentMessage[] → AgentMessage[])
let messages = context.messages;
// Revoke before invoking any caller-controlled transform so it cannot retain
// a live authenticated object and restore its role for a later custom stream.
expireProviderSafetyStopAuthority(messages);
if (messages !== context.messages) expireProviderSafetyStopAuthority(context.messages);
if (config.transformContext) {
messages = await config.transformContext(messages, signal, scope);
}

// Expire residual terminal safety-stop authority again after the transform:
// committed history (including a previously adjudicated stop) is handed
// to the stream through convertToLlm below, and a live mark would let a
// custom stream re-use the authenticated object to forge a terminal
// failure (#4777 review follow-up).
expireProviderSafetyStopAuthority(messages);
if (messages !== context.messages) expireProviderSafetyStopAuthority(context.messages);

// Convert to LLM-compatible messages (AgentMessage[] → Message[]) and normalize at the LLM boundary.
// Cache hits are keyed by provider-visible content hashes, never message object identity.
const normalizedMessages = await convertAndNormalizeMessages(messages, context, config);
Expand Down Expand Up @@ -4176,9 +4291,10 @@ async function streamAssistantResponse(

case "done":
case "error": {
const finished = sanitizeProviderSafetyStopProvenance(await finishResponse(), config.model);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Sanitize partial messages before publishing them

When a custom stream puts stopReason: "error" and a forged errorKind: "provider_safety_stop" in its start partial or a later update, sanitization happens only after the terminal event, so the earlier message_start/message_update has already exposed the label to Agent state, subscribers, and extension handlers. The coding-agent streaming component also treats a truthy stop reason as terminal and uses this field to render the model-switch safety hint (assistant-message.ts lines 513-559), allowing an unauthenticated stream to display a false provider refusal even when its eventual final message is cleaned; sanitize each partial before publishing it, not only the final result.

Useful? React with 👍 / 👎.

const finalMessage = config.fallbackManaged
? managedAssistantShell(await finishResponse(), config.model, managedDegradedFieldDiagnostics)
: await finishResponse();
? managedAssistantShell(finished, config.model, managedDegradedFieldDiagnostics, true)
Comment on lines +4294 to +4296

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Revoke the authenticated source after transferring authority

When a custom StreamFn delegates to a first-party adapter, it can retain the authenticated object returned by that adapter's result(). This managed rebuild transfers authority to finalMessage but never revokes finished; because only the clone is committed to context.messages, the dispatch-time expiry cannot reach the retained source. On a later invocation the stream can mutate and return that still-marked object as an arbitrary failure, and the sanitizer will authenticate it and suppress the fallback chain. Revoke finished immediately after transferring its authority to the runtime-owned shell.

Useful? React with 👍 / 👎.

: finished;
promoteTypedEmptyResponseStop(finalMessage);
if (addedPartial) {
context.messages[context.messages.length - 1] = finalMessage;
Expand All @@ -4199,9 +4315,10 @@ async function streamAssistantResponse(
closeIterator();
}

const finished = sanitizeProviderSafetyStopProvenance(await finishResponse(), config.model);
const trailing = config.fallbackManaged
? managedAssistantShell(await finishResponse(), config.model, managedDegradedFieldDiagnostics)
: await finishResponse();
? managedAssistantShell(finished, config.model, managedDegradedFieldDiagnostics, true)
: finished;
await finishChat(trailing);
return trailing;
Comment on lines +4318 to 4323

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Finalize the partial on trailing stream completion

When a custom stream emits a nonempty start partial containing a forged errorKind: "provider_safety_stop", then exhausts via end(cleanFinal) without a done/error event, this branch sanitizes only cleanFinal and emits no message_end for it. Fresh evidence at this exact head is that the previously staged message_start still contains the forged label; the outer Agent keeps that object as partial and appends it after iteration (agent.ts lines 1974-1984), so the unauthenticated label can still reach session state despite the trailing-result sanitizer. Mirror the terminal-event path here by replacing the committed partial and publishing the sanitized final message_end.

Useful? React with 👍 / 👎.

});
Expand Down
Loading
Loading