mirror of
https://github.com/Twigpine/zero.git
synced 2026-11-08 17:56:14 -10:00
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ecce5d9374 | ||
|
|
a053a6a8f0 |
No files matched your search
@@ -169,37 +169,6 @@ 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,9 +100,6 @@ 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,18 +707,6 @@ 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
|
||||
}
|
||||
@@ -734,7 +722,7 @@ func NewWriteStdinTool(manager *execSessionManager) Tool {
|
||||
parameters: Schema{
|
||||
Type: "object",
|
||||
Properties: map[string]PropertySchema{
|
||||
"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)},
|
||||
"session_id": {Type: "integer", Description: "Identifier of the running unified exec session."},
|
||||
"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)},
|
||||
@@ -796,7 +784,7 @@ func (tool writeStdinTool) RunWithOptions(ctx context.Context, args map[string]a
|
||||
}
|
||||
session, ok := tool.manager.get(sessionID)
|
||||
if !ok {
|
||||
return errorResult(UnknownExecSessionError(sessionID))
|
||||
return errorResult(fmt.Sprintf("Error: Unknown exec session_id %d.", 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, "still-running exec_command") {
|
||||
if poll.Status != StatusError || !strings.Contains(poll.Output, "Unknown exec session_id") {
|
||||
t.Fatalf("empty poll should reach tool without permission prompt, got status=%s output=%q", poll.Status, poll.Output)
|
||||
}
|
||||
|
||||
@@ -901,25 +901,8 @@ func TestWriteStdinReportsUnknownSession(t *testing.T) {
|
||||
if result.Status != StatusError {
|
||||
t.Fatalf("status = %s, want error", result.Status)
|
||||
}
|
||||
// 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)
|
||||
if !strings.Contains(result.Output, "Unknown exec session_id 1234") {
|
||||
t.Fatalf("unexpected output: %q", result.Output)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -253,6 +253,7 @@ var commandDefinitions = []commandDefinition{
|
||||
},
|
||||
{
|
||||
name: "/rewind",
|
||||
aliases: []string{"/undo"},
|
||||
usage: "/rewind [latest|<sequence>]",
|
||||
group: commandGroupSession,
|
||||
description: "Restore workspace files to a checkpoint and truncate the session.",
|
||||
|
||||
@@ -985,6 +985,31 @@ func TestParseBackgroundTerminalCommands(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// /undo is an alias for /rewind (issue #698): previously it fell through to
|
||||
// commandUnknown.
|
||||
func TestUndoAliasesRewind(t *testing.T) {
|
||||
rewind, ok := resolveCommand("/rewind")
|
||||
if !ok {
|
||||
t.Fatal("expected /rewind to resolve")
|
||||
}
|
||||
alias, ok := resolveCommand("/undo")
|
||||
if !ok || alias.kind != rewind.kind {
|
||||
t.Fatalf("expected /undo to alias /rewind, got ok=%v command=%#v", ok, alias)
|
||||
}
|
||||
// End-to-end: a typed /undo dispatches to the rewind handler with args intact,
|
||||
// for both the "latest" and numeric-<sequence> forms of the /rewind contract.
|
||||
if parsed := parseCommand("/undo latest"); parsed.kind != commandRewind || parsed.text != "latest" {
|
||||
t.Fatalf("parseCommand(%q) = %#v, want commandRewind with text=latest", "/undo latest", parsed)
|
||||
}
|
||||
if parsed := parseCommand("/undo 123"); parsed.kind != commandRewind || parsed.text != "123" {
|
||||
t.Fatalf("parseCommand(%q) = %#v, want commandRewind with text=123", "/undo 123", parsed)
|
||||
}
|
||||
// Guard: without the alias it would be commandUnknown.
|
||||
if parsed := parseCommand("/undo"); parsed.kind == commandUnknown {
|
||||
t.Fatal("/undo resolved to commandUnknown — the alias is not wired")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCommandSelectionRequiresInputFromUsage(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
|
||||
Reference in new issue
Block a user