feat(gateway): add optional role param to chat.inject RPC#60706
feat(gateway): add optional role param to chat.inject RPC#60706donut-wenzhang wants to merge 34 commits into
Conversation
Allow overriding the Telegram Bot API base URL via `channels.telegram.apiRoot`, enabling custom Bot API servers and bot routing proxies. Co-Authored-By: Claude Opus 4.6 <[email protected]>
Co-Authored-By: Claude Opus 4.6 <[email protected]>
…ons (#1) Allow hook API callers to pass sessionTarget="reuse" so that multiple hook invocations with the same sessionKey share the same agent session, enabling multi-turn conversation context retention. Previously sessionTarget was hardcoded to "isolated" for all hook calls, meaning every invocation created a brand new session regardless of sessionKey. With "reuse", the existing session freshness evaluation is used to determine whether to continue an existing session. Changes: - Add HookSessionTarget type and sessionTarget field to hook payload - Pass sessionTarget through to CronJob creation (default: "isolated") - Add "reuse" to CronSessionTarget union type across all validators - Add "reuse" to CronSessionTargetSchema for gateway cron API - Add assertSupportedJobSpec validation for "reuse" + "agentTurn" - Allow channel delivery for "reuse" sessions (same as "isolated") Co-authored-by: Claude Opus 4.6 <[email protected]>
When skills/ changes on main, triggers openclaw-updated repository_dispatch to DonutLabs-ai/donut-backend, which builds a skills tarball and hot-updates running D0 Gateway containers without restart. Co-authored-by: Regison <[email protected]> Co-authored-by: Claude Opus 4.6 <[email protected]>
…spaceFiles Allow operators to extend the built-in workspace file whitelist (bootstrap + memory files) by setting agents.defaults.extraWorkspaceFiles in openclaw.json. Co-Authored-By: Claude Opus 4.6 <[email protected]>
When senderId or senderUsername is available in the agent run context, inject X-TG-User-Id and X-TG-Username headers into outgoing LLM requests. This allows downstream proxies to attribute token usage to individual end users (e.g. Telegram senders). Co-authored-by: Claude Opus 4.6 <[email protected]>
Add two new streamFn wrappers that inject metadata into every outbound
LLM request so the proxy can apply quota and usage tracking:
- trigger_source: indicates what initiated the agent run ("user", "cron",
"heartbeat"), derived from the existing PluginHookAgentContext.trigger
field that is already set by cron jobs and user message handlers.
- turn_id: a per-turn UUID generated before activeSession.prompt(), shared
across all LLM calls within a single agent turn so the proxy can
correlate multi-call agent loops back to one user message.
Both values are injected via HTTP headers (X-Trigger-Source, X-Turn-Id)
and request body fields (trigger_source, turn_id) for maximum proxy
compatibility regardless of transport.
Co-authored-by: Regison <[email protected]>
Merged via squash. Prepared head SHA: e4183b3 Co-authored-by: Sid-Qin <[email protected]> Co-authored-by: shakkernerd <[email protected]> Reviewed-by: @shakkernerd
* feat(telegram): add apiRoot config passthrough to grammY Allow overriding the Telegram Bot API base URL via `channels.telegram.apiRoot`, enabling custom Bot API servers and bot routing proxies. Co-Authored-By: Claude Opus 4.6 <[email protected]> * docs(telegram): document apiRoot config option Co-Authored-By: Claude Opus 4.6 <[email protected]> * feat(hooks): add sessionTarget="reuse" for multi-turn hook conversations (#1) Allow hook API callers to pass sessionTarget="reuse" so that multiple hook invocations with the same sessionKey share the same agent session, enabling multi-turn conversation context retention. Previously sessionTarget was hardcoded to "isolated" for all hook calls, meaning every invocation created a brand new session regardless of sessionKey. With "reuse", the existing session freshness evaluation is used to determine whether to continue an existing session. Changes: - Add HookSessionTarget type and sessionTarget field to hook payload - Pass sessionTarget through to CronJob creation (default: "isolated") - Add "reuse" to CronSessionTarget union type across all validators - Add "reuse" to CronSessionTargetSchema for gateway cron API - Add assertSupportedJobSpec validation for "reuse" + "agentTurn" - Allow channel delivery for "reuse" sessions (same as "isolated") Co-authored-by: Claude Opus 4.6 <[email protected]> * feat(skills): scan sub-skills from index.md for hierarchical display Add scanSubSkills() to skills-status that reads subdirectory index.md files under each skill's baseDir. Router skills (SKILL.md) now include a children array in the skills.status RPC response, enabling the HIL Dashboard to display skills in a categorized hierarchy. Co-Authored-By: Claude Opus 4.6 <[email protected]> --------- Co-authored-by: arucil <[email protected]> Co-authored-by: Claude Opus 4.6 <[email protected]>
* feat(agents): inject feature_context and skill_names into LLM proxy requests Add onPayload wrapper that automatically classifies each LLM API call: - chat_response: user message triggered the call - tool_result: processing tool execution results - system: fallback for unclassifiable calls Also injects active skill_names array for per-skill token attribution. Both fields are read by the donut-backend LLM proxy for analytics. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * refactor(agents): remove client-side feature_context, keep skill_names only feature_context is now inferred server-side by the donut-backend LLM proxy from the messages array. Remove the client-side inference logic and rename the wrapper to createSkillNamesWrapper since that is its only remaining responsibility. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> --------- Co-authored-by: Regison <[email protected]> Co-authored-by: Claude Opus 4.6 (1M context) <[email protected]>
* feat: add triggerSource to GetReplyOptions Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * feat: use triggerSource from opts in agent runner trigger assignment Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * feat: read x-trigger-source header and attach to webhook update body Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * feat: extract _triggerSource from grammy update in message context Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * feat: thread triggerSource from context into replyOptions Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> --------- Co-authored-by: Claude Opus 4.6 (1M context) <[email protected]>
buildSyntheticContext() creates a new object with only {message, me,
getFile}, discarding ctx.update. So _triggerSource on the update root
was always lost. Fix by attaching it to body.value.message and reading
from primaryCtx.message in the context builder.
…jection (#10) The `createSkillNamesWrapper` was injecting `skill_names` into the LLM request body via `onPayload`. When the gateway sends requests directly to Anthropic (bypassing the LLM proxy), Anthropic rejects the unknown body field with "Extra inputs are not permitted". Switch to injecting via `X-Skill-Names` HTTP header instead, which is safely ignored by strict APIs. The donut-backend LLM proxy will be updated to read from this header (with body field as legacy fallback). Co-authored-by: Claude Opus 4.6 <[email protected]>
…_id wrappers (#11) Same issue as skill_names — strict APIs (Anthropic) reject unknown body fields. Both wrappers already inject via HTTP headers (X-Trigger-Source, X-Turn-Id), so the redundant onPayload body injection is removed. Co-authored-by: Claude Opus 4.6 <[email protected]>
…sion (TES-642) (#12) Cherry-pick upstream 7f2708a — fixes the core issue where messagingToolSentTargets with missing provider/to params caused shouldSuppressMessagingToolReplies to return false, leaking all assistant text blocks to Telegram. Key changes: - resolveTargetProviderForComparison: falls back to current provider when target provider is empty or "message" - targetsMatchForSuppression: Telegram-aware chatId+threadId matching - mergeDeliveryContext: prevents cross-channel route field inheritance - chat.ts: only inherit delivery routes for channel-scoped sessions Co-authored-by: Claude Opus 4.6 (1M context) <[email protected]>
) * fix: suppress block reply for text+toolCall mixed messages (TES-642) When an assistant message contains both text and tool call blocks, the text is intermediate reasoning (e.g. "Let me read the skill...") that should NOT be forwarded to the user. Real user-facing replies go through the messaging tool explicitly. Add hasToolCall check in handleMessageEnd to skip block reply emission when the message content includes toolCall or tool_use blocks. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * fix: track message tool sends without explicit target for suppression (TES-642) When the message tool is called without to/target (e.g. Telegram DMs where there's only one chat), extractMessagingToolSend returned undefined — so no target was tracked in messagingToolSentTargets. This caused shouldSuppressMessagingToolReplies to return false (empty targets array), allowing ALL assistant text blocks to leak as final payloads to Telegram. Fix: - extractMessagingToolSend: return { to: "" } instead of undefined when to/target is missing - shouldSuppressMessagingToolReplies: treat empty to as "matches origin target" since it means the message was implicitly sent to the originating conversation Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * fix(telegram): delete draft preview on tool start to prevent reasoning leak (TES-642) When the agent streams intermediate reasoning text ("Let me fetch data..."), the Telegram draft preview shows it as a live-updating message. Previously, these previews were only archived on the next assistant message start and deleted in the finally block — meaning users saw all intermediate thinking during the agent loop. Now when tool execution starts, the onToolStart callback immediately deletes the draft preview message. This is correct because: - Text before a tool call is always intermediate reasoning - The actual user-facing response comes via the message() tool - The draft preview for reasoning text should never persist Also made onToolStart always available (not gated on statusReactionController) so the cleanup works even without reaction support. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> --------- Co-authored-by: Claude Opus 4.6 (1M context) <[email protected]>
…nchain [codex] Gateway: inject donut runtime analytics IDs
…eded (#17) * feat(heartbeat): add beforeRun gate to skip LLM calls when no work needed Add a `beforeRun` config option that executes a user-supplied script before each heartbeat LLM call. If the script exits non-zero, the heartbeat is skipped with zero token cost. Bypassed for cron, exec-event, and wake-triggered heartbeats to ensure time-sensitive events always run. Made-with: Cursor * fix(heartbeat): use async execFile and improve beforeRun error handling - Replace execFileSync with async execFile to avoid blocking the event loop - Distinguish timeout, ENOENT, and intentional rejection in error logging - Strengthen workspace dir test assertion to verify actual path - Add test for non-existent script path (ENOENT) Made-with: Cursor
* feat(cron): add beforeRun gate to skip LLM for cron jobs Add a `beforeRun` field to CronJobBase that accepts a script path. The script runs before the LLM session — exit 0 proceeds, non-zero skips the entire LLM call (zero token cost). This allows cron jobs to run data scripts deterministically and only invoke the LLM when there is meaningful data to format and deliver. Changes: - types-shared.ts: add optional `beforeRun` field - normalize.ts: trim/validate beforeRun during normalization - service/jobs.ts: handle beforeRun in createJob and applyJobPatch - service/timer.ts: execute beforeRun gate before runIsolatedAgentJob with 120s timeout, tilde expansion, and structured logging Made-with: Cursor * test(cron): add unit tests for beforeRun gate 10 test cases covering: - Gate script exit 0 → LLM proceeds - Gate script exit 1/2 → LLM skipped - Missing script → LLM skipped (logged as rejection) - No beforeRun configured → normal execution - Job id passed as $1 argument - CRON_JOB_ID env var set - Logging: pass/reject/not-found Made-with: Cursor * fix(cron): use err.code instead of err.status for exit code logging execFile error uses `code` (number for exit codes, string for system errors like ENOENT), not `status`. The previous code always logged undefined for the exit code in gate rejection messages. Made-with: Cursor * fix(cron): normalize beforeRun field during store deserialization Add beforeRun normalization in store.ts ensureLoaded loop, consistent with how description and sessionKey are handled. Prevents malformed beforeRun values (whitespace, wrong type) from persisting in the store file. Made-with: Cursor
…tlabs [codex] Report TG tool lifecycle to backend
The cron system fully supports beforeRun (storage, execution, update patching) but CronAddParamsSchema was missing the field. Because additionalProperties is false, passing beforeRun to cron.add was rejected with "unexpected property 'beforeRun'". This forced a workaround of add + update to set the gate, which is error-prone for LLM agents creating dynamic cron jobs (e.g. price alerts). Adding beforeRun to CronCommonOptionalFields makes it available in both add and patch schemas. Co-authored-by: Claude Opus 4.6 <[email protected]>
…nchain feat(observability): emit D0 runtime run traces (PRODUCT-2628)
[codex] add tg tool detail analytics
When the LLM outputs NO_REPLY (a silent control token), the streaming partial "NO" enters the draft buffer. On finalization, stop() flushed this text to Telegram before clear() deleted it, causing a visible flash. Skip stop() when shouldClear=true (no final content delivered) and call clear() directly, which abandons pending content without flushing. Co-authored-by: Claude Opus 4.6 (1M context) <[email protected]>
Block reply messages (sent via deliverReplies) were not tracked after
delivery, so onToolStart could not delete them. This caused intermediate
reasoning text ("Let me load the skill...") to leak as visible TG
messages.
- Add onMessageSent callback to deliverReplies/deliverTextReply to
track sent message IDs
- In onToolStart, delete all pending block reply messages before
processing the tool (mirrors existing preview deletion logic)
Co-authored-by: Claude Opus 4.6 (1M context) <[email protected]>
When apiRoot is configured (e.g. for TG proxy mode), file downloads still hardcoded https://api.telegram.org, causing 404s with virtual tokens. Now downloadAndSaveTelegramFile respects apiRoot, and SSRF policy dynamically includes the proxy hostname. Co-authored-by: Claude Opus 4.6 <[email protected]>
Telegram webhook channels suffered two compounding bugs: 1. The health monitor's stale-socket heuristic falsely triggered on webhook-mode channels every 30 minutes because webhook channels receive messages via HTTP POST, not socket events. 2. The webhook shutdown() was fire-and-forget — server.close() and bot.stop() were not awaited, so a health-monitor restart could call startChannel before the old server released its port, causing EADDRINUSE or a broken message pipeline (HTTP 200 but zero dispatch). Changes: - channel-health-policy: add mode to snapshot, skip stale-socket for webhook-mode channels - webhook.ts: make shutdown await server.close() and bot.stop(), expose shutdownComplete() promise - monitor.ts: await shutdownComplete() after abort in webhook path Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Allow callers to specify role ("assistant" | "user") for injected
messages. Defaults to "assistant" for backward compatibility.
When role=user, assistant-specific fields (stopReason, usage, api,
provider, model) are omitted from the message body.
This enables the backend to inject user-role messages that the bot
treats as instructions to execute (e.g. refreshing tool knowledge
after d0-cli hot-update).
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Greptile SummaryAdds an optional Confidence Score: 5/5Safe to merge — focused, backward-compatible addition with correct conditional logic for role-specific metadata. The only finding is a P2 function naming issue; the logic, schema, and metadata-gating are all correct. No files require special attention.
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 5998c3aa19
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| const isChannelScopedSession = sessionPeerShapeCandidates.some((part) => | ||
| CHANNEL_SCOPED_SESSION_SHAPES.has(part), |
There was a problem hiding this comment.
Handle legacy channel keys when inheriting delivery route
This new gate only treats sessions as channel-scoped when one of the next two tokens is direct|dm|group|channel, so legacy keys like agent:main:telegram:12345 (still present in existing stores/tests) will fail canInheritDeliverableRoute even when lastChannel/lastTo are valid. In chat.send, that makes OriginatingChannel fall back to webchat, so follow-up sends from those sessions stop routing back to the original external conversation.
Useful? React with 👍 / 👎.
| if (!toRaw) { | ||
| return { tool: toolName, provider, accountId, to: "" }; | ||
| } | ||
| const to = normalizeTargetForProvider(provider, toRaw); | ||
| return to ? { tool: toolName, provider, accountId, to } : undefined; |
There was a problem hiding this comment.
Preserve threadId when tracking message tool deliveries
The message tool path now supports thread-aware suppression, but this return branch drops threadId entirely, so a send like { action: "send", provider: "telegram", to: "-100123", threadId: "77" } is recorded without topic context. Downstream suppression logic then cannot match topic-targeted sends correctly, which causes duplicate final replies in threaded Telegram conversations.
Useful? React with 👍 / 👎.
Summary
Add optional
roleparameter tochat.injectRPC, allowing callers to inject messages withrole=user(in addition to the existingrole=assistantdefault).Motivation: After d0-cli hot-update, we inject a message telling the bot to re-run
d0 list. With the previous hardcodedrole=assistant, the bot ignored it (doesn't treat its own past messages as instructions). Withrole=user, the bot executes it as a user command.Changes:
logs-chat.ts: Add optionalroletoChatInjectParamsSchema("assistant" | "user")chat.ts: Passrolethrough handler →appendAssistantTranscriptMessagechat-transcript-inject.ts: Useroleparam (default"assistant"); omit assistant-specific fields (stopReason,usage,api,provider,model) for user-role messagesBackward compatible — existing callers without
roleparam get the same behavior.Test plan
chat.injectcalls withoutrolestill producerole=assistantmessageschat.injectwithrole=userproduces user-role messages in session JSONLrole=usermessage as instruction and executes it