Preserve literal acceptance text and surface inconsistent chat idle states

This commit is contained in:
Dotta 2026-09-11 15:04:14 -05:00
parent cd3ac87cd8
commit a49ac10617
4 changed files with 136 additions and 2 deletions

View File

@ -2088,6 +2088,23 @@ fn redact_sensitive_text_values(input: &str) -> String {
.any(|delimiter| before.ends_with(delimiter))
};
let has_hyphenated_count_lead = token_phrase_has_lead("one-");
// An explicitly literal/exact token is a requested text value (for
// example an acceptance identifier), not a diagnostic credential pair.
// Keep this grammatical exception separate from credential syntax:
// assignments, quoted values/keys, CLI and compound keys still redact,
// as do the independent Bearer, key-prefix and JWT scanners above.
let is_literal_token_reference = key == "token"
&& !key_is_compound
&& whitespace_start == start + key.len()
&& separator > whitespace_start
&& !has_assignment_separator
&& bytes[whitespace_start..separator]
.iter()
.all(|value| matches!(value, b' ' | b'\t'))
&& quoted_value_start(separator).1.is_none()
&& ["literal ", "exact "]
.iter()
.any(|lead| token_phrase_has_lead(lead));
let is_benign_token_noun_phrase = key == "token"
&& (!key_is_compound || has_hyphenated_count_lead)
&& whitespace_start == start + key.len()
@ -2150,7 +2167,8 @@ fn redact_sensitive_text_values(input: &str) -> String {
|| (token_phrase_has_tail("can equal") && token_phrase_has_lead("one ")));
let has_whitespace_separator = separator > whitespace_start
&& (key != "authorization" || key_is_compound || has_authorization_scheme)
&& !is_benign_token_noun_phrase;
&& !is_benign_token_noun_phrase
&& !is_literal_token_reference;
if !has_assignment_separator && !has_whitespace_separator {
continue;
}
@ -3159,6 +3177,68 @@ mod tests {
assert_eq!(sanitized["accessToken"], json!("[REDACTED]"));
}
#[test]
fn semantic_handoff_preserves_literal_acceptance_identifiers() {
let description = "The document body must contain the literal token CHAT250ed7e4dc071. No code changes needed.";
let plan = "## Plan\n- Include the literal token CHAT250ed7e4dc071 in the document body.\n- Save the output document.";
let input = json!({
"title": "Write project description",
"description": description,
"initialPlan": plan,
"idempotencyKey": "write-description-1",
});
assert_eq!(
sanitize_semantic_tool_input("create_task", &input).unwrap(),
input
);
for text in [
description,
plan,
"Must include the literal token `CHAT66e7813a4f9d1` somewhere in the text.",
"Include the exact token ACCEPTANCE-42 in the final output.",
] {
assert_eq!(redact_text(text), text);
assert_eq!(
sanitize_value(&json!({"body": text})),
json!({"body": text})
);
}
}
#[test]
fn literal_token_prose_does_not_exempt_credential_syntax_or_shapes() {
for text in [
"auth token opaque-credential",
"the token opaque-credential",
"literal token=opaque-credential",
"literal token:opaque-credential",
"literal --token opaque-credential",
"literal access_token opaque-credential",
"literal \"token\" opaque-credential",
"literal token \"opaque-credential\"",
] {
assert!(!redact_text(text).contains("opaque-credential"), "{text}");
}
for secret in [
"sk-proj-secretvalue123456",
"ghp_secretvalue12345678901234567890",
"eyJhbGciOiJIUzI1NiJ9.c2VjcmV0LWNsYWlt.signaturesecret",
] {
let text = format!("Include the literal token {secret} in the document.");
assert!(!redact_text(&text).contains(secret), "{text}");
}
let input = json!({
"description": "Include the literal token ACCEPTANCE-42. Authorization: Bearer opaque-credential",
"token": "opaque-credential",
});
let safe = sanitize_semantic_tool_input("create_task", &input).unwrap();
assert!(safe["description"]
.as_str()
.unwrap()
.contains("ACCEPTANCE-42"));
assert!(!safe.to_string().contains("opaque-credential"));
}
#[test]
fn semantic_redaction_preserves_benign_token_system_prose() {
let prose = "Offer a simple token system so guests can exchange items even when their contributions differ in quantity.";

View File

@ -6,6 +6,7 @@ import {
chatQuestionPresentation,
chatRunFailure,
chatTaskCompletionFailure,
createChatIdleFailureDetector,
collectChatRunEvidence,
readRunningChatLog,
readChatOutputDocument,
@ -19,6 +20,7 @@ import type { RunnerApi } from "./api.js";
import { chatMarker } from "./chat-cases.js";
import { runnerMatrix } from "./catalog.js";
import { isPublicRunnerScreenshotRoute } from "./screenshot-policy.js";
import { classifyFailure, shouldRetryFailure } from "./failure-classifier.js";
const source: ChatIssue = {
id: "chat",
@ -265,6 +267,28 @@ describe("chat acceptance contracts", () => {
),
).toBeUndefined();
});
it("fails stable contradictory idle states promptly without paid retries or transient false alarms", () => {
const detect = createChatIdleFailureDetector(3);
const settled = {
resolved: true,
status: "blocked",
conversationState: "waiting",
providerRunCount: 3,
activeRuns: [] as string[],
};
expect(detect(settled)).toBeUndefined();
expect(detect({ ...settled, activeRuns: ["running"] })).toBeUndefined();
expect(detect(settled)).toBeUndefined();
const failure = detect(settled);
expect(failure).toContain("chat_idle_state_invariant");
expect(classifyFailure(failure)).toBe("candidate_failure");
expect(shouldRetryFailure(classifyFailure(failure))).toBe(false);
expect(detect({ ...settled, status: "in_review" })).toBeUndefined();
expect(detect(settled)).toBeUndefined();
expect(detect({ ...settled, providerRunCount: 2 })).toBeUndefined();
expect(detect({ ...settled, status: "in_progress" })).toBeUndefined();
expect(detect(settled)).toBeUndefined();
});
it("fails promptly on terminal provider failures while permitting only expected cancellations", () => {
expect(chatRunFailure([run])).toBeUndefined();
expect(chatRunFailure([{ ...run, status: "running" }])).toBeUndefined();

View File

@ -126,6 +126,34 @@ export function chatTaskCompletionFailure(
return chatRunFailure(runs);
}
/** Require two settled observations so an in-flight finalization is not a failure. */
export function createChatIdleFailureDetector(minimumProviderRuns: number) {
let priorInconsistentState: string | undefined;
return (state: {
resolved: boolean;
status?: string;
conversationState?: string;
providerRunCount: number;
activeRuns: string[];
}): string | undefined => {
const inconsistentState =
state.resolved &&
state.providerRunCount >= minimumProviderRuns &&
state.activeRuns.length === 0 &&
state.conversationState === "waiting" &&
["blocked", "done", "cancelled"].includes(state.status ?? "")
? `${state.status}:${state.providerRunCount}`
: undefined;
const stable =
inconsistentState !== undefined &&
inconsistentState === priorInconsistentState;
priorInconsistentState = inconsistentState;
return stable
? `chat_idle_state_invariant: conversation is ${state.status} while waiting after ${state.providerRunCount} settled provider runs`
: undefined;
};
}
/** Match the shared question form's durable/native presentation, including custom labels. */
export function chatQuestionPresentation(
payload: AskUserQuestionsPayload,
@ -254,6 +282,7 @@ export async function runChatFlow(input: {
a.createdAt.localeCompare(b.createdAt) || a.id.localeCompare(b.id),
);
const idle = async (minimumProviderRuns: number) => {
const inconsistentIdle = createChatIdleFailureDetector(minimumProviderRuns);
await pollUntil({
label: "chat turn settles to waiting",
deadlineAt: Date.now() + 240_000,
@ -274,7 +303,7 @@ export async function runChatFlow(input: {
failure: chatRunFailure(runs, caseId === "stop-new-resume"),
};
},
reject: (state) => state.failure,
reject: (state) => state.failure ?? inconsistentIdle(state),
accept: (state) =>
!state.failure &&
state.resolved &&

View File

@ -29,6 +29,7 @@ export function classifyFailure(error: unknown): FailureClass {
}
if (PERMANENT.test(message)) return "permanent_infrastructure";
if (
/chat_idle_state_invariant/.test(message) ||
NON_RETRYABLE_SESSION_CLOSE.test(message) ||
NON_RETRYABLE_ACPX_SESSION_OPEN.test(message)
)