Skip to content

feat: add retained-message replies and agent routing context - #1

Merged
leoli-dev merged 2 commits into
mainfrom
codex/collaboration-friction
Oct 19, 2026
Merged

leoli-dev merged 2 commits into
mainfrom
codex/collaboration-friction

Conversation

@leoli-dev

@leoli-dev leoli-dev commented Oct 19, 2026 •

Copy link
Copy Markdown
Owner

Problem and resulting behavior

Replying from a retained message previously required resolving its channel before choosing a direct or group send. Add text-only POST /messages/{message_id}/reply and matching CLI/MCP operations that preserve the original audience, recheck current access, and return one sent/duplicate result. Agent claim/history include computed routing context, ACK results include a nullable account read observation, and limits advertises the three implemented features.

Reply gateway HTTP 502/504 and non-service error JSON remain explicitly unknown in CLI/MCP, with no automatic resend. Constrained responder guidance requires a trusted gateway to map IDs from its own authenticated claim/history context to authorized audiences; otherwise it must omit opaque-ID reply.

Requirement and design impact

Adds deterministic baseline/comparison fixtures and capability-specific conformance checks. Preserves key plus exact-IP authentication, polling-only access, claim/ACK fencing, fan-out privacy, retention/deduplication, administrator history, and database schema 1 (REQ-10, REQ-13, REQ-16–17, REQ-24–46, REQ-54). Reply stores no parent/thread association and does not make sends idempotent. Multipart reply is rejected before staging files.

Updates the generated request contract, bilingual agent manuals/security/schedules/quickstarts, MCP guide, bundled skill, runtime and validation records. Conformance documents a future read-only doctor specification; no doctor command or gateway implementation is added. Public docs and test names describe behavior without private-plan phase labels.

Verification

  • Full local go test -race -count=1 ./..., build/vet, four Node i18n checks and actual baseline 54e58cc CLI/MCP compatibility checks passed for 69d91f6. Its hosted native Linux/macOS/Windows, Chromium, contract/smoke and documentation jobs all passed.
  • After the client error-classification changes, client/CLI/MCP race tests, renamed server fixtures, go vet ./..., real-service scripts/smoke.py, documentation, gofmt, generated OpenAPI equality and whitespace checks passed locally.
  • New regressions exercise gateway JSON errors, invalid service envelopes, definite service errors, visible unknown errors in CLI/MCP and exactly one POST per call.
  • Independent review of the final follow-up: Approve, no actionable findings. Hosted results for the updated head are tracked separately.

Assumptions and limitations

The selected synthetic channel-enumeration/reply workflows use 2 HTTP calls before and 1 after; known-recipient raw direct sends already need only 1. No actual collaboration, latency or model-wakeup benefit is inferred. A fresh history miss is not proof of non-delivery.

Actual-profile pilots, production-helper measurements, arbitrary third-party closed response parsers and actual gateway permission enforcement remain untested. Lookup/probe APIs, a reference local component, persistent threads, production deployment and release/platform claims are outside this delivery.

@leoli-dev leoli-dev left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed against the final collaboration-friction plan (R1a scope + R2/R4 fixtures).

Verdict: no blocking correctness issues. Re-ran on 69d91f6: go vet ./..., go test -race -count=1 ./... (all pass), gofmt -l . (clean), generated OpenAPI equality, and python3 scripts/smoke.py (passed).

Plan conformance checked and OK:

  • Reply runs entirely under the state lock; parent retention, channel authorization and private peer/host Enabled are rechecked there (the direct()-only target check is replicated, as the plan required).
  • Multipart reply never reaches parseSend/staging (legacyUploadRoute/replyRoute gating in serve()); reply rejects on media type before reading the body.
  • Strict input: DisallowUnknownFields plus the duplicate/case-variant key scan closes the "Body"/repeated-key holes in encoding/json.
  • message_unavailable is mapped to 404, invalid_target 400, text_too_long carries limit/revision; body validation precedes parent lookup, so no existence leak.
  • Context is attached only in the agent handlers; historyMatching and admin history are untouched; UseNumber keeps history sequence exact. Private context exposes only the one peer.
  • ACK keeps status order/token/expiry; read is null only without a Delivery.
  • No schema change, no persisted parent, no new record kind; requirements.md untouched.

