diff --git a/CHANGELOG.md b/CHANGELOG.md index 17d774c8f1..ae1fa2f071 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,6 +29,7 @@ - Discord: avoid category parent overrides for channel allowlists and refactor thread context helpers. (#588) — thanks @steipete - Discord: fix forum thread starters and cache channel lookups for thread context. (#585) — thanks @thewilloftheshadow - Discord: log gateway disconnect/reconnect events at info and add verbose gateway metrics. (#595) — thanks @steipete +- Signal: match own-mode reactions when target includes uuid + phone. (#632) — thanks @neist - Commands: accept /models as an alias for /model. - Commands: add `/usage` as an alias for `/status`. (#492) — thanks @lc0rp - Models/Auth: add MiniMax Anthropic-compatible API onboarding (minimax-api). (#590) — thanks @mneves75 diff --git a/patches/@buape__carbon.patch b/patches/@buape__carbon.patch index 5cb5a2353d..9873963279 100644 --- a/patches/@buape__carbon.patch +++ b/patches/@buape__carbon.patch @@ -1,6 +1,8 @@ +diff --git a/dist/src/classes/RequestClient.js b/dist/src/classes/RequestClient.js +index 4a3357a..1cd3130 100644 --- a/dist/src/classes/RequestClient.js +++ b/dist/src/classes/RequestClient.js -@@ -86,6 +86,9 @@ +@@ -118,6 +118,9 @@ export class RequestClient { } } this.abortController = new AbortController(); @@ -10,7 +12,7 @@ let body; if (data?.body && typeof data.body === "object" && -@@ -146,12 +149,26 @@ +@@ -178,12 +181,26 @@ export class RequestClient { body = JSON.stringify(data.body); } } @@ -40,6 +42,6 @@ + clearTimeout(timeoutId); + } + } - if (response.status === 429) { - const responseBody = await response.json(); - const rateLimitError = new RateLimitError(response, responseBody); + let rawBody = ""; + let parsedBody; + try { diff --git a/patches/@mariozechner__pi-ai.patch b/patches/@mariozechner__pi-ai.patch index 4ca0a2e45e..465ad4f3f8 100644 --- a/patches/@mariozechner__pi-ai.patch +++ b/patches/@mariozechner__pi-ai.patch @@ -1,8 +1,8 @@ diff --git a/dist/providers/google-gemini-cli.js b/dist/providers/google-gemini-cli.js -index 0000000..1111111 100644 +index 93aa26c..7d47d76 100644 --- a/dist/providers/google-gemini-cli.js +++ b/dist/providers/google-gemini-cli.js -@@ -248,6 +248,12 @@ async function* streamGeminiCli(model, context, credentials, options) { +@@ -248,6 +248,12 @@ export const streamGoogleGeminiCli = (model, context, options) => { break; // Success, exit retry loop } const errorText = await response.text(); @@ -12,15 +12,34 @@ index 0000000..1111111 100644 + console.log(`[pi-ai] 429 rate limit - failing fast to rotate account`); + throw new Error(`Cloud Code Assist API error (${response.status}): ${errorText}`); + } - // Check if retryable - if (attempt < MAX_RETRIES && isRetryableError(response.status, errorText)) { - // Use server-provided delay or exponential backoff - + // Check if retryable + if (attempt < MAX_RETRIES && isRetryableError(response.status, errorText)) { + // Use server-provided delay or exponential backoff +diff --git a/dist/providers/openai-codex-responses.js b/dist/providers/openai-codex-responses.js +index 188a829..20b3a32 100644 +--- a/dist/providers/openai-codex-responses.js ++++ b/dist/providers/openai-codex-responses.js +@@ -433,9 +433,15 @@ function convertMessages(model, context) { + } + else if (msg.role === "assistant") { + const output = []; ++ // OpenAI Responses rejects `reasoning` items that are not followed by a `message`. ++ // Tool-call-only turns (thinking + function_call) are valid assistant turns, but ++ // their stored reasoning items must not be replayed as standalone `reasoning` input. ++ const hasTextBlock = msg.content.some((b) => b.type === "text"); + for (const block of msg.content) { + if (block.type === "thinking" && msg.stopReason !== "error") { + if (block.thinkingSignature) { ++ if (!hasTextBlock) ++ continue; + const reasoningItem = JSON.parse(block.thinkingSignature); + output.push(reasoningItem); + } diff --git a/dist/providers/openai-responses.js b/dist/providers/openai-responses.js -index 0000000..1111111 100644 +index 20fb0a2..1f7cdd1 100644 --- a/dist/providers/openai-responses.js +++ b/dist/providers/openai-responses.js -@@ -397,9 +397,17 @@ function convertMessages(model, context) { +@@ -396,10 +396,16 @@ function convertMessages(model, context) { } else if (msg.role === "assistant") { const output = []; @@ -37,28 +56,3 @@ index 0000000..1111111 100644 const reasoningItem = JSON.parse(block.thinkingSignature); output.push(reasoningItem); } - } - else if (block.type === "text") { - -diff --git a/dist/providers/openai-codex-responses.js b/dist/providers/openai-codex-responses.js -index 0000000..1111111 100644 ---- a/dist/providers/openai-codex-responses.js -+++ b/dist/providers/openai-codex-responses.js -@@ -434,9 +434,17 @@ function convertMessages(model, context) { - } - else if (msg.role === "assistant") { - const output = []; -+ // OpenAI Responses rejects `reasoning` items that are not followed by a `message`. -+ // Tool-call-only turns (thinking + function_call) are valid assistant turns, but -+ // their stored reasoning items must not be replayed as standalone `reasoning` input. -+ const hasTextBlock = msg.content.some((b) => b.type === "text"); - for (const block of msg.content) { - if (block.type === "thinking" && msg.stopReason !== "error") { - if (block.thinkingSignature) { -+ if (!hasTextBlock) -+ continue; - const reasoningItem = JSON.parse(block.thinkingSignature); - output.push(reasoningItem); - } - } - else if (block.type === "text") { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 31c36f91f6..6784900f0c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -9,13 +9,13 @@ overrides: patchedDependencies: '@buape/carbon': - hash: 85885a1d47a37ae00bcd21f2efbeb025284ea98981c300f095fb94c0604ff9ac + hash: 35533fc422c2bdc75a3171794bf56af2f46a7e6b29a6c9d11955209b4378eab7 path: patches/@buape__carbon.patch '@mariozechner/pi-agent-core': hash: 01312ceb1f6be7e42822c24c9a7a4f7db56b24ae114a364855bd3819779d1cf4 path: patches/@mariozechner__pi-agent-core.patch '@mariozechner/pi-ai': - hash: 3f4c1f943c57dbe2980bf21b1768dc780355f9124eeffbc30b5d5e42d2ea4b7c + hash: 24a435c06627b93fb5b0eff150d2075b5f5dd1db66dd8ea96eb366e6999be711 path: patches/@mariozechner__pi-ai.patch '@mariozechner/pi-coding-agent': hash: 58af7c712ebe270527c2ad9d3351fac39d6cd4b81cc475a258d87840b446b90e @@ -45,7 +45,7 @@ importers: version: 0.42.1(patch_hash=01312ceb1f6be7e42822c24c9a7a4f7db56b24ae114a364855bd3819779d1cf4)(ws@8.19.0)(zod@4.3.5) '@mariozechner/pi-ai': specifier: ^0.42.1 - version: 0.42.1(patch_hash=3f4c1f943c57dbe2980bf21b1768dc780355f9124eeffbc30b5d5e42d2ea4b7c)(ws@8.19.0)(zod@4.3.5) + version: 0.42.1(patch_hash=24a435c06627b93fb5b0eff150d2075b5f5dd1db66dd8ea96eb366e6999be711)(ws@8.19.0)(zod@4.3.5) '@mariozechner/pi-coding-agent': specifier: ^0.42.1 version: 0.42.1(patch_hash=58af7c712ebe270527c2ad9d3351fac39d6cd4b81cc475a258d87840b446b90e)(ws@8.19.0)(zod@4.3.5) @@ -3786,7 +3786,7 @@ snapshots: '@mariozechner/pi-agent-core@0.42.1(patch_hash=01312ceb1f6be7e42822c24c9a7a4f7db56b24ae114a364855bd3819779d1cf4)(ws@8.19.0)(zod@4.3.5)': dependencies: - '@mariozechner/pi-ai': 0.42.1(patch_hash=3f4c1f943c57dbe2980bf21b1768dc780355f9124eeffbc30b5d5e42d2ea4b7c)(ws@8.19.0)(zod@4.3.5) + '@mariozechner/pi-ai': 0.42.1(patch_hash=24a435c06627b93fb5b0eff150d2075b5f5dd1db66dd8ea96eb366e6999be711)(ws@8.19.0)(zod@4.3.5) '@mariozechner/pi-tui': 0.42.1 transitivePeerDependencies: - '@modelcontextprotocol/sdk' @@ -3796,7 +3796,7 @@ snapshots: - ws - zod - '@mariozechner/pi-ai@0.42.1(patch_hash=3f4c1f943c57dbe2980bf21b1768dc780355f9124eeffbc30b5d5e42d2ea4b7c)(ws@8.19.0)(zod@4.3.5)': + '@mariozechner/pi-ai@0.42.1(patch_hash=24a435c06627b93fb5b0eff150d2075b5f5dd1db66dd8ea96eb366e6999be711)(ws@8.19.0)(zod@4.3.5)': dependencies: '@anthropic-ai/sdk': 0.71.2(zod@4.3.5) '@google/genai': 1.34.0 @@ -3820,7 +3820,7 @@ snapshots: dependencies: '@mariozechner/clipboard': 0.3.0 '@mariozechner/pi-agent-core': 0.42.1(patch_hash=01312ceb1f6be7e42822c24c9a7a4f7db56b24ae114a364855bd3819779d1cf4)(ws@8.19.0)(zod@4.3.5) - '@mariozechner/pi-ai': 0.42.1(patch_hash=3f4c1f943c57dbe2980bf21b1768dc780355f9124eeffbc30b5d5e42d2ea4b7c)(ws@8.19.0)(zod@4.3.5) + '@mariozechner/pi-ai': 0.42.1(patch_hash=24a435c06627b93fb5b0eff150d2075b5f5dd1db66dd8ea96eb366e6999be711)(ws@8.19.0)(zod@4.3.5) '@mariozechner/pi-tui': 0.42.1 chalk: 5.6.2 cli-highlight: 2.1.11 diff --git a/signal-reaction-debug.md b/signal-reaction-debug.md deleted file mode 100644 index 79999f09ee..0000000000 --- a/signal-reaction-debug.md +++ /dev/null @@ -1,28 +0,0 @@ -Signal reaction notifications ("own" mode) investigation - -Context -- Code path: `src/signal/monitor.ts` handles reaction-only envelopes and calls `enqueueSystemEvent`. -- Default mode is "own" in config, which should notify when someone reacts to a message authored by the bot account. - -Findings -- Signal reaction handling only runs when `envelope.reactionMessage` is present and `dataMessage` is absent. If signal-cli includes `reactionMessage` alongside `dataMessage`, the reaction is ignored because the handler only runs in the `reactionMessage && !dataMessage` branch. -- `resolveSignalReactionTarget()` prefers `targetAuthorUuid` over `targetAuthor`. If signal-cli includes both (common for sender identity fields), the target becomes `kind: "uuid"`, even when a phone number is also present. -- In "own" mode, `shouldEmitSignalReactionNotification()` compares the configured `signal.account` string against the target. With a phone-configured account (e.g., `+14154668323`) and a UUID target, the check fails. -- The tests in `src/signal/monitor.tool-result.test.ts` only cover reaction payloads with `targetAuthor` (phone), so UUID-first handling is not exercised. -- Discord "own" mode compares `messageAuthorId` to `botUserId` (`shouldEmitDiscordReactionNotification`), which is strictly an ID match. The Signal implementation mirrors this pattern, but the target identity type mismatch (UUID vs E.164) breaks the comparison. - -Likely root cause -- Signal-cli reaction payloads appear to include `targetAuthorUuid` even when `targetAuthor` (phone) is present. Because `resolveSignalReactionTarget()` always prefers UUID, "own" mode never matches when `signal.account` is configured as E.164, causing no notification. - -Secondary risk -- If signal-cli includes `reactionMessage` alongside `dataMessage` (instead of reaction-only envelopes), the current handler never emits system events for reactions. - -Debug logging to confirm (if needed) -- Add a verbose log around the reaction handler to capture the identity fields and decision: - - `targetAuthor`, `targetAuthorUuid`, computed `target.kind/id`, `account`, `mode`, and `shouldNotify`. - - Log when `reactionMessage` is present but `dataMessage` is also present to verify whether reactions arrive as combined payloads. - -Suggested fixes (not applied) -- When mode is "own", compare the configured account against both `targetAuthor` (normalized E.164) and `targetAuthorUuid`, rather than selecting UUID first. -- Or, in `resolveSignalReactionTarget()`, if both values exist and the configured account is a phone number, prefer `targetAuthor` over UUID for the "own" check. -- Consider emitting reaction notifications even if `reactionMessage` and `dataMessage` coexist (guarded to avoid double-processing). diff --git a/src/agents/pi-embedded-runner.ts b/src/agents/pi-embedded-runner.ts index d3a73a0026..7d923bf6a3 100644 --- a/src/agents/pi-embedded-runner.ts +++ b/src/agents/pi-embedded-runner.ts @@ -759,7 +759,6 @@ export async function compactEmbeddedPiSession(params: { const enqueueGlobal = params.enqueue ?? ((task, opts) => enqueueCommandInLane(globalLane, task, opts)); - const runAbortController = new AbortController(); return enqueueCommandInLane(sessionLane, () => enqueueGlobal(async () => { const resolvedWorkspace = resolveUserPath(params.workspaceDir); diff --git a/src/auto-reply/reply/abort.ts b/src/auto-reply/reply/abort.ts index 3543cf7399..eb31ae0f78 100644 --- a/src/auto-reply/reply/abort.ts +++ b/src/auto-reply/reply/abort.ts @@ -6,7 +6,6 @@ import { resolveStorePath, type SessionEntry, saveSessionStore, - type SessionEntry, } from "../../config/sessions.js"; import { parseAgentSessionKey } from "../../routing/session-key.js"; import { resolveCommandAuthorization } from "../command-auth.js";