Acknowledge a request in the connector before handing it off

Merged
RO
9h ago
+795-51

Problem

The connector sees a person's request at once, but the acknowledgement boost comes later, from the orchestrating Claude session. On R2-D2, the person's ping line was created at 22:29:06.4Z and the agent's boost landed at 22:29:19.7Z, 13.3s later, though the connector had the line from the live feed straight away.

What changes

The connector now posts the acknowledgement itself. When it hands off a request someone made of the agent (acknowledge: true: mentioned, assigned, pinged), it first boosts the request's recording as the agent with the configured content, then writes the request line. The default is 👀, so existing setups get this on upgrade. The session then rewords that boost with basecamp boost update (#917), off the critical path.

Context is not acknowledged: subscribed, completed and boosted (a go included, as before) get no boost. The target is the recording the skill boosted before: recording.recording_id, the same recording as recording.url (the chat line in a Ping or Campfire, the comment, the card or to-do for an assignment).

Data shape

The request line gains one optional key, present only when the boost landed:

"ack": {"boost_id": 555, "content": "👀"}

connect.json gains one optional key, written only when the setting differs from the default:

"ack_boost": {"content": "On it"}     // --ack-boost 'On it'
"ack_boost": {"off": true}            // --no-ack-boost

connect show reports the setting in effect, the default included (ack_boost in JSON, an ack line for a person).

Failure and crash handling

  • A failed boost never blocks a request. Any error is logged at WARN, and the line goes out without ack. The session then boosts as it did before.
  • No retries. The SDK sends a boost POST once (it is non-idempotent there), and the connector does not retry it, because a failed request may have landed. The boost has a 10s timeout, and a shutdown does not cancel it midway.
  • A reprint does not boost twice. The boost id is written to the ledger (migration 16, a new acks table keyed by event id) before the line is written. If the connector crashes after printing the line but before closing the record, the restart reprints the request with the recorded ack and does not boost again. The remaining window is a crash between Basecamp creating the boost and the ledger write, one local SQLite insert. That window is the same double post a timed-out boost that landed would cause, and the session's fallback already risks that.
  • --shadow and --hold post nothing. A shadow run does not start the handoff at all. A hold keeps every record out of the handoff query, so nothing is boosted until basecamp connect release.

Setup

basecamp connect setup -P <profile> --ack-boost '<content>'   # at most 16 characters, counted as runes like boost create; empty refused
basecamp connect setup -P <profile> --no-ack-boost            # the session acknowledges, as today

The two flags are mutually exclusive. Like any setup change, a new setting needs a connector restart.

Older CLIs. connect.json is parsed with DisallowUnknownFields. Setup therefore writes ack_boost only when it differs from the default, so an untouched file still opens in an older CLI. After --ack-boost or --no-ack-boost, an older CLI refuses the file (unknown field "ack_boost"). To downgrade, run --ack-boost 👀 first, which removes the key. Migration 16 works like every earlier migration: an older build refuses the newer ledger with ErrLedgerSchema.

Skill

  • a. Acknowledge. With ack on the line, the session runs basecamp boost update <ack.boost_id> '<phrase>' in the same retry loop. If the update fails, the 👀 still acknowledges, and the handoff says so. Without ack, the session boosts with boost create as before.
  • Removed the "the reply itself can be the acknowledgement" shortcut. With the connector's boost already in place by default, it no longer saved anything.
  • Documented the new pieces. The ack field is in the request-line list and example, --ack-boost and --no-ack-boost are in the setup table and an Acknowledgement paragraph, and the description and "Three pieces" now say the connector boosts.
  • subagent.md. An acknowledgement is owed only when no boost landed. A line with ack is already acknowledged.
  • Compaction budget. The per-request section ends at byte 13,877 of the 14,000 budget, where it ended at 13,951 before. To make room, the "starts from the present" note moved to Checking on it.
  • Eval. The repo-selection eval now rejects boost update as well as boost create.

Tests

Each test was run red against a mutation of the code it guards, then green:

TestMutation that turns it red
TestARequestIsAcknowledgedBeforeItsLineIsWritten (boost before any stdout, line carries ack)no acknowledge call
TestTheAckOnTheLineIsItsBoostIDAndContentno acknowledge call
TestAFailedAcknowledgementStillHandsTheRequestOff (line without ack, WARN logged, one attempt)failure returned instead of logged
TestARequestHandedOffAgainIsNotBoostedTwiceledger lookup removed (2 boosts)
TestNoAcknowledgementWhenItIsTurnedOff, TestContextIsNotAcknowledged (subscribed, completed, boosted)guards on AckContent and Acknowledge removed
TestAHoldAcknowledgesNothinghold clause dropped from the handoff query
TestTheAckBoostRoundTripsAndTheDefaultIsNotWritten, TestSettingTheDefaultAgainRemovesTheKeydefault no longer normalized to no key
TestAnAckBoostBasecampWouldRefuseIsRefused (17 runes, empty, blank, off with content; also a hand-edited file)File.Validate check removed
TestConnectSetupRefusesABadAckBoost (both flags)mutual exclusion removed
TestConnectShowSaysHowRequestsAreAcknowledgedshow's resolved ack_boost and display line removed

TestConnectSetupSetsTheAckBoost covers set, keep and off through the command. The request contract (requestLineContract, the skill example) includes ack.boost_id and ack.content as optional keys.

Shadow has no new test. Its guarantee is that runConnect never starts the handoff under --shadow, and nothing here runs runConnect end to end.

bin/ci exits 0. The SDK provenance check skips, as expected while the replace is active.

Dependencies

  • basecamp-sdk#1016 (UpdateBoost), through the stack's temporary replace.
  • #917 (basecamp boost update), the base of this PR, which is stacked on #914.

Not live-verified here. The coordinator runs R2-D2 live.


Summary by cubic

The connector now acknowledges each trusted request itself with a boost before handing it off, instead of waiting for the orchestration session, which on R2-D2 took 13.3 seconds.

  • Boosts the request's recording as the agent with the configured content (default 👀, set with --ack-boost, off with --no-ack-boost) before writing the request line; the line carries "ack": {"boost_id", "content"} and acknowledge: false, so a skill that predates ack does not put a second boost beside the connector's.
  • A failed boost never blocks a request: it is logged and the line goes out without ack, for the session to acknowledge as before. Boosts are never retried, since a failed POST may have landed.
  • The boost id is recorded in the ledger (migration 16) before the line is written, so a reprinted request reuses its boost instead of posting twice; a recorded boost is reused even after boosting is turned off, and recording has its own deadline so a boost that landed late is still noted.
  • Context (subscribed, completed, boosted) and shadow runs boost nothing. A hold boosts nothing, and one that lands while the boost is in flight stops the handoff, leaving the request held with its boost recorded for a later redispatch.
  • The skill now rewrites the connector's boost with boost update when ack is on the line, and creates one itself when it is not.

Migration

  • connect.json writes ack_boost only when it differs from the default, so existing setups get 👀 on upgrade and untouched files still open in older CLIs. connect show reports the boost in effect. After --ack-boost or --no-ack-boost, an older CLI refuses the file; to downgrade, run --ack-boost 👀.