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
18 changes: 14 additions & 4 deletions devlog/_plan/260906_d_integrations_delivery/020_tool_aliases.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,11 +43,13 @@ The exact original patch is the complete diff `git show c8240c51d664f7cfb790b6d6
Current anchors: `src/adapters/openai-chat.ts:1661` pending interface, `:1856` identity lookup, `:1873` budget opening, `:1912` argument-byte accounting, `:1679` budget closing. Replace the lookup block with:

```ts
if (typeof rawIndex === "number"
&& (!Number.isInteger(rawIndex) || rawIndex < 0)) {
if (rawIndex !== undefined && rawIndex !== null
&& (typeof rawIndex !== "number"
|| !Number.isSafeInteger(rawIndex)
|| rawIndex < 0)) {
return yield* terminateWithError({
...invalidToolCallsEvent(rawToolCalls, "stream", pendingUsage),
message: "upstream response contained invalid tool calls (invalid numeric index)",
message: "upstream response contained invalid tool calls (invalid index)",
});
}
const indexKey = typeof rawIndex === "number" ? `i:${rawIndex}` : undefined;
Expand Down Expand Up @@ -121,4 +123,12 @@ The implementation cycle certifies its published current-head candidate. Every d

## External review amendment: numeric index contract

Only non-negative integer indexes may become an alias. Immediately after reading rawIndex, if it is numeric but not an integer or is negative, terminate through the existing invalidToolCallsEvent/terminateWithError path; do not treat an invalid numeric index as absent and append its data to the last pending call. Other tolerated placeholder fields retain their existing rules. Add reachable negative/fractional numeric-index regressions with two distinct pending calls: one error, no done, no fragment reassignment, and all budget reservations released. Preserve all original positive and collision cases. This is an explicit source-patch amendment, not a claim the original commit already implements validation.
Only non-negative safe-integer indexes may become an alias. Immediately after reading rawIndex, if it is numeric but not an integer or is negative, terminate through the existing invalidToolCallsEvent/terminateWithError path; do not treat an invalid numeric index as absent and append its data to the last pending call. Other tolerated placeholder fields retain their existing rules. Add reachable negative/fractional numeric-index regressions with two distinct pending calls: one error, no done, no fragment reassignment, and all budget reservations released. Preserve all original positive and collision cases. This is an explicit source-patch amendment, not a claim the original commit already implements validation.

## Safe-integer review repair

The numeric guard uses Number.isSafeInteger: parsed indices beyond the safe range can already have lost identity precision. Add a raw-wire regression containing distinct large integer literals (not JS values rounded before serialization), and retain a positive MAX_SAFE_INTEGER boundary. Capture error/no tool success plus existing reservation-release coverage. The correction must be verified in this same unit; no original source tests are removed.

## Claimed-type boundary update

022 supersedes the earlier non-numeric-index tolerance assumption: only missing/null indexes are absent. Every other claimed value must be a non-negative safe integer; no coercion of strings/objects/bools/arrays. Repeated ID/name/argument-field tolerance is unchanged.
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Tool-call alias cycle P refresh

Historical refresh: its non-numeric-index policy is superseded by the explicit null/missing boundary in 022_index_type_repair.md.

Current parent: 22da7a4bc80040f66b819239c5028e578f9a1ede, after TOML delivery. Original source c8240c51d664f7cfb790b6d60679adfe0490b5c9 remains open and authored by Hako. Relevant baseline comparison is retained in scratch; implementation uses the current tree and preserves adjacent changes.

Apply the original commit, then the independently reviewed 020 numeric-index amendment. Missing/non-numeric placeholders keep existing tolerance; negative/fractional numeric indexes terminate before matching. Preserve the immutable reservation key and first observed valid index alias. Add direct malformed-index activation coverage alongside all original positive/collision/UTF-8 budget cases. Update the transport structure contract as planned.

Main owns cherry-pick/commits/PR/CI/merge. An inherited worker may edit only src/adapters/openai-chat.ts, tests/adapters/openai/openai-chat-parallel-stream.test.ts, and structure/04_transports-and-sidecars.md after A passes. Main owns this document and all other files. Independent reviewer checks resulting code; all tests/typechecks execute remotely or in GitHub Actions. Full-suite readiness remains remote; no local application checks. macmini shared test lock is respected.
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# 022 — Reject claimed invalid index types

## Loop specification

Class C2/C3 bounded parent repair. Source: current #3702 at d6bfb044a; late reviews PRRT_kwDOS-0Gi86fl4vM and fl4vC. Goal: a present invalid index cannot be mistaken for an absent index and routed to the last pending call. Non-goals: changing repeated ID/name/argument placeholder tolerance, parsing numeric strings, new adapters or unrelated Logs work. Remote/CI verification only; no local tests/typecheck. Same session resource bounds apply. Main owns Git/FSM/integration; one worker may edit only the adapter, its parallel-stream test and structure04. Main reclaims after two failed delegates.

This additive repair preempts unfinished Logs planning. No previous work-phase completion marks or final criteria were removed. Detailed review synthesis is in scratch. Resume Logs after this full cycle and cascade.

## Exact change map

MODIFY src/adapters/openai-chat.ts, before all key matching:

```ts
if (rawIndex !== undefined && rawIndex !== null
&& (typeof rawIndex !== "number"
|| !Number.isSafeInteger(rawIndex)
|| rawIndex < 0)) {
return yield* terminateWithError({
...invalidToolCallsEvent(rawToolCalls, "stream", pendingUsage),
message: "upstream response contained invalid tool calls (invalid index)",
});
}
```

Before: only invalid numbers reject; present strings/objects/bools become no indexKey and may select the last pending call. After: only missing/null is absent; every other claimed index must be a non-negative safe integer. The existing terminateWithError closes all budget reservations before the error is yielded. Keep the alias/key precedence and immutable reservation keys unchanged. No new fields/enums/dependencies.

MODIFY tests/adapters/openai/openai-chat-parallel-stream.test.ts: retain all Hako and safe-integer cases. Update expected diagnostic wording. Add labeled table cases for numeric string, empty string, true/false, object and array, with pending complete JSON calls so a silent fallback could otherwise produce success; assert one terminal502, no tool/done event and released reservations. Include explicit missing/null positive continuation through a later valid numeric alias. Use tuple wrappers for array-valued cases so test.each cannot mistake an index array for argument tuples.

MODIFY docs-site/src/content/docs/reference/adapters.md: specify non-negative safe integers; explicitly reject non-numeric values and negative/fractional/unsafe numbers; missing/null remain absent-index placeholders. Do not call valid JSON numbers malformed JSON.

MODIFY structure/04_transports-and-sidecars.md: align the same index contract and source/test ownership.

MODIFY 020_tool_aliases.md: carry the corrected guard and compatibility boundary. Annotate 021's former non-numeric-placeholder policy as superseded by this repair; retain its historical source snapshot.

## Verification and exit

- Independent plan and implementation review; original source authorship retained.
- Exact-head pinned remote typecheck/full suite/docs build, hosted CI registration and no unresolved findings. Full final integrated CI remains mandatory under c-2; build readiness is not merge permission.
- Existing numeric/unsafe/UTF-8/collision cases remain green; new claimed-type cases actually observe pending allocations before early failure, and null/missing positive cases still assemble one correct tool.
- Cascade new parent into Cursor with a merge preserving both authors' commits and both structure sections; fast-forward the still-unpublished Logs branch to updated Cursor. Verify both ancestry edges. Do not mark the updated Cursor head verified until its own new evidence exists.
- Main returns to parent for the repair receipt/D, then resumes original Logs planning. Shipping #3702 still requires strict merge verification and actual dev ancestry before source #3673 closes.
9 changes: 9 additions & 0 deletions docs-site/src/content/docs/reference/adapters.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,15 @@ provider — xAI, Kimi, DeepSeek, GLM, Groq, OpenRouter, Ollama (local), and mor
tiers, accepts reasoning deltas from either `delta.reasoning_content` or `delta.reasoning`, requests
streamed usage with `stream_options.include_usage`, and reads usage from non-stream response envelopes.

Streaming tool calls retain their identity when a provider first sends an ID,
then associates that ID with an index, and later sends index-only argument
fragments. Those fragments assemble into one call with the original name and
complete arguments; parallel calls retain separate identities.
When present, streamed tool-call indexes must be non-negative safe integers. Non-numeric
values and negative, fractional, or unsafe numbers terminate the stream with an upstream
error before identity matching. Missing and null indexes remain absent-index placeholders;
numeric strings are not coerced.

## `ollama-native`

**Targets:** Ollama's own **Chat API** (`POST /api/chat`) rather than its OpenAI-compatible
Expand Down
29 changes: 23 additions & 6 deletions src/adapters/openai-chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1660,6 +1660,7 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd
let bufferBytes = 0;
interface PendingToolCall {
key: string;
indexKey?: string;
id: string;
name: string;
args: string;
Expand Down Expand Up @@ -1848,17 +1849,29 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd
const rawId = rawToolCall.id;
const idDelta = typeof rawId === "string" ? rawId : "";
const rawIndex = rawToolCall.index;
// Only missing/null indexes are absent; every claimed index must be valid.
// Unsafe integers can collapse distinct wire indexes onto the same JS number.
// Reject before an alias can bind or any pending call can consume the fragment.
if (rawIndex !== undefined && rawIndex !== null
&& (typeof rawIndex !== "number"
|| !Number.isSafeInteger(rawIndex)
|| rawIndex < 0)) {
return yield* terminateWithError({
...invalidToolCallsEvent(rawToolCalls, "stream", pendingUsage),
message: "upstream response contained invalid tool calls (invalid index)",
});
}

// Resolve the pending call BEFORE judging the fields. Some OpenAI-compatible
// Resolve the pending call BEFORE judging repeated string fields. Some OpenAI-compatible
// streamers repeat an already-sent field as a non-string placeholder on a
// continuation delta; judging first meant the whole stream died with a 502 even
// though the value being repeated was already held in canonical form.
const key = typeof rawIndex === "number"
? `i:${rawIndex}`
: idDelta
? `id:${idDelta}`
: pendingToolCalls[pendingToolCalls.length - 1]?.key;
const indexKey = typeof rawIndex === "number" ? `i:${rawIndex}` : undefined;
const key = indexKey ?? (idDelta
? `id:${idDelta}`
: pendingToolCalls[pendingToolCalls.length - 1]?.key);
let call = key !== undefined ? pendingToolCalls.find(c => c.key === key) : undefined;
if (!call && indexKey !== undefined) call = pendingToolCalls.find(c => c.indexKey === indexKey);
if (!call && idDelta) call = pendingToolCalls.find(c => c.id === idDelta);
if (!call) {
call = {
Expand All @@ -1872,6 +1885,10 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd
pendingToolCalls.push(call);
budget.openCall(call.key);
}
// An ID-only call may learn its index from a later ID+index fragment. Retain that
// alias without changing the key that owns its argument budget. Only the first
// observed index binds: a repeated ID on a different index must not alias both.
if (indexKey !== undefined && call.indexKey === undefined) call.indexKey = indexKey;

// Tolerance is per FIELD, keyed on that field's own provenance. A canonical name
// says nothing about whether `arguments` was ever sent as a string, so it cannot
Expand Down
22 changes: 22 additions & 0 deletions structure/04_transports-and-sidecars.md
Original file line number Diff line number Diff line change
Expand Up @@ -1506,6 +1506,28 @@ shares the 12-image active cap. Bounded source labels are emitted in active user
root pruning cannot erase attachment provenance; the same text participates in token estimation.
Native Composer/MCP behavior and text-only historical replay remain unchanged.

## Chat streamed tool-call identity

`src/adapters/openai-chat.ts` retains a call's first observed non-negative safe integer
index as an alias when the call started by ID. Every present, non-null index must
be a number in that range: strings (including numeric and empty strings), booleans,
objects, arrays, negative numbers, fractions and unsafe integers terminate the stream
before any key, alias, ID or last-call matching. `Number.MAX_SAFE_INTEGER` is accepted;
larger integers are rejected because distinct wire literals can parse to the same number.
The invalid-index error releases all pending call reservations without emitting
those calls or a successful completion; invalid indexes are never treated as absent.
Only missing and null indexes are absent-index placeholders. Repeated ID, name and
argument string-field tolerance retains its existing rules.

For valid indexes, lookup preserves direct-key precedence, then index alias, then
ID fallback. The initial key continues to own all translator budget reservations
and release; learning an alias creates no additional owner. Unassociated index-only
fragments are not guessed onto pending ID-only calls.
`tests/adapters/openai/openai-chat-parallel-stream.test.ts` covers late aliases,
parallel/colliding identities, distinct unsafe raw JSON index literals, the maximum
safe-integer boundary, invalid index types, missing/null continuations and UTF-8
byte-limit boundaries.

## Sidecars

Web search and vision sidecars run only when the main request needs that capability and a usable
Expand Down
Loading
Loading