mirror of
https://github.com/Twigpine/zero.git
synced 2026-11-08 09:49:34 -10:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b739fdfb0e |
No files matched your search
@@ -169,6 +169,37 @@ func TestRunResetsEmptyTurnCounterOnToolCall(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// The whole point of #702's fix: the unknown-session error's normalized
|
||||
// signature must not vary with the session id, so a model probing ids 1, 2,
|
||||
// 3, … produces one repeated signature the failure guard can count. If the id
|
||||
// leaked into the first 80 normalized chars, each probe would reset the streak
|
||||
// and the halt would never fire.
|
||||
func TestUnknownExecSessionErrorSignatureIsIDInvariant(t *testing.T) {
|
||||
a := errorSignature(tools.UnknownExecSessionError(1))
|
||||
b := errorSignature(tools.UnknownExecSessionError(999999))
|
||||
if a != b {
|
||||
t.Fatalf("unknown-session signature varies with id:\n %q\n %q", a, b)
|
||||
}
|
||||
}
|
||||
|
||||
// End to end: with an id-invariant signature, probing a different unknown id
|
||||
// each turn now trips the repeated-failure halt at toolFailureStopAt, where
|
||||
// before the fix it never would.
|
||||
func TestUnknownExecSessionProbingTripsFailureHalt(t *testing.T) {
|
||||
var state guardState
|
||||
var stoppedAt int
|
||||
for i := 1; i <= toolFailureStopAt; i++ {
|
||||
out := state.observeToolResult(tools.WriteStdinToolName, true, tools.UnknownExecSessionError(i))
|
||||
if out.Stop {
|
||||
stoppedAt = i
|
||||
break
|
||||
}
|
||||
}
|
||||
if stoppedAt != toolFailureStopAt {
|
||||
t.Fatalf("probing distinct unknown ids stopped at %d, want %d", stoppedAt, toolFailureStopAt)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGuardStateResetsToolOnlyStreakOnEmptyNonToolTurn(t *testing.T) {
|
||||
var state guardState
|
||||
toolOnly := zeroruntime.CollectedStream{
|
||||
|
||||
@@ -100,6 +100,9 @@ work.
|
||||
- Use exec_command with `tty: true` for interactive terminal-style commands that
|
||||
need stdin beyond Ctrl-C. `/ps` and `/stop` are user-facing TUI commands; when
|
||||
you need to clean up a running foreground command yourself, use write_stdin.
|
||||
- write_stdin's session_id is only ever an id returned by a still-running
|
||||
exec_command; never guess or probe ids. If you have no such session, start one
|
||||
with exec_command, or use write_file/edit_file/apply_patch for file changes.
|
||||
- write_stdin with empty input polls an existing exec_command session, and
|
||||
`\u0003` interrupts it. Sending other stdin bytes may require approval because
|
||||
it can drive the running process beyond the original command. Non-tty sessions
|
||||
|
||||
@@ -707,6 +707,18 @@ type writeStdinTool struct {
|
||||
manager *execSessionManager
|
||||
}
|
||||
|
||||
// UnknownExecSessionError is the result returned when write_stdin targets a
|
||||
// session_id with no live exec session. The stable recovery guidance leads and
|
||||
// the numeric id trails on purpose: the agent's repeated-failure guard keys on
|
||||
// a normalized, truncated prefix of the error string, so keeping the id out of
|
||||
// that prefix makes a model probing ids 1, 2, 3, … produce ONE signature that
|
||||
// finally trips the halt instead of a fresh signature per id — while also
|
||||
// telling the model how to actually recover. See TestUnknownExecSessionErrorSignatureIsIDInvariant
|
||||
// in internal/agent, which pins this invariant against the real guard.
|
||||
func UnknownExecSessionError(sessionID int) string {
|
||||
return fmt.Sprintf("Error: write_stdin needs a session_id returned by a still-running exec_command; do not guess or probe session ids. Start a process with exec_command, or use write_file/edit_file/apply_patch for file changes. (no live session %d)", sessionID)
|
||||
}
|
||||
|
||||
func (writeStdinTool) outputCategory(map[string]any) outputCategory {
|
||||
return outputCategoryProcess
|
||||
}
|
||||
@@ -722,7 +734,7 @@ func NewWriteStdinTool(manager *execSessionManager) Tool {
|
||||
parameters: Schema{
|
||||
Type: "object",
|
||||
Properties: map[string]PropertySchema{
|
||||
"session_id": {Type: "integer", Description: "Identifier of the running unified exec session."},
|
||||
"session_id": {Type: "integer", Description: "Identifier of a running unified exec session, as returned by exec_command. Never guess or probe ids.", Minimum: intPtr(1)},
|
||||
"chars": {Type: "string", Description: "Bytes to write to stdin. Defaults to empty, which polls without writing.", Default: ""},
|
||||
"yield_time_ms": {Type: "integer", Description: "Wait before yielding output. Non-empty writes default to 250 ms and cap at 30000 ms; empty polls wait 5000-300000 ms by default.", Default: defaultPollYieldTimeMS, Minimum: intPtr(1), Maximum: intPtr(maxPollYieldTimeMS)},
|
||||
"max_output_tokens": {Type: "integer", Description: "Output token budget. Defaults to 10000 tokens; larger requests may be capped by policy.", Default: defaultMaxOutputTokens, Minimum: intPtr(1), Maximum: intPtr(maxExecOutputTokenRequest)},
|
||||
@@ -784,7 +796,7 @@ func (tool writeStdinTool) RunWithOptions(ctx context.Context, args map[string]a
|
||||
}
|
||||
session, ok := tool.manager.get(sessionID)
|
||||
if !ok {
|
||||
return errorResult(fmt.Sprintf("Error: Unknown exec session_id %d.", sessionID))
|
||||
return errorResult(UnknownExecSessionError(sessionID))
|
||||
}
|
||||
session.touch()
|
||||
interrupted := false
|
||||
|
||||
@@ -881,7 +881,7 @@ func TestRegistryHonorsWriteStdinArgumentPermission(t *testing.T) {
|
||||
registry.Register(NewWriteStdinTool(newExecSessionManager()))
|
||||
|
||||
poll := registry.Run(context.Background(), WriteStdinToolName, map[string]any{"session_id": 9999})
|
||||
if poll.Status != StatusError || !strings.Contains(poll.Output, "Unknown exec session_id") {
|
||||
if poll.Status != StatusError || !strings.Contains(poll.Output, "still-running exec_command") {
|
||||
t.Fatalf("empty poll should reach tool without permission prompt, got status=%s output=%q", poll.Status, poll.Output)
|
||||
}
|
||||
|
||||
@@ -901,8 +901,25 @@ func TestWriteStdinReportsUnknownSession(t *testing.T) {
|
||||
if result.Status != StatusError {
|
||||
t.Fatalf("status = %s, want error", result.Status)
|
||||
}
|
||||
if !strings.Contains(result.Output, "Unknown exec session_id 1234") {
|
||||
t.Fatalf("unexpected output: %q", result.Output)
|
||||
// The message must lead with recovery guidance and still identify the id.
|
||||
if !strings.Contains(result.Output, "still-running exec_command") {
|
||||
t.Fatalf("unknown-session error must carry recovery guidance, got: %q", result.Output)
|
||||
}
|
||||
if !strings.Contains(result.Output, "1234") {
|
||||
t.Fatalf("unknown-session error must name the offending id, got: %q", result.Output)
|
||||
}
|
||||
}
|
||||
|
||||
// The runtime rejects session_id < 1 (intArg min 1); the advertised schema must
|
||||
// say the same so a model sees the constraint before it probes id 0.
|
||||
func TestWriteStdinSchemaPinsSessionIDMinimum(t *testing.T) {
|
||||
schema := NewWriteStdinTool(nil).(writeStdinTool).parameters
|
||||
prop, ok := schema.Properties["session_id"]
|
||||
if !ok {
|
||||
t.Fatal("session_id property missing from write_stdin schema")
|
||||
}
|
||||
if prop.Minimum == nil || *prop.Minimum != 1 {
|
||||
t.Fatalf("session_id schema Minimum = %v, want 1 to match the runtime floor", prop.Minimum)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user