Skip to content
Closed
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
38 changes: 4 additions & 34 deletions Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -10,43 +10,13 @@
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<VersionPrefix>0.24.3</VersionPrefix>
<VersionSuffix>beta.1</VersionSuffix>
<PackageReleaseNotes>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))
<PackageReleaseNotes>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))</PackageReleaseNotes>
* **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))</PackageReleaseNotes>
</PropertyGroup>
<PropertyGroup>
<NetCoreTestVersion>net10.0</NetCoreTestVersion>
Expand All @@ -64,4 +34,4 @@
<ItemGroup>
<None Include="$(MSBuildThisFileDirectory)LICENSE" Pack="true" PackagePath="" Visible="false" />
</ItemGroup>
</Project>
</Project>
60 changes: 10 additions & 50 deletions src/Netclaw.Actors/Sessions/LlmSessionActor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -263,7 +262,7 @@ public LlmSessionActor(
ApplyTurnRecorded(evt);
_pendingToolInteractions.Clear();
_resolvedToolApprovals.Clear();
ClearApprovalTurnState();
ClearCurrentTurnContext();
ClearActiveToolBatchTracking();
});
Recover<SessionTitleSet>(evt => _state = _state.Apply(evt));
Expand All @@ -273,7 +272,7 @@ public LlmSessionActor(
_state = _state.Apply(evt);
_pendingToolInteractions.Clear();
_resolvedToolApprovals.Clear();
ClearApprovalTurnState();
ClearCurrentTurnContext();
ClearActiveToolBatchTracking();
});
Recover<ToolBatchStarted>(ApplyToolBatchStarted);
Expand Down Expand Up @@ -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();
}

Expand Down Expand Up @@ -1376,7 +1374,7 @@ private void DrainBufferOrReady()
}
else
{
ClearApprovalTurnState();
ClearCurrentTurnContext();
TransitionTo(SessionPhase.Ready);
}

Expand Down Expand Up @@ -2118,7 +2116,7 @@ private void DrainBufferedMessagesOrBecomeReady()
return;
}

ClearApprovalTurnState();
ClearCurrentTurnContext();
TransitionTo(SessionPhase.Ready);
}

Expand Down Expand Up @@ -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 =>
{
Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -3343,46 +3338,18 @@ 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}",
evt.CallId,
restoreFailure);
}

private void RecordWaitingApprovalState(TurnContext context, string callId, bool recovered)
{
var pendingCallIds = _approvalTurnState is WaitingApprovalTurn waiting
? new HashSet<string>(waiting.PendingCallIds, StringComparer.Ordinal)
: new HashSet<string>(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)
{
Expand All @@ -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)
Expand All @@ -3410,7 +3372,7 @@ private void ApplyToolBatchAbandoned(ToolBatchAbandoned evt)
});
_pendingToolInteractions.Clear();
_resolvedToolApprovals.Clear();
ClearApprovalTurnState();
ClearCurrentTurnContext();
ClearActiveToolBatchTracking();
}

Expand Down Expand Up @@ -4156,7 +4118,6 @@ private bool RedriveToolBatchForApproval(
_currentTurnContext = turnContext;
_currentTrustContext = _trustContextDeriver?.DeriveFromTurnContext(turnContext);
BindTurnTelemetry(turnContext);
MarkApprovalRedrive(pending, callId);

TransitionTo(SessionPhase.Processing);
DispatchToolBatch(
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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();
}

Expand Down
18 changes: 0 additions & 18 deletions src/Netclaw.Actors/Sessions/ToolApprovalState.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<string> 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<string, IReadOnlyList<string>>? OneTimeApprovalPreSeed,
IReadOnlyDictionary<string, ApprovalDecision>? DecisionOverride);
Expand Down
Loading