Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 19 additions & 15 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,23 +8,27 @@ branch.

### Fixed

- Goal progress now uses a bounded, persisted novelty ledger instead of
treating all read-only turns as stalled or all successful reads as progress.
New read/search results advance research, exact tool/argument/result repeats
do not, while cross-turn no-progress streaks are observational rather than a
4/6/10 stop condition. Each Goal Run now defaults to 16 model rounds (unless
the user explicitly configured `max_steps`), followed by one summary-only
response and a resumable `goal_run_budget` pause. Repeating the same host
failure three times or completing six successful rounds without new evidence
similarly yields a resumable `goal_stuck` pause. The public `update_goal:
continue` status is normalized to
the FSM's internal running state, so it carries `next_action` without being
mistaken for terminal progress. The 10/20/40 cross-Run turn budgets remain an
independent backstop, and Goal status now persists real provider request
counts alongside observational tokens.
- Goal now runs continuously by default: the former 16-round per-Run boundary,
10/20/40 cross-Run quotas, default wall-clock budget, and numeric
no-progress/Todo-stall pauses no longer stop valid work. Progress guards still
detect repeated host outcomes and zero-evidence work, but redirect the model
to re-plan instead of producing `goal_run_budget` or `goal_stuck`. Explicit
`[agent].goal_token_budget`, `--max-steps`, positive time/cost budgets, manual
pause/stop, genuine user/external blockers, and evaluator fail-closed behavior
remain available. The Goal token budget defaults to `0` (off); resuming its
`budget_spend` pause grants a fresh slice without clearing cumulative usage.
Goal status reports turns, provider requests, tokens, the optional configured
token threshold, and cumulative active work time. Bot `max_steps` also
defaults to `0` (continuous), while positive user configuration is enforced.

- Removed numeric Goal pauses in existing sidecars automatically normalize to
`running` without sending a model request. Active Goal sidecars write
`turnsLimit: -1` as a downgrade-safe unlimited sentinel while public runtime
APIs retain deprecated limit fields as `0`. The migration preserves unknown
fields, todos, checkpoints, usage, evidence, and historical metadata.

