mirror of
https://github.com/Twigpine/zero.git
synced 2026-11-10 05:01:28 -10:00
* tui: interactive /resume session picker + resume fixes
- /resume (no arg) now opens an interactive picker like /model and /provider:
one row per resumable session showing the title and session id (+ relative
age); arrow to highlight, type to filter, enter to load. /resume <id> and
/resume latest still resolve directly.
- Resume now loads the rehydrated (compaction-aware) event view instead of the
raw log, so a resumed session honors a prior /compact — matching the CLI's
`zero exec --resume` and the in-TUI /compact reload. Falls back to raw on a
rehydrate error.
- The resume summary reports the model/provider the continuation will actually
use (the active ones), noting the session's recorded model/provider when it
differs, instead of implying a switch that doesn't happen.
- Drop the dead, self-contradictory resumeText(arg) branch (it was unreachable
and its hint was circular); resumeText is now the no-session/error fallback.
Tests: session picker opens with title+id rows and hydrates on selection;
resume honors prior compaction (rehydrated, not raw events).
* tui: lead /resume picker rows with a precise timestamp
Same-titled sessions (e.g. the same first prompt run several times) looked like
duplicates because the row showed only the identical title plus a long id that
truncated the age away. Lead each row's label with a precise local timestamp
(HH:MM:SS today, "Jan _2 15:04" earlier this year, else the date) so distinct
sessions are obviously distinct; the full id stays as the right-aligned meta and
is what selection resolves. sessionWhen parses the RFC3339 UpdatedAt.
* tui: hide empty/no-output sessions from the /resume picker
Most of a heavily-retried prompt's sessions are empty failed runs — just the
prompt plus the no-output guardrail stop ("Agent stopped … with no output …"),
with no assistant text and no tool calls. They have nothing to resume and flood
the picker with identical-looking rows. Skip any session with no resumable
content (no tool call/result and no real non-user message) from the picker; they
stay on disk and are never deleted. Adds agent.IsNoProgressStop to recognize the
stop message from a recorded event, and a test that the picker hides an empty
run while keeping a real one.
* feat(tui): model-generated session titles (auto + /retitle backfill)
Sessions were titled from their first user message, so prompts that
started the same way (or empty/failed runs) looked identical in the
/resume picker. Generate a concise, specific title with the active
provider instead.
- sessions.Store.UpdateTitle(id, title): rewrites only Title under the
per-session lock, re-reading the latest metadata first; leaves
UpdatedAt untouched (a retitle is not activity and must not reorder
the resumable list), rejects a blank title, and no-ops an unchanged
one.
- Title generator: a bounded digest of the conversation (user/assistant
text + tool names, per-message and total caps, skipping the no-output
guardrail stop) is sent as a one-shot completion; the response is
cleaned (first content line, quotes/markup/"Title:" label stripped,
word- and rune-capped) before storing.
- Auto-title going forward: after a successful turn, a session still
carrying its default first-message title gets a title in the
background, at most once per session.
- /retitle: backfills existing resumable sessions that still have a
first-message title, one at a time, skipping empty/failed runs and
already-named sessions, with kickoff and completion status lines.
Generation runs off the Update goroutine; failures are non-fatal (the
first-message title simply stays). Adds store + TUI unit tests
(UpdateTitle semantics, digest/cleaning, auto-title one-shot, backfill
candidate selection). gofmt/vet/build(host+linux+windows)/test -race/
staticcheck all green.
* address review: tighten no-progress detection, fix fallback errors + nil guard
Three review findings on the /resume work:
- IsNoProgressStop matched any content CONTAINING the no-output marker
substring (strings.Contains), so a legitimate assistant/tool message that
quoted the marker could be misread as a failed empty run — wrongly hiding a
real session from the /resume picker and skipping its title generation. Now
it requires the full answer structure (prefix + marker + suffix); a quote no
longer matches.
- resumeEvents returned the earlier rehydration error when the raw ReadEvents
fallback itself failed, masking the actual fallback failure. Returns rawErr.
- resumeText dereferenced m.sessionStore unconditionally; a nil store (fallback/
test paths) would panic. Now renders a safe "store unavailable" message.
(newSessionPicker already guarded nil.)
Tests: IsNoProgressStop accepts the real answer and rejects a quoting message /
bare marker / prefix- or suffix-only; resumeText with a nil store returns the
safe message instead of panicking. Full gate green.
* address review (Vasanth): structural no-progress check, retryable auto-title, nil-store guard, deep resume assertion
- guardrails: IsNoProgressStop now matches the exact structure noOutputStopAnswer
emits — prefix + "<int> turns " + marker + " " + suffix — instead of
prefix && contains(marker) && suffix. A genuine message that merely quotes the
marker amid other prose is no longer misclassified as a failed empty run (which
would wrongly hide a real session from /resume and skip its title generation).
Adds the hostile-input regression cases (quoted marker, text between
marker/suffix, non-integer turn count).
- session_title: release the optimistic titledSessions gate when a generation
FAILS (provider error, empty title, store write error) so a later turn or
/retitle can retry; success keeps the one-shot gate. Kept the optimistic mark
(rather than mark-on-success) to preserve the no-double-fire guarantee while a
generation is in flight.
- session_title: guard maybeAutoTitleActiveSession against a nil sessionStore
(the title cmd calls store.UpdateTitle) — mirrors the resume nil-store guards.
- model_test: assert resumed sessionEvents with reflect.DeepEqual against the
rehydrated context, not just slice length, so a reordered/substituted-but-
same-length regression is caught.
37 lines
1.9 KiB
Go
37 lines
1.9 KiB
Go
package agent
|
|
|
|
import "testing"
|
|
|
|
func TestIsNoProgressStopRequiresFullStructure(t *testing.T) {
|
|
// The actual guardrail answer (any turn count) is recognized.
|
|
for _, turns := range []int{1, 3, 12} {
|
|
if !IsNoProgressStop(noOutputStopAnswer(turns)) {
|
|
t.Fatalf("the real no-output stop answer (turns=%d) should be recognized", turns)
|
|
}
|
|
}
|
|
|
|
// A legitimate message that merely QUOTES the marker must NOT be classified as
|
|
// a no-progress stop — that would wrongly hide a real session from /resume.
|
|
rejected := []string{
|
|
`The previous run ended "with no output (no visible text and no tool calls)" so here is what I did instead.`,
|
|
"with no output (no visible text and no tool calls)", // bare marker, no prefix/suffix
|
|
"Agent stopped after 3 turns.", // prefix only
|
|
"to avoid consuming tokens without making progress.", // suffix only
|
|
"Here is your e-commerce site.", // unrelated
|
|
"",
|
|
// Hostile input: prefix + marker + suffix ALL present, but the marker is
|
|
// quoted mid-sentence rather than being the guard's own structured answer.
|
|
// A loose prefix/contains/suffix check misclassifies this.
|
|
`Agent stopped after 3 turns. The marker is "with no output (no visible text and no tool calls)" so here it is: to avoid consuming tokens without making progress.`,
|
|
// Arbitrary text wedged between the marker and the suffix.
|
|
"Agent stopped after 3 turns with no output (no visible text and no tool calls) and then some more text to avoid consuming tokens without making progress.",
|
|
// Non-integer "turn count" between the prefix and the marker.
|
|
"Agent stopped after several turns with no output (no visible text and no tool calls) to avoid consuming tokens without making progress.",
|
|
}
|
|
for _, content := range rejected {
|
|
if IsNoProgressStop(content) {
|
|
t.Fatalf("content must not be classified as a no-progress stop: %q", content)
|
|
}
|
|
}
|
|
}
|