Skip to content

chore: sync upstream openabdev/openab v0.8.4-beta.5#5

Closed
MarcusC64 wants to merge 101 commits into
mainfrom
sync/upstream-v0.8.4
Closed

chore: sync upstream openabdev/openab v0.8.4-beta.5#5
MarcusC64 wants to merge 101 commits into
mainfrom
sync/upstream-v0.8.4

Conversation

@MarcusC64
Copy link
Copy Markdown
Owner

同步內容

從 upstream openabdev/openab 同步 2026-05-06 → 2026-05-27 的更新(v0.8.0 → v0.8.4-beta.5)。

Upstream 新功能

功能 PR
openab-agent — 原生 Rust coding agent,內建訂閱驗證 openabdev#924
Pi coding agent 支援(Dockerfile.pi) openabdev#920
Google Antigravity CLI adapter openabdev#896
cron 自動停用:目標達成後自動關閉排程任務 openabdev#818
fix: ACP agent stderr 捕捉 + JSON-RPC 錯誤解析 openabdev#885
fix(media): Content-Type + magic bytes 驗證 openabdev#793
fix(discord): 多 bot 重複警告 dedup openabdev#886
Helm chart: imagePullSecrets + serviceAccountName openabdev#911 openabdev#914

保留的自訂修改(未受影響)

  • Dockerfile — dual build(openab + openab-gateway)、kiro-cli 2.2.0、tini
  • docker-entrypoint.sh — 動態 config 生成、Telegram gateway 啟動、DISCORD_CHANNEL_ID 空值保護
  • gateway/src/adapters/telegram.rs — 完整 264 行、移除 parse_mode: Markdown(underscore 修復)
  • src/main.rsparse_id_set comma-separated ID 支援

https://claude.ai/code/session_01L2ZNFRhCDX2HxB9QEPJWgK


Generated by Claude Code

chihkang and others added 30 commits May 7, 2026 03:56
…ev#280)

* docs(helm): document supported chart values

* docs(helm): clarify fullnameOverride example

* docs(helm): fix --set-literal for botToken, add missing stt/reactions fields

---------

Co-authored-by: Chih-Kang Lin <8790142+chihkang@users.noreply.github.com>
Co-authored-by: masami-agent <masami-agent@users.noreply.github.com>
* feat: STT transcript echo to thread (Discord + Slack)

When STT transcribes a voice message, optionally post the transcript back
to the thread (no mentions) before the agent reply so users can verify what
was heard. Default is OFF — opt in via [stt] echo_transcript = true.

- New config: [stt] echo_transcript (default false, opt-in)
- New helper: stt::post_echo with platform-agnostic ChatAdapter handle —
  future LINE/Telegram/Teams adapters get echo for free
- Format: > 🎤 <transcript> per clip, all in one thread message
- Failure: > 🎤 (transcription failed) line + ⚠️ reaction on the user msg
- Helm: agents.<name>.stt.echoTranscript (camelCase) wired through configmap
- Docs: docs/stt.md and docs/config-reference.md updated

Rebased on top of openabdev#567 (gateway config rendering).

Tests: 133/133 cargo. helm-unittest: 28/28. Clippy --all-targets -D warnings clean.

* fix: close unclosed test fn delimiter + cargo fmt

---------

Co-authored-by: obrutjack <obrutjack@yahoo.com>
…enabdev#759)

* feat(discord): support role mention as trigger (allowed_role_ids)

Add allowed_role_ids config field to DiscordConfig. When a message
mentions a role in this list, it is treated as equivalent to a direct
@mention for trigger purposes.

- src/config.rs: add allowed_role_ids field (default empty)
- src/discord.rs: extend is_mentioned to check msg.mention_roles
  against allowed_role_ids; update resolve_mentions to strip
  triggering role mentions from prompt
- src/main.rs: parse allowed_role_ids via parse_id_set, pass to Handler
- charts/openab: add allowedRoleIds with snowflake validation
- config.toml.example: document new field

Closes openabdev#758

Discord Discussion URL: https://discord.com/channels/1488041051187974246/1501546581105705012

* docs(discord): document allowed_role_ids feature

Update docs/discord.md:
- Add allowed_role_ids config reference section with setup steps
- Update @Mention Behavior to include role mention trigger
- Update Helm Values example with allowedRoleIds
- Update troubleshooting to reflect role mention support

---------

Co-authored-by: chaodu-agent <chaodu-agent@users.noreply.github.com>
Co-authored-by: openab-app[bot] <274185012+openab-app[bot]@users.noreply.github.com>
* docs: add ECS Fargate Spot reference architecture

* docs: add gist config to diagram and Phase 4

* docs: add AI-agent usage callout at top

* docs: separate AWS boundary from external services in diagram

* docs: fix right border alignment in diagram

* docs: fix diagram border alignment

* docs: fix diagram alignment (consistent column width)

* docs: use ASCII-only diagram for consistent rendering

* docs: use full GitHub URL in example prompt

---------

Co-authored-by: chaodu-agent <chaodu-agent@users.noreply.github.com>
…penabdev#744)

* feat(gateway): feishu thread participation tracking (involved mode)

Once the bot replies in a thread, subsequent messages in that thread
bypass @mention gating — matching Discord's default 'involved' mode.

- Add participated_threads cache (HashMap<thread_id, Instant>)
- Bypass mention gating when message is in a participated thread
- Record participation on successful reply to a thread
- TTL controlled by FEISHU_SESSION_TTL_HOURS (default 24h)
- Cache eviction at 1000 entries (oldest-half strategy)
- 3 new tests for participation logic

* refactor(gateway): address review nits on openabdev#744

- Extract check_thread_participated() helper to reduce duplication
- Add comments explaining intentional poisoned-mutex recovery
- Improve eviction: drop TTL-expired entries first, then oldest half

* fix(gateway): address second-round review nits on openabdev#744

- Add comment clarifying session_ttl_secs=0 disables participation tracking
- Update bot_turns comment: remove TODO, note existing eviction pattern

* fix(gateway): early-return in record_participation when TTL=0

Skip cache insertion entirely when session_ttl_secs is 0 (feature
disabled), avoiding unnecessary mutex lock and cache accumulation.

---------

Co-authored-by: wangyuyan-agent <265828726+wangyuyan-agent@users.noreply.github.com>
Co-authored-by: masami-agent <masami-agent@users.noreply.github.com>
…bdev#760)

