Skip to content

perf: cache deps layer + drop arm64 QEMU build - #2

Merged
thepagent merged 2 commits into
mainfrom
perf/build-speed
Apr 3, 2026
Merged

perf: cache deps layer + drop arm64 QEMU build#2
thepagent merged 2 commits into
mainfrom
perf/build-speed

Conversation

@thepagent

@thepagent thepagent commented Apr 3, 2026

Copy link
Copy Markdown
Collaborator

Problem

Build took 45 min because arm64 was compiled under QEMU emulation on an amd64 runner.

Fix

  1. Cache deps layer — build dependencies with dummy main.rs first (Docker layer cache)
  2. Native multi-arch matrix — parallel builds on native runners, then merge manifests

Before (sequential, QEMU)

┌─────────────────────────────────────────────────────────┐
│  ubuntu-latest (amd64)                                  │
│                                                         │
│  cargo build (amd64)  ──>  cargo build (arm64 / QEMU)  │
│       ~4 min                     ~40 min 🐌             │
│                                                         │
└─────────────────────────────────────────────────────────┘
                          Total: ~45 min

After (parallel, native runners)

┌──────────────────────────┐
│  ubuntu-latest (amd64)   │
│  cargo build  ~4 min     │──┐
└──────────────────────────┘  │  ┌──────────────────┐  ┌────────────┐
                              ├─>│ merge manifests   │─>│ bump-chart │
┌──────────────────────────┐  │  │ multi-arch image  │  │ + release  │
│  ubuntu-24.04-arm (arm64)│──┘  └──────────────────┘  └────────────┘
│  cargo build  ~4 min     │
└──────────────────────────┘
                          Total: ~5 min

@thepagent
thepagent merged commit 4b5bf93 into main Apr 3, 2026
henrieopenclaw added a commit to henrieopenclaw/agent-broker that referenced this pull request Apr 6, 2026
* perf: cache dependency build layer in Dockerfile

* perf: native multi-arch build with matrix runners

---------

Co-authored-by: thepagent <thepagent@users.noreply.github.com>
Reese-max pushed a commit to Reese-max/openab that referenced this pull request Apr 12, 2026
* perf: cache dependency build layer in Dockerfile

* perf: native multi-arch build with matrix runners

---------

