Skip to content

Commit 3776fcd

Browse files
authored
Acknowledge a request in the connector before handing it off (#918)
The connector boosts a request someone made of the agent (a mention, an assignment, a ping) on its recording, as the agent, before writing its line: 👀 by default, so the person sees the request landed within a second rather than when the session gets to it (13.3s measured before). The line carries the boost as ack, says acknowledge: false so an older skill doesn't boost again, and the skill rewords that boost as the work goes, or leaves it when the reply is immediate. connect setup --ack-boost sets the content (16 characters at most) and --no-ack-boost turns it off; connect.json records it only when it differs from the default. A failed boost is logged and never retried, and the request goes out without one. The boost is recorded in the ledger (migration 16, acks), with a deadline of its own, before the line is written, so a request handed off again after a crash, even with boosting since turned off, reuses it. A hold that lands while the boost is in flight keeps the request from being handed off.
1 parent d67b0d2 commit 3776fcd

17 files changed

Lines changed: 795 additions & 51 deletions

File tree

‎internal/commands/connect.go‎

Lines changed: 60 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,11 @@ func NewConnectCmd() *cobra.Command {
4141
Long: `Run a local agent connector: it listens to the account event feed as a
4242
Basecamp agent, admits what a trusted person asks of that agent, and prints
4343
each trusted request for the session that started it to handle. It starts no
44-
workers and posts nothing itself: the basecamp-connect skill, run in your own
45-
Claude Code session, acknowledges each request, picks the repo and hands it to
46-
a subagent that replies as the agent.
44+
workers and posts only the acknowledgement boost, unless setup turns it off
45+
(--no-ack-boost): a request someone made of the agent is boosted as the agent
46+
before it is printed. The basecamp-connect skill, run in your own Claude Code
47+
session, picks the repo and hands each request to a subagent that replies as
48+
the agent.
4749
4850
Connect the agent to a profile first (basecamp auth agent connect -P <profile>),
4951
then run setup on that profile: it records who may drive the agent, maps
@@ -95,7 +97,8 @@ func newConnectShowCmd() *cobra.Command {
9597
Use: "show",
9698
Short: "Show a profile's connector setup without changing it",
9799
Long: `Show the connect.json a profile's setup wrote: the agent, the operator and
98-
trust mode, each served project, and who is let into a Ping.
100+
trust mode, each served project, who is let into a Ping, and the boost the
101+
connector acknowledges requests with (ack_boost, the default included).
99102
100103
The file is read through the same checks setup and the connector apply:
101104
it is refused, and nothing of it shown, when it is a symlink, is not a
@@ -156,7 +159,7 @@ func runConnectShow(app *appctx.App) error {
156159
// agent, the trust and the served projects live, so a person's formats get them
157160
// flattened to one line each; JSON keeps the file's own shape.
158161
markdown := app.Output.EffectiveFormat() == output.FormatMarkdown
159-
return app.OK(connectShowResult{Path: path, File: f},
162+
return app.OK(connectShowResult{Path: path, File: f, AckBoost: f.Ack()},
160163
output.WithDisplayData(connectShowDisplay(path, f, markdown)),
161164
output.WithSummary(fmt.Sprintf("Connector setup for profile %q: trust %s, %d served project(s)", name, f.Trust.Mode, len(f.Projects))))
162165
}
@@ -207,6 +210,7 @@ func connectShowDisplay(path string, f setup.File, markdown bool) map[string]any
207210
"trust": trust,
208211
"agents": agents,
209212
"projects": strconv.Itoa(len(f.Projects)) + " served",
213+
"ack": connectAckDisplay(f.Ack()),
210214
}
211215
for id, entry := range f.Pings {
212216
ids := make([]string, len(entry.Participants))
@@ -228,6 +232,14 @@ func connectShowDisplay(path string, f setup.File, markdown bool) map[string]any
228232
return d
229233
}
230234

235+
// connectAckDisplay says how the connector acknowledges a request.
236+
func connectAckDisplay(ack setup.AckBoost) string {
237+
if ack.Off {
238+
return "off: the session boosts each request"
239+
}
240+
return "boosts " + strconv.Quote(ack.Content) + " before handing each request off"
241+
}
242+
231243
// escapeControls writes every control or non-printable rune in s, the
232244
// backslash and "<" (which the renderer would read as HTML) as a Go escape
233245
// (\x1b, \u009b, \\, \x3c), so a path can reach a
@@ -267,9 +279,12 @@ func markdownCode(s string) string {
267279
}
268280

269281
// connectShowResult is show's data: where the file is, and what it holds.
282+
// AckBoost is the acknowledgement in effect, the default included, which
283+
// the file leaves out.
270284
type connectShowResult struct {
271285
Path string `json:"path"`
272286
setup.File
287+
AckBoost setup.AckBoost `json:"ack_boost"`
273288
}
274289

275290
// connectSetupFlags are setup's flags, as typed.
@@ -297,6 +312,11 @@ type connectSetupFlags struct {
297312
allowInPing []string
298313
disallowInPing []string
299314

315+
// ackBoost is --ack-boost, the boost the connector acknowledges a
316+
// request with; noAckBoost turns it off.
317+
ackBoost optionalString
318+
noAckBoost bool
319+
300320
serve []string
301321
classes []string
302322
watch []string
@@ -385,6 +405,14 @@ person's in basecamp pings show <ping-id> --json. Anyone else's line in a
385405
Ping is discarded, as is any agent's. Restart the connector after changing
386406
who is let into a Ping.
387407
408+
Acknowledgement. When someone asks the agent for something (a mention, an
409+
assignment, a ping), the connector boosts that recording as the agent before
410+
printing the request, and the request line carries the boost's id. Followed
411+
threads, completions and boosts are context and are not acknowledged.
412+
--ack-boost <content> sets the boost (default 👀, at most 16 characters);
413+
--no-ack-boost turns it off, and the session acknowledges instead. Restart
414+
the connector after changing it.
415+
388416
No directory is associated with a project here: the session handling the
389417
requests chooses the repo for each one.
390418
@@ -423,7 +451,9 @@ Examples:
423451
basecamp connect setup -P agent --class 12345=internal
424452
basecamp connect setup -P agent --allow-agent 333
425453
basecamp connect setup -P agent --allow-in-ping 2085958513:111
426-
basecamp connect setup -P agent --agent-thread-cap 5 --agent-daily-cap 30`,
454+
basecamp connect setup -P agent --agent-thread-cap 5 --agent-daily-cap 30
455+
basecamp connect setup -P agent --ack-boost 'On it'
456+
basecamp connect setup -P agent --no-ack-boost`,
427457
Annotations: map[string]string{AnnotationProfileMayCreate: "true"},
428458
Args: cobra.NoArgs,
429459
RunE: func(cmd *cobra.Command, args []string) error {
@@ -457,7 +487,10 @@ Examples:
457487
fl.StringArrayVar(&f.classes, "class", nil, "Classify a served project: <project-id>=<class>, or <project-id>= to clear it (repeatable)")
458488
fl.StringArrayVar(&f.watch, "watch-completions", nil, "Admit every trusted completion in a served project (repeatable)")
459489
fl.StringArrayVar(&f.unwatch, "no-watch-completions", nil, "Stop watching a project's completions (repeatable)")
490+
fl.Var(&f.ackBoost, "ack-boost", fmt.Sprintf("Boost the connector acknowledges each request with, as the agent, before handing it off (default %s; at most %d characters)", setup.DefaultAckContent, setup.AckContentMax))
491+
fl.BoolVar(&f.noAckBoost, "no-ack-boost", false, "Don't acknowledge requests in the connector; the session reading them boosts instead")
460492
cmd.MarkFlagsMutuallyExclusive("operator", "operator-profile")
493+
cmd.MarkFlagsMutuallyExclusive("ack-boost", "no-ack-boost")
461494

462495
return cmd
463496
}
@@ -980,6 +1013,12 @@ func (f *connectSetupFlags) changes() (setup.Changes, error) {
9801013
}
9811014
ch.Remove = append(ch.Remove, id)
9821015
}
1016+
switch {
1017+
case f.ackBoost.set:
1018+
ch.AckBoost = &setup.AckBoost{Content: f.ackBoost.value}
1019+
case f.noAckBoost:
1020+
ch.AckBoost = &setup.AckBoost{Off: true}
1021+
}
9831022

9841023
return ch, nil
9851024
}
@@ -1441,3 +1480,18 @@ func (n *optionalInt) Set(s string) error {
14411480
n.set, n.value = true, v
14421481
return nil
14431482
}
1483+
1484+
// optionalString is a string flag that remembers whether it was passed, so
1485+
// an empty value given is told apart from none.
1486+
type optionalString struct {
1487+
set bool
1488+
value string
1489+
}
1490+
1491+
func (o *optionalString) String() string { return o.value }
1492+
func (o *optionalString) Type() string { return "string" }
1493+
1494+
func (o *optionalString) Set(s string) error {
1495+
o.set, o.value = true, s
1496+
return nil
1497+
}
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
//go:build unix
2+
3+
package commands
4+
5+
import (
6+
"bytes"
7+
"encoding/json"
8+
"fmt"
9+
"os"
10+
"strings"
11+
"testing"
12+
13+
"github.com/stretchr/testify/assert"
14+
"github.com/stretchr/testify/require"
15+
16+
"github.com/basecamp/basecamp-cli/internal/connector/setup"
17+
"github.com/basecamp/basecamp-cli/internal/output"
18+
)
19+
20+
func loadAckBoost(t *testing.T) setup.AckBoost {
21+
t.Helper()
22+
f, err := setup.Load(connectSetupPath(t, "agent"))
23+
require.NoError(t, err)
24+
return f.Ack()
25+
}
26+
27+
// --ack-boost sets the boost the connector acknowledges with, --no-ack-boost
28+
// turns it off, and a run that passes neither keeps it.
29+
func TestConnectSetupSetsTheAckBoost(t *testing.T) {
30+
s := startConnectSetupServer(t)
31+
firstSetup(t, s)
32+
assert.Equal(t, setup.AckBoost{Content: "👀"}, loadAckBoost(t), "on by default")
33+
34+
out, err := runConnectSetupCmd(t, newConnectSetupApp(t, s, "agent"), "--ack-boost", "On it")
35+
require.NoError(t, err, out)
36+
assert.Equal(t, setup.AckBoost{Content: "On it"}, loadAckBoost(t))
37+
38+
out, err = runConnectSetupCmd(t, newConnectSetupApp(t, s, "agent"), serveArg())
39+
require.NoError(t, err, out)
40+
assert.Equal(t, setup.AckBoost{Content: "On it"}, loadAckBoost(t), "kept by a run that does not pass it")
41+
42+
out, err = runConnectSetupCmd(t, newConnectSetupApp(t, s, "agent"), "--no-ack-boost")
43+
require.NoError(t, err, out)
44+
assert.Equal(t, setup.AckBoost{Off: true}, loadAckBoost(t))
45+
}
46+
47+
// A boost Basecamp would refuse, and both flags at once, are refused before
48+
// anything is written.
49+
func TestConnectSetupRefusesABadAckBoost(t *testing.T) {
50+
for name, args := range map[string][]string{
51+
"empty": {"--ack-boost", ""},
52+
"seventeen runes": {"--ack-boost", strings.Repeat("👀", 17)},
53+
"both flags": {"--ack-boost", "👍", "--no-ack-boost"},
54+
} {
55+
t.Run(name, func(t *testing.T) {
56+
s := startConnectSetupServer(t)
57+
args = append([]string{"--operator", fmt.Sprint(setupOperatorPerson), serveArg()}, args...)
58+
out, err := runConnectSetupCmd(t, connectSetupApp(t, s, "agent"), args...)
59+
require.Error(t, err, out)
60+
assertNotWritten(t, "agent")
61+
})
62+
}
63+
}
64+
65+
// Show says how requests are acknowledged, the default included, which the
66+
// file itself leaves out.
67+
func TestConnectShowSaysHowRequestsAreAcknowledged(t *testing.T) {
68+
s := startConnectSetupServer(t)
69+
firstSetup(t, s)
70+
71+
show := func(t *testing.T, format output.Format) string {
72+
t.Helper()
73+
app := newConnectSetupApp(t, s, "agent")
74+
var buf bytes.Buffer
75+
app.Output = output.New(output.Options{Format: format, Writer: &buf})
76+
out, err := runConnectShowCmd(t, app)
77+
require.NoError(t, err, out)
78+
return buf.String()
79+
}
80+
ackOf := func(t *testing.T) setup.AckBoost {
81+
t.Helper()
82+
var envelope struct {
83+
Data struct {
84+
AckBoost setup.AckBoost `json:"ack_boost"`
85+
} `json:"data"`
86+
}
87+
raw := show(t, output.FormatJSON)
88+
require.NoError(t, json.Unmarshal([]byte(raw), &envelope), raw)
89+
return envelope.Data.AckBoost
90+
}
91+
92+
data, err := os.ReadFile(connectSetupPath(t, "agent"))
93+
require.NoError(t, err)
94+
require.NotContains(t, string(data), "ack_boost", "the default is not in the file")
95+
assert.Equal(t, setup.AckBoost{Content: "👀"}, ackOf(t))
96+
assert.Contains(t, show(t, output.FormatStyled), `boosts "👀"`)
97+
98+
out, err := runConnectSetupCmd(t, newConnectSetupApp(t, s, "agent"), "--no-ack-boost")
99+
require.NoError(t, err, out)
100+
assert.Equal(t, setup.AckBoost{Off: true}, ackOf(t))
101+
assert.Contains(t, show(t, output.FormatStyled), "off: the session boosts each request")
102+
}

‎internal/commands/connect_run.go‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -352,13 +352,17 @@ func runConnect(cmd *cobra.Command, f *connectRunFlags) error {
352352
return connector.RunAdmission(ctx, connector.AdmissionOptions{Ledger: ledger, Queue: queue, Admitter: admitter, Lines: lines, Logger: logger})
353353
})
354354
if !f.shadow {
355-
// Each trusted request goes to stdout for the session that started
356-
// the connector, and the connector is done with it.
355+
// Each trusted request someone made of the agent is acknowledged
356+
// with a boost, unless connect.json turns that off, then goes to
357+
// stdout for the session that started the connector, and the
358+
// connector is done with it. A shadow runs no handoff, so it posts
359+
// nothing; a hold hands nothing off, so it boosts nothing.
357360
started := time.Now()
358361
runPart("handoff", func(ctx context.Context) error {
359362
return connector.RunHandoff(ctx, connector.HandoffOptions{
360363
Ledger: ledger, Served: served.Current, Buckets: buckets, AgentID: agentID,
361364
Lines: lines, Logger: logger, Started: started,
365+
AckContent: file.Ack().Content, Booster: connector.SDKBooster{Client: accountClient},
362366
})
363367
})
364368
}

‎internal/commands/connect_setup_guided.go‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ var connectSetupPolicyFlags = []string{
4949
"serve", "unserve", "class", "watch-completions", "no-watch-completions",
5050
"allow-assignments-from-authorized",
5151
"allow-agent", "disallow-agent", "agent-thread-cap", "agent-daily-cap",
52-
"allow-in-ping", "disallow-in-ping",
52+
"allow-in-ping", "disallow-in-ping", "ack-boost", "no-ack-boost",
5353
}
5454

5555
// connectGOOS is the platform guided setup checks before connecting. A

0 commit comments

Comments
 (0)