8000 Separate team send, ask and explicit reply by request ID by bu5hm4nn · Pull Request #4 · bu5hm4nn/pi-agent-team · GitHub
[go: up one dir, main page]

Skip to content

Separate team send, ask and explicit reply by request ID - #4

Draft
bu5hm4nn wants to merge 10 commits into
wave-i18nfrom
feature-response-policy
Draft

bu5hm4nn wants to merge 10 commits into
wave-i18nfrom
feature-response-policy

Conversation

@bu5hm4nn
@bu5hm4nn bu5hm4nn commented Oct 9, 2026 •
Copy link
Copy Markdown
Owner

Summary

Separate informational messages, new questions and explicit replies:

  • team_send({ to, text }): informational; no response reminder.
  • team_ask({ to, text }): requests an answer; returns local transport acceptance immediately, not the answer.
  • team_reply({ requestId, text }): answers the exact incoming message; missing, unknown or already-answered IDs fail.

Shared internals are named transmit/doTransmit. Short receiving-agent instructions include exact request IDs. All new user-visible text uses the English and Chinese catalogs.

Pending-request rules

  • Send/ask fails only for recipients with an unanswered incoming ask; unrelated peers remain usable.
  • Multi-recipient sends validate atomically and transmit the same frozen recipient set. Bulk confirmation rechecks obligations.
  • Multiple requests from one peer remain individually answerable. Reminder settings do not erase obligations.
  • Reply consumes only its exact request after a successful local transport write. Failed writes retain it; concurrent duplicate replies cannot both transmit.
  • Exact IDs are preserved without trimming/coercion. Full reminder payloads are capped while compact unanswered obligations remain retained.

Commands mirror the tools: /team send, /team ask, /team reply <requestId> <text>. Reminder policy moves to /team replies; legacy single-mode /team reply off|remind|mirror remains supported with a migration hint.

Stack