Co-authored-by: thepagent <thepagent@users.noreply.github.com>
brettchien added a commit to brettchien/openab that referenced this pull request Apr 19, 2026
Before this change, `openab-claude:0.7.8-beta.7` ships:
  - claude-agent-acp@0.25.0 — hardcoded model list, no Opus 4.7
  - claude-code@2.1.104     — knows up to Opus 4.6 only
  - ENV CLAUDE_CODE_EXECUTABLE=/usr/local/bin/claude (openabdev#447)

With openabdev#447 making the pinned claude-code binary load-bearing, neither
the adapter's availableModels nor the CLI's model resolver knows about
Opus 4.7 — users get Sonnet 4.6 regardless of ANTHROPIC_MODEL=opus.

This PR:
  - introduces `CLAUDE_AGENT_ACP_VERSION` ARG (pattern parity with
    `CLAUDE_CODE_VERSION` from openabdev#326/openabdev#412)
  - bumps adapter 0.25.0 → 0.29.2 (brings claude-agent-sdk 0.2.111+
    whose availableModels includes Opus 4.7)
  - bumps claude-code 2.1.104 → 2.1.114

anthropics/claude-code#49512 (parallel-mkdir ENOENT race) was filed
against 2.1.112 and is still OPEN but has zero comments. Inspection
of the 2.1.114 install shows the CLI moved to a per-platform native
binary (openabdev#2.1.113) and session state relocated from
`/tmp/claude-<uid>/…/tasks/` to `$HOME/.claude/{projects,tasks}/`,
so the vulnerable filesystem layout no longer exists — binary-grep
for `/home-//tasks` returns 0 matches. 50 parallel `claude -p` calls
as the same user (40ms stagger) produced 0/50 errors, 0 bytes stderr,
and no `/tmp/claude-*` directories.

Refs openabdev#326, openabdev#412, openabdev#418, openabdev#447
Closes nothing explicitly (no issue filed; repro + rationale in body).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
masami-agent added a commit to masami-agent/openab that referenced this pull request May 1, 2026
- CONTRIBUTING.md now has all 8 sections (0-7) matching the ADR table
- Added missing sections: At a Glance (openabdev#2), split Proposed Solution (openabdev#4)
  and Why This Approach (openabdev#5) into separate sections
- ADR status updated from Proposed to Accepted
wangyuyan-agent added a commit to wangyuyan-agent/openab that referenced this pull request May 3, 2026
…nd nits

- Remove dead variables in markdown_to_post() (blocker openabdev#2)
- Fix allowlist bypass: check allowlist before bot classification (suggested openabdev#3)
- Fix thread key mismatch: use thread_id.unwrap_or(channel_id) (suggested openabdev#4)
- Add /models and /agents slash commands
- NITs: let mut→let, cancel format, DM comment, bot_turns TODO,
  timeout 5s, dead_code allow, warn on parse error
thepagent pushed a commit that referenced this pull request May 3, 2026
…o-bot readiness (#706)

* feat(gateway): feishu slash commands, rich text, and bot-to-bot readiness

- /reset and /cancel interception in OAB core gateway adapter (src/gateway.rs)
- Rich text (post) messaging: markdown_to_post() converter + send_post_message()
- Bot-to-bot: AllowBots enum, trusted_bot_ids, max_bot_turns, bot detection heuristic
- Updated docs/feishu.md with new sections and env vars

* feat(gateway): streaming (typewriter) support for Feishu adapter

- OAB core: send_message request-response for Feishu (real message_id)
- OAB core: edit_message sends command via WebSocket
- OAB core: use_streaming returns true for Feishu only
- GatewayResponse: add message_id field (backward compatible)
- Gateway service: edit_feishu_message via PUT API
- Gateway service: handle_reply returns message_id in response

* docs: add streaming section to feishu.md

* fix: address PR #706 review — blockers, suggested changes, and nits

- Remove dead variables in markdown_to_post() (blocker #2)
- Fix allowlist bypass: check allowlist before bot classification (suggested #3)
- Fix thread key mismatch: use thread_id.unwrap_or(channel_id) (suggested #4)
- Add /models and /agents slash commands
- NITs: let mut→let, cancel format, DM comment, bot_turns TODO,
  timeout 5s, dead_code allow, warn on parse error

* docs: fix bot-to-bot description after allowlist bypass fix

* docs: add bot-to-bot env vars to gateway README

* fix: use split_once per clippy

* refactor: remove /models /agents (follow-up PR), replace platform hardcode with supports_streaming()

- Remove /models and /agents slash command interception (will be a separate PR
  with exact matching, ambiguity handling, get_or_create, category aliasing)
- Replace channel.platform == "feishu" with supports_streaming() method
- supports_streaming() uses matches!(platform_name, "feishu" | "lark")

* refactor: remove /models /agents (follow-up PR), config-driven streaming

- Remove /models and /agents slash commands (separate PR with design review)
- Replace platform-specific checks with config-driven streaming flag
- send_message: all platforms use request-response + 5s timeout fallback
- use_streaming: reads self.streaming from GatewayConfig
- Add gateway.streaming config field (default false)
- Update docs/feishu.md: remove /models /agents, streaming via config

* fix: streaming-only request-response, proper inline code parsing

- send_message: request-response only when self.streaming is true,
  fire-and-forget otherwise (no Telegram regression)
- parse_inline: paired backtick parsing preserves literal content
  inside code spans, no longer strips markers inside inline code

* fix(gateway): slash command responses use fire-and-forget, not request-response

- Extract SharedWsTx type alias (Arc<Mutex<SplitSink>>) so ws_tx can be
  shared between GatewayAdapter and the event loop
- Add send_fire_and_forget() helper for slash command responses that
  don't need message_id back (no 5s timeout penalty)
- send_message() still uses request-response when streaming=true
  (needed for placeholder message_id in streaming edit flow)

Addresses review feedback: slash command responses (/reset, /cancel)
no longer go through the request-response path, avoiding unnecessary
latency when gateway.streaming is enabled.

* docs(gateway): add TODO for bot-to-bot core guard gap (擺渡法師 finding)

Gateway core unconditionally drops is_bot events, but feishu adapter's
AllowBots filtering happens before events reach core. When Feishu lifts
the bot-to-bot delivery restriction, this guard needs to become
adapter-aware. Telegram adapter does not filter bots, so we cannot
simply remove the guard without introducing regression.

* docs(feishu): narrow bot-to-bot claim to gateway-side scaffolding only

Per 擺渡法師 review: PR claimed 'bot-to-bot readiness' but OAB core
unconditionally drops is_bot events (src/gateway.rs:430). The gateway
adapter's AllowBots filtering works, but events never reach the router.

Updated docs/feishu.md to explicitly state both blockers:
1. Feishu platform doesn't deliver bot messages to other bots
2. OAB core drops is_bot events before router

Claim narrowed from 'readiness' to 'gateway-side scaffolding only'.

* fix(feishu): paired-only marker stripping + send failure response (普渡法師 findings)

1. parse_inline(): only strip *paired* markdown markers (**, *, ~~).
   Unpaired markers kept as literal text. Fixes: ~/.ssh → /.ssh,
   *.rs → .rs, 3 * 4 → 3  4

2. handle_reply(): send GatewayResponse { success: false } when
   send_post_message() fails. Prevents core from waiting 5s timeout
   then sending edit_message to a non-existent message_id.

---------

Co-authored-by: wangyuyan-agent <265828726+wangyuyan-agent@users.noreply.github.com>
Co-authored-by: chaodu-agent <chaodu-agent@openab.dev>
thepagent pushed a commit that referenced this pull request May 4, 2026
* RFC 002: PR contribution guidelines with mandatory prior art research

Add RFC, PR template, and CONTRIBUTING.md requiring contributors to
research OpenClaw and Hermes Agent before proposing solutions.

- docs/rfcs/002-pr-guidelines.md — full RFC (follows RFC 001 format)
- .github/pull_request_template.md — auto-populated PR form
- CONTRIBUTING.md — contributor guide linking to RFC

* feat: add Discord Discussion URL field to PR template, CONTRIBUTING.md, and RFC

* docs: reformat RFC 002 as ADR under docs/adr/

Move PR contribution guidelines from docs/rfcs/ to docs/adr/ format,
matching the existing ADR structure (line-adapter, custom-gateway).

* docs: implement tiered PR contribution guidelines

Rework PR #302 to address review feedback from @shaun-agent and @masami-agent:

- Prior art research: required for architectural/runtime changes, N/A for docs/chore/CI
- Validation section: multi-surface (Rust, Helm, CI, docs) instead of Rust-only
- Discord Discussion URL: strongly recommended instead of auto-close policy
- CONTRIBUTING.md: add link to ADR as specified in implementation table
- ADR: update to reflect tiered policy (Option 3) as adopted approach
- Fix trailing whitespace in PR template

* docs: align CONTRIBUTING.md sections with ADR, update status to Accepted

- CONTRIBUTING.md now has all 8 sections (0-7) matching the ADR table
- Added missing sections: At a Glance (#2), split Proposed Solution (#4)
  and Why This Approach (#5) into separate sections
- ADR status updated from Proposed to Accepted

---------

Co-authored-by: OpenAB Bot <openab-bot@users.noreply.github.com>
Co-authored-by: chaodufashi <chaodu-agent@openab.dev>
Co-authored-by: chaodu-agent <chaodu-agent@users.noreply.github.com>
Co-authored-by: Masami <masami-agent@users.noreply.github.com>
brettchien added a commit to brettchien/openab that referenced this pull request May 5, 2026
…ts, fix ADR path

- main.rs: collapse 3x repeated (cap, grouping, idle) match blocks into
  dispatch::dispatch_params(mode, max_buffered).
- dispatch.rs: replace magic 4 / 512 in estimate_tokens with named
  CHARS_PER_TOKEN_ESTIMATE / TOKENS_PER_IMAGE_ESTIMATE constants.
- dispatch.rs: fix top-level ADR reference to point at the actual
  docs/adr/turn-boundary-batching.md path landing in openabdev#598.

Addresses chaodu-agent NITs openabdev#1, openabdev#2, openabdev#5 from PR openabdev#686.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
chaodu-agent pushed a commit that referenced this pull request Jun 10, 2026
🔴 #1: Thread key is available in <sender_context> JSON block that OAB
   already injects into every prompt. Adapter parses thread_id from it.
   No OAB changes needed. Added detailed explanation + fallback.

🟡 #2: session/load now documents expired session handling — if 404,
   transparently re-invoke (AgentCore auto-mounts persisted filesystem).

🟡 #3: Cancel strategy is now configurable via --cancel-strategy flag
   (noop = ignore cancel, stop = StopRuntimeSession). Default: stop.
chaodu-agent pushed a commit that referenced this pull request Jun 10, 2026
- Fix #1: synthesis uses absolute path /opt/agentcore-acp/agentcore_acp.py
- Fix #2: Dockerfile.agentcore adds curl, installs uv as agent user,
  installs boto3 system-wide, sets PATH for agent
- Fix #3: replace value-based heuristic with command_explicit bool flag
  (immune to OPENAB_AGENT_COMMAND env pollution)
- Fix #4: cancel_strategy is now a validated enum (stop|noop), rejects typos
- Fix #6: docs priority rules clarify env var interaction
- Fix #7: docs use absolute path consistently
chaodu-agent pushed a commit that referenced this pull request Jun 12, 2026
…on destroy

- #1 (🔴) Fix None dereference: check session is not None before accessing
- #2 (🟡) Log shell reader errors to stderr instead of silently swallowing
- #3 (🟡) Clear stale response queue on reconnect
- #4 (🟡) Remove unused ACP_READY_MARKER constant
- #5 (🟡) Add session/destroy handler to clean up resources
thepagent pushed a commit that referenced this pull request Jun 13, 2026
…ions (#1089)

* feat(agentcore): use InvokeAgentRuntimeCommandShell for persistent sessions

Replace InvokeAgentRuntime (stateless HTTP) with InvokeAgentRuntimeCommandShell
(persistent WebSocket PTY).  This opens a long-lived shell in the microVM, runs
kiro-cli in native ACP mode, and bridges JSON-RPC bidirectionally.

Fixes: each Discord thread message was treated as a new conversation because
InvokeAgentRuntime hit healthcheck.py which spawned a fresh kiro-cli per call.

Now: same shell_id = same kiro-cli process = continuous session memory.

Key changes:
- Switch from boto3 invoke_agent_runtime to bedrock-agentcore SDK open_shell()
- Deterministic shell_id derived from thread_id for reconnection support
- Async architecture (asyncio) for WebSocket + stdin multiplexing
- Auto-reconnect via ReconnectConfig on transient drops
- Forwards kiro-cli ACP notifications (streaming) directly to OAB stdout

* fix: address review — None deref, error logging, queue cleanup, session destroy

- #1 (🔴) Fix None dereference: check session is not None before accessing
- #2 (🟡) Log shell reader errors to stderr instead of silently swallowing
- #3 (🟡) Clear stale response queue on reconnect
- #4 (🟡) Remove unused ACP_READY_MARKER constant
- #5 (🟡) Add session/destroy handler to clean up resources

* nit: remove redundant import sys in _read_loop

* fix: address 擺渡 review — ANSI stripping, JSON-RPC validation, dispatch guard

- F1: Dispatch loop now skips messages without 'method' field (no false error responses)
- F2: Strip ANSI escape codes before JSON parse; require line starts with '{' and
  contains 'jsonrpc' field to be treated as valid JSON-RPC
- Disable PTY echo via 'stty -echo' before launching kiro-cli to reduce noise

* fix: use _extract_json_object for PTY-robust JSON parsing

Handles shell prompt prefix (e.g. 'agent@vm:~$ {"jsonrpc"...}')
by reusing the existing brace-counting parser instead of startswith.

* nit: move import time to top-level

* feat(agentcore): replace Python adapter with native Rust WebSocket bridge

Drop agentcore/acp/ Python adapter entirely.  The bridge now lives in
src/acp/agentcore.rs and runs as an 'openab agentcore-bridge' subprocess
spawned by config.rs when [agentcore] is set.

Architecture:
  OAB pool → spawn('openab agentcore-bridge --runtime-arn ... --region ...')
           → opens WebSocket to InvokeAgentRuntimeCommandShell
           → launches 'kiro-cli acp --trust-all-tools' inside PTY
           → bidirectional JSON-RPC over binary frames (0x00=stdin, 0x01=stdout)

Key benefits over Python version:
- Single binary, no Python/uv/pip dependency
- Native tokio async, zero overhead
- SigV4 signing via aws-sigv4 crate (same as agctl)
- Deterministic shell_id for reconnection on transient drops
- PTY noise filtering (ANSI strip, JSON boundary detection)

New dependencies (behind 'agentcore' feature flag, included in default):
  aws-sigv4, aws-credential-types, urlencoding, hex, http

Note: Cannot verify full compilation in this environment (missing cc linker),
but rustfmt --edition 2021 confirms all files parse correctly.

* fix: port F1 guard to Rust — skip messages without method field

* fix: address 覺渡 review — channel constants + brace-counting JSON extraction

- Replace magic 0x00/0x01/0x02 with named CHANNEL_STDIN/STDOUT/STDERR constants
- Replace naive find('{') with brace-counting extract_json_object() that handles
  PTY noise prefixes containing '{' characters (same approach as Python version)

* fix: add session/destroy handler + address remaining review findings

- Add session/destroy and session/stop handlers (parity with Python version)
- Sessions are removed from HashMap on destroy, dropping ShellHandle and
  aborting the pump task (Rust Drop semantics handle cleanup)

* fix: exit pump task when receiver is dropped (prevent leak)

* docs: update Dockerfile.agentcore and docs for native Rust bridge

- Dockerfile.agentcore: remove Python/pip/uv/boto3, single binary only (~20MB)
- docs/agentcore.md: update architecture diagram, prerequisites, IAM policy,
  config examples to reflect WebSocket shell bridge

* fix: resolve borrow checker errors + deprecated aws_config::from_env

- Take line_rx out of session via std::mem::replace to avoid holding
  &mut self across await points while also writing to stdout
- Replace deprecated aws_config::from_env() with aws_config::defaults()

* fix: resolve clippy lints — unused imports, dead code, iterator, useless into

- Remove unused tracing imports (debug, error, warn)
- Allow dead_code on runtime_session_id and cancel_strategy (future use)
- Use iter().position() instead of manual index loop
- Remove useless .into() on Vec<u8> → Vec<u8>

* fix: remove unnecessary to_string in handle_cancel

* fix: update agentcore test to expect self-spawn instead of uv

* fix(agentcore): send initialize + session/new to kiro-cli in PTY

The bridge was intercepting initialize/session/new from OAB but never
forwarding them to kiro-cli. Without initialization, kiro-cli silently
drops all subsequent messages (session/prompt returns empty).

Fix: after opening the WebSocket PTY and launching kiro-cli acp,
send initialize → wait for response → send session/new → store the
kiro sessionId → use it when forwarding prompts.

* feat(agentcore): make bridge agent-agnostic with --command flag

The bridge is now a generic ACP proxy that works with any ACP agent,
not just kiro-cli. The command to run in the PTY is configurable:

  [agentcore]
  runtime_arn = "arn:..."
  shell_command = "kiro-cli acp --trust-all-tools"  # default

Other examples:
  shell_command = "claude-agent-acp"
  shell_command = "codex-acp"
  shell_command = "opencode acp"

CLI flag: openab agentcore-bridge --command "..."

* fix(agentcore): retry initialize send instead of fixed 2s sleep

* fix(agentcore): eager shell init during session/new

Move open_shell + initialize + session/new handshake from handle_prompt
into the session/new handler. By the time session/prompt arrives, the
shell is already connected and kiro-cli is initialized.

This eliminates the race where the first prompt times out because
shell opening + agent boot takes longer than OAB's dispatch timeout.

* fix(agentcore): remove runtimeSessionId from query string

Per the Python SDK, session_id belongs in the signed header
(X-Amzn-Bedrock-AgentCore-Runtime-Session-Id), not as a query param.
The extra query param was causing 400 Bad Request.

* fix(agentcore): remove qualifier=DEFAULT from shell URL

* fix(agentcore): switch to presigned URL auth for WebSocket shell

Use SigV4 query string auth (presigned URL) instead of header auth.
Matches the Python SDK's connect_shell_presigned approach.
Session ID passed as query param covered by signature.

* fix: add qualifier=DEFAULT back

* fix(agentcore): retry initialize with 10s wait per attempt

kiro-cli needs several seconds to boot in the PTY before it starts
listening on stdin. Send initialize, wait 10s for response, retry
up to 5 times.

* fix(agentcore): skip notifications in init handshake, add cwd to session/new

kiro-cli sends notifications before responding to session/new.
The init loop now checks for matching 'id' field and skips
notifications. session/new includes cwd and mcpServers params
as required by kiro-cli.

* fix(agentcore): copy OAuth DB to /tmp before starting agent

AgentCore's /mnt/agent filesystem doesn't support SQLite file locking.
Copy the DB to /tmp (local filesystem) and set XDG_DATA_HOME=/tmp so
kiro-cli finds auth on a lockable filesystem.

* docs(agentcore): add architecture diagram and shell_command config

---------

Co-authored-by: 超渡法師 <chaodu@openab.dev>
Co-authored-by: chaodu-agent <chaodu-agent@users.noreply.github.com>
chaodu-agent pushed a commit that referenced this pull request Jun 15, 2026
…ft path

- edit_message with reply_to='draft': route to sendRichMessageDraft (rich)
  or silently drop (non-rich). Never attempt editMessageText on dummy ref.
- edit_message with real message_id: perform actual editMessageText for
  legacy streaming placeholder updates.
- Stream finalization: when placeholder is dummy 'draft' ref, send final
  content as new message (send_message) so it gets persisted. This ensures
  rich path uses sendRichMessage for the final reply, not just a draft.

Addresses 擺渡法師's 🔴 #2 — streaming finalization must persist.
thepagent pushed a commit that referenced this pull request Jun 16, 2026
* docs: add ADR for Telegram Rich Messages (Bot API 10.1)

Propose integrating sendRichMessage and sendRichMessageDraft into the
TG gateway adapter for structured formatting and AI streaming UX.

Refs: https://core.telegram.org/bots/api#rich-message-formatting-options

* feat(gateway): implement Telegram Rich Messages (Bot API 10.1)

- Add sendRichMessage() for structured content (tables, code, headings)
- Add sendRichMessageDraft() for future AI streaming support
- Add is_complex_markdown() classifier to route complex replies
- Feature-gated via TELEGRAM_RICH_MESSAGES=true env var (default: off)
- Falls back to sendMessage on sendRichMessage failure

When enabled, replies containing tables, fenced code blocks, headings,
or content >4096 chars will use sendRichMessage with InputRichMessage
markdown format. This passes agent markdown directly (GFM-compatible)
without needing any conversion layer.

* fix: improve ATX heading detection in is_complex_markdown

Address review from 擺渡法師:
- Detect h1-h6 headings (not just h1-h3)
- Handle headings at line start (not just after \n)
- Handle indented headings (leading whitespace)
- Reject #hashtag (no space after #)

* fix: add 32768 char truncation before sendRichMessage

Address review from 口渡法師: clamp content to Bot API limit
(32768 UTF-8 chars) before sending, avoiding wasted round-trip
on oversized payloads.

* feat: default TELEGRAM_RICH_MESSAGES to true (opt-out)

Users get rich messages automatically. Set TELEGRAM_RICH_MESSAGES=false
to opt out.

* fix: robust table separator detection for aligned GFM tables

Address 擺渡法師 review: LLM outputs commonly use alignment markers
like |:---|, |---:|, | :---: | which the naive |---| check missed.

Now parses the separator row properly: starts/ends with |, each cell
between pipes contains only dashes (optionally wrapped in colons).

* fix: detect tilde fences (~~~) in is_complex_markdown

Address 口渡法師 review: some agents/tools use ~~~ instead of backtick
fences. Both are valid GFM code fences.

* docs: add inline design decision comments

Explain rationale for:
- Classify at adapter layer (not agent) — zero prompt changes needed
- Conservative heuristic — only route when legacy visibly breaks
- 4096 threshold — sendMessage hard limit, prefer rich over chunking
- GFM table parsing — avoid false positives on plain pipe text
- Fallback strategy — one extra round-trip worst case, never lost delivery
- sendRichMessageDraft — wired for Phase 2 streaming

* feat(gateway/telegram): wire edit_message to sendRichMessageDraft for streaming

* feat(gateway/telegram): send RichBlockThinking draft on first reaction (👀)

* feat(gateway/telegram): state-aware thinking animations (👀🤔👨‍💻🔥⚡)

* fix(gateway/telegram): use html field for tg-thinking drafts

* fix(gateway/telegram): drop custom emoji from thinking (RICH_MESSAGE_EMOJI_INVALID)

* fix(gateway/telegram): skip short streaming drafts, let thinking show

* fix(gateway/telegram): suppress streaming placeholder, use thinking draft instead

* Revert "fix(gateway/telegram): suppress streaming placeholder, use thinking draft instead"

This reverts commit bad8749.

* feat: add streaming_placeholder config to suppress '…' placeholder

When streaming_placeholder = false, the core skips sending the initial
'…' message. The gateway's edit loop still works via a dummy MessageRef
since the gateway adapter uses sendRichMessageDraft (no real msg_id needed).

Config:
  [gateway]
  streaming_placeholder = false

* fix: MessageRef field is message_id not id

* fix(gateway/telegram): address review findings from musingfox

🔴 1: Rich truncation now counts chars, not bytes (CJK safe)
🔴 2: Legacy sendMessage chunks at 4096 chars (no more lost replies)
🟡 3: Code blocks stay on legacy path (preserves syntax highlighting);
     only tables + headings route to sendRichMessage

* fix(gateway): update test — code blocks no longer trigger is_complex_markdown

* fix: address review findings — draft guard, draft_id collision, docs/ADR sync

- Guard edit_message when rich_messages=false (drop silently instead of
  attempting editMessageText with dummy 'draft' message_id)
- Incorporate thread_id in draft_id derivation to prevent forum topic
  collision
- ADR: remove code blocks from classification, fix MarkdownV2→Markdown,
  update config reference from TOML to env var
- docs/telegram.md: update markdown rendering section, add
  TELEGRAM_RICH_MESSAGES to env var table
- docs/config-reference.md: add streaming + streaming_placeholder fields
- config.toml.example: add streaming/streaming_placeholder examples

* fix: proper edit_message handling — finalize via send_message for draft path

- edit_message with reply_to='draft': route to sendRichMessageDraft (rich)
  or silently drop (non-rich). Never attempt editMessageText on dummy ref.
- edit_message with real message_id: perform actual editMessageText for
  legacy streaming placeholder updates.
- Stream finalization: when placeholder is dummy 'draft' ref, send final
  content as new message (send_message) so it gets persisted. This ensures
  rich path uses sendRichMessage for the final reply, not just a draft.

Addresses 擺渡法師's 🔴 #2 — streaming finalization must persist.

* docs(adr): fix remaining MarkdownV2 reference, update status to Accepted

- Context section: MarkdownV2 → Markdown (matches actual code)
- Status: Proposed → Accepted (feature is implemented)

---------

Co-authored-by: chaodu-agent <bot@openab.dev>
Co-authored-by: chaodu-agent <chaodu-agent@users.noreply.github.com>
Co-authored-by: chaodu-agent <chaodu-agent@openab.dev>
chaodu-agent pushed a commit that referenced this pull request Jun 18, 2026
Addresses review feedback from 口渡法師:

🔴 #1: register_thread() was never called
- Discord: call register_thread(registry, channel_id, "discord") before submit
- Slack: call register_thread(registry, thread_id, "slack") before submit
- Pass ctl_registry through run_slack_adapter → handle_message
- Add ctl_registry field to Discord Handler struct

🟡 #2: SlackAdapter missing rename_thread
- Implement via conversations.rename API
- Works for channels; message-thread rename not supported by Slack API
thepagent pushed a commit that referenced this pull request Jun 19, 2026
* feat: add `openab set/get` subcommands with Unix socket IPC

Phase 1 implementation of runtime control via IPC:

- Add `openab set <key> <value>` and `openab get <key>` subcommands
- IPC over Unix domain socket (/tmp/openab.sock, configurable via OPENAB_SOCK)
- `openab run` spawns a control socket server on startup
- Client (set/get) connects to the running daemon, sends JSON, gets response

Supported keys (Phase 1):
- thread.name — rename the current Discord/Slack thread

Architecture:
- Same binary, different modes (like consul/vault)
- `openab run` = daemon, `openab set/get` = client
- Handler reads OPENAB_THREAD_ID env var for target channel
- Extensible: new keys just add match arms in RuntimeHandler

Closes #1131

* feat: add thread.archived and agent.status to openab set

New keys:
- thread.archived (true/false) — archive/unarchive Discord thread
- agent.status — set bot custom presence (Discord gateway)

Implementation:
- Add archive_thread() to ChatAdapter trait (default no-op)
- Implement via EditThread in DiscordAdapter
- agent.status uses ShardMessenger::set_presence with ActivityData::custom
- ShardMessenger shared via Arc<OnceLock<>> set in ready handler

* feat: add --thread flag and adapter routing via ThreadRegistry

- openab set/get now accept --thread <ID> (falls back to $OPENAB_THREAD_ID)
- RuntimeHandler maintains a HashMap<platform, adapter> for multi-platform
- ThreadRegistry (thread_id → platform) resolves which adapter handles a thread
- Exposes ctl::new_registry() and ctl::register_thread() for adapters to call
  on message dispatch

Usage:
  openab set --thread 1517277368 thread.name "new name"
  OPENAB_THREAD_ID=1517277368 openab set thread.name "new name"

* feat: default rename_thread/archive_thread returns unsupported error

Agent gets explicit feedback when the platform doesn't support the
operation, rather than silent no-op success.

* fix: wire ThreadRegistry into dispatch + implement Slack rename_thread

Addresses review feedback from 口渡法師:

🔴 #1: register_thread() was never called
- Discord: call register_thread(registry, channel_id, "discord") before submit
- Slack: call register_thread(registry, thread_id, "slack") before submit
- Pass ctl_registry through run_slack_adapter → handle_message
- Add ctl_registry field to Discord Handler struct

🟡 #2: SlackAdapter missing rename_thread
- Implement via conversations.rename API
- Works for channels; message-thread rename not supported by Slack API

* fix: chmod 0600 on control socket after bind (defense-in-depth)

Per 覺渡法師 review — restrict socket to owner-only even in
single-tenant pods, as a low-cost hardening measure.

* fix: remove broken Slack rename_thread — fall back to unsupported error

Per 擺渡法師 review:
- conversations.rename only works on channels, not threads
- Calling it on a thread returns invalid_channel
- Calling it on a channel renames the entire public channel (dangerous)

Correct behavior: Slack threads don't support rename, so use the
trait default which returns 'not supported on this platform'.

* fix: address 🟡 review items — log spam + agent.status messaging

- dispatch.rs: downgrade rename_thread failure from warn → debug
  (expected on platforms that don't support it, not log-worthy)
- ctl.rs: clarify agent.status error — 'only supported on Discord'
  instead of misleading 'shard not ready'

* fix: clone ctl_registry before async move to avoid use-after-move

E0382: ctl_registry was moved into the Slack tokio::spawn closure,
then referenced again when constructing the Discord Handler.
Clone before the closure so the original remains available.

* fix: remove unsafe set_var from tests, use explicit socket path

- Add spawn_server_at(path) and send_request_to(path) variants
- Test uses explicit PathBuf instead of env var mutation
- Avoids Rust 1.80+ UB with set_var in multi-threaded tests

---------

Co-authored-by: 超渡法師 <超渡法師@openab.dev>
chaodu-agent pushed a commit that referenced this pull request Jun 24, 2026
- Add is_denied_user access control gate (🔴 #1)
- Kill orphaned child process on empty-output early return (🔴 #2)
- Add single-flight AtomicBool guard to prevent concurrent /auth (🟡 #4)
- Truncate output to fit Discord 2000-char limit (🟡 #5)
- Scope readers in a block to drop cleanly before wait (🟡 #6)
chaodu-agent pushed a commit that referenced this pull request Jun 24, 2026
…r) wait

- Extract the UTF-16-code-unit truncation into a pure, module-level
  truncate_to_utf16_budget() helper (matching the repo's testable
  decision-helper convention) and cover it with unit tests: short body,
  prefix/suffix budgeting, surrogate-pair counting, no-scalar-splitting,
  zero-budget saturation, and an assembled-total-within-limit regression
  guard for the original scalar-count miscount. (addresses review #2 / tests)
- Add the missing tracing::error on the final child.wait() Ok(Err(e)) arm,
  for symmetry with the spawn-failure path.
chaodu-agent pushed a commit that referenced this pull request Jun 26, 2026
Resolved:
- [口渡 #1] channels empty = disabled (fail-safe), not all channels
- [口渡 #2] Buffer lifecycle: swap-and-drain model, flush clears buffer
- [口渡 #3] @mention triggers immediate flush + separate normal dispatch
- [口渡 #4] Dedicated ambient session pool, isolated from main pool
- [口渡 #5] Bot echo prevention: own messages never enter buffer
- [口渡 #6] max_concurrent_flushes cap for global rate limiting
- [口渡 #9] context_window = Discord API fetch, clarified semantics
- [口渡 #10] Error handling table added (timeout, tool calls, etc.)
- [核渡 #1] OpenClaw described as cross-platform group chat feature
- [核渡 #2] Hermes backfill scope clarified (mention-only, skips free-response)
- [核渡 #3] Added Hermes inline reply + per-user session detail
- [覺渡 #1] Race condition resolved: swap-and-drain buffer model
- [覺渡 #2] Session strategy fully specified (separate pool, rolling window)
- [覺渡 #3] Mention detection fires before buffer, reuses existing logic
- [覺渡 #4] flush_hard_cap = 50 as safety cap
- [覺渡 #5] ±20% jitter on flush interval to prevent thundering herd
- [覺渡 #6] context_window semantics clarified (API fetch, not buffer)
chaodu-agent pushed a commit that referenced this pull request Jun 26, 2026
[擺渡 #1] Add concurrent reply prevention via per-channel flushing flag
[擺渡 #2] Ambient dispatches post directly, no thinking placeholder
[擺渡 #3] Handle None on first rx.recv() (channel closed → exit)
[擺渡 #4] Bot loop prevention: MAX_CONSECUTIVE_BOT_TURNS + prompt instruction
thepagent added a commit that referenced this pull request Jun 27, 2026
* docs: add ADR for Ambient Mode

* docs: update ADR with batch flush strategy and prior art

* docs: address all review findings (口渡, 核渡, 覺渡)

Resolved:
- [口渡 #1] channels empty = disabled (fail-safe), not all channels
- [口渡 #2] Buffer lifecycle: swap-and-drain model, flush clears buffer
- [口渡 #3] @mention triggers immediate flush + separate normal dispatch
- [口渡 #4] Dedicated ambient session pool, isolated from main pool
- [口渡 #5] Bot echo prevention: own messages never enter buffer
- [口渡 #6] max_concurrent_flushes cap for global rate limiting
- [口渡 #9] context_window = Discord API fetch, clarified semantics
- [口渡 #10] Error handling table added (timeout, tool calls, etc.)
- [核渡 #1] OpenClaw described as cross-platform group chat feature
- [核渡 #2] Hermes backfill scope clarified (mention-only, skips free-response)
- [核渡 #3] Added Hermes inline reply + per-user session detail
- [覺渡 #1] Race condition resolved: swap-and-drain buffer model
- [覺渡 #2] Session strategy fully specified (separate pool, rolling window)
- [覺渡 #3] Mention detection fires before buffer, reuses existing logic
- [覺渡 #4] flush_hard_cap = 50 as safety cap
- [覺渡 #5] ±20% jitter on flush interval to prevent thundering herd
- [覺渡 #6] context_window semantics clarified (API fetch, not buffer)

* docs: integrate with existing Dispatcher/turn-boundary batching infra

Ambient Mode reuses the Dispatcher, BufferedMessage, consumer_loop,
and pack_arrival_event infrastructure from PR #686 (message_processing_mode).
Key difference: ambient consumer uses timer-based flush instead of
turn-boundary drain.

* docs: add semaphore + error handling to ambient consumer pseudocode

[覺渡 R2#1] flush_semaphore.acquire() before dispatch
[覺渡 R2#2] match on dispatch result, warn + discard on error

* docs: address 擺渡 R2 findings — race condition, thinking msg, bot loop

[擺渡 #1] Add concurrent reply prevention via per-channel flushing flag
[擺渡 #2] Ambient dispatches post directly, no thinking placeholder
[擺渡 #3] Handle None on first rx.recv() (channel closed → exit)
[擺渡 #4] Bot loop prevention: MAX_CONSECUTIVE_BOT_TURNS + prompt instruction

* docs: strengthen F1 (discard buffer on mention) + F4 (bot msgs off by default)

[擺渡 F1] @mention discards ambient buffer instead of flushing it;
           cancel in-flight ambient on mention arrival
[擺渡 F4] allow_bot_messages defaults to 'off' for ambient channels

* docs: fix wording inconsistency — mention discards buffer, not flushes

[口渡 R3#1] Unify Message Filtering wording with Immediate flush section

* docs(adr): add detailed ASCII architecture diagrams for ambient mode

- Architecture Overview: full message routing decision tree
- Dual-Path Concurrency: timeline showing mention vs ambient interaction
- Shows buffer lifecycle, flush phases, and response routing

* docs(adr): unify ambient config under top-level [ambient] section

- Group all ambient settings under [ambient] instead of scattered
  [discord.ambient], [pool.ambient], [ambient.limits]
- Platform-specific config in [ambient.discord] (extensible to slack/telegram)
- Simplify pool key names: remove redundant ambient_ prefix
  (session_ttl_minutes, context_flushes)

* docs(adr): move ambient mode ADR to docs/adr/ambient.md

Addresses external reviewer feedback from @thepagent and team consensus.
docs/steering/ is for process guides; docs/adr/ is for ADRs.

* docs(adr): address review findings from 法師團隊

Fixes:
- Unify @mention handling path (discard buffer, not flush) — resolves
  3 contradictions across flush triggers table, concurrent reply section,
  and implementation details
- Replace non-existent MAX_CONSECUTIVE_BOT_TURNS with actual max_bot_turns
- Explain flush_hard_cap vs flush_max_messages relationship
- Add post_guard for atomic check-and-post (TOCTOU race fix)
- Add flush_timeout_seconds for AtomicBool safety recovery
- Reconcile Buffer Lifecycle (conceptual) with mpsc channel (implementation)
- Remove contradictory 'extend enum' wording from message_processing_mode
- Add allow_bot_messages to [ambient.discord] config
- Simplify pool key names (drop ambient_ prefix)

* docs(adr): fix author attribution to chaodu-agent

---------

Co-authored-by: 超渡法師 <超渡法師@openab.dev>
Co-authored-by: chaodu-agent <chaodu-agent@openab.dev>
Co-authored-by: thepagent <hehsieh1010@gmail.com>
chaodu-agent pushed a commit that referenced this pull request Jun 28, 2026
Addresses 覺渡 review findings #2 and #3 — adds model selection
instructions and MCP configuration guide to match cursor.md depth.
chaodu-agent pushed a commit that referenced this pull request Jun 28, 2026
Addresses 覺渡 review findings #2 and #3 — adds model selection
instructions and MCP configuration guide to match cursor.md depth.
thepagent pushed a commit that referenced this pull request Jun 28, 2026
* feat: add Devin CLI agent backend support

Add Devin CLI (formerly Windsurf/Codeium) as a new agent backend.
Devin CLI natively supports ACP via `devin acp` (JSON-RPC over stdio).

Changes:
- Dockerfile.unified: new `devin` target
- docker-bake.hcl: add devin to default group and targets
- config.toml.example: add commented Devin config block
- docs/devin.md: full integration guide

* fix: address review feedback on Devin CLI PR

- Fix binary path: install script puts binary at ~/.local/bin/devin,
  not ~/.devin/bin/devin. Copy resolved binary to /usr/local/bin/.
- Add standalone Dockerfile.devin for consistency with other agents.
- Update docker-bake.hcl to reference Dockerfile.devin.

* fix: address all review feedback

- Pin Devin CLI version (2026.8.18) with SHA256 verification (same
  pattern as grok target) — fixes curl|bash supply chain concern
- Remove incorrect WINDSURF_API_KEY claim (Devin is Cognition AI,
  not Codeium/Windsurf)
- Remove redundant mkdir .config/devin (agent homedir suffices)
- Add config mount note to Known Limitations in docs
- Standalone Dockerfile.devin also uses pinned download

* docs: add Model Selection and MCP Usage sections to devin.md

Addresses 覺渡 review findings #2 and #3 — adds model selection
instructions and MCP configuration guide to match cursor.md depth.

* docs: fix install method description to match pinned tarball approach

* fix: correct binary path to /tmp/bin/devin (tarball extracts to bin/ subdir)

* docs: add Devin CLI to README agent list

* ci: add devin to all workflow matrices and build lists

---------

Co-authored-by: chaodu-agent <chaodu-agent@openab.dev>
thepagent pushed a commit that referenced this pull request Jul 13, 2026
* docs(adr): revise identity-trust-none to three-layer architecture

Receiver → Trust Gate → Handler replaces the previous
'gate at handle_message()' design. Addresses all findings
from the PR #1263 mob review (howie + 3 LLM reviewers).

Key changes:
- §4.2: Trust Gate is a dedicated ingress layer upstream of Handler
- §5: New architecture diagram showing three-layer separation
- §7: Implementation plan starts with Receiver/Handler split
- Address #1: gate at actual convergence point (not handle_message)
- Address #2: trust lookup keys off per-event platform (not adapter)
- Address #3: slash commands gated (Handler is downstream of gate)
- Address #4: exhaustive scattered-checks inventory
- Address #5: explicit empty-vs-missing semantics
- Address #6: phased rollout (Phase 0-3)
- Address #7: echo rate-limit + bot exclusion + DM-preferred
- Address #8: gateway vs first-class section precedence
- Address #9: no static HashSet (runtime construction)
- Address #10: structured logging on allow + deny
- Address #11-#15: minor fixes (Teams ID, bot semantics, etc.)

* docs(adr): address team review findings

- Add type-level guarantee (GatedEvent vs InboundEvent) — compile-time
  enforcement, not just convention (#4)
- Clarify Gateway Receiver is one receiver that demuxes by platform (#11)
- Fix layer numbering inconsistency — use names, not numbers (#21)
- Add sender ID format table with per-platform gotchas (#22, #23, #24)
- Clarify is_bot bypass is caller-side, not inside decide() (擺渡-1)
- Change echo group fallback to silent drop (avoid UID leakage) (#6)

* docs(adr): add event loop binding design + fix is_bot L2 bypass

- Add §5 'Event loop binding' section: run_platform generic pipeline,
  EventReceiver/EventHandler traits, main.rs startup wiring
- Gateway platforms: one shared WS, demux by event.platform, fan-out
  to per-platform Handlers
- Fix is_bot bypass: bots skip L3 but STILL enforce L2 scope (擺渡-1 🔴)
- Add cross-crate boundary note for Gateway Receiver (擺渡-2 🟡)
- Include binding topology summary diagram

* docs(adr): address round 3 findings — tighten pseudocode precision

- GatedEvent: private field in narrow module (not pub(crate)), with
  read-only accessors and module layout diagram (諸葛村夫-1)
- gate_event: use configs.get().surface_allowed() to match real API (擺渡-3)
- Phase table: add Phase 0.5 for current partially-wired state on main,
  clarify Phase 2 means 'refuse to start' (諸葛村夫-2)

* docs(adr): fix seal() visibility — private fn, not pub(super)

seal() lives in the same module as gate_event(), so it should be a plain
private fn. pub(super) would unnecessarily expose it to the parent module.

* docs(adr): fix stale module layout comment — constructor is private, not pub(super)

* docs(adr): address LINE/Slack/Feishu review feedback

- Replace line-number refs with symbol+semantic descriptions (drift-proof)
- Rewrite echo section: platform-specific echo trait (LINE=Reply only,
  Slack=chat.postEphemeral, Discord=DM); leak-safe content by scope
- Add is_bot per-platform derivation table (pinned canonical rules)
- Document trusted_bot_ids as shared config (resolves Feishu circular dep)
- Clarify slash commands scope (Slack doesn't consume them)
- Update Slack sender ID: Enterprise Grid composite key (team_id, sender_id)
- Add non-message events section (assistant_thread_started must gate)
- Add Slack scope notes (Socket Mode only, MPIM=channel)
- Add LINE group policy: open/members dual-mode in decide()
- Add LINE @mention pre-filter as documented Receiver exception
- Feishu: gateway=L1 only, eliminate double-gating, empty list=deny-all

Addresses feedback from:
- @luffy-aiagent (LINE platform review)
- @antigenius0910 (Slack platform review)
- @wangyuyan-agent (Feishu platform review)

* docs(adr): address 9 review findings — API contract gaps + hardening

Fixes identified during group review:

F1+F6: Add workspace_id to InboundEvent; define Slack Enterprise Grid
       canonical sender_id format and config examples for Grid deployments
F2:    Replace HashMap<String, TrustConfig> with enum PlatformTrustConfig
       (Base/Line/Slack) — LINE group policy and Slack workspace-scoped
       trust now have proper type representations
F3:    Add cron bypass in gate_event() — system-initiated events skip
       L2/L3 (platform='cron' or sender_id='openab-cron')
F4:    Add #[cfg(test)] assume_trusted_for_test() constructor for
       GatedEvent — enables Handler unit testing without full pipeline
F5+F9: Change into_inner() to pub(crate); adjust safety claim wording
       from 'bypass impossible' to 'accidental bypass compile error'
F7:    Change gate_event() signature to take InboundEvent by value —
       zero-copy hot path (no .clone() on RawPlatformEvent)
F8:    Specify bounded LRU cache (max_capacity + TTL) for rate-limit
       state — prevents OOM from random sender_id flooding

* fix(adr): address review findings F1-F3 on identity-trust-none

F1 (critical): Remove sender_id spoofing hole in cron bypass — only check
    platform == "cron" since WeCom allows freeform UserIDs that could
    match any synthetic value. Update rationale accordingly.

F2: Add WeCom, Google Chat, MS Teams to pinned is_bot derivation and
    echo-delivery tables (all 8 platforms now covered).

F3: Fix pseudocode precision — unwrap_or no longer borrows a temporary;
    add PlatformTrustConfigs::get() and PlatformTrustConfig::surface_allowed()
    delegating method used by gate_event.

* fix(adr): address group review round 2 — 9 findings from B1/B4/B5/B8/B11/B12/B15

Fixes:
- WeCom is_bot: remove enter_agent (user-initiated, not bot); keep only
  trusted_bot_ids (B5 F1, B12 F1)
- SlackTrustConfig::decide(): workspace_users is now strict override
  (ignore allow_all_users); base fallback supports team_id:user_id composite
  key for Enterprise Grid (B4 F1, B5 F2, B12 F3)
- LINE group policy: fix prose vs code contradiction — unconfigured groups
  use default_group_policy, not DenyScope (B12 F2)
- Reserved platform validation: MUST-level requirement for all external
  Receivers to reject reserved platform names; cron bypass invariant
  documented; Phase 1 SHOULD for typed InboundSource enum (B1)
- decide() simplified to use self.get() — remove duplicate default logic (B5 F3)
- Echo rate-limit key updated to (platform, workspace_id, sender_id) (B12 F4, B15 F2)
- Module layout: InboundEvent in mod.rs (public), gate.rs narrow (B12 F5)
- into_inner() trust boundary doc: module-level vs crate-level explained,
  Phase 1 SHOULD for lint/annotation (B11, B15 F1)
- Slack Enterprise Grid gotcha: split dense table cell into footnote (B8 F2)

* fix(adr): add platform lowercase invariant to InboundEvent (B14 F2)

InboundEvent.platform MUST be lowercase — Receivers normalize before
constructing. This ensures consistency between gate_event's == "cron"
check and PlatformTrustConfigs::get()'s to_lowercase() lookup.

* docs(adr): clarify gw_event.platform source in run_gateway_platforms

Add comment noting platform field is assigned by gateway routing config,
not from webhook payload body — satisfies reserved platform invariant.

* docs(adr): WeCom corrections from external review (canyugs#18)

Three verified adjustments to the v2 trust architecture text:

- WeCom callback mode is DM-only: channel_type always "direct" with a
  per-user channel id, so "group routing" does not exist for WeCom and
  its L2 scope is effectively allow_dm only (verified wecom.rs:1059-71)
- Sender-id table: plain UserIDs are self-built-app only; external
  contact / ISV callbacks carry wm/wo-prefixed external_userid or
  encrypted OpenUserID (verified against official WeCom API docs)
- Bot-bypass wording: is_bot is hardcoded false in the WeCom Receiver
  today, so the L3 bot-bypass is a no-op for WeCom — the bypass is
  available uniformly but effective only where the Receiver can derive
  is_bot

Credit: canyugs.

* docs(adr): refresh Phase 0.5 snapshot to shipped state

Since the last revision, main gained: Slack L3 gate (#1363), per-platform
[section] trust for all 8 platforms (#1365/#1366/#1385), full
config-first parity with conformance guard (#1375/#1387), and the L1
unenforceable-auth startup warning (#1373). The Phase 0.5 row now lists
the actual inventory and names the one outstanding Phase 1 prerequisite
(standalone WS path still on should_skip_event, tracked on #1356).

---------

Co-authored-by: chaodu-agent <chaodu-agent@openab.dev>
Co-authored-by: chaodu-agent <chaodu-agent@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants