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);