Based directly on wave-i18n (#2), independent of hyperswarm (#1) and membership restoration (#3). combined-internal contains all changes for internal testing. No fork planning documents are included.

Verification

  • Final fresh-context GPT-6 Astra review: APPROVE after five concrete findings were repaired by GPT-6 Astra at low thinking.
  • Parent combined-stack npm test: 470 tests, 464 pass, 0 fail, 6 existing SWIM-sidecar skips; actual executor harness ran.
  • Feature-only repair run: 382 tests, 376 pass, 0 fail, 6 skips; 173 focused tests passed without skips.
  • Regression tests cover real registered executors, missing/stale reply IDs, recipient-specific blocking, groups/confirmation, parallel replies, write failures, overflow and exact-ID collisions.
  • Catalog parity/placeholder and sink guards pass; whitespace and syntax checks clean. Conformance suite unchanged; no new dependencies.
  • Subsequent changes only corrected two stale documentation sentences identified by the final reviewer.

Four-participant E2E follow-up

Added e2e/four-participant.test.js and its child fixture. Four separate processes load the actual extension, invoke registered tools, and communicate through a real loopback broker/native WebSockets with no LLM and no mocked transport.

Per run: 39 concurrent/interleaved questions to A, 40 exactly correlated replies overall, 87 actual received socket deliveries/model injections, and 36 once-only reminders. Tests cover out-of- 8000 order replies, the 32-payload cap, recipient-specific blocking, missing/stale IDs, simultaneous same-ID replies, informational no-reminder/no-ping-pong behavior, and bounded cleanup even when OS kill fails.

Focused E2E passed three consecutive final runs. Final Astra review: approved. Parent combined npm test -- --test-concurrency=2: 472 tests, 466 pass, 0 fail, 6 existing SWIM skips; all four participants executed. Syntax and diff checks clean. No production/package/conformance changes.

The default-concurrency feature run hit the host thread limit (pthread_create: Resource temporarily unavailable) and timed out. Bounded concurrency is an explicitly approved validation workaround, not a passing claim for that default run; package scripts are unchanged.

Limits

Pending request IDs remain runtime-scoped across reload/reconnect. Successful transmission means local transport acceptance, not recipient receipt or processing. Compact unanswered metadata grows with accepted obligations; full reminder payload storage is bounded. Executor tests require newer Node module hooks/type stripping. Live multi-machine validation of the new API remains an internal smoke-test step.

…sked

Messages are now informational by default: team_send delivers and wakes the
peer but does not ask for a reply. A pending-reply entry and a one-time
reminder are created only when the sender sets requireResponse true
(tool `requireResponse: true`, command `/team send --require-response`).

Why: the old shape treated every request (re empty, non-fyi) as something to
answer and remind about, so routine notifications cost the peer a full model
turn. The flag is opt-in and lives on the message, not the config.

- session: classifyInbound returns requireResponse (strictly body.requireResponse
  === true); fyi and re-bound replies never require a response, even with the
  flag set. No-response requests are injected with an explicit model-facing
  "no reply needed" note.
- session: separate incomingIds association keeps re correlation for optional
  replies to no-response messages without creating reminders; consumed once.
  bindReply can skip it (allowAssociation:false) so an explicit requireResponse
  is not swallowed and still reaches the peer as a new request.
- session/dispatch/index: thread the flag end-to-end, including the bulk-
  confirmation party (so confirming a broadcast cannot drop it) and the envelope
  body via sendBodyFrom (true written; absent/false both mean false).
- i18n: dispatch.send.usage, dispatch.send.awaitReply, tool.requireResponse and
  ui.acRequireResponse in both catalogs; sink-guard whitelist updated.
- docs: README, docs/how-it-works.md and the shipped skill describe the default
  and the opt-in.

Tests: red->green regressions for omitted/false/true classification,
pending/reminder, reply suppression, reply=off, optional-reply association,
no-ping-pong, tool/command intention threading and bulk confirmation.
…s a reply

Public API now offers two clear tools instead of an opt-in boolean:
  team_send({to,text})  delivered + wakes the peer, no reply requested
  team_ask({to,text})   asks for a reply; reminded once if unanswered
and the same pair on the command line: /team send vs /team ask.

- remove requireResponse from the public tool schema, and
  --require-response from command parsing / completion / help / docs
- keep requireResponse as the private wire field (absent for send,
  true for ask) so envelopes stay interoperable
- rename the shared internal delivery helpers to the generic transmit
  vocabulary: dispatch transmit/doTransmit/transmitResult and session
  transmit/transmitBodyFrom. transport.send and the wire `send`
  intention are untouched
- both tool wrappers share one parameter schema, render and execute
  helper; menu offers send vs ask; completion exposes ask
- i18n: usage is parameterised by subcommand, new menu keys in both
  catalogs, old flag-only key dropped
- new src/index.test.js: real index.ts harness via module.registerHooks
  (fake transport + recording typebox) asserting public schema, wire
  flags, immediate receipt, and completion/menu wiring; skips on old Node
- docs updated: README, docs/how-it-works.md, skill
…peer request

Reviewer observation: an ask sent to a peer that already had an unanswered
request from them was silently bound as a reply (re set) and its
requireResponse normalized to false, so the peer was never reminded.
Required semantics: team_ask ALWAYS requests a reply; team_send is the
explicit reply path.

- doTransmit: when wantsReply (ask) skip bindReply entirely — re null,
  hops 0, replyTo null — and do not consume the peer's pendingReplies or
  incomingIds, so a later team_send can still answer the original request
- bindReply: drop the now-unused allowAssociation option and its doc
- tests: dispatch command + tool regressions (pending ask from peer ->
  local ask stays a new request with the old pending retained -> send
  answers the original correlated request); also no consumption of the
  optional incomingIds association; plus an index.ts harness regression
  driving an inbound ask through the fake transport and checking the
  team_ask / team_send wire
- model guidance + docs: team_ask always starts a new request
  (system prompt, team_ask guideline, README, how-it-works, skill)
- private wire replies with re set still suppress reminders on the receiver
… a reply is pending

Replies are now explicit. An incoming request is answered only by
team_reply({ requestId, text }) (tool) / /team reply <requestId> <text>
(command): the id routes the answer to the stored sender and clears only
that one request, so two pending requests from the same peer stay
independently answerable. Missing, unknown, already-answered, or stale ids
fail without sending an envelope and without touching any pending record.

team_send / team_ask are purely new messages again (re=null, hops=0): they
no longer bind to, or consume, an incoming request. Answering a teammate is
always team_reply.

send/ask to a peer with an unanswered request are rejected before anything
is transmitted; the error lists the pending request id(s) and points at
team_reply. Multi-recipient sends (@Label, *, comma lists) are validated as
a whole and rejected atomically, and bulk broadcasts are re-checked inside
doTransmit because a request can arrive while the confirm dialog is open.
Unrelated peers are unaffected.

Pending requests are keyed by request id and separated from reminder
emission: reply=off and the once-only reminder no longer erase an
obligation, and mirror never satisfies one. onTurnSettled reminds each
pending request at most once while it remains unanswered.

The reminder strategy moves to /team replies <off|remind|mirror>; the old
/team reply <mode> (single mode word) still works and prints a note, as do
/team announce, auto, always, and the reply= flags.

Injected payloads are short and carry the request id with one team_reply
instruction; plain sends say no reply is required (optional explicit reply
still allowed). New M keys in both catalogs; menu and autocomplete expose
team_reply; README / how-it-works / SKILL updated. e2e/conformance.js is
unchanged and there are no new dependencies.
Review finding: explicitReplyResult consumed the matched request as soon as
doTransmit produced the send intention -- before the real socket write. A
rejected write therefore lost the obligation and could still report
delivered:true.

- doTransmit stamps the reply send intention with a private replyRequestId
  (never written to the envelope; transmitBodyFrom ignores it).
- runIntentions consumes it only after transport.send returns true, and turns
  false/throw into a failure record plus a localized notify; a thrown send is
  caught safely and the pending request is retained.
- runParty (confirmBulk) and invoke propagate send failures, so the tool and
  command paths report delivered:false instead of success for a rejected write.
- add notify.sendError to both catalogs for the thrown case.
- menu request-id matching is now exact, so one id cannot prefix-match another.
- regressions in src/index.test.js: failed/throwing team_reply retains the
  pending and still blocks send/ask to that peer, retry success clears exactly
  that request; optional (non-request) association retained the same way; bulk
  confirm failure propagates; menu prefix safety.
- dispatch tests updated: dispatch no longer consumes; consumption is the
  caller's job after a confirmed write.
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

0