From a6238139c4520d0a2eff20fc4ff3cdbe00c19af3 Mon Sep 17 00:00:00 2001 From: SivanCola <32437197+SivanCola@users.noreply.github.com> Date: Sun, 9 Aug 2026 09:10:28 +0800 Subject: [PATCH 1/2] fix(goal): harden contextual tool and migration boundaries Problem: Context-dependent tools could be called outside their owning Goal, Plan, Jobs, or child-agent context, and failed legacy AutoResearch migration lost its retry identity after restart. Root cause: Availability was enforced by scattered execution checks while provider-visible registration, host metadata, mixed batches, and inherited child context used different state. The Goal sidecar writer also fenced every new write without preserving a pending legacy task id. Fix: Add a shared ContextualTool execution contract with one-repair run-loop handling, isolate child Goal/Jobs/memory state, project contextual schemas only for host metadata, and retain pending legacy task ids until a Goal-only sidecar is durably committed. Verification: go test ./... -count=1 cd desktop && go test ./... -count=1 go test -race ./internal/control ./internal/agent ./internal/tool ./internal/tool/builtin ./internal/jobs ./internal/memory -count=1 go vet ./... golangci-lint run --timeout=5m scripts/cache-guard.sh git diff --check --- CHANGELOG.md | 12 ++- internal/agent/agent.go | 25 +++++ internal/agent/contextual_tool_test.go | 102 ++++++++++++++++++ internal/agent/execute_one.go | 39 +++++++ internal/agent/extensions.go | 3 +- internal/agent/run_loop.go | 56 +++++++--- internal/agent/subagent_identity.go | 10 +- internal/agent/subagent_store.go | 4 +- internal/agent/task.go | 21 +++- internal/control/autoresearch_manager.go | 2 + internal/control/controller.go | 4 +- internal/control/goal.go | 40 +++++-- internal/control/goal_legacy.go | 13 +++ internal/control/goal_legacy_restore_test.go | 55 +++++++++- internal/jobs/context_isolation_test.go | 17 +++ internal/jobs/jobs.go | 7 ++ internal/memory/queue.go | 7 ++ internal/memory/queue_context_test.go | 21 ++++ internal/tool/builtin/bgjobs.go | 15 +++ internal/tool/builtin/completestep.go | 7 ++ .../builtin/contextual_visibility_test.go | 48 +++++++++ internal/tool/builtin/updategoal.go | 5 + internal/tool/tool.go | 45 ++++++++ 23 files changed, 520 insertions(+), 38 deletions(-) create mode 100644 internal/agent/contextual_tool_test.go create mode 100644 internal/jobs/context_isolation_test.go create mode 100644 internal/memory/queue_context_test.go create mode 100644 internal/tool/builtin/contextual_visibility_test.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 0bbb0a1df6..9870ec1c07 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,10 +10,18 @@ branch. - Goal is now the sole long-task runtime. Historical AutoResearch sidecars migrate transactionally into research-budget Goals. Invalid archives block - fail closed and remain read-only; successful Goal-only sidecars omit the old - task id and write an explicit downgrade fence so previous readers cannot + 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 reactivate the removed runtime. +- Context-dependent workflow tools now share one host-side execution boundary. + Goal, Plan sign-off, and background-job calls cannot reach permissions, + hooks, leases, or Execute outside their owning context; mixed batches execute + valid calls once and stop safely after one repair. Child agents also isolate + inherited Goal, Jobs, and live memory queues, while persisted tool identity + records the effective child schema projection. + - **Issue #7575:** Linux Bash under bubblewrap no longer mounts a fresh empty `--tmpfs /tmp` on every call. Consecutive commands in the same logical session now share a private temporary directory (bound at `/tmp` on Linux, exported via diff --git a/internal/agent/agent.go b/internal/agent/agent.go index 742dc7111e..e014a85b29 100644 --- a/internal/agent/agent.go +++ b/internal/agent/agent.go @@ -139,6 +139,27 @@ func PlanModeFromContext(ctx context.Context) bool { return ok && cc.planMode } +// withAgentContext establishes the agent-owned workflow capabilities for a +// model round and for tool availability checks. Missing capabilities shadow +// inherited values so child agents cannot reach parent Goal, Jobs, or memory +// state accidentally. +func (a *Agent) withAgentContext(ctx context.Context) context.Context { + if a == nil { + return ctx + } + if a.jobs != nil { + ctx = jobs.WithManager(ctx, a.jobs) + } else { + ctx = jobs.WithoutManager(ctx) + } + if a.memQueue != nil { + ctx = memory.WithQueue(ctx, a.memQueue) + } else { + ctx = memory.WithoutQueue(ctx) + } + return planmode.WithActive(ctx, a.planMode.Load()) +} + // WithParentSession stamps the active parent session ID onto a turn context so // persisted sub-agents can record and enforce their owning conversation. func WithParentSession(ctx context.Context, parentSession string) context.Context { @@ -1076,6 +1097,9 @@ type Options struct { // Jobs is the session's background-job manager (nil disables background tools). Jobs *jobs.Manager + // MemoryQueue optionally gives a child agent an explicitly owned live-memory + // queue. When nil, child construction shadows inherited queues. + MemoryQueue memory.Queue // WriteScheduler is the session-scoped subagent concurrency/write-claim // controller. When set on the parent executor, write-capable tools reserve @@ -1246,6 +1270,7 @@ func New(prov provider.Provider, tools *tool.Registry, session *Session, opts Op configWriteApprover: configWriteApprover, hooks: hooks, jobs: opts.Jobs, + memQueue: opts.MemoryQueue, writeScheduler: opts.WriteScheduler, writeWorkspaceRoot: strings.TrimSpace(opts.WriteWorkspaceRoot), workspaceLease: opts.WorkspaceLease, diff --git a/internal/agent/contextual_tool_test.go b/internal/agent/contextual_tool_test.go new file mode 100644 index 0000000000..60d4c9d709 --- /dev/null +++ b/internal/agent/contextual_tool_test.go @@ -0,0 +1,102 @@ +package agent + +import ( + "context" + "encoding/json" + "strings" + "sync/atomic" + "testing" + + "reasonix/internal/event" + "reasonix/internal/provider" + "reasonix/internal/tool" +) + +type unavailableTool struct { + fakeTool + calls *int32 +} + +func (u unavailableTool) ProviderVisible(context.Context) bool { return false } + +func (u unavailableTool) Execute(ctx context.Context, args json.RawMessage) (string, error) { + if u.calls != nil { + atomic.AddInt32(u.calls, 1) + } + return u.fakeTool.Execute(ctx, args) +} + +func TestContextualToolHostGateRunsBeforePermissionAndExecute(t *testing.T) { + var executions int32 + reg := tool.NewRegistry() + reg.Add(unavailableTool{fakeTool: fakeTool{name: "phase_tool", readOnly: false}, calls: &executions}) + gate := &recordingPermissionGate{allow: true} + a := New(nil, reg, NewSession("sys"), Options{Gate: gate}, event.Discard) + + out := a.executeOne(context.Background(), provider.ToolCall{ID: "phase", Name: "phase_tool", Arguments: `{}`}) + if !out.blocked || !strings.Contains(out.output, "unavailable") { + t.Fatalf("contextual tool outcome = %+v", out) + } + if executions != 0 || len(gate.calls) != 0 { + t.Fatalf("unavailable tool crossed host gate: executions=%d permission=%+v", executions, gate.calls) + } +} + +func TestMixedContextualBatchExecutesAvailableCallsOnce(t *testing.T) { + var executions int32 + reg := tool.NewRegistry() + reg.Add(unavailableTool{fakeTool: fakeTool{name: "phase_tool", readOnly: true}, calls: &executions}) + reg.Add(fakeTool{name: "read_file", readOnly: true, calls: &executions}) + prov := &scriptedProvider{name: "p", turns: [][]provider.Chunk{ + {toolCallChunk("phase", "phase_tool", `{}`), toolCallChunk("read", "read_file", `{}`), {Type: provider.ChunkDone}}, + {{Type: provider.ChunkText, Text: "answer after the available read"}, {Type: provider.ChunkDone}}, + }} + a := New(prov, reg, NewSession("sys"), Options{}, event.Discard) + if err := a.Run(context.Background(), "inspect the file"); err != nil { + t.Fatalf("mixed contextual batch failed: %v", err) + } + if got := atomic.LoadInt32(&executions); got != 1 { + t.Fatalf("available tool executions = %d, want exactly one", got) + } + if got := lastToolResult(a.Session(), "phase_tool"); !strings.Contains(got, "unavailable") { + t.Fatalf("contextual result = %q", got) + } +} + +func TestRepeatedMixedContextualBatchStopsAllCalls(t *testing.T) { + var executions int32 + reg := tool.NewRegistry() + reg.Add(unavailableTool{fakeTool: fakeTool{name: "phase_tool", readOnly: true}, calls: &executions}) + reg.Add(fakeTool{name: "read_file", readOnly: true, calls: &executions}) + prov := &scriptedProvider{name: "p", turns: [][]provider.Chunk{ + {toolCallChunk("phase-1", "phase_tool", `{}`), toolCallChunk("read-1", "read_file", `{}`), {Type: provider.ChunkDone}}, + {toolCallChunk("phase-2", "phase_tool", `{}`), toolCallChunk("read-2", "read_file", `{}`), {Type: provider.ChunkDone}}, + }} + a := New(prov, reg, NewSession("sys"), Options{}, event.Discard) + err := a.Run(context.Background(), "inspect the file") + if err == nil || !strings.Contains(err.Error(), "context-unavailable tools") { + t.Fatalf("repeated contextual batch error = %v", err) + } + if got := atomic.LoadInt32(&executions); got != 1 { + t.Fatalf("available tool was re-executed after repair: %d", got) + } + if got := lastToolResult(a.Session(), "read_file"); !strings.Contains(got, "called again") { + t.Fatalf("second legal call was not paired with stop result: %q", got) + } +} + +func TestRepeatedPureContextualCallWithVisibleAnswerFinishes(t *testing.T) { + reg := tool.NewRegistry() + reg.Add(unavailableTool{fakeTool: fakeTool{name: "phase_tool", readOnly: true}}) + prov := &scriptedProvider{name: "p", turns: [][]provider.Chunk{ + {toolCallChunk("phase-1", "phase_tool", `{}`), {Type: provider.ChunkDone}}, + {{Type: provider.ChunkText, Text: "visible answer"}, toolCallChunk("phase-2", "phase_tool", `{}`), {Type: provider.ChunkDone}}, + }} + a := New(prov, reg, NewSession("sys"), Options{}, event.Discard) + if err := a.Run(context.Background(), "answer normally"); err != nil { + t.Fatalf("visible answer after repeated contextual call failed: %v", err) + } + if got := lastAssistantContent(a.Session()); got != "visible answer" { + t.Fatalf("final answer = %q", got) + } +} diff --git a/internal/agent/execute_one.go b/internal/agent/execute_one.go index bc0e8333c6..6019875eff 100644 --- a/internal/agent/execute_one.go +++ b/internal/agent/execute_one.go @@ -63,6 +63,7 @@ type toolCallPlan struct { // — the caller emits ToolDispatch/ToolResult — so it is safe to invoke from // parallel goroutines. Stages: parse → policy → prepare → finish. func (a *Agent) executeOne(ctx context.Context, call provider.ToolCall) (out toolOutcome) { + ctx = a.withAgentContext(ctx) plan := &toolCallPlan{call: call} defer func() { if plan.mutationObserved && !plan.mutationAfterDone { @@ -170,6 +171,9 @@ func (a *Agent) resolveToolPolicy(ctx context.Context, plan *toolCallPlan) (tool if blocked, early := a.applyPlanModeAndProxy(ctx, plan); early { return blocked, true } + if blocked, early := a.applyContextualToolGate(ctx, plan); early { + return blocked, true + } if blocked, early := a.applyDeliveryPolicyGates(plan); early { return blocked, true } @@ -186,6 +190,38 @@ func (a *Agent) resolveToolPolicy(ctx context.Context, plan *toolCallPlan) (tool return toolOutcome{}, false } +func (a *Agent) applyContextualToolGate(ctx context.Context, plan *toolCallPlan) (toolOutcome, bool) { + if plan == nil || plan.tool == nil { + return toolOutcome{}, false + } + if outcome, blocked := contextualToolGateOutcome(ctx, plan.tool, plan.canonicalName); blocked { + return outcome, true + } + if plan.execTool != nil { + if outcome, blocked := contextualToolGateOutcome(ctx, plan.execTool, plan.permName); blocked { + return outcome, true + } + } + return toolOutcome{}, false +} + +func contextualToolGateOutcome(ctx context.Context, target tool.Tool, name string) (toolOutcome, bool) { + contextual, ok := target.(tool.ContextualTool) + if !ok || contextual.ProviderVisible(ctx) { + return toolOutcome{}, false + } + msg := fmt.Sprintf("blocked: tool %q is unavailable in the current workflow context", name) + switch name { + case "update_goal": + msg = "update_goal is only available while an active goal turn is running — no goal state was changed" + case "complete_step": + msg = "blocked: complete_step is only available after plan approval. While planning, keep task state with todo_write and present the plan for user approval." + case "bash_output", "wait", "kill_shell": + msg = "background jobs are not available in this context" + } + return toolOutcome{output: msg, blocked: true, errMsg: firstLine(msg)}, true +} + // applyMutationDependencyBarrier blocks later mutations and verifications in the // same provider batch after an earlier modification failed. Host-proven // read-only diagnosis (resolved ReadOnly with no verification classification) @@ -272,6 +308,9 @@ func (a *Agent) applyPlanModeAndProxy(ctx context.Context, plan *toolCallPlan) ( if rc.Target != nil { plan.execTool = rc.Target } + if outcome, blocked := contextualToolGateOutcome(ctx, plan.execTool, plan.permName); blocked { + return outcome, true + } plan.readOnly = rc.ReadOnly if outcome, blocked := a.readOnlyExecutionBlock(t, &rc); blocked { return outcome, true diff --git a/internal/agent/extensions.go b/internal/agent/extensions.go index 20a8131f62..16eae72788 100644 --- a/internal/agent/extensions.go +++ b/internal/agent/extensions.go @@ -111,9 +111,10 @@ func (a *Agent) interceptAgentStart(ctx context.Context) error { if d == nil { return nil } + providerCtx := a.withAgentContext(ctx) payload := dispatch.AgentStartPayload{ Model: a.prov.Name(), - ToolCount: len(a.tools.Schemas()), + ToolCount: len(a.tools.SchemasForContext(providerCtx)), SessionID: ParentSession(ctx), } result, err := d.Intercept(ctx, extension.PointAgentBeforeStart, &payload) diff --git a/internal/agent/run_loop.go b/internal/agent/run_loop.go index a7cde3b8c8..3472fa82c7 100644 --- a/internal/agent/run_loop.go +++ b/internal/agent/run_loop.go @@ -27,7 +27,7 @@ type runLoopState struct { emptyFinalBlocks int handoffNudges int usedAnyTool bool - goalToolRepairs int + contextToolRepairs int graceRound bool recoveryGraceRound bool @@ -327,6 +327,7 @@ func (a *Agent) beginRunTurn(ctx context.Context, input string) (rawInput string // runToolLoop owns the main tool-round budget and dispatches each streamed // assistant turn into final-response or tool-round handling. func (a *Agent) runToolLoop(ctx context.Context, state *runLoopState) error { + ctx = a.withAgentContext(ctx) for step := 0; state.runMaxSteps <= 0 || step < state.runMaxSteps || state.graceRound || state.recoveryGraceRound; step++ { // Consume a queued steer and persist it to the session so it // survives tab switches and history replay. The model sees it as @@ -955,7 +956,20 @@ func (a *Agent) handleFinalResponse(ctx context.Context, state *runLoopState, te func (a *Agent) handleToolRound(ctx context.Context, state *runLoopState, step int, text, reasoning string, calls []provider.ToolCall, usage *provider.Usage) (cont bool, err error) { state.emptyFinalBlocks = 0 state.usedAnyTool = true - outOfContextGoalOnly := toolCallsAreOutOfContextGoalReports(ctx, calls) + unavailableContextTools := a.unavailableContextualToolCalls(ctx, calls) + if len(unavailableContextTools) > 0 && state.contextToolRepairs > 0 { + msg := fmt.Sprintf("blocked: context-unavailable tools were called again after the repair instruction: %s", strings.Join(unavailableContextTools, ", ")) + for _, call := range calls { + a.session.Add(provider.Message{Role: provider.RoleTool, Content: msg, ToolCallID: call.ID, Name: call.Name}) + } + if hasVisibleFinalAnswer(text) { + return a.handleFinalResponse(ctx, state, text, reasoning, usage) + } + if len(unavailableContextTools) == 1 && unavailableContextTools[0] == "update_goal" { + return false, fmt.Errorf("model repeatedly called update_goal outside Goal mode without a visible answer") + } + return false, fmt.Errorf("model repeatedly called context-unavailable tools without a visible answer: %s", strings.Join(unavailableContextTools, ", ")) + } // Grace round guard: if we already gave the model one extra response // and it still wants to call tools, stop here. @@ -1012,17 +1026,15 @@ func (a *Agent) handleToolRound(ctx context.Context, state *runLoopState, step i a.recordInterruptedDisplay("", "", nil, true, state.workDurationMs()) return false, ctx.Err() } - if outOfContextGoalOnly { + if len(unavailableContextTools) > 0 { if hasVisibleFinalAnswer(text) { // Keep the assistant tool call and host error paired in the transcript, - // but accept the co-streamed answer instead of spending another model - // request repairing harmless Goal bookkeeping outside Goal mode. + // but accept a co-streamed answer without another repair request. return a.handleFinalResponse(ctx, state, text, reasoning, usage) } - state.goalToolRepairs++ - if state.goalToolRepairs > 1 { - return false, fmt.Errorf("model repeatedly called update_goal outside Goal mode without a visible answer") - } + state.contextToolRepairs++ + nudge := fmt.Sprintf("The following tools are unavailable in the current workflow phase: %s. Do not call them again. Respond to the user's request with visible answer text now; call a different tool only if it is still needed to complete the request.", strings.Join(unavailableContextTools, ", ")) + a.session.Add(provider.Message{Role: provider.RoleUser, Content: a.withTurnPreferences(nudge)}) } if !a.planMode.Load() { nextProgress, nextTracking := a.canonicalTodoProgress() @@ -1095,17 +1107,29 @@ func (a *Agent) handleToolRound(ctx context.Context, state *runLoopState, step i return true, nil } -func toolCallsAreOutOfContextGoalReports(ctx context.Context, calls []provider.ToolCall) bool { +func (a *Agent) unavailableContextualToolCalls(ctx context.Context, calls []provider.ToolCall) []string { if len(calls) == 0 { - return false + return nil } - if _, ok := tool.GoalTurnRecorderFromContext(ctx); ok { - return false + if a == nil || a.tools == nil { + return nil } + names := make([]string, 0, len(calls)) + seen := make(map[string]struct{}, len(calls)) for _, call := range calls { - if call.Name != "update_goal" { - return false + t, canonical, ambiguous := a.tools.ResolveCall(call.Name) + if t == nil || len(ambiguous) > 0 { + continue + } + contextual, ok := t.(tool.ContextualTool) + if !ok || contextual.ProviderVisible(ctx) { + continue + } + if _, ok := seen[canonical]; ok { + continue } + seen[canonical] = struct{}{} + names = append(names, canonical) } - return true + return names } diff --git a/internal/agent/subagent_identity.go b/internal/agent/subagent_identity.go index 94850892e5..0d6dfb7efc 100644 --- a/internal/agent/subagent_identity.go +++ b/internal/agent/subagent_identity.go @@ -1,19 +1,23 @@ package agent import ( + "context" "encoding/json" "sort" "reasonix/internal/tool" ) -func toolIdentity(reg *tool.Registry) ([]string, string) { +func toolIdentity(reg *tool.Registry, ctx context.Context) ([]string, string) { if reg == nil { return nil, bytesHash(nil) } - names := reg.Names() + schemas := normalizeToolSchemas(reg.SchemasForContext(ctx)) + names := make([]string, 0, len(schemas)) + for _, schema := range schemas { + names = append(names, schema.Name) + } sort.Strings(names) - schemas := normalizeToolSchemas(reg.Schemas()) data, _ := json.Marshal(schemas) return names, bytesHash(data) } diff --git a/internal/agent/subagent_store.go b/internal/agent/subagent_store.go index 833f532187..b605fb05bc 100644 --- a/internal/agent/subagent_store.go +++ b/internal/agent/subagent_store.go @@ -1,6 +1,7 @@ package agent import ( + "context" "crypto/rand" "crypto/sha256" "encoding/hex" @@ -77,6 +78,7 @@ type SubagentSpec struct { ParentToolCallID string SystemPrompt string Registry *tool.Registry + ToolContext context.Context Model string Effort string } @@ -742,7 +744,7 @@ func (s *SubagentStore) LoadMeta(ref string) (SubagentMeta, error) { } func metaFromSpec(ref string, status SubagentStatus, created, updated time.Time, spec SubagentSpec) SubagentMeta { - scope, schemaHash := toolIdentity(spec.Registry) + scope, schemaHash := toolIdentity(spec.Registry, spec.ToolContext) return SubagentMeta{ Ref: ref, CreatedAt: created, diff --git a/internal/agent/task.go b/internal/agent/task.go index ab0ee93eee..c05a76e781 100644 --- a/internal/agent/task.go +++ b/internal/agent/task.go @@ -19,6 +19,7 @@ import ( "reasonix/internal/event" "reasonix/internal/evidence" "reasonix/internal/jobs" + "reasonix/internal/memory" "reasonix/internal/permission" "reasonix/internal/planmode" "reasonix/internal/provider" @@ -873,7 +874,7 @@ func (t *TaskTool) RunProfileSpec(ctx context.Context, spec ProfileExecSpec) (re modelRef, effortRef := spec.Model, spec.Effort usageModelRef := t.usageModelRef(modelRef, effortRef) parentID, _, _, _ := CallContext(ctx) - run, err := t.prepareTranscriptRunWithPrompt(subReg, modelRef, effortRef, ParentSession(ctx), parentID, spec.ContinueFrom, spec.ForkFrom, spec.SystemPrompt, spec.Kind, spec.Name) + run, err := t.prepareTranscriptRunWithPrompt(ctx, subReg, modelRef, effortRef, ParentSession(ctx), parentID, spec.ContinueFrom, spec.ForkFrom, spec.SystemPrompt, spec.Kind, spec.Name) if err != nil { return "", err } @@ -1055,7 +1056,7 @@ func (t *TaskTool) bashCanEnforceWriteRoots() bool { return false } -func (t *TaskTool) prepareTranscriptRunWithPrompt(subReg *tool.Registry, modelRef, effortRef, parentSession, parentID, continueFrom, legacyForkFrom, systemPrompt, kind, name string) (*SubagentRun, error) { +func (t *TaskTool) prepareTranscriptRunWithPrompt(ctx context.Context, subReg *tool.Registry, modelRef, effortRef, parentSession, parentID, continueFrom, legacyForkFrom, systemPrompt, kind, name string) (*SubagentRun, error) { continueFrom = strings.TrimSpace(continueFrom) legacyForkFrom = strings.TrimSpace(legacyForkFrom) parentSession = strings.TrimSpace(parentSession) @@ -1089,6 +1090,7 @@ func (t *TaskTool) prepareTranscriptRunWithPrompt(subReg *tool.Registry, modelRe ParentToolCallID: parentID, SystemPrompt: systemPrompt, Registry: subReg, + ToolContext: childToolIdentityContext(ctx), Model: identityModel, Effort: identityEffort, } @@ -1101,6 +1103,13 @@ func (t *TaskTool) prepareTranscriptRunWithPrompt(subReg *tool.Registry, modelRe return t.transcripts.PrepareFresh(spec) } +func childToolIdentityContext(ctx context.Context) context.Context { + ctx = tool.WithoutGoalTurnRecorder(ctx) + ctx = memory.WithoutQueue(ctx) + ctx = jobs.WithoutManager(ctx) + return planmode.WithActive(ctx, PlanModeFromContext(ctx)) +} + func (t *TaskTool) effectiveIdentity(modelRef, effort string) (string, string) { if t.identityProfile != nil { model, eff := t.identityProfile(modelRef, effort) @@ -1830,6 +1839,14 @@ func RunSubAgentWithSession(ctx context.Context, prov provider.Provider, reg *to } // Isolate temporary files for this run before any tool execution. ctx = tool.WithoutGoalTurnRecorder(ctx) + if opts.MemoryQueue != nil { + ctx = memory.WithQueue(ctx, opts.MemoryQueue) + } else { + ctx = memory.WithoutQueue(ctx) + } + if opts.Jobs == nil { + ctx = jobs.WithoutManager(ctx) + } ctx, releaseTemp := withSubagentSessionTemp(ctx) defer releaseTemp() if opts.SubagentDepth > 0 { diff --git a/internal/control/autoresearch_manager.go b/internal/control/autoresearch_manager.go index 9c2ba87c5e..ebfcae97bb 100644 --- a/internal/control/autoresearch_manager.go +++ b/internal/control/autoresearch_manager.go @@ -137,6 +137,7 @@ func (c *Controller) restorePendingLegacyGoal(legacy legacyGoalRestore) bool { c.clearLegacyRestore(legacy.taskID, legacy.epoch) } } else { + c.goals.clearLegacyTaskID(epoch) c.clearLegacyRestore(legacy.taskID, legacy.epoch) } } else { @@ -200,6 +201,7 @@ func (c *Controller) retryBlockedLegacyGoal() (handled, resumed bool) { if !persisted { return true, false } + c.goals.clearLegacyTaskID(resumedEpoch) c.replaceLegacyRestore(legacyGoalRestore{}) if setup.notice != "" { c.notice(setup.notice) diff --git a/internal/control/controller.go b/internal/control/controller.go index 0091651972..9c2e62632b 100644 --- a/internal/control/controller.go +++ b/internal/control/controller.go @@ -2659,7 +2659,7 @@ func (c *Controller) SetGoalDurable(goal string) error { var data []byte var persist bool if setup.blockReason != "" { - path, data, persist = c.goals.setLegacyArchiveBlocked(resolved, setup.budgetClass, setup.blockReason, c.goalTodos()) + path, data, persist = c.goals.setLegacyArchiveBlockedWithTaskID(resolved, setup.budgetClass, setup.blockReason, setup.legacyTaskID, c.goalTodos()) c.replaceLegacyRestore(legacyGoalRestore{taskID: setup.legacyTaskID, epoch: c.goals.continuationToken(), explicit: setup.explicit}) } else { path, data, persist = c.goals.set(resolved, setup.budgetClass, c.goalTodos()) @@ -2695,7 +2695,7 @@ func (c *Controller) SetGoalWithResearchMode(goal string, researchMode GoalResea var data []byte var ok bool if setup.blockReason != "" { - path, data, ok = c.goals.setLegacyArchiveBlocked(resolved, setup.budgetClass, setup.blockReason, c.goalTodos()) + path, data, ok = c.goals.setLegacyArchiveBlockedWithTaskID(resolved, setup.budgetClass, setup.blockReason, setup.legacyTaskID, c.goalTodos()) c.replaceLegacyRestore(legacyGoalRestore{taskID: setup.legacyTaskID, epoch: c.goals.continuationToken(), explicit: setup.explicit}) c.notice("legacy research archive resume failed: " + setup.blockReason) } else { diff --git a/internal/control/goal.go b/internal/control/goal.go index c2b3853e59..8c7ccebc03 100644 --- a/internal/control/goal.go +++ b/internal/control/goal.go @@ -98,6 +98,11 @@ type goalMachine struct { lastEvaluatorReason string stopCause string budgetExtensions int // turn extensions from resume (compat field name) + // legacyTaskID is retained only while a historical AutoResearch archive is + // awaiting migration. It is serialized on fail-closed blocked sidecars so a + // restart can retry the migration without treating the raw archive path as a + // new Goal. + legacyTaskID string // statePath is the persisted goal-state sidecar; empty disables persistence. statePath string @@ -290,7 +295,12 @@ func (g *goalMachine) set(goal, preferredBudgetClass string, todos []evidence.To // archive goal. A concurrent Goal replacement cannot be blocked between two // separate FSM mutations. func (g *goalMachine) setLegacyArchiveBlocked(goal, preferredBudgetClass, reason string, todos []evidence.TodoItem) (string, []byte, bool) { + return g.setLegacyArchiveBlockedWithTaskID(goal, preferredBudgetClass, reason, "", todos) +} + +func (g *goalMachine) setLegacyArchiveBlockedWithTaskID(goal, preferredBudgetClass, reason, taskID string, todos []evidence.TodoItem) (string, []byte, bool) { goal = strings.TrimSpace(goal) + taskID = strings.TrimSpace(taskID) if goal != "" && preferredBudgetClass == "" { preferredBudgetClass = taskintent.ClassifyGoalBudget(goal) } @@ -302,6 +312,7 @@ func (g *goalMachine) setLegacyArchiveBlocked(goal, preferredBudgetClass, reason } g.stopCause = stopCauseLegacyArchive g.block = clipGoalReason(reason) + g.legacyTaskID = taskID return g.buildStateLocked(todos) } @@ -326,6 +337,8 @@ func (g *goalMachine) installGoalLocked(goal, preferredBudgetClass string) { g.tokensLimit = 0 // no token hard limit g.noProgressLimit = defaultNoProgressLimit } + // Installing a normal Goal always abandons any pending legacy migration. + g.legacyTaskID = "" } func (g *goalMachine) setStrict(strict bool, todos []evidence.TodoItem) (string, []byte, bool) { @@ -645,11 +658,15 @@ func (g *goalMachine) buildStateLocked(todos []evidence.TodoItem) (path string, StopCause: g.stopCause, BudgetExtensions: g.budgetExtensions, } - // GoalResearchOff is a downgrade fence: old readers must not infer or inject - // the removed AutoResearch runtime. Legacy task identity is decode-only and - // remains in the Controller-owned recovery boundary; it is never written into - // a new sidecar. - state.ResearchMode = GoalResearchOff + // GoalResearchOff is a downgrade fence for ordinary Goal sidecars. A + // fail-closed legacy migration keeps its task identity and compatibility mode + // until the archive has been validated and the Goal-only state is committed. + if g.legacyTaskID != "" && g.status == GoalStatusBlocked && g.stopCause == stopCauseLegacyArchive { + state.AutoResearchTaskID = g.legacyTaskID + state.ResearchMode = GoalResearchOn + } else { + state.ResearchMode = GoalResearchOff + } b, err := json.Marshal(state) if err != nil { slog.Warn("controller: marshal goal state", "err", err) @@ -741,12 +758,21 @@ func (g *goalMachine) restoreFromState(sessionPath string) (path string, data [] if g.status == "" { g.status = GoalStatusStopped } - // Legacy task identity is decode-only compatibility data. It is returned to - // the Controller's migration boundary and never enters active Goal memory. + // Legacy task identity is migration-only compatibility data. It is returned to + // the Controller's archive boundary and retained in the machine only while a + // fail-closed migration remains pending. legacy = legacyGoalRestore{ taskID: strings.TrimSpace(state.AutoResearchTaskID), todos: append([]evidence.TodoItem(nil), state.Todos...), } + // A task id is pending only when the sidecar has no Goal text. A legacy + // sidecar that already contains an objective can be migrated directly and + // must serialize as ordinary Goal state on the first write. + if g.goal == "" { + g.legacyTaskID = legacy.taskID + } else { + g.legacyTaskID = "" + } if legacy.taskID != "" && g.goal != "" { // Sidecars that already carry the Goal objective do not depend on the // historical archive. Complete the migration immediately. diff --git a/internal/control/goal_legacy.go b/internal/control/goal_legacy.go index 03b211281c..615f5ac761 100644 --- a/internal/control/goal_legacy.go +++ b/internal/control/goal_legacy.go @@ -67,6 +67,19 @@ func (g *goalMachine) failLegacyRestorePersistence(expectedEpoch uint64, reason return g.continuationEpoch, true } +// clearLegacyTaskID completes the sidecar migration after the Goal-only state +// has been durably written. The epoch check prevents a late completion from +// clearing the identity of a newer migration. +func (g *goalMachine) clearLegacyTaskID(expectedEpoch uint64) bool { + g.mu.Lock() + defer g.mu.Unlock() + if g.continuationEpoch != expectedEpoch { + return false + } + g.legacyTaskID = "" + return true +} + func (g *goalMachine) legacyArchiveBlockedState() (goal string, epoch uint64, ok bool) { g.mu.Lock() defer g.mu.Unlock() diff --git a/internal/control/goal_legacy_restore_test.go b/internal/control/goal_legacy_restore_test.go index adc3fe7ddd..c89061bbdc 100644 --- a/internal/control/goal_legacy_restore_test.go +++ b/internal/control/goal_legacy_restore_test.go @@ -138,7 +138,7 @@ func TestGoalSetIdempotencyUsesEffectiveBudgetClass(t *testing.T) { } } -func TestLegacySidecarArchiveFailureBlocksWithoutPersistingTaskID(t *testing.T) { +func TestLegacySidecarArchiveFailureBlocksWithRetryableTaskID(t *testing.T) { root := t.TempDir() if resolved, err := filepath.EvalSymlinks(root); err == nil { root = resolved @@ -184,7 +184,7 @@ func TestLegacySidecarArchiveFailureBlocksWithoutPersistingTaskID(t *testing.T) if err := json.Unmarshal(failedRaw, &failed); err != nil { t.Fatal(err) } - if failed.Status != GoalStatusBlocked || failed.ResearchMode != GoalResearchOff || failed.AutoResearchTaskID != "" || failed.StopCause != stopCauseLegacyArchive || failed.Block == "" { + if failed.Status != GoalStatusBlocked || failed.ResearchMode != GoalResearchOn || failed.AutoResearchTaskID != taskID || failed.StopCause != stopCauseLegacyArchive || failed.Block == "" { t.Fatalf("failed restore state = %+v, want retryable blocked legacy migration", failed) } if failed.ScopeID != scopeID || failed.DeliveryCheckpoint != wantCheckpoint || len(failed.Todos) != 1 || failed.Todos[0] != wantTodo { @@ -244,6 +244,53 @@ func TestLegacySidecarArchiveFailureBlocksWithoutPersistingTaskID(t *testing.T) } } +func TestLegacySidecarPendingTaskRetriesAfterRestart(t *testing.T) { + root := t.TempDir() + sessionPath := filepath.Join(root, "sessions", "restart.jsonl") + if err := os.MkdirAll(filepath.Dir(sessionPath), 0o755); err != nil { + t.Fatal(err) + } + const taskID = "restartable-legacy" + raw, err := json.Marshal(goalState{ + Status: GoalStatusRunning, ResearchMode: GoalResearchOn, + AutoResearchTaskID: taskID, BudgetClass: budgetClassResearch, TurnsUsed: 4, TurnsLimit: 40, + }) + if err != nil { + t.Fatal(err) + } + if err := os.WriteFile(goalStatePath(sessionPath), raw, 0o644); err != nil { + t.Fatal(err) + } + + firstSession := agent.NewSession("sys") + first := New(Options{WorkspaceRoot: root, SessionDir: root, Executor: agent.New(nil, nil, firstSession, agent.Options{}, event.Discard)}) + first.Resume(firstSession, sessionPath) + if first.GoalStatus() != GoalStatusBlocked { + t.Fatalf("first restore status = %q, want blocked", first.GoalStatus()) + } + first.Close() + + writeLegacyGoalArchive(t, root, taskID, "recover after process restart") + secondSession := agent.NewSession("sys") + second := New(Options{WorkspaceRoot: root, SessionDir: root, Executor: agent.New(nil, nil, secondSession, agent.Options{}, event.Discard)}) + defer second.Close() + second.Resume(secondSession, sessionPath) + if second.GoalStatus() != GoalStatusRunning || second.Goal() != "recover after process restart" { + t.Fatalf("restart migration = goal:%q status:%q", second.Goal(), second.GoalStatus()) + } + persisted, err := os.ReadFile(goalStatePath(sessionPath)) + if err != nil { + t.Fatal(err) + } + var state goalState + if err := json.Unmarshal(persisted, &state); err != nil { + t.Fatal(err) + } + if state.AutoResearchTaskID != "" || state.ResearchMode != GoalResearchOff || state.BudgetClass != budgetClassResearch { + t.Fatalf("restart migration left compatibility fields: %+v", state) + } +} + func TestLegacySidecarInvalidArchivesRemainRetryableAndReadOnly(t *testing.T) { tests := []struct { name string @@ -302,7 +349,7 @@ func TestLegacySidecarInvalidArchivesRemainRetryableAndReadOnly(t *testing.T) { if err := json.Unmarshal(persistedRaw, &persisted); err != nil { t.Fatal(err) } - if persisted.AutoResearchTaskID != "" || persisted.ResearchMode != GoalResearchOff || persisted.StopCause != stopCauseLegacyArchive { + if persisted.AutoResearchTaskID != taskID || persisted.ResearchMode != GoalResearchOn || persisted.StopCause != stopCauseLegacyArchive { t.Fatalf("retry state = %+v", persisted) } archiveAfter, err := os.ReadFile(target) @@ -543,7 +590,7 @@ func TestExplicitLegacyGoalRetryNeverRunsArchivePathAsGoal(t *testing.T) { if err := json.Unmarshal(persistedRaw, &blocked); err != nil { t.Fatal(err) } - if blocked.Status != GoalStatusBlocked || blocked.StopCause != stopCauseLegacyArchive { + if blocked.Status != GoalStatusBlocked || blocked.StopCause != stopCauseLegacyArchive || blocked.AutoResearchTaskID != taskID || blocked.ResearchMode != GoalResearchOn { t.Fatalf("blocked sidecar = %+v", blocked) } if c.ResumeGoal() { diff --git a/internal/jobs/context_isolation_test.go b/internal/jobs/context_isolation_test.go new file mode 100644 index 0000000000..de9c839e7f --- /dev/null +++ b/internal/jobs/context_isolation_test.go @@ -0,0 +1,17 @@ +package jobs + +import ( + "context" + "testing" +) + +func TestWithoutManagerShadowsAncestorManager(t *testing.T) { + parent := WithManager(context.Background(), &Manager{}) + child := WithoutManager(parent) + if _, ok := FromContext(child); ok { + t.Fatal("child context inherited the parent Jobs manager") + } + if _, ok := FromContext(parent); !ok { + t.Fatal("parent Jobs manager was lost") + } +} diff --git a/internal/jobs/jobs.go b/internal/jobs/jobs.go index bfe75fbfd1..a5dcbd936a 100644 --- a/internal/jobs/jobs.go +++ b/internal/jobs/jobs.go @@ -1911,6 +1911,7 @@ func jobKey(parentSession, id string) string { type ctxKey struct{} type sessionCtxKey struct{} type jobCtxKey struct{} +type noManager struct{} // WithManager stamps ctx with the job manager so tools can reach it via // FromContext. The agent sets this on every tool call's context. @@ -1918,6 +1919,12 @@ func WithManager(ctx context.Context, m *Manager) context.Context { return context.WithValue(ctx, ctxKey{}, m) } +// WithoutManager shadows an ancestor manager. Child agents without an owned +// Jobs manager must not operate the parent's background jobs by inheritance. +func WithoutManager(ctx context.Context) context.Context { + return context.WithValue(ctx, ctxKey{}, noManager{}) +} + // FromContext returns the job manager set by the agent, if any. ok is false for a // plain context (headless tests, calls outside the run loop). func FromContext(ctx context.Context) (*Manager, bool) { diff --git a/internal/memory/queue.go b/internal/memory/queue.go index 36db70a881..911258edb4 100644 --- a/internal/memory/queue.go +++ b/internal/memory/queue.go @@ -17,12 +17,19 @@ type autoMemoryWriteClaimer interface { } type queueKey struct{} +type noQueue struct{} // WithQueue stamps q onto ctx for the remember/forget tools to find. func WithQueue(ctx context.Context, q Queue) context.Context { return context.WithValue(ctx, queueKey{}, q) } +// WithoutQueue shadows an ancestor queue. Child agents may persist memory but +// must not inject turn-tail notes into the parent's live conversation. +func WithoutQueue(ctx context.Context) context.Context { + return context.WithValue(ctx, queueKey{}, noQueue{}) +} + // QueueFromContext returns the memory queue the agent stamped, if any. func QueueFromContext(ctx context.Context) (Queue, bool) { q, ok := ctx.Value(queueKey{}).(Queue) diff --git a/internal/memory/queue_context_test.go b/internal/memory/queue_context_test.go new file mode 100644 index 0000000000..0fc928c17c --- /dev/null +++ b/internal/memory/queue_context_test.go @@ -0,0 +1,21 @@ +package memory + +import ( + "context" + "testing" +) + +type queueContextProbe struct{} + +func (queueContextProbe) QueueMemory(string) {} + +func TestWithoutQueueShadowsAncestorQueue(t *testing.T) { + parent := WithQueue(context.Background(), queueContextProbe{}) + child := WithoutQueue(parent) + if _, ok := QueueFromContext(child); ok { + t.Fatal("child context inherited the parent memory queue") + } + if _, ok := QueueFromContext(parent); !ok { + t.Fatal("parent memory queue was lost") + } +} diff --git a/internal/tool/builtin/bgjobs.go b/internal/tool/builtin/bgjobs.go index 226bd75e2c..1f1d9edda3 100644 --- a/internal/tool/builtin/bgjobs.go +++ b/internal/tool/builtin/bgjobs.go @@ -42,6 +42,11 @@ func (bashOutput) Schema() json.RawMessage { func (bashOutput) ReadOnly() bool { return true } +func (bashOutput) ProviderVisible(ctx context.Context) bool { + _, ok := jobs.FromContext(ctx) + return ok +} + func (bashOutput) Execute(ctx context.Context, args json.RawMessage) (string, error) { var p struct { JobID string `json:"job_id"` @@ -109,6 +114,11 @@ func (killShell) Schema() json.RawMessage { func (killShell) ReadOnly() bool { return false } +func (killShell) ProviderVisible(ctx context.Context) bool { + _, ok := jobs.FromContext(ctx) + return ok +} + func (killShell) Execute(ctx context.Context, args json.RawMessage) (string, error) { var p struct { JobID string `json:"job_id"` @@ -145,6 +155,11 @@ func (waitJob) Schema() json.RawMessage { func (waitJob) ReadOnly() bool { return true } +func (waitJob) ProviderVisible(ctx context.Context) bool { + _, ok := jobs.FromContext(ctx) + return ok +} + func (waitJob) Execute(ctx context.Context, args json.RawMessage) (string, error) { var p struct { JobIDs []string `json:"job_ids"` diff --git a/internal/tool/builtin/completestep.go b/internal/tool/builtin/completestep.go index c9704e0867..20f032014e 100644 --- a/internal/tool/builtin/completestep.go +++ b/internal/tool/builtin/completestep.go @@ -9,6 +9,7 @@ import ( "reasonix/internal/evidence" "reasonix/internal/instruction" + "reasonix/internal/planmode" "reasonix/internal/provider" "reasonix/internal/tool" ) @@ -80,6 +81,12 @@ func (completeStep) Schema() json.RawMessage { // effect), so it never needs approval and stays available alongside todo_write. func (completeStep) ReadOnly() bool { return true } +// complete_step signs off execution work and is unavailable during planning. +// The host Plan gate remains authoritative for stale or hallucinated calls. +func (completeStep) ProviderVisible(ctx context.Context) bool { + return !planmode.Active(ctx) +} + // PlanModeSafe reports false: although complete_step is read-only, it signs off a // completed execution step, which is meaningful only after plan approval — not // during planning. This explicit phase opt-out is the Plan gate's enforced diff --git a/internal/tool/builtin/contextual_visibility_test.go b/internal/tool/builtin/contextual_visibility_test.go new file mode 100644 index 0000000000..d381350dee --- /dev/null +++ b/internal/tool/builtin/contextual_visibility_test.go @@ -0,0 +1,48 @@ +package builtin + +import ( + "context" + "testing" + + "reasonix/internal/jobs" + "reasonix/internal/planmode" + "reasonix/internal/tool" +) + +type visibilityRecorder struct{} + +func (visibilityRecorder) RecordGoalReport(tool.GoalReport) (string, error) { return "", nil } + +func TestContextualBuiltinVisibilityFollowsOwningContext(t *testing.T) { + goal, _ := tool.LookupBuiltin("update_goal") + step, _ := tool.LookupBuiltin("complete_step") + jobNames := []string{"bash_output", "wait", "kill_shell"} + + if goal.(tool.ContextualTool).ProviderVisible(context.Background()) { + t.Fatal("update_goal visible without a Goal recorder") + } + if !goal.(tool.ContextualTool).ProviderVisible(tool.WithGoalTurnRecorder(context.Background(), visibilityRecorder{})) { + t.Fatal("update_goal hidden during an active Goal turn") + } + if !step.(tool.ContextualTool).ProviderVisible(context.Background()) { + t.Fatal("complete_step hidden outside Plan mode") + } + if step.(tool.ContextualTool).ProviderVisible(planmode.WithActive(context.Background(), true)) { + t.Fatal("complete_step visible during Plan mode") + } + for _, name := range jobNames { + t.Run(name, func(t *testing.T) { + candidate, ok := tool.LookupBuiltin(name) + if !ok { + t.Fatal("missing builtin") + } + contextual := candidate.(tool.ContextualTool) + if contextual.ProviderVisible(context.Background()) { + t.Fatal("job tool visible without a manager") + } + if !contextual.ProviderVisible(jobs.WithManager(context.Background(), &jobs.Manager{})) { + t.Fatal("job tool hidden with an owned manager") + } + }) + } +} diff --git a/internal/tool/builtin/updategoal.go b/internal/tool/builtin/updategoal.go index d38635bfb8..7c67cb1595 100644 --- a/internal/tool/builtin/updategoal.go +++ b/internal/tool/builtin/updategoal.go @@ -43,6 +43,11 @@ func (updateGoal) Schema() json.RawMessage { // tool permissions or bypass sandbox policy. func (updateGoal) ReadOnly() bool { return true } +func (updateGoal) ProviderVisible(ctx context.Context) bool { + _, ok := tool.GoalTurnRecorderFromContext(ctx) + return ok +} + // PlanModeSafe reports true: the tool is read-only host bookkeeping, and // outside an active goal turn its Execute fails closed anyway. func (updateGoal) PlanModeSafe() bool { return true } diff --git a/internal/tool/tool.go b/internal/tool/tool.go index 90512f95d5..2b1720d8cf 100644 --- a/internal/tool/tool.go +++ b/internal/tool/tool.go @@ -33,6 +33,14 @@ type Tool interface { ReadOnly() bool } +// ContextualTool is an execution-time availability contract for tools whose +// ownership depends on the active workflow context. Provider schemas remain +// static for cache stability; the host must still consult this contract before +// permissions, hooks, leases, or Execute so stale transcripts fail closed. +type ContextualTool interface { + ProviderVisible(context.Context) bool +} + // Previewer is an optional capability a writer Tool may implement: given the // same raw JSON args Execute would receive, compute the file change the call // *would* make — without touching disk. ctx must be Execute's, so the preview @@ -540,3 +548,40 @@ func (r *Registry) Schemas() []provider.ToolSchema { } return out } + +// SchemasForContext returns the contextual projection for host metadata and +// diagnostics. Provider requests intentionally use Schemas so phase changes do +// not churn the cache-stable tool contract. +func (r *Registry) SchemasForContext(ctx context.Context) []provider.ToolSchema { + if ctx == nil { + ctx = context.Background() + } + r.mu.RLock() + names := append([]string(nil), r.order...) + entries := make(map[string]struct { + t Tool + data json.RawMessage + }, len(names)) + for _, name := range names { + if t := r.tools[name]; t != nil { + entries[name] = struct { + t Tool + data json.RawMessage + }{t: t, data: r.canon[name]} + } + } + r.mu.RUnlock() + sort.Strings(names) + out := make([]provider.ToolSchema, 0, len(names)) + for _, name := range names { + entry, ok := entries[name] + if !ok || entry.t == nil { + continue + } + if contextual, ok := entry.t.(ContextualTool); ok && !contextual.ProviderVisible(ctx) { + continue + } + out = append(out, provider.ToolSchema{Name: entry.t.Name(), Description: entry.t.Description(), Parameters: entry.data}) + } + return out +} From 5781174bad347a85a6846d9a3cc16d6639f280b4 Mon Sep 17 00:00:00 2001 From: SivanCola <32437197+SivanCola@users.noreply.github.com> Date: Sun, 9 Aug 2026 09:17:37 +0800 Subject: [PATCH 2/2] chore(repolint): baseline contextual boundary growth Problem: The repository lint baseline rejected the intentional size and complexity increase from the contextual execution gate, mixed-batch repair handling, child context isolation, and retryable Goal migration state. Root cause: The new owner-level safeguards add measured branches and lines to existing shared files, so the current baseline was lower than the post-fix repository metrics. Fix: Update only the affected file budgets and the aggregate complexity budget; unrelated baseline drift is left unchanged. Verification: go run ./tools/repolint git diff --check --- tools/repolint/baseline.json | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/tools/repolint/baseline.json b/tools/repolint/baseline.json index bb6ab421f3..b87d0df169 100644 --- a/tools/repolint/baseline.json +++ b/tools/repolint/baseline.json @@ -2,7 +2,7 @@ "limits": { "banner": 15, "commented-code": 0, - "complexity": 2079, + "complexity": 2081, "essay": 4141, "file-size": 110524, "function-size": 9191, @@ -432,7 +432,7 @@ "internal/agent/agent.go": { "complexity": 61, "essay": 109, - "file-size": 2718, + "file-size": 2743, "function-size": 124 }, "internal/agent/ask.go": { @@ -493,9 +493,9 @@ "function-size": 199 }, "internal/agent/execute_one.go": { - "complexity": 12, + "complexity": 13, "essay": 23, - "file-size": 115, + "file-size": 154, "function-size": 19 }, "internal/agent/extensions.go": { @@ -570,10 +570,10 @@ "essay": 1 }, "internal/agent/run_loop.go": { - "complexity": 5, + "complexity": 8, "essay": 45, - "file-size": 311, - "function-size": 46 + "file-size": 335, + "function-size": 57 }, "internal/agent/save.go": { "complexity": 51, @@ -1068,7 +1068,7 @@ "internal/control/controller.go": { "complexity": 11, "essay": 167, - "file-size": 5339, + "file-size": 5348, "function-size": 77, "narrative": 4 }, @@ -1094,7 +1094,7 @@ "complexity": 7, "essay": 20, "file-size": 318, - "function-size": 7 + "function-size": 11 }, "internal/control/goal_runtime_test.go": { "test-file-size": 14 @@ -1406,7 +1406,7 @@ }, "internal/jobs/jobs.go": { "essay": 42, - "file-size": 1264, + "file-size": 1271, "function-size": 12 }, "internal/jobs/jobs_extra_test.go": {