- Goal is now the sole long-task runtime. Historical AutoResearch sidecars
migrate transactionally into research-budget Goals. Invalid archives block
migrate transactionally into Goals with research compatibility metadata. Invalid archives block
fail closed and remain read-only, retaining the task id and compatibility mode
for a restart or `/goal resume` retry; successful Goal-only sidecars omit the
old task id and write an explicit downgrade fence so previous readers cannot
Expand Down
10 changes: 6 additions & 4 deletions desktop/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -6758,15 +6758,16 @@ type Meta struct {

type GoalRuntimeView struct {
TurnsUsed int `json:"turnsUsed"`
TurnsLimit int `json:"turnsLimit"`
TurnsLimit int `json:"turnsLimit"` // Deprecated: always 0.
TokensUsed int `json:"tokensUsed"`
RequestsUsed int `json:"requestsUsed,omitempty"`
WorkDurationMs int64 `json:"workDurationMs,omitempty"`
TokensLimit int `json:"tokensLimit"` // Deprecated: always 0; retained for bridge compatibility.
NoProgressTurns int `json:"noProgressTurns"`
NoProgressLimit int `json:"noProgressLimit"`
NoProgressLimit int `json:"noProgressLimit"` // Deprecated: always 0.
LastReason string `json:"lastReason,omitempty"`
StopCause string `json:"stopCause,omitempty"`
BudgetExtensions int `json:"budgetExtensions"`
BudgetExtensions int `json:"budgetExtensions"` // Deprecated: always 0.
}

func goalRuntimeViewFromController(ctrl control.SessionAPI) *GoalRuntimeView {
Expand All @@ -6779,6 +6780,7 @@ func goalRuntimeViewFromController(ctrl control.SessionAPI) *GoalRuntimeView {
TurnsLimit: rt.TurnsLimit,
TokensUsed: rt.TokensUsed,
RequestsUsed: rt.RequestsUsed,
WorkDurationMs: rt.WorkDurationMs,
TokensLimit: rt.TokensLimit,
NoProgressTurns: rt.NoProgressTurns,
NoProgressLimit: rt.NoProgressLimit,
Expand Down Expand Up @@ -6959,7 +6961,7 @@ func (a *App) ClearGoalForTab(tabID string) error {
}

// ResumeGoalForTab re-enters a blocked or stopped Goal while preserving its
// delivery scope, budget history, and persisted verification checkpoint.
// delivery scope, runtime history, and persisted verification checkpoint.
func (a *App) ResumeGoalForTab(tabID string) bool {
tab := a.tabByID(tabID)
if tab == nil {
Expand Down
12 changes: 12 additions & 0 deletions desktop/frontend/src/__tests__/goal-runtime.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { formatGoalWorkTime } from "../lib/goalRuntime";

function equal(actual: unknown, expected: unknown, label: string) {
if (actual !== expected) throw new Error(`${label}: expected ${String(expected)}, got ${String(actual)}`);
}

equal(formatGoalWorkTime(undefined), "0s", "old payload without workDurationMs");
equal(formatGoalWorkTime(0), "0s", "zero duration");
equal(formatGoalWorkTime(42 * 60_000), "42m", "minute duration");
equal(formatGoalWorkTime((2 * 60 + 7) * 60_000), "2h 7m", "hour duration");

console.log("goal runtime formatting: passed");
6 changes: 3 additions & 3 deletions desktop/frontend/src/components/Composer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,8 @@ import {
type SelectedTextInsertRequest,
type SelectedTextReference,
} from "../lib/selectedTextContext";
import { formatGoalWorkTime } from "../lib/goalRuntime";

interface Attachment {
path: string;
previewUrl?: string;
Expand Down Expand Up @@ -4025,11 +4027,9 @@ export function Composer({
<span className="composer-intent-menu__goal-runtime-line">
{t("composer.goalRuntimeLine", {
turnsUsed: goalRuntime.turnsUsed,
turnsLimit: goalRuntime.turnsLimit,
tokensUsed: formatTokens(goalRuntime.tokensUsed),
requestsUsed: goalRuntime.requestsUsed ?? 0,
noProgressTurns: goalRuntime.noProgressTurns,
extensions: goalRuntime.budgetExtensions,
workTime: formatGoalWorkTime(goalRuntime.workDurationMs),
})}
</span>
)}
Expand Down
2 changes: 1 addition & 1 deletion desktop/frontend/src/lib/bridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1620,7 +1620,7 @@ function makeMockApp(): AppBindings {
enabled: !freshMock,
model: "",
toolApprovalMode: "ask",
maxSteps: 25,
maxSteps: 0,
debounceMs: 1500,
queueMode: "steer",
queueCap: 20,
Expand Down
10 changes: 10 additions & 0 deletions desktop/frontend/src/lib/goalRuntime.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export function formatGoalWorkTime(durationMs: number | undefined): string {
if (!Number.isFinite(durationMs) || (durationMs ?? 0) <= 0) return "0s";
const totalSeconds = Math.max(1, Math.round((durationMs ?? 0) / 1000));
if (totalSeconds < 60) return `${totalSeconds}s`;
const totalMinutes = Math.round(totalSeconds / 60);
if (totalMinutes < 60) return `${totalMinutes}m`;
const hours = Math.floor(totalMinutes / 60);
const minutes = totalMinutes % 60;
return minutes > 0 ? `${hours}h ${minutes}m` : `${hours}h`;
}
8 changes: 4 additions & 4 deletions desktop/frontend/src/lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -922,21 +922,21 @@ export type ToolApprovalMode = "ask" | "auto" | "yolo";
export type TokenMode = "full" | "economy" | "delivery" | "light" | "balanced";
export type AgentPreset = "light" | "balanced" | "delivery";
export type GoalStatus = "running" | "complete" | "blocked" | "stopped";
// GoalRuntime is the optional Goal budget/runtime summary the backend attaches
// to Meta. Absent for old hosts or when no goal is active.
// Optional Goal runtime summary; absent for old hosts or when no goal is active.
export interface GoalRuntime {
turnsUsed: number;
turnsLimit: number;
turnsLimit: number; // Deprecated: Goal exposes no turn limit and returns 0.
tokensUsed: number;
requestsUsed?: number;
workDurationMs?: number;
/** @deprecated Goal has no hard token limit; retained as 0 for old hosts/clients. */
tokensLimit: number;
noProgressTurns: number;
/** @deprecated No longer enforced; retained for old hosts/clients. */
noProgressLimit: number;
lastReason?: string;
stopCause?: string;
budgetExtensions: number;
budgetExtensions: number; // Deprecated: resumes no longer extend a numeric quota.
}
export function normalizeCollaborationMode(mode?: string, goal?: string, legacyMode?: Mode): CollaborationMode {
if (mode === "plan" || mode === "goal" || mode === "normal") return mode;
Expand Down
6 changes: 3 additions & 3 deletions desktop/frontend/src/locales/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -714,7 +714,7 @@ export const en = {
"composer.taskModeResumeGoal": "Resume goal",
"composer.goalPaused": "Goal paused",
"composer.goalBlocked": "Goal blocked — resume to continue",
"composer.goalRuntimeLine": "turns {turnsUsed}/{turnsLimit} · tokens {tokensUsed} · requests {requestsUsed} · no-progress {noProgressTurns} (observational) · extensions {extensions}",
"composer.goalRuntimeLine": "turns {turnsUsed} · requests {requestsUsed} · tokens {tokensUsed} · work time {workTime}",
"composer.accessMenuTitle": "Tool access ({shortcut} toggles YOLO)",
"composer.accessAsk": "Ask first",
"composer.accessAskTitle": "Ask before approval-gated tool calls; Ask is not read-only",
Expand Down Expand Up @@ -1998,8 +1998,8 @@ export const en = {
"settings.botModelHint": "Leave empty to use the default model.",
"settings.botModelDefault": "Use default model",
"settings.botRuntime": "Runtime limits",
"settings.botRuntimeHint": "Gateway-level limits that control how far each bot task can run.",
"settings.botMaxSteps": "Max steps",
"settings.botRuntimeHint": "Optional gateway-level limits. A max-steps value of 0 keeps running automatically without a step limit.",
"settings.botMaxSteps": "Max steps (0 = continuous)",
"settings.botDebounceMs": "Debounce ms",
"settings.botQueueCap": "Queue cap",
"settings.botQueueMode.steer": "Steer running turn",
Expand Down
6 changes: 3 additions & 3 deletions desktop/frontend/src/locales/zh-TW.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2444,7 +2444,7 @@ export const zhTW: Record<DictKey, string> = {
"composer.taskModeResumeGoal": "恢復目標",
"composer.goalPaused": "目標已暫停",
"composer.goalBlocked": "目標被阻擋 — 恢復後繼續",
"composer.goalRuntimeLine": "輪次 {turnsUsed}/{turnsLimit} · token {tokensUsed} · 請求 {requestsUsed} · 無進展 {noProgressTurns}(僅觀測)· 追加 {extensions}",
"composer.goalRuntimeLine": "輪次 {turnsUsed} · 請求 {requestsUsed} · token {tokensUsed} · 工作時間 {workTime}",
"composer.accessMenuTitle": "工具權限({shortcut} 切換 Yolo)",
"composer.accessAsk": "需要批准",
"composer.accessAskTitle": "需審批的工具呼叫會先詢問;詢問不是唯讀模式",
Expand Down Expand Up @@ -2819,8 +2819,8 @@ export const zhTW: Record<DictKey, string> = {
"settings.botModelHint": "留空時使用預設模型。",
"settings.botModelDefault": "使用預設模型",
"settings.botRuntime": "執行限制",
"settings.botRuntimeHint": "閘道器行程級限制,控制每次機器人任務最多走多遠。",
"settings.botMaxSteps": "最大步數",
"settings.botRuntimeHint": "可選的閘道器行程級限制;最大步數為 0 時自動持續執行,不設步數上限。",
"settings.botMaxSteps": "最大步數(0 = 持續執行)",
"settings.botDebounceMs": "合併視窗 ms",
"settings.botQueueCap": "佇列上限",
"settings.botQueueMode.steer": "引導目前回合",
Expand Down
6 changes: 3 additions & 3 deletions desktop/frontend/src/locales/zh.ts
Original file line number Diff line number Diff line change
Expand Up @@ -715,7 +715,7 @@ export const zh: Record<DictKey, string> = {
"composer.taskModeResumeGoal": "恢复目标",
"composer.goalPaused": "目标已暂停",
"composer.goalBlocked": "目标被阻塞 — 恢复后继续",
"composer.goalRuntimeLine": "轮次 {turnsUsed}/{turnsLimit} · token {tokensUsed} · 请求 {requestsUsed} · 无进展 {noProgressTurns}(仅观测)· 追加 {extensions}",
"composer.goalRuntimeLine": "轮次 {turnsUsed} · 请求 {requestsUsed} · token {tokensUsed} · 工作时间 {workTime}",
"composer.accessMenuTitle": "工具权限({shortcut} 切换 Yolo)",
"composer.accessAsk": "需要批准",
"composer.accessAskTitle": "需审批的工具调用会先询问;询问不是只读模式",
Expand Down Expand Up @@ -2000,8 +2000,8 @@ export const zh: Record<DictKey, string> = {
"settings.botModelHint": "留空时使用默认模型。",
"settings.botModelDefault": "使用默认模型",
"settings.botRuntime": "运行限制",
"settings.botRuntimeHint": "网关进程级限制,控制每次机器人任务最多走多远。",
"settings.botMaxSteps": "最大步数",
"settings.botRuntimeHint": "可选的网关进程级限制;最大步数为 0 时自动持续执行,不设步数上限。",
"settings.botMaxSteps": "最大步数(0 = 持续执行)",
"settings.botDebounceMs": "合并窗口 ms",
"settings.botQueueCap": "队列上限",
"settings.botQueueMode.steer": "引导当前回合",
Expand Down
8 changes: 4 additions & 4 deletions docs/CLI.md
Original file line number Diff line number Diff line change
Expand Up @@ -445,10 +445,10 @@ the displayed list matches the commands the TUI accepts.
| `/output-style` | Select an answer style. |
| `/verbose` | Toggle expanded reasoning display. |
| `/sandbox` | Inspect sandbox status. |
| `/goal [objective]` | Start a long-running goal, or inspect the current goal and its budget runtime. |
| `/goal status` | Show the active goal plus outer turns, observational tokens/requests/no-progress, extensions, and the last continuation/evaluator reason. |
| `/goal pause` | Pause the running goal (keeps todos, Delivery checkpoint, and budget). |
| `/goal resume` | Resume a paused or blocked goal (budget pauses add one more budget slice). |
| `/goal [objective]` | Start a continuous goal, or inspect its runtime statistics. |
| `/goal status` | Show the active goal plus turns, requests, tokens, work time, and the last continuation/evaluator reason. |
| `/goal pause` | Pause the running goal (keeps todos, Delivery checkpoint, and runtime history). |
| `/goal resume` | Resume a manually paused or genuinely blocked goal without changing a numeric quota. |
| `/goal clear` | End goal mode permanently. |
| `/docs [question]` | Show the embedded corpus identity, or search it locally and ask the configured AI to answer from version-matched evidence. |
| `/reasonix:docs [question]` | Preferred built-in fallback when an existing custom command or compatible plugin/skill alias owns `/docs`; if this spelling is also owned, the menu selects the next free `reasonix:`-qualified name without displacing it. |
Expand Down
5 changes: 3 additions & 2 deletions docs/COLLABORATION_MODES.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ Reasonix 桌面端输入框左下角的菜单包含两条互相独立的轴:
- 任务需要探索、实现、验证多个阶段。
- 你希望减少中途反复下指令,让 Reasonix 在目标范围内持续推进。
- 目标明显是长周期研究、排障或优化,例如“持续排查直到根因明确”“彻底实现并验证”
“不要原地打转”。这类目标会自动使用 40 轮研究预算,但仍由同一个 Goal 状态机推进
“不要原地打转”。Goal 默认持续执行,不会因为固定轮数或无进展计数而暂停

### 推荐目标写法

Expand All @@ -65,7 +65,8 @@ Goal 模式会把这些部分当作任务边界;除非下一步涉及不可逆
- 目标模式不是跳过审批。遇到高风险操作、权限限制、阻塞或需要产品判断时,仍可能停下来询问。
- 如果目标过大或边界不清,Reasonix 可能需要更多探索轮次,也会消耗更多 token。
- 普通聊天不会因为目标文本看起来复杂或长周期而自动切换模式。只有明确选择“目标”或使用
`/goal` 后,Reasonix 才会进入 Goal,并自动选择简单、写入或研究轮次预算。
`/goal` 后,Reasonix 才会进入 Goal;旧简单/写入/研究参数仍可解析,但不再改变额度。
- Goal 没有默认轮数、turn 数或时间上限。`--max-steps`、正数时间/成本预算仍可由用户显式选择。
- 旧 `.reasonix/autoresearch/...` 目录不会被新版本创建或改写;显式引用旧任务路径时只会读取并恢复成普通 Goal。
- 目标模式和计划模式是同一协作轴。切到计划模式时,会退出目标草稿/目标显示状态;运行模式不会因此改变。

Expand Down
Loading