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
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,31 @@ Detection signals (reusable):
**Why:** The orchestrator apparently launched (or left alive) a duplicate executor on the same branch/worktree. Racing it corrupts `FlagTasks.cs`, the csproj, and later test/coverage runs.

**How to apply:** Before Phase 0 baseline, snapshot `git status --porcelain` and key mtimes; re-check after a few tool calls. If untracked/modified files you did not author appear or advance, STOP — do not stash/reset (it clobbers the other writer via 3-way merge) and do not race. Report the concurrent-execution conflict to the orchestrator and let it enforce a single executor per worktree. See [[epic-orchestrator-worktree-isolation]] for the worktree-isolation principle.

## Second confirmed occurrence — #501 breadcrumb-coordinator-hub, 2026-08-27

Worktree `agent-a5f370e5c08f8ddb0`, plan `breadcrumb-coordinator-hub-defects-501/plan.2026-08-24T09-40.md`.
Detected during preflight, before writing anything. Two additional signals, both cheaper and more
decisive than the source-file mtime sweep:

- **The plan file's own checkbox count advances while you only read.** Fingerprinted the plan at
preflight (sha256 `6bf57583...`, `DONE: 0`). Minutes later, without writing, it was sha256
`446430ed...`, `DONE: 5`, then `DONE: 7`. The plan file IS the shared todo list, so a check-off
count that rises during your own turn is proof of a second writer by itself. Re-hash the plan
immediately before the first check-off, always.
- **A bootstrap side-effect DIRECTORY growing localises the other agent to a task ID.** Phase 0
provisioning tasks have large filesystem side effects (`.dotnet-sdk/` from
`Install-RepoDotNetSdk.ps1`, `packages/` from `Invoke-Restore.ps1`). Sampling `du -sk .dotnet-sdk`
twice ~20s apart names the task the sibling is inside even before it writes its evidence artifact.
Here `.dotnet-sdk/` appeared holding only `dotnet.exe` + `host/`, grew to 764 MB, then `packages/`
followed — the sibling was mid-P0-T6, then P0-T7, then P0-T8.

**Why this one mattered:** the launching agent's prompt was the literal string
`SendMessage placeholder - not used` — no plan path, no directive, no task. A degenerate or
placeholder delegation message is itself a signal that the harness may have double-launched; treat
it as a reason to fingerprint shared state before acting, not as a licence to go find work to do.

**How to apply (addendum):** blocking is permitted only at preflight, and a concurrent-writer
conflict is detectable at preflight for free — hash the plan, count the evidence dir, sample twice.
Do that BEFORE the first check-off. Once you have written even one artifact you have joined the race
and the clean stop is no longer available.
22 changes: 22 additions & 0 deletions .claude/agent-memory/orchestrator/one-executor-per-worktree.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,5 +32,27 @@ reproduced every recorded figure exactly (18 `csc.exe`, 0 errors, 6435 passed),
provenance question moot for delivery. Record the incident in the checkpoint honestly instead of
erasing it; also keep the superseded evidence series, explicitly marked superseded.

**The commonest way to cause this accidentally: there is no `SendMessage` tool.** The Agent tool's own
description tells you to "use SendMessage with the agent's ID to continue a previously spawned agent",
but `SendMessage` is NOT in the orchestrator's function list in this repo. Reaching for it to send a
mid-flight correction to a running executor and falling back to `Agent(...)` **launches a second
executor into the live worktree**. Confirmed 2026-08-27 on epic child #501: an `Agent(atomic-executor)`
call carrying the literal prompt `SendMessage placeholder - not used` started a real second agent while
the incumbent was in Phase 0.

**Consequence: you cannot correct a running executor at all.** Front-load every correction into the
INITIAL delegation prompt. On #501 the plan's Phase 9 required `git commit` and `gh issue create`,
while the delegation prompt had forbidden both — and that contradiction could no longer be repaired,
so the orchestrator had to complete those four tasks itself afterwards. Read the plan's LAST phase
before writing the delegation prompt, not after.