Three non-blocking comments inline (security-doc gap for reply allowlisting, unknown-outcome classification for gateway JSON errors, private phase labels in public docs/test names).

Comment thread docs/agent-security.md Outdated
and prevent alternate access paths.

`agent_post_reply` accepts text only and preserves the retained original
channel. It still needs an argument-level policy for the parent’s authorized

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Security doc gap (medium). "It still needs an argument-level policy for the parent's authorized private peer or group" — but the tool's only routing argument is an opaque message_id. With no R1b GET /messages/{id}, a gateway cannot derive the audience from the call's arguments. In practice, allowlisting agent_post_reply lets the responder post to any private peer or group the account can read a retained message from, which is broader than a group_send allowlist pinned to one group_id.

Suggest saying explicitly how to enforce this today: the gateway must resolve message_id against the context.channel it recorded from its own claim/history responses and deny IDs it has not seen (or whose recorded channel is outside the profile). Otherwise, constrained responders should not be given reply. Same wording applies to docs/mcp.md ("argument-level recipient/channel policy using the retained parent's route"), the zh-CN security doc, and the SKILL. Not a code bug; the server-side authorization is correct.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in eb692f4 — verified.

}
if err != nil {
var apiErr *APIError
if errors.As(err, &apiErr) {

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unknown-outcome classification (low). Any non-2xx response with a valid JSON body becomes *APIError and is reported as a definite failure. That holds for this server (every error path rolls state back before responding). It does not hold for an intermediary: a reverse proxy or API gateway that returns a JSON-bodied 502/504 after forwarding the request may already have let the reply commit. The agent then sees a definite failure and may resend. That is exactly the unknown-send case the docs say must not be re-POSTed.

Cheap fix: treat the error as definite only when the payload matches the service envelope {"error":{"code":...}}. Alternatively, always classify 502/504 as reply outcome is unknown. HTML/non-JSON gateway errors are already handled correctly via the invalid-JSON path.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in eb692f4 — verified.

Comment thread docs/conformance.md Outdated
validation, reference local components, and persistent threads require their own
fixtures before their capabilities can be advertised.

## R1a checks and call-count fixtures

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Public-doc hygiene (low). R0a/R0b/R1a/R1b/R4 are phase labels from the maintainer's private plan. No public document defines them (git grep R0a 54e58cc is empty). They now appear in conformance.md, validation.md, and test names (TestR0aLegacy…, TestR1…, TestR4SelectedRouteReplyCallCounts). AGENTS.md asks that public docs stay usable without the private development log.

Suggest neutral names, for example ## Route-reply and context checks, TestLegacyWorkflowCallCounts, TestRouteReplyCallCounts, TestRouteReply*/TestAgentContext*. Update the -run regexes in this file to match. The alternative is to define the phases in docs/implementation-plan.md.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in eb692f4 — verified.

@leoli-dev leoli-dev left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-review of eb692f4: all three earlier comments are resolved; LGTM to merge.

  • Reply allowlisting: agent-security (en/zh), mcp.md, agent-operations (en/zh) and the SKILL now state that an opaque message_id cannot constrain the audience. They require a trusted gateway to keep identity-bound message_id → context.channel mappings from its own claim/history responses and to deny unseen IDs. Without that, reply is omitted for constrained responders. The docs are explicit that no gateway or key scope ships.
  • Unknown outcomes: isServiceErrorEnvelope plus forcing 502/504 to unknown is correct. Null, empty, numeric and duplicate-key code/error values all fall to unknown because of Go's last-key-wins rule. The unknown error drops both raw and the *APIError type, and legacy send behavior is pinned unchanged. The new CLI/MCP tests assert exactly one POST.
  • Phase labels: removed from public docs and test names (git grep 'R0a\|R1a\|R1b\|R0b\|TestR4' over docs/internal/skills is empty). The -run regexes in conformance.md match the renamed tests.

Re-ran locally on eb692f4: go vet, go test -race -count=1 ./..., gofmt, OpenAPI equality and scripts/smoke.py all pass. Hosted checks (contract, documents, admin-ui, native Linux/macOS/Windows) are green. The server runtime is unchanged since the previous review.

@leoli-dev
leoli-dev merged commit fb61d6b into main Oct 19, 2026
12 checks passed
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.

1 participant