Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 2 additions & 0 deletions scripts/test-layout/layout.json
Original file line number Diff line number Diff line change
Expand Up @@ -1219,8 +1219,10 @@
"responses-custom-tool-repair-dispatch.test.ts": "responses",
"responses-custom-tool-repair.test.ts": "responses",
"responses-custom-tool-stream-consistency.test.ts": "responses",
"responses-freeform-wrapper-keys.test.ts": "responses",
"responses-fetch-helpers-boundary.test.ts": "responses",
"responses-hosted-tool-declaration.test.ts": "responses",
"responses-hosted-tool-min-spread.test.ts": "responses",
"responses-field-backfill.test.ts": "responses",
"responses-forward-dangling-call.test.ts": "responses",
"responses-forward-incomplete-quota.test.ts": "responses",
Expand Down
22 changes: 13 additions & 9 deletions src/adapters/devin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { getCachedCatalog, type CacheEntry } from "./devin/cloud-direct/catalog"
import { collapseDevinModelUid } from "./devin/live-models";
import { buildNonOpenAIToolCatalogNudgeForTools } from "./tool-catalog-nudge";
import { DEVIN_DEFAULT_API_SERVER, resolveDevinApiServer } from "../oauth/devin";
import { isProviderIssuedThinkingSignature } from "../responses/reasoning-envelope";
import { SendBudgetExhaustedError } from "../lib/upstream-retry";

/**
Expand Down Expand Up @@ -329,23 +330,26 @@ function assistantText(message: OcxAssistantMessage): string {
* clients of the same service write #11 thinking with #12 signature and #18
* signature_type on the assistant prompt.
*
* The signature attests the thinking it was produced with, so a block without
* one contributes its text and nothing else rather than borrowing a neighbour's.
* Field #12 attests the exact text at #11, and the wire has room for one pair.
* Every block that carries text is replayed, so the chain stays intact; the
* signature rides along only when the text being replayed IS the text it
* attests, which is exactly the single-block case. Several independently signed
* blocks send an unsigned prompt rather than pairing one block's attestation
* with another block's words. A signature-only block attests encrypted thinking
* that is not being replayed at all, so it is not one of these blocks and
* cannot contribute the pair.
*/
function assistantThinking(
message: OcxAssistantMessage,
): { thinking?: string; signature?: string } {
const blocks = message.content.filter(
(part): part is Extract<typeof part, { type: "thinking" }> => part.type === "thinking",
);
).filter(part => Boolean(part.thinking));
if (blocks.length === 0) return {};
const thinking = blocks.map(b => b.thinking).filter(Boolean).join("\n");
// Only one signature can ride the prompt, so take the last block that has
// one: that is the block the turn actually ended on.
const signature = blocks.filter(b => b.signature).at(-1)?.signature;
const signature = blocks.length === 1 ? blocks[0]!.signature : undefined;
return {
...(thinking ? { thinking } : {}),
...(signature ? { signature } : {}),
thinking: blocks.map(part => part.thinking).join("\n"),
...(isProviderIssuedThinkingSignature(signature) ? { signature } : {}),
};
}

Expand Down
14 changes: 8 additions & 6 deletions src/adapters/openai-responses/image-gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,14 +101,17 @@ export function preferConfiguredHostedTools(
}

let input = body.input;
const strippedAdditionalToolsIndices = new Set<number>();
// Indices arrive in increasing order, so the first stripped container is already
// the minimum — tracking it directly avoids spreading an attacker-sized Set into
// Math.min's argument list.
let firstStrippedAdditionalToolsIndex: number | undefined;

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 Preserve co-author credit for the carried PRs

The commit message explicitly says this change carries #5132, #5150, and #5140, but commit 3fe3dca9577baa76a6bff24dc46dcf9657a198b3 contains no Co-authored-by trailer. Referencing the source PRs in prose does not credit their author in GitHub’s contributor graph; add the original contributor’s trailer to the PR description or a branch commit so it survives the squash.

AGENTS.md reference: AGENTS.md:L337-L347

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

The trailers are present. Co-authored-by: luvs01 <27862058+luvs01@users.noreply.github.com> is on each of the three carrying commits: c9893d2ca5 for #5132, 351ea99c87 for #5150, and c906f8128a for #5140. The fourth commit is this lane's own work, so it correctly carries none. .github/scripts/pr-carry-attribution.cjs is the gate AGENTS.md names for this, and hygiene passes on the current head.

The earlier hygiene failure on ba773dae6d was real, but it was not about credit. The Devin commit described protobuf fields as "#11" and "#12", and the carry-window regex read those as pull request references in this repository, then failed to resolve #11. They are written as "field 11" and "field 12" now.

if (Array.isArray(body.input)) {
let nestedChanged = false;
const mappedInput = body.input.map((item, index) => {
if (!isPlainObject(item) || item.type !== "additional_tools" || !Array.isArray(item.tools)) return item;
const nestedTools = stripGroup(item.tools);
if (nestedTools === item.tools) return item;
strippedAdditionalToolsIndices.add(index);
firstStrippedAdditionalToolsIndex ??= index;
nestedChanged = true;
return { ...item, tools: nestedTools };
});
Expand All @@ -127,17 +130,16 @@ export function preferConfiguredHostedTools(
|| (Array.isArray(input) && input.some(item => isPlainObject(item)
&& item.type === "additional_tools"
&& hasHostedImageGenTool(item.tools)));
if ((strippedTopLevelImageGenTool || strippedAdditionalToolsIndices.size > 0) && !hasHostedImageGenDeclaration) {
if ((strippedTopLevelImageGenTool || firstStrippedAdditionalToolsIndex !== undefined) && !hasHostedImageGenDeclaration) {
if (strippedTopLevelImageGenTool && Array.isArray(tools)) {
tools = [...tools, { type: HOSTED_IMAGE_GENERATION_TOOL }];
} else if (strippedAdditionalToolsIndices.size > 0 && Array.isArray(input)) {
} else if (firstStrippedAdditionalToolsIndex !== undefined && Array.isArray(input)) {
// Restore into the FIRST stripped container only. Tool declarations are
// request-scoped, not container-scoped — the containers are separate carriers for
// one tool set, so a single hosted declaration covers the request. An earlier
// revision restored into every stripped container and put `image_generation` on
// the wire twice; review caught it.
const firstStripped = Math.min(...strippedAdditionalToolsIndices);
input = input.map((item, index) => index === firstStripped
input = input.map((item, index) => index === firstStrippedAdditionalToolsIndex
&& isPlainObject(item)
&& Array.isArray(item.tools)
? { ...item, tools: [...item.tools, { type: HOSTED_IMAGE_GENERATION_TOOL }] }
Expand Down
57 changes: 38 additions & 19 deletions src/grok/reset-coupons.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,27 +52,48 @@ export function encodeVarint(value: number | bigint): Uint8Array {
return new Uint8Array(bytes);
}

const MAX_PROTOBUF_VARINT_BYTES = 10;

/**
* Decodes a protobuf varint from bytes at offset.
*/
export function decodeVarint(bytes: Uint8Array, offset: number): { value: number; bytesRead: number } {
if (!Number.isInteger(offset) || offset < 0 || offset >= bytes.length) {
throw new Error("Invalid protobuf varint offset");
}

let result = 0;
let shift = 0;
let count = 0;

while (offset + count < bytes.length) {
// Protobuf caps a varint at the ten bytes a 64-bit value needs. The safe-integer guard
// below cannot stand in for this bound, because a continuation byte carrying no payload
// bits contributes a part of zero, which IS a safe integer: without an explicit length
// limit an arbitrarily long run of 0x80 followed by 0x00 decoded as a valid zero, and an
// overlong zero length can normalize a malformed body into an empty coupon list.
if (count >= MAX_PROTOBUF_VARINT_BYTES) {
throw new Error("Overlong protobuf varint");
}
const b = bytes[offset + count];
count++;
result |= (b & 0x7f) << shift;
if ((b & 0x80) === 0) break;
shift += 7;
if (shift > 35) {
// For timestamps seconds, JS safe integers suffice.
break;
const part = (b & 0x7f) * (2 ** (7 * count));
if (!Number.isSafeInteger(part) || result > Number.MAX_SAFE_INTEGER - part) {
throw new Error("Protobuf varint exceeds JavaScript safe integer range");
}
result += part;
count++;
if ((b & 0x80) === 0) return { value: result, bytesRead: count };
}

return { value: result, bytesRead: count };
throw new Error("Truncated protobuf varint");
}

function decodeLength(bytes: Uint8Array, offset: number): { start: number; end: number } {
const { value: length, bytesRead } = decodeVarint(bytes, offset);
const start = offset + bytesRead;
if (!Number.isSafeInteger(length) || length < 0 || length > bytes.length - start) {
throw new Error("Invalid protobuf length-delimited field");
}
return { start, end: start + length };
}

/**
Expand Down Expand Up @@ -109,8 +130,8 @@ function decodeTimestamp(bytes: Uint8Array): number {
offset += bytesRead;
if (fieldNum === 1) seconds = value;
} else if (wireType === 2) {
const { value: len, bytesRead } = decodeVarint(bytes, offset);
offset += bytesRead + len;
const { end } = decodeLength(bytes, offset);
offset = end;
} else {
break;
}
Expand All @@ -135,10 +156,9 @@ function decodeConsumerResetToken(bytes: Uint8Array): GrokResetCoupon | null {
const wireType = tag & 0x7;

if (wireType === 2) {
const { value: len, bytesRead: lenRead } = decodeVarint(bytes, offset);
offset += lenRead;
const sub = bytes.subarray(offset, offset + len);
offset += len;
const { start, end } = decodeLength(bytes, offset);
const sub = bytes.subarray(start, end);
offset = end;

if (fieldNum === 10) {
tokenId = new TextDecoder("utf-8").decode(sub);
Expand Down Expand Up @@ -178,10 +198,9 @@ export function decodeGetRemainingResetsResponse(payload: Uint8Array): GrokReset
const wireType = tag & 0x7;

if (wireType === 2) {
const { value: len, bytesRead: lenRead } = decodeVarint(payload, offset);
offset += lenRead;
const sub = payload.subarray(offset, offset + len);
offset += len;
const { start, end } = decodeLength(payload, offset);
const sub = payload.subarray(start, end);
offset = end;

if (fieldNum === 10) {
const token = decodeConsumerResetToken(sub);
Expand Down
12 changes: 0 additions & 12 deletions src/responses/apply-patch-envelope.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,18 +34,6 @@ function stripMarkdownCodeFence(text: string, toolName: string): string {
return match ? match[1] : text;
}

/**
* The single-field wrappers `unwrapFreeformToolInput` accepts for one tool name, besides the
* canonical `input`.
*
* Exported so the streaming side can hold a buffer that is still turning into one of these.
* A second list of key names beside this one is how the streamed bytes and the completed item
* come to disagree, which is the defect it exists to prevent (#5047).
*/
export function freeformFallbackKeys(toolName: string): readonly string[] {
return FREEFORM_FALLBACK_KEYS[toolName] ?? [];
}

/** Unwrap the `{input:string}` function-call wrapper used for freeform tools. */
export function unwrapFreeformToolInput(argumentsText: unknown, toolName = ""): string {
if (typeof argumentsText !== "string") return "";
Expand Down
Loading
Loading