diff --git a/Directory.Build.props b/Directory.Build.props index bd4e1e39c..5e8abca61 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -10,43 +10,13 @@ true 0.24.3 beta.1 - Netclaw v0.24.2 — DwarfStar provider support, sub-agent operating rule inheritance, systemd update reliability, and reliability improvements - -**Features** - -* **DwarfStar (ds4) provider support** — Added DwarfStar as a supported provider via the OpenAI-compatible backend strategy. ([#1349](https://github.com/netclaw-dev/netclaw/pull/1349)) - -* **Sub-agents inherit embedded AGENTS.md operating rules** — Sub-agents spawned by a parent session now inherit the embedded `AGENTS.md` operating rules from the parent's identity context, ensuring consistent operating constraints across the agent hierarchy. ([#1490](https://github.com/netclaw-dev/netclaw/pull/1490)) - -* **Schema-aware meta argument coercion** — ChatGPT-trained models (Qwen3.6, etc.) were being hard-rejected for using non-canonical meta argument names (`TimeoutSeconds` vs `_timeout_seconds`). This adds schema-aware near-miss resolution that coercively maps model-supplied key variants to canonical names when no real tool parameter shadows them. Fixes "tool was NOT executed" errors that pushed models off tools and into sandbox-style output. ([#1470](https://github.com/netclaw-dev/netclaw/pull/1470)) - -* **Preserve per-call meta hints across tool-batch re-drive** — Tool calls awaiting approval that passivate and cold-recover were losing meta hints (`_timeout_seconds`, `_rationale`, `_background`), silently falling back to defaults. Long-running tools approved after session recovery now respect the original timeout. ([#1470](https://github.com/netclaw-dev/netclaw/pull/1470)) + Netclaw v0.24.3-beta.1 — Reminder duplicate-execution guard fix, autonomous session workspace write support **Bug Fixes** -* **Subagents: fixed premature kill by parent watchdog during approval waits** — Sub-agents waiting for human approval were incorrectly killed by the parent session's inactivity watchdog. Sub-agents now manage their own liveness end-to-end and are no longer subject to parent-side timeouts. ([#1481](https://github.com/netclaw-dev/netclaw/pull/1481)) - -* **Subagents: honor approval-pause in every tool-watchdog mode** — In WallClock mode (applied to every opaque tool), the human-approval pause was ignored: the wall-clock budget kept ticking through a human approval, killing healthy tools mid-wait when humans took longer than the budget. Prevents premature tool termination during human-in-the-loop approvals. ([#1473](https://github.com/netclaw-dev/netclaw/pull/1473)) - -* **Subagents: record spawn lifecycle in the session transcript** — Sub-agent actor logs went through Akka's async logger bridge and were lost because `SessionDiagnosticsContext.AsyncLocal` was gone. Spawns that failed early (guard rejection, watchdog kill, child-actor failure) left the session transcript completely silent. Parent-side breadcrumbs now log spawn lifecycle under the parent session scope. ([#1468](https://github.com/netclaw-dev/netclaw/pull/1468)) - -* **Skills: block agent mutations to server-feed skill directories** — Agents could freely edit/delete/write files into `.server-feeds/` skill directories via both `skill_manage` and direct file-write tools. Changes were silently overwritten on the next sync cycle. Two enforcement layers added: `skill_manage` guard + `ToolPathPolicy` write-deny for server-feed paths. Prevents wasted agent effort and misleading session state from server-feed mutations. ([#1466](https://github.com/netclaw-dev/netclaw/pull/1466)) - -* **Providers: refresh inference OAuth tokens** — OpenAI Copilot and other inference providers with OAuth tokens now refresh tokens at runtime rather than relying on stale tokens. Includes Copilot probe-time token refresh. Prevents authentication failures during long-running sessions when tokens expire. ([#1465](https://github.com/netclaw-dev/netclaw/pull/1465)) - -* **`netclaw update` no longer crash-loops systemd on Linux** — On Linux hosts with Netclaw installed as a systemd `--user` service, `netclaw update` was restarting the daemon as a detached process (bypassing systemd). This left the still-enabled systemd unit in a permanent crash-loop — every 5 seconds it tried to start, failed because the detached daemon held the exclusive lock file, and retried indefinitely (restart counters in the tens of thousands). `netclaw update` now delegates start/stop to `systemctl --user` when an enabled user unit exists. ([#1469](https://github.com/netclaw-dev/netclaw/pull/1469)) - -**Performance** - -* **Optimize `LlmSessionActor` storage** — Only retain the most recent snapshot + last N messages in the journal, instead of storing full history. Reduces memory consumption for long sessions with many messages. ([#1464](https://github.com/netclaw-dev/netclaw/pull/1464)) - -**Dependency Updates** - -* **Bump MessagePack from 2.5.301 to 3.1.7** — Major version bump with serialization improvements. Please test your workflows after upgrading. ([#1420](https://github.com/netclaw-dev/netclaw/pull/1420)) - -* **Bump Termina from 0.14.0-beta3 to 0.14.0** — TUI framework promoted to stable; includes a fix for the MCP tool permissions page layout. ([#1480](https://github.com/netclaw-dev/netclaw/pull/1480), [#1483](https://github.com/netclaw-dev/netclaw/pull/1483)) +* **Reminders: release duplicate-execution guard when a Mode A reminder session wedges** — When a Mode A reminder session wedged, the duplicate-execution guard was never released, blocking subsequent executions. The guard is now properly released when a session wedges. ([#1492](https://github.com/netclaw-dev/netclaw/pull/1500)) -* **Bump Anthropic SDK** — 12.29.1 → 12.30.0 (semver-minor). ([#1460](https://github.com/netclaw-dev/netclaw/pull/1460)) +* **Tools: allow autonomous sessions to write the workspace directory** — Autonomous sessions were blocked from writing to the workspace directory. This fix grants them write access. ([#1493](https://github.com/netclaw-dev/netclaw/pull/1498)) net10.0 @@ -64,4 +34,4 @@ - \ No newline at end of file + diff --git a/src/Netclaw.Actors/Sessions/LlmSessionActor.cs b/src/Netclaw.Actors/Sessions/LlmSessionActor.cs index aa34da912..e9d7c335f 100644 --- a/src/Netclaw.Actors/Sessions/LlmSessionActor.cs +++ b/src/Netclaw.Actors/Sessions/LlmSessionActor.cs @@ -88,7 +88,6 @@ public sealed class LlmSessionActor : ReceivePersistentActor, IWithTimers private MessageSource? _currentTurnSource; private TurnContext? _currentTurnContext; private bool _processingStateActive; - private ApprovalTurnState _approvalTurnState = ApprovalTurnState.None; private readonly ToolRegistry? _fullRegistry; private readonly ToolAccessPolicy? _toolAccessPolicy; private readonly TrustContextDeriver? _trustContextDeriver; @@ -263,7 +262,7 @@ public LlmSessionActor( ApplyTurnRecorded(evt); _pendingToolInteractions.Clear(); _resolvedToolApprovals.Clear(); - ClearApprovalTurnState(); + ClearCurrentTurnContext(); ClearActiveToolBatchTracking(); }); Recover(evt => _state = _state.Apply(evt)); @@ -273,7 +272,7 @@ public LlmSessionActor( _state = _state.Apply(evt); _pendingToolInteractions.Clear(); _resolvedToolApprovals.Clear(); - ClearApprovalTurnState(); + ClearCurrentTurnContext(); ClearActiveToolBatchTracking(); }); Recover(ApplyToolBatchStarted); @@ -990,7 +989,6 @@ private void HandleToolExecutionCompleted(ToolExecutionCompleted msg) _resolvedToolApprovals.Clear(); TurnLog().Info("turn_tool_execution_complete iteration={Iteration} callCount={CallCount} max={Max} resultCount={ResultCount}", _turnState.ToolIterationCount, _turnState.ToolCallCount, _config.MaxToolIterationsPerTurn, msg.ToolResults.Count); - MarkApprovalRunningAfterRedrive(); FireLlmCall(); } @@ -1376,7 +1374,7 @@ private void DrainBufferOrReady() } else { - ClearApprovalTurnState(); + ClearCurrentTurnContext(); TransitionTo(SessionPhase.Ready); } @@ -2118,7 +2116,7 @@ private void DrainBufferedMessagesOrBecomeReady() return; } - ClearApprovalTurnState(); + ClearCurrentTurnContext(); TransitionTo(SessionPhase.Ready); } @@ -2206,8 +2204,6 @@ private void HandleIncomingUserMessage(SendUserMessage cmd) // API, which would otherwise wedge every subsequent turn. if (_pendingToolInteractions.Count > 0) { - if (_approvalTurnState is WaitingApprovalTurn waiting) - _approvalTurnState = new AbandoningApprovalTurn(waiting.Context, "superseded_by_new_message"); var abandoned = BuildToolBatchAbandonedEvent(); Persist(abandoned, evt => { @@ -2251,7 +2247,6 @@ private void ContinueIncomingUserMessage(SendUserMessage cmd) _sessionId, _activeTurnId ?? new Protocol.TurnId(IdGen.ShortId()), cmd.Source); - _approvalTurnState = new RunningApprovalTurn(_currentTurnContext); _currentTrustContext = _trustContextDeriver?.DeriveFromTurnContext(_currentTurnContext); PersistAdoptedContextIfNeeded(cmd.Source); @@ -3343,8 +3338,10 @@ private void ApplyToolApprovalRequested(ToolApprovalRequested evt, bool persistA evt.Candidates); _resolvedToolApprovals.Remove(evt.CallId); + // Restore the parked turn context so a later re-drive authorizes the + // approval at the audience/boundary the request was originally made under. if (persistApprovalState && turnContext is not null) - RecordWaitingApprovalState(turnContext, evt.CallId, recovered: _phase.Current == SessionPhase.Recovering); + _currentTurnContext = turnContext; else if (persistApprovalState && restoreFailure is not null) _log.Warning( "Approval request {CallId} could not restore turn context: {Reason}", @@ -3352,37 +3349,7 @@ private void ApplyToolApprovalRequested(ToolApprovalRequested evt, bool persistA restoreFailure); } - private void RecordWaitingApprovalState(TurnContext context, string callId, bool recovered) - { - var pendingCallIds = _approvalTurnState is WaitingApprovalTurn waiting - ? new HashSet(waiting.PendingCallIds, StringComparer.Ordinal) - : new HashSet(StringComparer.Ordinal); - pendingCallIds.Add(callId); - - _currentTurnContext = context; - _approvalTurnState = new WaitingApprovalTurn(context, pendingCallIds, recovered); - } - - private void MarkApprovalRedrive(PendingToolInteraction pending, string callId) - { - if (pending.TurnContext is null) - return; - - _currentTurnContext = pending.TurnContext; - _approvalTurnState = new RedrivingApprovalTurn(pending.TurnContext, callId); - } - - private void MarkApprovalRunningAfterRedrive() - { - if (_approvalTurnState is RedrivingApprovalTurn redriving) - _approvalTurnState = new RunningApprovalTurn(redriving.Context); - } - - private void ClearApprovalTurnState() - { - _approvalTurnState = ApprovalTurnState.None; - _currentTurnContext = null; - } + private void ClearCurrentTurnContext() => _currentTurnContext = null; private void ApplyToolApprovalResolved(ToolApprovalResolved evt) { @@ -3391,12 +3358,7 @@ private void ApplyToolApprovalResolved(ToolApprovalResolved evt) : ApprovalDecision.Denied; if (_pendingToolInteractions.Remove(evt.CallId, out var pending)) - { _resolvedToolApprovals[evt.CallId] = new ResolvedToolApproval(pending, decision); - - if (_pendingToolInteractions.Count == 0 && pending.TurnContext is not null) - _approvalTurnState = new RunningApprovalTurn(pending.TurnContext); - } } private void ApplyToolBatchAbandoned(ToolBatchAbandoned evt) @@ -3410,7 +3372,7 @@ private void ApplyToolBatchAbandoned(ToolBatchAbandoned evt) }); _pendingToolInteractions.Clear(); _resolvedToolApprovals.Clear(); - ClearApprovalTurnState(); + ClearCurrentTurnContext(); ClearActiveToolBatchTracking(); } @@ -4156,7 +4118,6 @@ private bool RedriveToolBatchForApproval( _currentTurnContext = turnContext; _currentTrustContext = _trustContextDeriver?.DeriveFromTurnContext(turnContext); BindTurnTelemetry(turnContext); - MarkApprovalRedrive(pending, callId); TransitionTo(SessionPhase.Processing); DispatchToolBatch( @@ -4208,7 +4169,7 @@ private void FailCurrentTurn(string errorMessage, Exception cause, ErrorCategory _deliveryRetry.Clear(); _pendingToolInteractions.Clear(); _resolvedToolApprovals.Clear(); - ClearApprovalTurnState(); + ClearCurrentTurnContext(); _state = _state.AddErrorReply(errorMessage); var correlationId = Guid.NewGuid(); @@ -4531,7 +4492,6 @@ private void CompleteToolBatch(int resultCount) ClearActiveToolBatchTracking(); TurnLog().Info("turn_tool_execution_complete iteration={Iteration} callCount={CallCount} max={Max} resultCount={ResultCount}", _turnState.ToolIterationCount, _turnState.ToolCallCount, _config.MaxToolIterationsPerTurn, resultCount); - MarkApprovalRunningAfterRedrive(); FireLlmCall(); } diff --git a/src/Netclaw.Actors/Sessions/ToolApprovalState.cs b/src/Netclaw.Actors/Sessions/ToolApprovalState.cs index 282680f29..83620b2e4 100644 --- a/src/Netclaw.Actors/Sessions/ToolApprovalState.cs +++ b/src/Netclaw.Actors/Sessions/ToolApprovalState.cs @@ -48,24 +48,6 @@ internal sealed record ToolInteractionRequestDispatch( SessionProtocol.ToolInteractionRequest Request, bool PersistApprovalState) : INoSerializationVerificationNeeded; -internal abstract record ApprovalTurnState : INoSerializationVerificationNeeded -{ - public static ApprovalTurnState None { get; } = new NoActiveApprovalTurn(); -} - -internal sealed record NoActiveApprovalTurn : ApprovalTurnState; - -internal sealed record RunningApprovalTurn(TurnContext Context) : ApprovalTurnState; - -internal sealed record WaitingApprovalTurn( - TurnContext Context, - ISet PendingCallIds, - bool Recovered) : ApprovalTurnState; - -internal sealed record RedrivingApprovalTurn(TurnContext Context, string CallId) : ApprovalTurnState; - -internal sealed record AbandoningApprovalTurn(TurnContext Context, string Reason) : ApprovalTurnState; - internal sealed record ApprovalRedrivePlan( IReadOnlyDictionary>? OneTimeApprovalPreSeed, IReadOnlyDictionary? DecisionOverride);