Skip to content

Commit da9fb50

Browse files
fix(tools): give write_stdin's invalid-session errors the same recovery guidance (#749 follow-up) (#768)
* fix(tools): give write_stdin's invalid-session errors the same recovery guidance Follow-up to #749 (write_stdin session-id probing) / #702. write_stdin had three "no valid live session" error paths with three different messages and signatures: a missing session_id returned "session_id is required", a zero/negative/non-integer one returned "session_id must be at least 1", and only a valid-but-unknown id got the rich UnknownExecSessionError recovery guidance ("do not guess or probe; start a session with exec_command, or use edit_file/ write_file"). So a model that called write_stdin with session_id 0 (the common "I have no id" placeholder) got a terse error with no way forward, and naming the minimum nudged it toward trying 1, 2, 3... the exact id-probing #749 suppresses. The three signatures also meant a model moving 0 to 1 to 2 reset the repeated-failure streak on each class of mistake. Route the missing / non-integer / <1 cases through the same UnknownExecSessionError message. Now every "no live session" mistake gives the same recovery guidance and, because that message is id-invariant, shares one repeated-failure signature, so any mix of missing/zero/probing accumulates toward the halt. Updates TestWriteStdinRequiresPositiveSessionID to cover missing/zero/negative/ non-integer and assert the recovery message. Guardrail signature/halt tests unchanged. * test(tools): cover the explicit session_id: nil case for write_stdin
1 parent 6849011 commit da9fb50

2 files changed

Lines changed: 40 additions & 16 deletions

File tree

‎internal/tools/exec_command.go‎

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -775,12 +775,20 @@ func (tool writeStdinTool) Run(ctx context.Context, args map[string]any) Result
775775
}
776776

777777
func (tool writeStdinTool) RunWithOptions(ctx context.Context, args map[string]any, _ RunOptions) Result {
778-
if value, ok := args["session_id"]; !ok || value == nil {
779-
return errorResult("Error: Invalid arguments for write_stdin: session_id is required")
780-
}
781-
sessionID, err := intArg(args, "session_id", 0, 1, 0)
782-
if err != nil {
783-
return errorResult("Error: Invalid arguments for write_stdin: " + err.Error())
778+
// A missing, non-integer, or < 1 session_id all mean the same thing: the model
779+
// has no live session to write to. Route them to the SAME recovery guidance the
780+
// no-live-session case uses (UnknownExecSessionError) instead of a terse
781+
// "session_id must be at least 1". The terse error gives no way forward and, by
782+
// naming the minimum, nudges the model to try 1, 2, 3... — the exact id-probing
783+
// #749 works to suppress. The recovery message instead tells it to start a
784+
// session or edit files directly, and because that message is id-invariant, the
785+
// missing/zero/non-integer entry points and id-probing collapse to ONE
786+
// repeated-failure signature, so any mix of them accumulates toward the halt
787+
// rather than resetting the streak on each class of mistake.
788+
value, present := args["session_id"]
789+
sessionID, sessionErr := intArg(args, "session_id", 0, 1, 0)
790+
if !present || value == nil || sessionErr != nil {
791+
return errorResult(UnknownExecSessionError(sessionID))
784792
}
785793
chars, err := stringArgWithEmpty(args, "chars", "", false, true)
786794
if err != nil {

‎internal/tools/exec_command_test.go‎

Lines changed: 26 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -923,19 +923,35 @@ func TestWriteStdinSchemaPinsSessionIDMinimum(t *testing.T) {
923923
}
924924
}
925925

926+
// A missing, zero, negative, or non-integer session_id all mean the model has no
927+
// live session, so write_stdin returns the SAME recovery guidance the no-live-
928+
// session case uses (start a session with exec_command, or edit files directly),
929+
// not a terse "session_id must be at least 1" that gives no way forward and names
930+
// a minimum that nudges the model to probe ids 1, 2, 3... Sharing one id-invariant
931+
// message also keeps a single repeated-failure signature, so any mix of these
932+
// mistakes accumulates toward the halt instead of resetting the streak (#749).
926933
func TestWriteStdinRequiresPositiveSessionID(t *testing.T) {
927934
tool := NewWriteStdinTool(newExecSessionManager())
928-
for _, args := range []map[string]any{
929-
{},
930-
{"session_id": 0},
935+
want := UnknownExecSessionError(0)
936+
for name, args := range map[string]map[string]any{
937+
"missing": {},
938+
"nil": {"session_id": nil},
939+
"zero": {"session_id": 0},
940+
"negative": {"session_id": -3},
941+
"non-integer": {"session_id": "abc"},
931942
} {
932-
result := tool.Run(context.Background(), args)
933-
if result.Status != StatusError {
934-
t.Fatalf("Run(%#v) status = %s, want error", args, result.Status)
935-
}
936-
if !strings.Contains(result.Output, "Invalid arguments for write_stdin") {
937-
t.Fatalf("Run(%#v) output = %q, want invalid arguments", args, result.Output)
938-
}
943+
t.Run(name, func(t *testing.T) {
944+
result := tool.Run(context.Background(), args)
945+
if result.Status != StatusError {
946+
t.Fatalf("status = %s, want error", result.Status)
947+
}
948+
if result.Output != want {
949+
t.Fatalf("output = %q,\n want the recovery message %q", result.Output, want)
950+
}
951+
if strings.Contains(result.Output, "must be at least 1") {
952+
t.Fatalf("still returns the terse minimum error: %q", result.Output)
953+
}
954+
})
939955
}
940956
}
941957

0 commit comments

Comments
 (0)