* fix(acp): clean up pending + cancel agent on abandoned prompts (openabdev#732)

The flat 600s recv_timeout in adapter.rs:386 fires "Agent stopped responding"
without removing pending[id] or sending session/cancel. The agent keeps
running the abandoned prompt and eventually emits its final response with
the original id. The reader at connection.rs:284 looks up pending[id], sees
the now-stale entry, and forwards the message to the *current* notify_tx
subscriber — which belongs to the next prompt. The next prompt's loop sees
notification.id.is_some() and breaks immediately with empty text_buf,
returning "(no response)". Each new prompt sent before the agent drains its
backlog inherits the previous prompt's stale id and the cascade persists.

Fix follows the issue's recommended A+B+C:

(A) Replace flat 600s timeout with a tokio::select! loop in stream_prompt_blocks.
    Recv arm + 30s liveness arm. Liveness arm checks conn.alive() (cheap,
    just !reader_handle.is_finished()) and a configurable hard ceiling.
    Default ceiling is 30 min via pool.prompt_hard_timeout_secs. Long-running
    tools no longer trip the timeout — only a dead reader task or the hard
    ceiling abandon the prompt.

(B) Add AcpConnection::abandon_request(request_id) called on every abandon
    path: drops pending[request_id] so a late response cannot route to a
    future subscriber, and best-effort writes session/cancel so the agent
    stops working on a request the broker has given up on.

(C) Capture request_id from session_prompt() (was discarded as `_`) and
    skip notifications whose id doesn't match. Defense-in-depth at the
    routing layer; complements (B)'s cleanup if any future abandon path
    forgets to call abandon_request.

No unit test for abandon_request — the connection has no test seam without
spawning a real subprocess. Behavior is exercised end-to-end via the
adapter loop on real ACP backends.

Refs:
- openabdev#76 (Assumption 2: prompts always complete)
- openabdev#307 (sibling: same family, different visible symptom)
- openabdev#470 (added the 600s recv timeout this issue exposes)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* chore(acp): apply chaodu-agent NITs from PR openabdev#760

- pool.liveness_check_secs: hoist the recv-loop poll cadence out of a
  hard-coded const onto PoolConfig so deployments can tune it. Default
  remains 30s.
- adapter: change hard-timeout error message from ({}m) to ({}s) so
  non-multiple-of-60 ceilings render correctly (e.g. 90s → "(90s)").
- acp/connection: emit a tracing::trace! line when an id-bearing message
  arrives whose pending entry was already abandoned. Behaviour is
  unchanged — the adapter recv loop still filters by request_id; this
  just makes the stale-response path observable at trace level.

cargo check + cargo clippy -- -D warnings + cargo test --bin openab all
clean (238 passed).

* fix(acp): add precision doc + id to session/cancel

- Add ±liveness_check_secs precision note to prompt_hard_timeout_secs doc
- Add JSON-RPC id field to session/cancel in abandon_request

Co-authored-by: 超渡法師 <chaodu@openab.dev>

* chore(acp): apply chaodu-agent round-2 NITs from PR openabdev#760

- adapter::AdapterRouter::new: emit a tracing::warn! when
  liveness_check_secs >= prompt_hard_timeout_secs, since in that case
  the hard ceiling can only fire on the next liveness tick and may be
  effectively bypassed. Operator-visible warning, not a silent clamp.
- adapter: switch prompt_start from std::time::Instant to
  tokio::time::Instant so the timer shares tokio's clock with the
  tokio::time::sleep in the same select! arm (cohesive with future
  tokio::time::pause()-based tests).
- adapter + acp/connection: extend the stale-id filter / fall-through
  comments to note that the path is only exercised against a live
  subprocess and is covered by manual repro, not a unit test.

Note: chaodu-agent NIT 2 (cancel response noise) requires no code
change. abandon_request emits a JSON-RPC notification (no id field) per
the ACP spec, so a spec-compliant agent must not respond, and even a
non-compliant reply with no id would not hit the stale-id trace path.
PR comment to follow.

cargo check + cargo clippy -- -D warnings + cargo test --bin openab all
clean (238 passed).

* test(acp): add reader-loop unit tests for stale-id path (openabdev#732)

Extract the reader-loop body in AcpConnection::spawn into a free generic
function `run_reader_loop<R, W>` so tests can drive it with
`tokio::io::duplex()` halves instead of a real subprocess. Production
path is unchanged — spawn() now calls
`tokio::spawn(run_reader_loop(...))` with the same args.

Two new tests cover:
- stale-id response forwarded to subscriber when `pending` is empty
  (the openabdev#732 fall-through path that the adapter recv loop filters by
  request_id)
- matched-id response resolves the pending oneshot AND forwards a
  copy to the subscriber (regression guard for the dual branch)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* docs(acp): clarify abandon_request stale-id intent (openabdev#732 NIT 2)

session/cancel carries a fresh JSON-RPC id but is intentionally not
registered in `pending`, so the agent's reply lands in the stale-id
branch of run_reader_loop and only emits a trace! log. We never wait
on the cancel response; the adapter recv loop's request_id filter is
the actual safety net against leakage into the next prompt.

Doc-only — no behavioural change.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* chore(acp): apply chaodu-agent round-3 NITs from PR openabdev#760

NIT 1: `abandon_request` was sending `session/cancel` with a JSON-RPC id,
making it request-shaped. Per ACP spec, `session/cancel` is a client
notification (no id, no response expected). Pool-side `cancel_session`
and `reset_session` were already notification-style; this aligns
`abandon_request` with both spec and existing convention. Doc comment
reverted to notification semantics.

NIT 2: Reject `pool.liveness_check_secs = 0` in `parse_config`. Zero
would make the `tokio::time::sleep` arm in the recv `select!` loop
immediately ready, spinning the loop while the prompt is still under
the hard timeout.

---------

Co-authored-by: Brett Chien <1193046+brettchien@users.noreply.github.com>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: 超渡法師 <chaodu@openab.dev>
…penabdev#755)

- §6.4 Rule 1: add adapter-layer scope clarification caveat
  (resolve_mentions() runs before {prompt} construction, not subject
  to the rule — rule applies from Dispatcher::submit onward)
- §6.4 Rule 8: scope to retry-failed case only (first SendError
  triggers transparent retry per §2.5; only failed retry surfaces
  ❌ + ⚠️ + Err)
- §3.2: mark slack_ts_to_iso8601 as (proposed helper)
- Metadata: reword self-reference for clarity
- §6.6: clarify threshold formula units (count × tokens > 500 tokens)

Closes openabdev#754

Co-authored-by: masami-agent <masami-agent@users.noreply.github.com>
* fix(discord): pass video attachments to agents

* docs(discord): add Attachment Handling section

Document supported attachment types (audio, text, image, video) and
the new video metadata forwarding behavior.

---------

Co-authored-by: chaodu-agent <chaodu-agent@openab.dev>
* feat(gateway): feishu multibot-mentions mode

Add AllowUsers enum (Involved/Mentions/MultibotMentions) controlled by
FEISHU_ALLOW_USER_MESSAGES env var. In multibot-mentions mode, once
another bot is @mentioned in a participated thread, require @mention
for all bots — prevents multiple bots from responding simultaneously.

Multibot detection strategy:
- If FEISHU_TRUSTED_BOT_IDS configured: exact match
- Otherwise: infer from allowed_users (mention not self and not in
  allowed_users → assumed to be another bot)
- Only triggers in threads where bot has already participated

This avoids requiring users to discover per-app open_ids for other bots.

* refactor(gateway): extract detect_and_mark_multibot() helper

Deduplicate the multibot detection block (~30 lines) that was repeated
in both handle_ws_message and webhook(). Both now call a shared
detect_and_mark_multibot() helper that handles:
- Thread participation check
- @mention-based other-bot detection (trusted IDs or inference)
- Multibot cache marking with eviction
- Computing is_thread_participated based on allow_user_messages mode

Also update PARTICIPATION_CACHE_MAX comment to note it is intentionally
shared between participated_threads and multibot_threads caches.

* refactor(gateway): address review nits on openabdev#746

1. session_ttl_secs doc comment: clarify conversion from FEISHU_SESSION_TTL_HOURS
2. Rename is_thread_participated → bypass_mention_gating in parse_message_event
   with doc comment explaining the parameter semantics

* fix(gateway): add missing attachments field in googlechat tests

Content struct gained an attachments field in openabdev#744 merge but googlechat
test constructors were not updated.

* refactor: rename is_thread_participated to bypass_mention for clarity

Rename the local variable at both call sites (WebSocket and webhook) to
match the parameter name in parse_message_event, making the semantic
intent clearer: this is the final mode-computed bypass decision, not raw
participation status.

---------

Co-authored-by: wangyuyan-agent <265828726+wangyuyan-agent@users.noreply.github.com>
Co-authored-by: masami-agent <masami-agent@users.noreply.github.com>
Co-authored-by: chaodu-agent <chaodu-agent@users.noreply.github.com>
…ev#774)

* docs: add steering design guide for AI agent hot/cold memory

* docs: incorporate Claude Code feedback — fix CC memory mapping

- CLAUDE.md is hierarchical (global → project → subdir)
- settings.json is config, not instructions
- Add MEMORY.md index (200-line cap) as hot memory
- Individual memory files are cold storage
- Add CC auto-memory as real-world hot/cold example

* docs: incorporate Codex + Gemini feedback — fix agent mappings

- Codex: AGENTS.md hierarchical, 32KB cap, not .codex/instructions.md
- Gemini: GEMINI.md hierarchical (global → project → subdir) + MEMORY.md
- Update size guidance: caps vary by agent, attention dilution is real constraint
- Add note about common hierarchical loading pattern across CC/Codex/Gemini

* docs: incorporate Copilot feedback — fix mapping

- Not single-file: repo-wide + path-specific + AGENTS.md (nearest-in-tree)
- Layered precedence: Personal > Path-specific > Repo-wide > Agent > Org
- No hard size cap for Chat/Agent; code review reads first 4K chars
- Remove 'pending confirmation' note

* docs: add Loading Model Comparison section

Three distinct loading models across agents:
- Always loaded (every session)
- Directory-scoped (processing files in that tree)
- File-scoped (Copilot applyTo glob match)

Based on feedback from 口渡法師 (Copilot-backed agent).

* docs: fix stale .codex/instructions.md references

- Remove .codex/instructions.md from Terminology and Architecture sections
- Codex uses AGENTS.md, not .codex/instructions.md
- Fix duplicated lines in Architecture Pattern block

* docs: address Copilot NITs — add support qualifier and 'documented' cap

- AGENTS.md nearest-in-tree: add 'where supported: cloud agent / CLI'
- 'No hard size cap' → 'No documented hard size cap'

* docs: add anti-pattern — task-scoped rules in file-scoped locations

Review SOP, response format, collaboration protocol should be in
always-loaded layer, not path/directory-specific locations.
Based on 口渡法師 feedback.

* docs: fix broken code fence + add stale links anti-pattern + maintenance note

- Remove duplicate MEMORY.md line that broke markdown rendering
- Add anti-pattern: stale links in hot memory
- Add maintenance rule: document loading model before adding new agent
Based on 擺渡法師 feedback.

* docs: add principles — structure over prose, WHAT/HOW only in hot

- Structured constraints (lists, tables) > natural language paragraphs
- Hot memory = WHAT + HOW. WHY goes in cold storage (ADRs).
Based on 覺渡法師 feedback.

* docs: evolve to three-tier memory architecture (hot/warm/cold)

Based on unanimous feedback from all five monks:
- Add Warm/Conditional layer between Hot and Cold
- Warm = trigger metadata in hot, body loads on demand
- Document three trigger mechanisms: rule-based, semantic, explicit
- Update terminology, architecture diagram, decision flowchart
- Add 'What Goes in Warm Context' section
- Add anti-pattern: mandatory behavior hidden in cold without trigger
- Key insight: 'put the table of contents in hot, put the chapters in warm'

* docs: replace column layout with ASCII layered diagram

Visual hierarchy makes the three-tier model immediately clear:
Hot (top, small) → Warm (middle, triggered) → Cold (bottom, unlimited)

* docs: fix inconsistencies flagged by 口渡

- Opening line updated to three-tier model
- Terminology: CC/Gemini individual memory files are warm (body), not the index
- Real-world example: memory files are warm, not cold
- Consistent: index = hot trigger, body = warm, no-trigger docs = cold

* docs: add Self-Reflection Prompt section

Agents can use this prompt to audit their own memory allocation
against the guide's principles. Turns the doc from passive reference
into an active diagnostic tool.

* docs: upgrade Self-Reflection Prompt to 6-step audit checklist

Incorporates feedback from all four monks:
- 擺渡: add warm layer, loading trigger, fresh-session test
- 普渡: add trigger precision check
- 口渡: add canonical source identification, layer/trigger classification
- 覺渡: validated via live execution

Now covers: inventory → classify → violations → trigger quality →
optimization plan → verification

* docs: update self-reflection prompt to reference specific doc path

Points agents to docs/steering-design-guide.md in OpenAB repo
so they can fetch and read the guide before auditing.

* docs: add Problem section — why this guide exists

Without deliberate organization: bloated instructions, duplicated
rules, missing triggers, no shared standard across agents.

* docs: add OpenAB agent-agnostic context to Problem section

OpenAB supports multiple agents side by side; this guide provides
a shared memory architecture standard for consistent behavior
across all supported coding agents.

---------

Co-authored-by: chaodu-agent <chaodu-agent@users.noreply.github.com>
…openabdev#777)

* feat: agent-controlled reply-to via [[reply_to:message_id]] directive

Problem:
Agents currently cannot reply to a specific message in a thread.
All output is sent as plain messages, losing conversational context
in busy multi-bot threads.

Solution:
Two-layer change enabling agents to specify reply targets:

1. Input: SenderContext now includes message_id, so agents know
   the ID of each incoming message.

2. Output: Agents can prefix their response with directives:
   [[reply_to:1502606076451885136]]
   Actual reply content...

   OAB parses consecutive [[key:value]] lines as a header block,
   strips them from content, and uses them for platform-specific
   message delivery (Discord: message_reference).

Design decisions evaluated:
- Option A (chosen): Inline directives in output text. Minimal change,
  no ACP protocol modification, forward-compatible (unknown keys ignored).
- Option B (deferred): ACP metadata field. Cleaner but requires protocol
  change across all backends. Appropriate when more directives are needed
  (ephemeral, components, attachments).

Directive format:
- Consecutive [[key:value]] lines at output start = header block
- First non-[[...]] line = content begins
- Unknown keys silently ignored (forward compatible)
- reply_to value must be numeric snowflake (validated)
- Extensible: future directives like [[ephemeral:true]] can be added

Implementation:
- adapter.rs: parse_output_directives() + OutputDirectives struct
- adapter.rs: ChatAdapter::send_message_with_reply() (default: fallback)
- discord.rs: CreateMessage::reference_message() for reply-to
- discord.rs: build_sender_context() includes msg.id
- slack.rs, cron.rs, gateway.rs: message_id field added to SenderContext

* fix: address review findings on reply-to directive

F1 (streaming path): reply_to only works in send-once mode. This is
acceptable because streaming is disabled in multi-bot threads (where
reply-to matters most). Added explanatory comment.

F2 (CRLF offset): Fixed parse_output_directives to handle both \n
and \r\n line endings correctly instead of assuming +1.

F3 (API error fallback): send_message_with_reply now catches Discord
API errors (unknown message, cross-channel reference) and falls back
to plain send_message instead of propagating the error.

* fix: reply_to directive works in streaming path too

When reply_to directive is present and streaming mode created a
placeholder, the placeholder is blanked (zero-width space) and the
real content is sent as a new reply message. This ensures reply_to
has consistent semantics regardless of streaming mode.

Behavior:
- Streaming + no reply_to: normal edit-in-place (unchanged)
- Streaming + reply_to: blank placeholder, send as reply
- Send-once + reply_to: send as reply (unchanged)

* docs: add output directives documentation

Covers:
- Directive format spec ([[key:value]] header block)
- reply_to directive usage and behavior
- SenderContext.message_id for getting message IDs
- Multi-agent use case example
- Comparison with OpenClaw/Hermes Agent
- Future directives roadmap

* docs: add agent-controlled reply-to to README features

* test: add unit tests for parse_output_directives

8 tests covering:
- Normal reply_to parsing
- No directives (plain content)
- Multiple directives (unknown keys ignored)
- Invalid reply_to (non-numeric) rejected
- Empty reply_to rejected
- CRLF line endings handled correctly
- Directive-only output (no content)
- Non-directive first line stops parsing

* fix: simplify streaming + reply_to path (remove redundant edits)

Per review: the three sequential edits were wasteful and caused
brief content duplication. Simplified to:
1. Single edit to zero-width space (hide placeholder)
2. Send real content as reply

No more flicker or ghost content.

* fix: add fallback logging + 2 more edge case tests

- Discord: tracing::warn on reply_to fallback (was silent)
- Test: duplicate reply_to (last wins)
- Test: CRLF with multiple directives

Total directive tests: 10

* fix: relax message_id validation for cross-platform compatibility

Was: numeric-only (Discord snowflake)
Now: alphanumeric + dots + hyphens + underscores, max 64 chars

This allows:
- Discord snowflakes: 1502606076451885136
- Slack ts: 1234567890.123456
- UUID-style: 550e8400-e29b-41d4-a716-446655440000

Rejects: whitespace, control chars, empty, >64 chars

Added test: parse_slack_ts_format_accepted
Updated test: rejection now checks whitespace (not hyphens)

* fix: guard against empty content after directive stripping

If agent output is directive-only (e.g. just [[reply_to:123]] with
no actual content), stripped_content would be empty. Discord rejects
empty messages, causing silent failures.

Fix: if content is empty/whitespace after stripping, fall back to
'_(no response)_' — same behavior as when agent returns no text.

* fix: delete placeholder instead of zero-width space on reply_to

Adds delete_message to ChatAdapter trait (default no-op) and
implements it for Discord. Streaming + reply_to path now deletes
the placeholder entirely instead of editing to zero-width space.

No more ghost empty bubbles in Discord threads.

* fix: delete_message default falls back to edit zero-width space

Per review: default no-op would leave placeholder visible if delete
fails or adapter doesn't support it. Default now edits to zero-width
space (existing behavior), Discord overrides with real delete.

* fix: clippy errors (unnecessary_unwrap + too_many_arguments)

- Replace is_some() + unwrap() with if let Some(ref reply_id)
- Allow clippy::too_many_arguments on build_sender_context (8 params)

* fix: log unknown directives at debug level

Helps agent developers diagnose typos like [[reply-to:...]] vs
[[reply_to:...]]. Forward compatible: unknown keys still ignored
at runtime, just logged for debugging.

* fix: remaining clippy unnecessary_unwrap in streaming path

* docs: note Slack reply_to is parsed but not yet implemented

* docs: remove Future Directives section (avoid premature commitment)

* fix: send-before-delete order + parse directives before markdown

F1: Send reply first, then delete placeholder. If send fails,
    placeholder remains visible (no message loss for user).

F2: Parse directives before markdown::convert_tables. Directives
    are meta-layer and should be stripped before content transforms.

* fix: parse directives from raw text_buf + check send before delete

F1: Directives now parsed from raw text_buf BEFORE compose_display,
    ensuring tool call output cannot interfere with directive parsing.

F3: Send result is checked — placeholder only deleted if first chunk
    sends successfully. On send failure, placeholder remains visible
    (no message loss).

* fix: [[X]] without colon stops parsing (preserves agent content)

B2: Lines like [[Note]], [[Summary]], [[Thought]] (no colon) are
legitimate agent content, not directives. Parser now only advances
content_start when split_once(':') succeeds. Without colon → break.

Added test: parse_bracket_without_colon_preserved

* docs: fix value spec accuracy + document duplicate-key behavior

D1: Value spec now correctly describes cross-platform validation
    (non-empty, ≤64 chars, no whitespace) instead of 'numeric only'.
D2: Added rule: 'If the same key appears multiple times, last wins.'
Also: clarified [[X]] without colon stops parsing.

* fix: add warn logging on send/delete failure + align docs with parser

- tracing::warn on reply send failure (ops can diagnose permission issues)
- tracing::warn on placeholder delete failure
- Docs: 'no whitespace' → 'ASCII alphanumeric plus ., -, _' (matches code)

---------

Co-authored-by: chaodu-agent <chaodu-agent@users.noreply.github.com>
Co-authored-by: openab-app[bot] <274185012+openab-app[bot]@users.noreply.github.com>
* fix: make output directive parser lenient for inline content

The parser previously required [[key:value]] to be the entire line
(using strip_suffix). If an agent put trailing content on the same line
(e.g. '[[reply_to:123]]  Hello world'), the directive was not recognized
and rendered as plain text.

Now the parser finds the first ']]' after '[[' and extracts the directive,
treating any remaining text on that line as the start of content. This is
backward compatible — strict format still works.

Adds tests for inline content scenarios.

Closes openabdev#779

* test: add more edge case tests for lenient directive parser

- No space between ]] and content (Chinese text)
- Inline Discord mention after directive
- Trailing spaces only (empty content)
- Brackets in content after directive

* test: rename @超渡法師 to @bot in test data

---------

Co-authored-by: chaodu-agent <chaodu-agent@users.noreply.github.com>
Co-authored-by: openab-app[bot] <274185012+openab-app[bot]@users.noreply.github.com>
…penabdev#785)

* fix(cron): translate POSIX day-of-week to match documented semantics

The underlying `cron` crate (0.16.0) uses a non-POSIX day-of-week
numbering (Sun=1, Mon=2, ..., Sat=7). openab documents POSIX semantics
(Sun=0/7, Mon=1, ..., Sat=6), so numeric day-of-week values were
effectively shifted by one day — e.g. `0 7 * * 1-5` (intended Mon-Fri)
fired Sun-Thu.

Add a small translator that expands each POSIX day-of-week field to a
set of days and re-serializes it in the cron crate form (+1 shift,
normalizing 7 to 0). Name-based tokens (Mon, Sun, Mon-Fri, ...) pass
through unchanged because the cron crate's name-to-ordinal map is
internally consistent.

Includes regression tests for the observed 2026-05-10 (Sunday)
firing of `0 7 * * 1-5` in Asia/Taipei, plus coverage of `0`, `7`,
`6`, ranges, lists, steps, and invalid inputs.

Closes openabdev#784

* fix(cron): expand singleton+step DOW and reject mixed notation

- n/step (e.g. 1/2) now expands to (n..=6) before applying step filter,
  matching POSIX crontab(5) semantics where 1/2 means Mon,Wed,Fri
- Mixed numeric+name notation (e.g. 1,Mon) now returns Err instead of
  silently passing through with incorrect numeric translation

Addresses review findings from 法師團隊.

* fix(cron): handle 7/step edge case and update stale doc comment

- Normalize 7 (Sunday alias) to 0 before singleton+step expansion,
  preventing 7..=6 empty range error
- Update doc comment to reflect that mixed notation now returns Err
- Add test for 7/2 edge case

* docs(cron): add Known Limitations section for DOW notation

Document that mixed numeric/name notation and wrap-around ranges
are not supported, with workarounds.

* fix(cron): satisfy clippy manual_is_multiple_of lint

---------

Co-authored-by: chaodu-agent <chaodu-agent@openab.dev>
Add optional receiver_id field to SenderContext so agents can identify
themselves when multiple agents share the same backend runtime.

- Discord: injects bot_id from ctx.cache.current_user().id
- Slack: injects bot_user_id from get_bot_user_id()
- Gateway/Cron: None (no receiver identity available yet)

Field is optional and skip_serializing_if None, so this is a
non-breaking additive change to openab.sender.v1.

Closes openabdev#786

Co-authored-by: chaodu-agent <chaodu-agent@openab.dev>
Co-authored-by: openab-app[bot] <274185012+openab-app[bot]@users.noreply.github.com>
* feat(discord): add /remind slash command for one-shot delayed mentions

Implements a /remind slash command that lets humans schedule delayed
mentions to users/roles in the channel.

- Command: /remind <targets> <message> <delay>
- Delay range: 1m to 30d (supports m/h/d and combinations)
- Only humans can invoke (bots rejected)
- Persistence: reminders.json survives restarts
- Re-schedules pending reminders on bot ready

Closes openabdev#796

* fix(remind): enable chrono/serde, fix broken test, lock-free persist, input validation

- Cargo.toml: add serde feature to chrono (fixes E0277 CI failure)
- remind.rs: release mutex before sync file I/O to avoid blocking executor
- remind.rs: replace unwrap_or_default() with explicit error log on serialization failure
- remind.rs: fix test_parse_delay_too_short (remove wrong assertion that bare "30" errors)
- discord.rs: add 1800-char cap on reminder message
- discord.rs: neutralize @everyone/@here in message via zero-width space

* fix(remind): add rate limit, create_dir_all, dedup scheduling, safe removal

- F4: Per-user rate limit (max 5 active reminders)
- F7: create_dir_all before writing reminders.json
- F8: Deduplicate reminder scheduling on reconnect via scheduled_ids
- F9: Only remove reminder from store after successful send

* docs: update /remind examples to English, add rate limit and length constraints

* test(remind): add comprehensive tests for edge cases, store, and validation

- parse_delay: empty, invalid unit, case-insensitive, whitespace, boundaries
- format_delay: zero, pure units
- ReminderStore: add/remove/pending, persistence across reload
- sanitize_message: @everyone/@here neutralization
- validate_message: length cap enforcement
- Extract sanitize_message/validate_message as testable helpers

* fix: use validate_message helper in handler to resolve dead_code warning

* docs: mention @everyone/@here neutralization in /remind constraints

* feat(remind): add max 10 targets limit with test and docs

- Add MAX_TARGETS = 10 constant
- Reject /remind with >10 mentions (suggest using @ROLE)
- Update docs/slash-commands.md with constraint
- Add test for constant

* fix(remind): insert new reminder ID into scheduled_ids to prevent duplicate on reconnect

---------

Co-authored-by: chaodu-agent <chaodu-agent@users.noreply.github.com>
Co-authored-by: 普渡法師 <pudu@openab.dev>
Co-authored-by: openab-app[bot] <274185012+openab-app[bot]@users.noreply.github.com>
…on (openabdev#799)

Co-authored-by: chaodu-agent <chaodu-agent@users.noreply.github.com>
…penabdev#783)

* feat(gateway): support [[reply_to]] directive for gateway platforms

- Add quote_message_id field to GatewayReply protocol
- Override send_message_with_reply in GatewayAdapter (refactored via
  send_gateway_reply helper to eliminate duplication)
- Feishu: use quote_message_id as reply target with fallback to plain
  send on failure
- Update docs/feishu.md and docs/output-directives.md for Feishu support

When an agent outputs [[reply_to:message_id]], the gateway sends the
message as a native reply/quote on the platform. Feishu shows it with
the quote reference UI via POST /im/v1/messages/{id}/reply.

Tested: 274 core + 126 gateway tests pass. E2E verified on Feishu.

* fix(gateway): prevent pending map leak and add chunked reply fallback

- src/gateway.rs: clean up pending map entry before returning error on
  WebSocket send failure (prevents slow memory leak)
- gateway/src/adapters/feishu.rs: retry chunked messages with thread_id
  when quote_message_id causes all chunks to fail (prevents silent
  message loss)
- Improve fallback warn logs with structured fields for debugging

* fix(gateway): improve doc comments, timeout logging, and extract constant

- Add doc comments distinguishing reply_to (routing) from
  quote_message_id (visual reply UI) in both core and gateway schema
- Split timeout catch-all into distinct arms with warn-level logging
  for failure, channel closed, and timeout cases
- Extract 5s timeout to GATEWAY_REPLY_TIMEOUT_SECS constant

* fix(gateway): address review findings on openabdev#783

- F1: Add comment explaining defensive pending.remove() calls
- F2: Add 3 unit tests for quote_message_id priority logic
  (quote > thread_id > None)

* fix: address review findings — remove redundant pending.remove, add fallback test

- F1: Remove redundant pending.lock().await.remove(id) in Ok(Ok(_resp))
  and Ok(Err(_)) branches of send_gateway_reply. The response handler
  (line 616) already removes the entry before sending on the oneshot
  channel. Only the timeout branch needs the remove.

- F2: Add quote_message_id_fallback_on_reply_failure test verifying that
  send_post_message returns None for an invalid reply target and succeeds
  on retry without quote (the same pattern handle_reply uses).

* fix: upgrade F2 test to exercise handle_reply fallback path directly

Per 普渡法師 feedback: the previous test called send_post_message
directly twice, which didn't cover the quote_message_id.is_some()
guard in handle_reply.

New test:
- Adds api_base_override to FeishuConfig (always None in prod)
- Constructs a full FeishuAdapter with mock server
- Calls handle_reply with quote_message_id=Some("om_invalid")
- wiremock expect(1) verifies both the reply API call AND the
  fallback plain send were triggered by handle_reply's guard logic

---------

Co-authored-by: wangyuyan-agent <265828726+wangyuyan-agent@users.noreply.github.com>
Co-authored-by: CHC-Agent <CHC-Agent@users.noreply.github.com>
Co-authored-by: chaodu-agent <chaodu-agent@users.noreply.github.com>
* feat(gateway): add WeCom (企业微信) channel adapter

Implement WeCom as a new gateway platform for receiving and sending
messages via enterprise app callback API.

Features:
- AES-256-CBC message decryption (WeCom uses PKCS7 block_size=32)
- SHA1 signature verification with constant-time comparison
- Access token cache with auto-refresh and expiry margin
- Message deduplication (30s TTL, 10k max entries)
- Long message splitting at line boundaries (2048 byte limit)
- GatewayResponse with message_id for OAB core integration

Env vars: WECOM_CORP_ID, WECOM_SECRET, WECOM_TOKEN,
WECOM_ENCODING_AES_KEY, WECOM_AGENT_ID, WECOM_WEBHOOK_PATH

Note: Streaming (edit_message) is intentionally disabled — WeCom has
no native message edit API; recall+resend shows disruptive notifications.
Uses plain text msgtype since WeCom app message markdown is too limited
(no code blocks, tables, or lists).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs(wecom): add setup guide and update READMEs

Add comprehensive WeCom setup documentation covering:
- Prerequisites and enterprise app creation
- Callback URL configuration
- Environment variables reference
- Docker/Kubernetes deployment
- Troubleshooting guide

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat(wecom): re-enable streaming with recall+resend and msg_id tracking

Add handle_edit_message and recall_message methods to support OAB streaming.
When OAB sends edit_message commands, the adapter recalls the previous
message and re-sends updated content, tracking message ID changes via
msg_id_map to handle the WeCom limitation of no native edit API.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat(wecom): add image receiving support

Download images from WeCom PicUrl, resize/compress via image crate,
and forward as base64 attachment in gateway event. Also fix upstream
googlechat test compilation (missing attachments field).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat(wecom): replace recall+resend streaming with thinking placeholder + debounce flush

Instead of recalling and resending on every streaming update, send a
single "⏳..." placeholder and buffer all edits. After 3 seconds of
inactivity, recall the placeholder once and send the complete response.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat(wecom): add text file receiving support

Download files via WeCom media API and forward text-based files
(code, config, data files) as text_file attachments to OAB.
Also fix split_text tests to match renamed split_text_lines function.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(wecom): address PR review feedback

- Add 5-minute max timeout to debounce flush task to prevent leaks
- Make WECOM_AGENT_ID required (was defaulting to "0")
- Align Helm chart: require token + encodingAesKey in $hasWecom condition
- Fix README: mark WeCom env vars as required
- Update docs/wecom.md feature matrix to reflect implemented features
- Add WeCom vars to "no adapters configured" warning message

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(wecom): split long lines at char boundaries and add flush debug logging

split_text_lines now handles single lines exceeding the limit by
splitting at UTF-8 char boundaries. Previously long lines were sent
as-is, causing WeCom to silently truncate messages.

Also add detailed logging to flush_thinking for easier debugging.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(wecom): address Copilot review feedback (round 2)

- Validate PKCS#7 padding bytes match before stripping
- Validate WECOM_AGENT_ID is numeric at startup (fail-fast)
- Gate group messages: drop when no @mention present (group_require_mention)
- Add agentId to Helm $hasWecom condition (required field)
- Fix docs: "2048 chars" → "2048 bytes"

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs(wecom): simplify deployment section and add group verification note

- Remove standalone K8s manifest (Helm chart is the canonical way)
- Keep docker run as env var quick-start example (matches other adapters)
- Add note: group chat requires enterprise real-name verification

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(wecom): address reviewer feedback on group support and config validation

- Remove WECOM_GROUP_REQUIRE_MENTION and strip_bot_mention.
  WeCom self-built app callbacks only deliver 1:1 DMs; the previous
  default of true silently dropped all DM messages without an @-prefix.
  Group chat support requires the appchat API and is deferred.

- Validate decode_aes_key output is 32 bytes to prevent panics on
  malformed base64 input.

- Add Default derive to Content and Attachment so future schema
  extensions don't force every adapter test fixture to update.

- Update docs to declare group chat as not supported.

- Wire gateway/** into CI: add cargo check/clippy/test job for the
  gateway crate so PRs touching gateway/ run the existing 22 tests.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* ci(gateway): drop clippy from gateway CI job

cargo clippy -- -D warnings surfaces ~14 pre-existing warnings across
feishu.rs and googlechat.rs (collapsible_if, dead_code, manual_strip,
needless_range_loop, too_many_arguments). Fixing those is unrelated to
this PR's scope; track separately. Keep cargo check + cargo test, which
covers the 22 wecom unit tests reviewers asked us to wire up.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(wecom): make streaming opt-in and debounce configurable

Per chaodu-agent's review, the recall+resend streaming pattern causes a
brief client flicker on WeCom (recall toast + new-message notification).
Default the placeholder/recall path off and let operators opt in with
WECOM_STREAMING_ENABLED=true once they've understood the tradeoff. With
streaming disabled, chunks are still buffered via the same debounce
channel, so the agent transparently sees one consolidated final reply
without any UI artifact.

Also expose the debounce quiet-period as WECOM_DEBOUNCE_SECS so 1-1.5s
deployments can reduce perceived latency without forking the code.

flush_thinking() now takes Option<&str> for thinking_msg_id to skip the
recall API call when no placeholder was sent.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(wecom): explain decode_aes_key base64 config

Per wangyuyan-agent round 1 NIT: clarify why decode_aes_key uses
Indifferent padding mode + allow_trailing_bits. WeCom's EncodingAESKey
is a 43-char base64 string (not 44 with trailing =) whose 43rd char
carries 2 unused bits. The default base64 decoder rejects this; we
relax both knobs so we can decode the spec-compliant input as-is.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(wecom): unify token-expiry retry across send paths

Per chaodu-agent's latest review (F1, blocking): flush_thinking()
fetched a token via get_token() and POSTed directly without checking
errcode 42001 or retrying. A long streaming session whose cached token
expired mid-flight would silently lose its accumulated reply — the
recall would fail, the final chunk would never reach the user.

Extract the retry-on-token-expiry pattern into post_with_token_retry()
so send_text and flush_thinking share one code path. Both recall and
final-chunk send in flush_thinking now go through the helper.

Also (F3, NIT): the envelope ToUserName field was parsed but never
read, so it carried #[allow(dead_code)]. Use it to validate that
inbound callbacks are actually addressed to our configured Corp ID
before crypto runs — surfaces misrouting earlier and removes the
dead-code allow.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(wecom): close chaodu's defense-in-depth gaps (F1-F4)

F1: Streaming race — debounce task could miss late writes that arrive
between the timeout firing and pending.remove(). Now the task acquires
the pending lock first, captures any final rx.borrow().clone() while
holding the lock (which blocks handle_reply from sending more chunks),
then removes the entry.

F2: Replay protection — reject callbacks whose timestamp is more than
5 minutes off from now. WeCom's signature doesn't bind freshness, so
without this an attacker who captured a signed payload could replay it
indefinitely after the 30s dedup window.

F3: SSRF defense-in-depth — only fetch pic_url over HTTPS. WeCom's CDN
is HTTPS; rejecting non-HTTPS prevents attacks if the AES key leaks
and an attacker forges callbacks pointing at internal hosts.

F4: Helm gap — expose WECOM_STREAMING_ENABLED and WECOM_DEBOUNCE_SECS
in values.yaml + gateway.yaml so deployers don't need extraEnv.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(wecom): close TOCTOU and add token retry to file download

Per chaodu-agent's 4th re-review:

F1: download_wecom_file used a token directly without retry. If the
cached token expired between get_token() and the GET, the file silently
failed. Added fetch_media_with_retry() which sniffs the response
Content-Type — WeCom's media API returns JSON {errcode:42001,...}
on token expiry instead of binary — and retries once after a forced
refresh. download_wecom_file now takes &WecomTokenCache and runs the
retry helper itself.

F2: TOCTOU in handle_reply's has_pending branch. The first has_pending
read happens under a lock that's then released; by the time we re-take
the lock to append, the debounce task may have removed the entry,
and we'd silently drop the chunk. Now: re-check inside the second
lock and, if the entry is gone, fall through to the direct-send path
so the chunk still reaches the user.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(wecom): address chaodu round-5 NITs (F2, F3, F4)

F2 (debounceSecs:0 silently ignored): document the minimum is 1 in
docs/wecom.md and values.yaml. The Helm template's truthy check treats
0 as unset; since 0-second debounce defeats the buffer purpose anyway,
documenting the floor is more honest than reshaping the truthy check
to accept a value with no real use case.

F3 (env::set_var in tests is parallel-unsafe): refactor from_env to
delegate to from_reader, which takes a closure. Tests now build a
HashMap-backed reader and never touch process-wide env vars, so
cargo's parallel runner can't race them. Also fixes the wecom
collapsible_match clippy warning while we're in there (XML parser
nested if-in-match collapsed to match guards).

F4 (no rate-limiting docs): added a "Production Hardening" section
explaining that the timestamp-freshness check rejects stale replays
cheaply but fresh-but-invalid requests still consume CPU, and pointing
to edge / LB / reverse-proxy layer for IP-level rate limits, plus the
WeCom Trusted IP allowlist as the strongest control.

F1 (clippy CI gap) and F5 (mutex poison logging) intentionally not
addressed — see the follow-up reply for rationale.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* ci(gateway): enable strict clippy and clear pre-existing warnings

Restores the strict-clippy parity that the root crate has and that
reviewers (chaodu round 4 and 5) flagged as a CI consistency gap.
The mechanical fixes don't change behavior:

- dead_code on serde-deserialize fields → #[allow(dead_code)] with
  fact-only "parsed by serde, not consumed in current code paths"
  (no speculation about future intent)
- needless_range_loop in markdown rendering → buf.extend(slice.iter())
- manual_strip in fenced code block detection → strip_prefix
- useless_conversion on tungstenite Message::Binary → drop the .into()
- too_many_arguments on ws_connect_loop / handle_ws_message →
  #[allow(clippy::too_many_arguments)]; refactoring 11-arg async fn
  signatures is a larger change that doesn't belong here

Any further warnings exposed by this strict job will be visible in
the CI run and addressed in a follow-up.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(gateway): clear remaining clippy warnings exposed by strict CI

After merging upstream main and enabling strict clippy on the gateway
job, 5 more warnings surfaced:

- feishu.rs: collapsible_if — flatten outer + !(in_thread &&
  bypass_mention_gating) into one if condition
- feishu.rs: nonminimal_bool — !is_some_and(<) → is_none_or(>=)
- wecom.rs: too_many_arguments on flush_thinking (8 args) →
  #[allow(clippy::too_many_arguments)]
- wecom.rs: useless_vec on parts vec → use 4-element array, sort_unstable
- main.rs: explicit_auto_deref on &*text → &text

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(wecom): address chaodu round-6 NITs (F1, F2 comments + F3/F4 known limits)

F1 (corpsecret in URL): WeCom's gettoken API requires the secret as a
query param; we cannot move it to a header. Added a comment in code
clarifying the protocol constraint and a "Redact corpsecret from
access logs" section in docs/wecom.md instructing operators to redact
query strings at the proxy layer for /cgi-bin/gettoken outbound calls.

F2 (byte-vs-char comment): added a doc comment to split_text_lines
making explicit that the limit and all len() comparisons are in
bytes (matching WeCom's server-side truncation), and that lines
exceeding the limit are split at UTF-8 char boundaries.

F3 (streaming task lifetime on shutdown): documented as known
limitation. The fix would add a JoinSet/CancellationToken on the
adapter; non-trivial scope, and impact is bounded since streaming
defaults off. Recorded in the new "Known limitations" docs section.

F4 (DedupeCache eviction is lazy): unchanged, documented under known
limitations. ~500 KB max memory bound is acceptable; correctness
(dedup window honored) is unaffected. Repeated finding from
Copilot/chaodu earlier rounds; canyugs's prior "won't fix" rationale
still applies.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
…penabdev#761)

* feat(gateway): feishu voice message STT via gateway audio attachment

- Add msg_type=audio support to feishu adapter (parse, download, base64 encode)
- Add MediaRef::Audio variant and download_feishu_audio() function
- Add "audio" attachment type to core gateway handler (decode → stt::transcribe)
- Pass SttConfig to gateway handler via GatewayParams
- Update docs/feishu.md and docs/stt.md for multi-platform voice support

Feishu voice messages (opus/ogg) are downloaded by the gateway, passed as
base64-encoded audio attachments to core, and transcribed via the existing
[stt] infrastructure (Groq Whisper by default). This is the first gateway
platform to support audio — LINE/Telegram can reuse the core-side handler.

Tested: 102 gateway tests + 197 core tests pass. E2E verified.

* fix(gateway): read Content-Type from response, URL-encode path params in audio download

- F1: Use actual Content-Type header from Feishu response instead of
  hardcoding "audio/ogg", with fallback to audio/ogg if header missing.
- F2: URL-encode message_id and file_key in the download URL path to
  prevent potential path injection from untrusted JSON values.

---------

Co-authored-by: wangyuyan-agent <265828726+wangyuyan-agent@users.noreply.github.com>
Co-authored-by: chaodu-agent <chaodu-agent@users.noreply.github.com>
* feat(discord): add thread export command

# Conflicts:
#	docs/slash-commands.md
#	src/discord.rs

* fix(discord): address review findings F1-F3 for export-thread

F1: Add tracing::warn when attachment_size_limit < 2048
F2: Add code comment documenting newest-first fetch direction assumption
F3: Use YYYYMMDD date fallback instead of generic 'thread' for non-ASCII names

* feat(discord): add limit/since/days params to /export-thread

- Add optional slash command options: limit (Integer), since (String),
  days (Integer) — mutually exclusive
- Validate ranges: limit 1..=5000, days 1..=365, since YYYY-MM-DD UTC
- Ephemeral error on mutual exclusion violation or invalid input
- Refactor export_channel_messages to accept ExportFilter enum:
  All (before pagination), Limit (before, capped), After (after pagination)
- Implement timestamp_ms_to_snowflake for since/days conversion
- Update docs/slash-commands.md with parameter table and examples
- Add unit tests for snowflake conversion edge cases

* fix(discord): use before-pagination for since/days to keep newest messages on cap

ExportFilter::After now fetches newest-first (before pagination) and
stops when hitting messages at or before the filter boundary. This
ensures that when the 5000-message cap is reached, the most recent
messages in the time window are preserved — matching the existing
disclosure text and user expectations.

Addresses 擺渡法師 review finding: after-pagination would keep the
oldest 5000 in the window, which is counterintuitive for days/since.

* fix(discord): use result.fetched in hit_cap disclosure instead of hardcoded 5000

When limit:N is used and hits cap, the disclosure now correctly shows
the actual number fetched rather than always saying 5000.

* fix(discord): resolve clippy warnings (contains, if-let)

- Use (1..=5000).contains(&n) instead of manual range comparison
- Use (1..=365).contains(&d) instead of manual range comparison
- Replace match Ok/Err with if-let for probe (single-arm match)

* refactor(discord): change since param from UTC date to message ID

Replace YYYY-MM-DD date parsing with direct message ID input.
Users can right-click any message → Copy Message ID and use it
directly. This avoids timezone ambiguity entirely.

days param still uses synthetic snowflake for relative time filtering.

* refactor(discord): default export to last 100, add all:true for full dump

- No params → last 100 messages (more practical default)
- all:true → full dump up to 5000
- all is mutually exclusive with limit/since/days
- Docs updated with new default and examples

* docs: clarify days param is rolling N×24h window

* docs+tests: update for new DX (default 100, since=message_id, all:true)

- PR description updated (done via gh pr edit)
- docs/slash-commands.md: summary table, rolling 72h note, all examples
- Add 4 unit tests verifying ExportFilter cap logic:
  default=100, all=5000, custom limit, after=global cap

---------

Co-authored-by: 超渡法師 <chaodu-agent@openab.dev>
…dev#809)

The usercron_path base directory change was introduced in v0.8.2.
Annotate the CAUTION block so users know which version requires migration.

Co-authored-by: openab-triage[bot] <openab-triage[bot]@users.noreply.github.com>
Co-authored-by: openab-app[bot] <274185012+openab-app[bot]@users.noreply.github.com>
Explicitly list gateway/Cargo.lock in the pull_request paths filter
so dependency-only updates (e.g. Dependabot PRs) are clearly covered
by the gateway CI job.

While gateway/** already matches this path, listing it explicitly
makes the intent clear and prevents accidental removal if the glob
is later narrowed.

Closes openabdev#805

Co-authored-by: chaodu-agent <chaodu-agent@users.noreply.github.com>
Co-authored-by: chaodufashi <chaodu-agent@users.noreply.github.com>
chaodu-agent and others added 24 commits May 22, 2026 06:32
…abdev#902)

Co-authored-by: Pahud Hsieh <pahud@Pahuds-MacBook-Neo.local>
…penabdev#901)

* feat(openab): add existingSecret support for Slack agent credentials

Add `agents.<name>.slack.existingSecret` to the openab chart. When set,
the chart references the named Kubernetes Secret for SLACK_BOT_TOKEN and
SLACK_APP_TOKEN instead of creating a chart-managed Secret from values.

Adapts the existingSecret pattern from the openab-telegram chart (openabdev#873)
to the multi-agent structure of openab, scoped per-agent.

Enables ESO/Vault/SealedSecrets workflows where Slack tokens rotate
without requiring a Helm re-apply.

Behavior:
- existingSecret unset: chart creates Secret with slack tokens (unchanged)
- existingSecret set, slack-only agent: no chart-managed Secret created
- existingSecret set + discord/stt/gateway: chart Secret omits slack keys;
  deployment references existingSecret for slack envs only (dual-secret)

Closes openabdev#900

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(helm): address review nits — trim existingSecret, add mixed-adapter and multi-agent tests

- Pipe existingSecret through | trim in openab.slackSecretName helper to
  handle whitespace-only values gracefully
- Add mixed-adapter deployment test verifying Discord refs chart-managed
  Secret while Slack refs existingSecret in the same Deployment
- Add multi-agent scoping test confirming agent A's existingSecret does
  not affect agent B's inline token resolution

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: 超渡法師 <noreply@openab.dev>
…openabdev#906)

Replace --continue with --conversation <ID> to fix two bugs:
1. Full conversation history repeated on every turn (openabdev#905)
2. Concurrent sessions unsafe (--continue targets most recent globally)

Now tracks per-session: agy conversation ID (from conversations dir)
and cumulative output length. Only emits the delta on each turn.

Fixes openabdev#905

Co-authored-by: Pahud Hsieh <pahud@Pahuds-MacBook-Neo.local>
Co-authored-by: openab-app[bot] <274185012+openab-app[bot]@users.noreply.github.com>
…enabdev#891)

* feat: rename xai-proxy → openab-auth-proxy, make provider-generic

BREAKING: xai-proxy binary renamed to openab-auth-proxy.

- Extract xAI-specific OAuth values into a TOML config file
- Default to xAI preset when no config is provided (backward compat)
- Support any OIDC provider via auth-proxy.toml
- Add AUTH_PROXY_TOKEN_PATH env var (XAI_PROXY_TOKEN_PATH still works)
- Token storage moved to ~/.openab-auth-proxy/<provider>/tokens.json
- Update CI workflow, Dockerfile, README, and docs
- Add docs/refarch/sidecar-proxy.md for the generic pattern

* fix(auth-proxy): remove needless borrow to satisfy clippy

---------

Co-authored-by: chaodu-agent <chaodu-agent@users.noreply.github.com>
* feat: add openab-feishu chart (colocated OAB + gateway, WebSocket default)

Single-pod Helm chart for Feishu/Lark deployments:
- OAB agent and gateway as colocated containers
- WebSocket mode (default): outbound-only, no public endpoint needed
- Optional webhook mode with cloudflared sidecar
- Supports both Feishu (feishu.cn) and Lark (larksuite.com)
- Only 2 required --set flags: feishu.appId, feishu.appSecret
- existingSecret support for production credential management
- Security contexts: non-root, read-only rootfs, drop all caps

* fix(openab-feishu): address chart review findings from chaodu-agent

Six issues fixed across deployment.yaml, configmap.yaml, and pvc.yaml:

F1 (🔴) existingSecret + webhook mode silently dropped env vars:
  FEISHU_VERIFICATION_TOKEN and FEISHU_ENCRYPT_KEY secretKeyRefs are
  now rendered whenever connectionMode=webhook (not only when values
  are non-empty). Added optional: true so pods start even if those
  keys are absent from the secret (both are optional security hardening).

F2 (🟡) Boolean default trap in reactions config:
  Removed `| default true/false` pipes from configmap.yaml. Defaults
  are declared in values.yaml; the pipes caused `false` to be treated
  as empty and substituted with `true`, making reactions un-disableable.

BUG1 (🔴) tunnel.enabled=true without token caused silent CrashLoop:
  Added a `fail` guard that aborts helm template/install with a clear
  error message when the tunnel is enabled but no token is provided
  and no existingSecret is set.

BUG2 (🟡) storageClass: "-" rendered as literal "-" storageClassName:
  Applied the standard Helm convention: "-" is mapped to
  storageClassName: "" (static PV / empty class), any other non-empty
  value is passed through as-is.

BUG3 (🟡) checksum/secret annotation had wrong semantics in existingSecret mode:
  When existingSecret is set, secret.yaml renders empty and the
  checksum was a constant — external secret rotations would not
  trigger a rolling restart. Annotation is now skipped when
  existingSecret is set.

BUG4 (🟡) TOML env map rendered in non-deterministic order:
  Replaced manual $first-flag iteration with `keys | sortAlpha` +
  index lookup. Env keys now render alphabetically, eliminating
  spurious checksum/config diffs in GitOps pipelines.

* docs(openab-feishu): document existingSecret rotation limitation

Helm cannot track changes to externally-managed Secrets, so rotating
credentials does not automatically trigger a Pod rollout when
existingSecret is set. Added a comment in values.yaml explaining this
limitation and pointing to Reloader as the recommended solution.

---------

Co-authored-by: wangyuyan-agent <wangyuyan-agent@users.noreply.github.com>
* docs: add PR lifecycle flow to CONTRIBUTING.md

* docs: fix box alignment and split label transition table rows

* docs: note immediate closing-soon for missing Discord URL

* docs: align stale to 2 days, clarify re-check may re-apply closing-soon

* docs: rewrite lifecycle diagram with checks-first flow and re-check loop

* docs: clarify maintainer flips to pending-contributor when pending actions exist

* docs: add LGTM/approve/merge path from pending-maintainer

---------

Co-authored-by: chaodu-agent[bot] <chaodu-agent[bot]@users.noreply.github.com>
- pending-maintainer.yml: remove closing-soon skip, add closing-soon
  label removal when author comments and all checks pass
- close-stale-prs.yml: update auto-close message to generic stale
  message instead of Discord-URL-specific

Co-authored-by: chaodu-agent[bot] <chaodu-agent[bot]@users.noreply.github.com>
…evel (openabdev#914)

* feat(chart): add serviceAccountName support at per-agent and global level

Per-agent value (agents.<name>.serviceAccountName) wins when set; otherwise
falls back to chart-global $.Values.serviceAccountName. Both empty preserves
current behaviour (no serviceAccountName rendered, Kubernetes uses cluster
default SA). This is required to activate IRSA on EKS — without an explicit
serviceAccountName, the pod-identity-webhook never injects AWS credentials
and workloads silently fall back to the broad EC2 node role, breaking
least-privilege.

Scope: string reference to an existing SA only. The chart does NOT create a
new SA or manage IRSA annotations (operators provision out-of-band via
Terraform / IDP / kubectl), matching how PR openabdev#901 (existingSecret) and openabdev#910
(imagePullSecrets) reference existing K8s resources rather than creating
them.

Closes openabdev#913

* docs: generalize serviceAccountName descriptions, remove EKS/IRSA-specific wording

---------

Co-authored-by: chaodu-agent[bot] <chaodu-agent[bot]@users.noreply.github.com>
…el (openabdev#911)

Per-agent value (agents.<name>.imagePullSecrets) wins when set; otherwise
falls back to chart-global $.Values.imagePullSecrets. Both empty preserves
current behaviour (no imagePullSecrets rendered). This enables
multi-agent deployments where only some agents pull from a private
registry without forcing pull credentials onto every pod.

Follows the same per-agent K8s-native secrets pattern as PR openabdev#901
(slack existingSecret).

Closes openabdev#910
…openabdev#886)

* ci: add PR watch workflow — notify Discord on upstream activity

* fix(discord): dedup WarnAndStop warning across multiple bot processes

When N bots share a thread and all hit the soft turn limit simultaneously,
each bot's per-process BotTurnTracker independently fires WarnAndStop,
resulting in N duplicate warnings posted to the thread.

Fix: before posting the warning, fetch the last 10 messages in the thread
and skip if any bot message already contains 'Bot turn limit reached'.
Fail-open: if the API call fails, the warning is still posted.

Closes openabdev#530

* test(discord): add unit tests for turn_limit_warning_present dedup helper (openabdev#530)

* chore: remove pr-watch.yml — unrelated to openabdev#530 fix

* refactor(discord): extract BOT_TURN_LIMIT_WARNING_PREFIX constant; clarify best-effort semantics

- Add BOT_TURN_LIMIT_WARNING_PREFIX const to bot_turns.rs so the dedup
  check and the warning text share a single source of truth
- Update turn_limit_warning_present() to use the constant
- Add doc comment clarifying best-effort / race-window limitation
- Update tests to use the constant

Addresses review feedback from internal review.

* fix(bot_turns): use BOT_TURN_LIMIT_WARNING_PREFIX in hard limit message and test

Ensures the constant is the true single source of truth for the warning
prefix — both soft and hard limit messages now use it.

Addresses 臥龍 re-review feedback.

* fix(discord): use (bool, &str) tuples in turn_limit_warning_present to avoid serenity Message construction in tests

Follows existing codebase convention (see format_thread_export boundary
comment) of not constructing serenity::model::channel::Message in unit
tests. Call site maps Message → (is_bot, content) pairs before passing
to the helper. Tests now use plain tuple slices — no serde_json needed.

* fix(bot_turns): remove duplicate user_message line; restore hard limit message

- Remove extraneous duplicate 'user_message: format!(' in hard limit test
  (CI blocker — caused compilation error)
- Revert hard limit message to standalone '🛑 Hard bot turn limit reached'
  instead of using BOT_TURN_LIMIT_WARNING_PREFIX, which produced confusing
  '🛑 ⚠️ Bot turn limit reached' double-emoji output (Option B per review)
- BOT_TURN_LIMIT_WARNING_PREFIX remains the single source of truth for the
  soft limit warning and the dedup check — hard limit is rare enough that
  dedup is not needed there

Addresses masami-agent review findings.

---------

Co-authored-by: feiyun968-agent <feiyun968-agent@users.noreply.github.com>
openabdev#917)

* docs(codex): add troubleshooting for bubblewrap unavailable in sandboxed runtimes

When Codex runs inside an already-isolated OpenAB runtime without bubblewrap
installed, its inner sandbox fails with 'bubblewrap is unavailable'. Document
both resolution options: installing bwrap or disabling the inner sandbox.

Closes openabdev#908

* docs(codex): add non-privileged container reminder to sandbox note

* docs(codex): remove Dockerfile option, we provide the images

* fix(codex): install bubblewrap in Dockerfile.codex

Aligns with Dockerfile.claude which already includes bubblewrap.
This resolves the 'bubblewrap is unavailable' error at runtime.

---------

Co-authored-by: chaodu-agent <chaodu-agent@users.noreply.github.com>
…T signer email (openabdev#909)

* fix(gateway/googlechat): handle HTTP endpoint URL events + correct JWT signer email

Two independent bugs prevent the Google Chat adapter from working with the
connection mode recommended by docs/google-chat.md:

1. Envelope schema only deserialized the Pub/Sub-wrapped shape, but HTTP
   endpoint URL connections deliver top-level fields (message, user, space).
   All real webhooks silently dropped at `envelope.chat is None` with a
   200 response.

2. JWT email allow-list expected @gcp-sa-gsuiteaddons.iam.gserviceaccount.com
   (Workspace Add-ons signer), but Google Chat HTTP webhooks are signed by
   chat@system.gserviceaccount.com. Setting GOOGLE_CHAT_AUDIENCE per docs
   returned 401 to every webhook.

Together, following the docs produced a 100% non-working bot.

This change:
- Extends GoogleChatEnvelope with optional top-level fields and adds a
  fallback branch in the webhook handler; existing wrapped-shape tests
  continue to pass unchanged.
- Renames GOOGLE_CHAT_EMAIL_SUFFIX → GOOGLE_CHAT_SIGNER_EMAIL, changes
  the value to chat@system.gserviceaccount.com, and tightens the check
  from `ends_with` to exact equality.
- Updates the existing email-claim test to assert the new signer.

Verified end-to-end on a production cc-agent deployment (1:1 DM + Space
@mention) via Cloudflare Tunnel sidecar; gateway forwards events to OAB
and replies arrive in Chat.

Closes openabdev#899

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* test: add unit test for HTTP endpoint URL top-level envelope parsing

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: chaodu-agent[bot] <chaodu-agent[bot]@users.noreply.github.com>
* feat(pi): support Pi coding agent via Dockerfile.pi

* fix(pi): install git in Dockerfile.pi

* chore: fix CI matrices and Dockerfile count

* docs(pi): remove standalone Pi guide

* chore: remove redundant Anthropic API key env from config-reference.md

* chore: correct Helm NOTES.txt instructions for Pi agent authentication

* docs(pi): add docs/pi.md with advantages over other native coding agents

- No auth proxy required (native subscription support like Codex/Copilot)
- Minimal tool surface (4 tools) maximizes context window
- Multi-model support (15+ providers, switchable mid-session)
- Branching session trees for code exploration

* docs(pi): add Pi agent to README.md

---------

Co-authored-by: chaodu-agent <chaodu-agent@users.noreply.github.com>
Co-authored-by: 小喬 <xiaoqiao@openab.internal>
Co-authored-by: openab-app[bot] <274185012+openab-app[bot]@users.noreply.github.com>
dorny's GitHub account has been suspended, breaking the paths-filter
action for all users. Replace with a simple git diff + grep approach
that has zero third-party dependencies.

Functionally equivalent: detects changes in src/, gateway/, operator/
and conditionally runs the corresponding CI jobs.

Co-authored-by: chaodu-agent <chaodu-agent@users.noreply.github.com>
…penabdev#924)

* feat: openab-agent v0.1 — native Rust coding agent with ACP

Implements the v0.1 scope from ADR (PR openabdev#922):
- ACP layer: stdin/stdout JSON-RPC (initialize, session/new, session/prompt, session/cancel)
- LLM client: Anthropic provider with non-streaming API (BoxStream deferred to v0.2)
- 4 tools: read, write, edit, bash
  - Path traversal protection (canonicalize + boundary check)
  - Environment variable filtering (allow-list only)
  - Process group kill on timeout (setsid + kill(-pgid))
- Agent core: system prompt + tool dispatch loop (max 50 iterations)
- Unit tests: hand-written MockLlmProvider, tests for prompt assembly,
  tool dispatch, error handling, and multi-step tool chains
- Flat session (session tree deferred to v0.2)

Architecture:
  openab ──stdio JSON-RPC──► openab-agent ──HTTP──► Anthropic API

No external runtime required. Single binary, ~20MB when compiled.

* fix: address review findings from 覺渡 (PR openabdev#924)

- Fix validate_path: remove create_dir_all side-effect that could create
  directories outside working_dir before boundary check. Now uses parent
  traversal to find nearest existing ancestor without modifying filesystem.
- Fix unit test isolation: mark all FS/process tests with #[ignore] per
  Unit Test Strategy ADR. Only pure logic tests run by default.
- Fix unsafe setsid: check return value, propagate error on failure.
- Fix OPENAB_AGENT_BASH_ENV_ALLOW: read comma-separated env var and pass
  allowed keys to build_env for subprocess inheritance.
- Fix streaming capability: set to false in ACP initialize response since
  v0.1 uses non-streaming Anthropic Messages API.

* fix: address review findings from 普渡 (PR openabdev#924)

- 🔴 F1: Add context window truncation (MAX_CONTEXT_MESSAGES=100),
  drops oldest messages preserving first user prompt
- 🟡 F3: Return error when MAX_TOOL_LOOPS exhausted instead of empty string
- 🟡 F5: Fail-fast on missing/empty ANTHROPIC_API_KEY at session creation
- 🟡 F6: Add retry with exponential backoff for 429/529 (max 3 retries)
- 🟡 F7: Await child process after SIGKILL to prevent zombies
- 🟡 F8: Add TODO(v0.2) for session/cancel implementation
- 🟡 F9: Validate empty prompt, return -32602 error
- 🟡 F10: Add TODO(v0.2) for session TTL/cleanup
- 🟡 F11: Remove std::env::set_var from tests (UB in multi-thread)

* fix: truncate_context pair-drain + test_session_new CI fix

- N1: truncate_context now drains in pairs (assistant+user) to maintain
  strict role alternation required by Anthropic API
- N2: test_session_new sets fake ANTHROPIC_API_KEY for CI environments;
  added test_session_new_missing_key for error case

* feat: add subscription auth via OAuth device flow

Add support for Codex/OpenAI subscription authentication:

CLI:
  openab-agent auth codex-oauth  # device flow login
  openab-agent auth status       # show stored credentials
  openab-agent                   # ACP mode (default)

Auth flow:
- Device code flow against OpenAI auth endpoints
- Prints verification URL + user code for headless environments
- Polls until user authorizes in browser
- Stores tokens at ~/.openab/agent/auth.json (0600 perms)
- Auto-refreshes expired tokens with 120s skew

Provider fallback in ACP mode:
1. ANTHROPIC_API_KEY env var → Anthropic provider
2. ~/.openab/agent/auth.json → OpenAI provider (subscription)
3. Error with instructions if neither available

OpenAI provider:
- Full OpenAI chat completions API format
- Tool call translation (Anthropic format ↔ OpenAI format)
- Retry with exponential backoff on 429/529

* fix: address auth review findings from 覺渡

- 🔴 save_tokens permission race: use OpenOptions::mode(0o600) to create
  file with correct permissions atomically (no window for exposure)
- 🔴 OPENAB_AGENT_PROVIDER ignored: respect env var to force provider
  selection (anthropic|openai|codex), auto-detect only when unset
- 🟡 Token masking: safe display for any token length (>12 chars shows
  first 8 + last 4, otherwise shows ****)

* fix: address 普渡 findings F2-F8 (auth + OpenAI provider)

- F2: Add OPENAB_AGENT_OPENAI_MODEL + OPENAB_AGENT_OPENAI_BASE_URL env
  vars so OpenAI and Anthropic model namespaces don't conflict
- F3: Increase poll interval on 'slow_down' per RFC 8628 Section 3.5
- F4: Add 10-minute wall-clock timeout to device flow polling loop
- F5: Enforce minimum 5s polling interval regardless of server response
- F6: Retry on 401 with token refresh; fetch fresh token each attempt
- F8: Token masking already fixed in previous commit

F1 (client_id) and F9 (scope) pending 主人 decision.

* fix: make OAuth client_id configurable (F1)

Default: 'app_scp_codex_prod_001' (same as Codex CLI, public client)
Override: OPENAB_AGENT_OAUTH_CLIENT_ID env var

This allows users to register their own OAuth app if needed, while
maintaining compatibility with existing Codex subscriptions by default.

* fix: address remaining 普渡 findings F6-F8

- F6: Add force_refresh() that bypasses expiry check; 401 handler now
  calls force_refresh() to guarantee a new token on next retry
- F7: Add unit tests for parse_openai_response (text, tool_call, empty)
  and auth module (is_expired, auth_path, codex_client_id)
- F8: Token masking confirmed already fixed (len>12 check at line 244)

* ci: add CI workflow + Dockerfile for openab-agent

CI (.github/workflows/ci-openab-agent.yml):
- cargo fmt --check
- cargo clippy -- -D warnings
- cargo test (unit tests)
- cargo test -- --ignored (integration tests)

Dockerfile (Dockerfile.openab-agent):
- Multi-stage build: rust:1-bookworm → distroless/cc-debian12
- Final image ~20MB, runs as nonroot
- Entrypoint: openab-agent (ACP mode by default)

* fix: CI compilation errors

- Fix borrow-after-move: capture child pid before wait_with_output
- Fix unused import: restructure cfg(unix) block for pre_exec
- Fix unused variable: remove dead content Vec in OpenAiProvider
- Fix extra closing brace

* fix: resolve all clippy warnings

- Remove unused imports (anyhow::Result, serde_json::Value, Path)
- Mark Agent::new as #[cfg(test)] (only used in tests)
- Allow dead_code on LlmEvent::Error variant (reserved for future use)
- Move CommandExt import to top-level #[cfg(unix)]
- Remove unnecessary mut on child variable
- Collapse if-in-match per clippy suggestion

* fix: CI errors — cannot move in pattern guard + unused import

- Revert collapsed match (cannot move l in guard), add clippy allow
- Move CommandExt import inside unsafe block with allow(unused_imports)

* fix(openab-agent): correct device code auth flow for OpenAI

- Use https://auth.openai.com/api/accounts/deviceauth/usercode endpoint
- Use server-provided code_verifier (not client PKCE)
- Token exchange at /oauth/token with redirect_uri=https://auth.openai.com/deviceauth/callback
- Handle OpenAI's nested error format (error.code)
- Correct client_id: app_EMoamEEZ73f0CkXaXp7hrann

* refactor(openab-agent): remove unused PKCE deps (base64, sha2, getrandom)

* refactor(openab-agent): remove unused PKCE deps from Cargo.toml

* ci: add ACP smoke test — verify binary starts and speaks ACP

Sends an 'initialize' JSON-RPC request to the built binary and verifies
the response contains valid ACP agentInfo. This catches protocol-level
regressions without needing LLM credentials.

* style: cargo fmt

* fix: remove unused constants (CODEX_SCOPES, CODEX_AUDIENCE)

* feat(openab-agent): browser PKCE + device code auth flows

* feat(openab-agent): switch to Responses API (chatgpt.com/backend-api/codex/responses)

* feat(openab-agent): add codex-device subcommand + --no-browser flag

* fix(openab-agent): test cleanup for OAuth auth

* feat(openab-agent): add deps for browser PKCE flow (base64, sha2, getrandom, urlencoding, open, url)

* docs(openab-agent): guide headless users through copy-paste callback flow

* feat(openab-agent): paste-based callback for headless auth + simplified flow params

* fix(openab-agent): stop tool loop when text response received

* feat(openab-agent): Responses API with SSE stream parsing

* fix(openab-agent): correct Responses API input format for tool results (function_call_output)

* feat(openab-agent): read AGENTS.md from cwd as custom system prompt

* feat: add Dockerfile.openab-agent — native Rust agent (~20MB image)

* rename: Dockerfile.openab-agent → Dockerfile.native

* rename: remove old Dockerfile.openab-agent

* docs: add native-agent.md

* docs(README): add Native Agent to Other Agents table

* docs(native-agent): clarify AGENTS.md support, note Skills not yet supported

* docs(native-agent): note MCP not yet supported

* docs(native-agent): note MCP not yet supported

* style: cargo fmt

* style: cargo fmt

* style: cargo fmt

* style: cargo fmt

* fix: resolve clippy warnings (dead_code, is_multiple_of)

---------

Co-authored-by: chaodu-agent <chaodu-agent@users.noreply.github.com>
Co-authored-by: thepagent <hehsieh1010@gmail.com>
Co-authored-by: openab-app[bot] <274185012+openab-app[bot]@users.noreply.github.com>
* docs(adr): propose openab-agent — native Rust coding agent with built-in ACP

Single binary, no external runtime, no adapter wrapper needed.
Inspired by Pi's minimal design (4 tools, tiny prompt, session trees)
but implemented in Rust for zero-overhead ACP integration with openab core.

* docs(adr): add required crates and key advantage sections

* docs(adr): address review findings from 普渡 and 覺渡

- Fix LlmProvider trait: use BoxStream instead of bare Stream trait
  (compile error: Stream is a trait, not a concrete type)
- Fix tokio-process deprecation: use tokio::process (merged since 0.2)
- Add futures crate for Stream/BoxStream
- Add Testing Strategy section (unit test boundaries, hand-written
  mocks, integration test tags, CI pipeline)
- Remove deprecated sandbox-exec reliance on macOS
- Add explanatory notes on trait object safety design decisions

---------

Co-authored-by: chaodu-agent <chaodu-agent@users.noreply.github.com>
Merged upstream commits from 2026-05-06 to 2026-05-27:
- feat: openab-agent — native Rust coding agent (openabdev#924)
- feat: Pi coding agent support via Dockerfile.pi (openabdev#920)
- feat: Google Antigravity CLI adapter (openabdev#896)
- feat(cron): goal-driven auto-disable usercron jobs (openabdev#818)
- fix: capture ACP agent stderr + JSON-RPC error extraction (openabdev#885)
- fix(media): validate Content-Type and magic bytes (openabdev#793)
- fix(discord): dedup WarnAndStop warning across bots (openabdev#886)
- feat(chart): imagePullSecrets + serviceAccountName support
- Many other upstream fixes and improvements

Preserved local customizations:
- Dockerfile: dual build (openab + openab-gateway), kiro-cli 2.2.0, tini
- docker-entrypoint.sh: dynamic config from env vars, Telegram gateway,
  conditional allowed_channels (fix for empty DISCORD_CHANNEL_ID)
- gateway/src/adapters/telegram.rs: complete file, no parse_mode Markdown
- src/main.rs: comma-separated ID support in parse_id_set

https://claude.ai/code/session_01L2ZNFRhCDX2HxB9QEPJWgK
@github-actions
Copy link
Copy Markdown

⚠️ This PR is missing a Discord Discussion URL in the body.

All PRs must reference a prior Discord discussion to ensure community alignment before implementation.

Please edit the PR description to include a link like:

Discord Discussion URL: https://discord.com/channels/...

This PR will be automatically closed in 3 days if the link is not added.

claude added 2 commits May 27, 2026 02:15
gateway/src/media.rs exports utility functions (resize_and_compress,
audio_extension, is_text_extension) and constants not yet used by
current adapters. Add #![allow(dead_code)] so CI clippy -D warnings
does not treat them as errors.

https://claude.ai/code/session_01L2ZNFRhCDX2HxB9QEPJWgK
@github-actions
Copy link
Copy Markdown

🔒 Auto-closing: this PR has had the closing-soon label for more than 3 days without a Discord Discussion URL being added.

Feel free to reopen after adding the discussion link to the PR body.

@github-actions github-actions Bot closed this May 30, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.