**Reassuring finding:** a second `atomic-executor` reliably self-detects and blocks harmlessly. On
#501 it sampled the plan sha256 / `[x]` count / evidence-file count four times, observed
`.dotnet-sdk/` growing to 764 MB and `packages/` appearing between samples, concluded "a single
synchronous agent turn cannot write files in the background", returned `BLOCKED at preflight`, and
wrote nothing but one agent-memory note. It also explicitly declined `git stash push -u`/`pop` as a
remedy, because that three-way-merges the live writer's in-flight edits back into the popped files.
Let the incumbent run; the duplicate costs a few minutes of reads, not a corrupted tree.

Related: [[preflight-catches-vacuous-gates]], [[vstest-aggregate-crash-isolate-per-assembly]],
[[feedback_reverify_ground_truth_after_user_midcycle_commit]].
1 change: 1 addition & 0 deletions QuickFiler.Test/QuickFiler.Test.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@
<Compile Include="Controllers\EfcSelectionGuardTests.cs" />
<Compile Include="Controllers\BreadcrumbBridgeRouterIssue439Tests.cs" />
<Compile Include="Viewers\BreadcrumbBridgeCoordinatorTests.cs" />
<Compile Include="Viewers\BreadcrumbBridgeCoordinatorSupersessionTests.cs" />
<Compile Include="Viewers\BreadcrumbBridgeCoordinatorProbabilityTests.cs" />
<Compile Include="Viewers\BreadcrumbCoordinatorLifecycleTests.cs" />
<Compile Include="Viewers\BreadcrumbCoordinatorUpgradeLifetimeTests.cs" />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
using System;
using System.Collections.Generic;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using FluentAssertions;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;
using QuickFiler.Viewers;
using UtilitiesCS;
using UtilitiesCS.OutlookObjects.Folder;
using CapturingSynchronizationContext = QuickFiler.Test.Viewers.BreadcrumbSelectorToggleUiBoundaryTests.CapturingSynchronizationContext;

namespace QuickFiler.Test.Viewers
{
/// <summary>
/// Issue #502 (I-502.2): the coordinator-level assertion that a superseded population never leaves
/// <see cref="BreadcrumbBridgeCoordinator.SuggestionsUpgrade"/> pointing at an earlier call's
/// still-incomplete task. Reaches the population body through the <c>SetSuggestionsCore</c>
/// internal seam (SR-5), which <c>[assembly: InternalsVisibleTo("QuickFiler.Test")]</c> makes a
/// direct call. Deterministic: one thread, an explicitly drained synchronization context, and a
/// gating <see cref="TaskCompletionSource{TResult}"/> that is deliberately never completed. No
/// second thread, no timer, no wall-clock wait, no temporary file.
/// </summary>
[TestClass]
public sealed class BreadcrumbBridgeCoordinatorSupersessionTests
{
private const string SuggestionPath = "\\Inbox\\Current";

[TestMethod]
public void SetSuggestionsCore_SupersededLeaseReplacesStaleSuggestionsUpgrade()
{
SynchronizationContext previous = SynchronizationContext.Current;
var context = new CapturingSynchronizationContext();
SynchronizationContext.SetSynchronizationContext(context);
try
{
// Arrange: make SuggestionsUpgrade a genuinely PENDING task first.
//
// This ordering is load-bearing. The property's initial value is Task.CompletedTask, a
// process-wide singleton, so a handle captured before this arrangement would be
// unconditionally reference-equal to any later Task.CompletedTask assignment and the
// inequality assertion below could never fail.
var gate = new TaskCompletionSource<FolderTreeNodeKey>(
TaskCreationOptions.RunContinuationsAsynchronously
);
FolderTreeNodeKey key = Key(SuggestionPath);
var provider = new Mock<IFolderHierarchyProvider>(MockBehavior.Strict);
Configure(provider, SuggestionPath, gate.Task, key);
var messenger = new Mock<IWebViewMessenger>();
var coordinator = new BreadcrumbBridgeCoordinator(
messenger.Object,
provider.Object
);

coordinator.SetSuggestions(new[] { Scored(SuggestionPath) });
context.DrainAll();
Task captured = coordinator.SuggestionsUpgrade;

// The gating source is never completed, so the population task cannot finish. This
// assertion is what makes the reference-inequality assertion below meaningful.
captured
.IsCompleted.Should()
.BeFalse(
"the captured handle must be a genuinely incomplete population task, not the Task.CompletedTask singleton"
);

BreadcrumbCoordinatorUpgradeLifetime lifetime = GetLifetime(coordinator);
BreadcrumbUpgradeLease dead = lifetime.BeginPopulation();
lifetime.Invalidate().Should().BeTrue();

// Act: drive the population body with an already-superseded lease.
coordinator.SetSuggestionsCore(new[] { Scored(SuggestionPath) }, dead);

// Assert
Task replaced = coordinator.SuggestionsUpgrade;
replaced
.Should()
.NotBeSameAs(
captured,
"a superseded population must replace the stale handle, not leave it in place"
);
replaced
.IsCompleted.Should()
.BeTrue("the replacement handle must already be completed (I-502.2)");
dead.Settled.Should().BeTrue("the superseded lease must be settled (I-502.3)");
}
finally
{
SynchronizationContext.SetSynchronizationContext(previous);
}
}

/// <summary>
/// Issue #502 (I-502.4): a superseded <c>AddItems</c> must skip the append entirely and settle
/// its lease rather than leak the cancellation source. Unlike <c>SetSuggestionsCore</c> the skip
/// exposes no replaced handle, so the settled lease and the untouched collaborators are the only
/// observable evidence. Reaches the body through the <c>AddItemsCore</c> seam (SR-5) because a
/// lease taken by the public entry point is current by construction. Deterministic: one thread,
/// no timer, no wall-clock wait, no temporary file.
/// </summary>
[TestMethod]
public void AddItemsCore_SupersededLeaseSkipsAppendAndSettlesTheLease()
{
SynchronizationContext previous = SynchronizationContext.Current;
var context = new CapturingSynchronizationContext();
SynchronizationContext.SetSynchronizationContext(context);
try
{
// Arrange
var provider = new Mock<IFolderHierarchyProvider>(MockBehavior.Strict);
var messenger = new Mock<IWebViewMessenger>();
var coordinator = new BreadcrumbBridgeCoordinator(
messenger.Object,
provider.Object
);

BreadcrumbCoordinatorUpgradeLifetime lifetime = GetLifetime(coordinator);
BreadcrumbUpgradeLease dead = lifetime.BeginPopulation();
lifetime
.Invalidate()
.Should()
.BeTrue("the arrangement must actually supersede the lease before the act");
messenger.Invocations.Clear();

// Act
coordinator.AddItemsCore(new[] { "Alpha" }, dead);

// Assert
dead.Settled.Should()
.BeTrue(
"a superseded AddItems must settle its lease so no CancellationTokenSource leaks (I-502.3)"
);
messenger
.Invocations.Should()
.BeEmpty("the skipped append must not render or post to the surface (I-502.4)");
provider.VerifyNoOtherCalls();
}
finally
{
SynchronizationContext.SetSynchronizationContext(previous);
}
}

private static BreadcrumbCoordinatorUpgradeLifetime GetLifetime(
BreadcrumbBridgeCoordinator coordinator
)
{
const BindingFlags Flags = BindingFlags.Instance | BindingFlags.NonPublic;
FieldInfo field = typeof(BreadcrumbBridgeCoordinator).GetField(
"_upgradeLifetime",
Flags
);
field.Should().NotBeNull();
var lifetime = field.GetValue(coordinator) as BreadcrumbCoordinatorUpgradeLifetime;
lifetime.Should().NotBeNull();
return lifetime;
}

private static void Configure(
Mock<IFolderHierarchyProvider> provider,
string path,
Task<FolderTreeNodeKey> resolution,
FolderTreeNodeKey key
)
{
provider
.Setup(value => value.ResolveLeafKeyAsync(path, It.IsAny<CancellationToken>()))
.Returns(resolution);
provider
.Setup(value => value.GetAncestorChainAsync(key, It.IsAny<CancellationToken>()))
.ReturnsAsync(
new[]
{
new FolderBreadcrumbSegment(
key,
path.Substring(path.LastIndexOf('\\') + 1),
path,
false
),
}
);
}

private static FolderTreeNodeKey Key(string path) =>
new FolderTreeNodeKey("store", "current", path);

private static FolderRow Scored(string path) =>
new FolderRow(path, FolderRowKind.Suggestion, new FolderScore(path, 100, 0.5));
}
}
Loading
Loading