Every emit site hands FlowTraceCollector the same AuditEvent it hands the
writer, but redaction and payload capping lived inside the writers, which
return a NEW filtered record — so the inline copy that rides
ScriptCallResult.TraceEvents / RouteToCallResponse.TraceEvents was the RAW
event: no per-target SQL-parameter redaction, no header scrubbing, no 8 KB /
64 KB truncation. Live and Replay of the same execution could therefore show
different payloads, and the 500-event-only cap left the reply free to breach
the 4 MB gRPC receive limit — which would fail a real call, since the same
reply carries the script's return value.
The collector now applies the host IAuditRedactor itself (outside the lock;
the writers keep their own idempotent Apply) and enforces a byte budget
(DefaultMaxBytes = 1 MB over the redacted DetailsJson) alongside the event
cap, flagging Truncated when either trips. It moves to Commons
(Types/Debugging) so the central FlowDebugRunner can share it.
The redactor is a singleton, so ScriptRunLauncher resolves it from the ROOT
provider and keeps constructing the collector before the try — the traced
ScriptRun row is emitted from the catch blocks too, including the
shed-at-dequeue path that throws before the DI scope exists.
Claude-Session: https://claude.ai/code/session_01NcMG6xXdsJbmD2wYAgkK29
A method-entry Live run had no site-scope check at all. RunValueChangeAsync
calls EnforceSiteScope on its named instance, but a method run has no single
target site up front — the operator picks a METHOD, and where its script routes
is only known once it runs — so a site-scoped Deployer could reach any site the
method happened to route to.
The scope is therefore pushed into RouteHelper and enforced at route time:
- RouteHelper.WithSiteScope(IReadOnlySet<string>?) carries a set of site
IDENTIFIERS (null = unscoped, the state of every ordinary inbound HTTP
request; an EMPTY set permits nothing, the fail-closed outcome for a scoped
user whose sites no longer exist). Threaded through every With* copy and To().
- The gate lives in RouteTarget.ResolveSiteAsync — the single choke point that
Call, GetAttributes, WaitForAttribute and SetAttributes all pass through — so
it fires BEFORE any site traffic and throws the new RouteSiteScopeException.
- InboundScriptExecutor maps it to error code SITE_SCOPE_DENIED. The type is
InboundAPI's own (that assembly cannot reference ManagementService), and it
derives from InvalidOperationException so base-type callers are unaffected.
- FlowDebugRunner.RunMethodAsync resolves the user's PermittedSiteIds (numeric)
to identifiers via ISiteRepository and converts SITE_SCOPE_DENIED back to
SiteScopeViolationException (-> ManagementUnauthorized), AFTER writing the
inbound audit row so a denied attempt is still recorded against the operator.
Administrators and unscoped users get a null scope and are unaffected, exactly
as EnforceSiteScope treats them. Every contract change is a trailing optional
parameter. Mutation-checked: disabling the gate fails 3 of the 5 new RouteHelper
tests and 1 of the runner tests, while the in-scope/unscoped positives still pass.
Claude-Session: https://claude.ai/code/session_01NcMG6xXdsJbmD2wYAgkK29
Design D6 / §9 says a Live run is a Deployer act while Replay needs only
OperationalAudit, but the page and all three FlowDebug* commands were gated on
Deployer. The Audit Log and Execution Chain pages -- themselves OperationalAudit
-- show an "Open in Flow Debugger" drill-in that an auditor could not follow.
Implements the split:
- New `RequireFlowDebugger` policy over `FlowDebuggerAccessRoles`
{Deployer, Administrator, Viewer}; the page now carries it instead of
`RequireDeployment`.
- `FlowDebugReplayCommand` takes the same role set in the ManagementActor
matrix; `FlowDebugRunMethodCommand` / `FlowDebugRunValueChangeCommand` stay
DeployerOnly.
- The page resolves `_canLive` (Deployer) in OnInitializedAsync and, without it,
pins the mode to Replay, hides the entry-point and mode toggles, the Run
button and the Live pickers, honours only the `?executionId=` drill-in, and
leaves the parked-node Retry/Discard callbacks unset so those buttons never
render. `CanRun`, `SetMode(Live)` and `ActOnStepAsync` repeat the check so the
split does not rest on markup alone.
The NavMenu entry stays inside the Deployer-gated Deployment section -- auditors
arrive through the audit drill-ins, which is the point of the finding.
Claude-Session: https://claude.ai/code/session_01NcMG6xXdsJbmD2wYAgkK29
GetTraceEventsAsync re-implemented GetExecutionTreeAsync's execution-chain walk
as a second copy: the upward ParentExecutionId climb, the root MIN(OccurredAtUtc)
window, and the recursive Edges/Chain CTE with its DISTINCT-edges trick and
MAXRECURSION cap. Two walkers that had to stay in step on window slack, depth and
the edge distillation.
Extracted into three shared members:
- ChainAnchor (root execution id + root's first event timestamp, with the
Edges window predicate derived from it)
- ResolveChainAnchorAsync — phase 1 up-walk plus the root window read
- ChainCtePrefix / AddChainParameters — the Edges/Chain CTE prefix and its
@root/@winStart/@winEnd binding
Each walker appends its own projection to the prefix: GetExecutionTreeAsync the
ChainIds CTE plus the per-execution aggregate, GetTraceEventsAsync the bare id
union. Both keep OPTION (MAXRECURSION 32).
Pure refactor. The tree walker's generated SQL is byte-identical (verified by
diffing the composed command text against the pre-change literal); the trace
walker's differs only by the SQL comment lines it now inherits from the shared
prefix. Behaviour is pinned by the existing MSSQL-backed tests
(GetExecutionTree_MultiLevelChain / _StubParentNode / _RespectsMaxRecursion,
ExecutionTree_Bounds_EdgeScan_To_Root_Window, and both
AuditLogRepositoryTraceEventsTests) — 410 passed / 0 skipped before and after.
Claude-Session: https://claude.ai/code/session_01NcMG6xXdsJbmD2wYAgkK29
The Flow Debugger trace arm was recorded when a traced write was ISSUED, not
when the DCL acknowledged it. A write the device rejected (or that timed out)
left the arm standing for the whole TraceArmWindowSeconds (30 s), so the next
unrelated subscription update on that attribute was stamped with the debugger's
execution id and spawned a traced, parented run — the debugger drew a "device
echo" of a write that never landed.
The write's continuation runs off the actor's dispatcher, so it cannot touch
_traceArms directly; it Tells self a new DisarmTrace message instead, handled in
a Receive<>. The disarm is keyed on the writer's execution id, so a late failure
cannot steal an arm a newer traced write has since taken.
Claude-Session: https://claude.ai/code/session_01NcMG6xXdsJbmD2wYAgkK29
Full review of the Flow Debugger commit range (v1.0, v1.1 outbound calls, v1.2 follow-ups,
outcome-merge fix, two perf commits). Also records the exact 11 role-negative Playwright
tests that cannot pass on the login-disabled rig in the cluster-ops skill.
Claude-Session: https://claude.ai/code/session_01NcMG6xXdsJbmD2wYAgkK29
Measuring the tracking-store queue on the rig moved nothing: the DB-done -> Notify.Send
gap stayed at 108 ms. The cost was the audit half of CachedCallTelemetryForwarder:
SqliteAuditWriter.WriteAsync enqueues synchronously but returns the BATCH completion,
and the writer loop coalesces for FlushIntervalMs (50 ms) before it commits, so each
awaited audit row cost ~50 ms + fsync and a cached call awaited three (submit,
attempted, resolve) on the script thread. Every other script-path emitter already
fired-and-observed; only this forwarder awaited.
TryEmitAuditAsync now hands the row to the writer and observes the completion on a
fault-logging continuation (the FallbackAuditWriter ring-buffer fallback still runs
inside that task). Best-effort contract unchanged; per-store FIFO unchanged; nothing
joins the audit and tracking stores by commit order.
Rig, FlowOutboundDemo, 5-run medians: inbound 194 -> 25 ms, site script run 176 -> 17 ms,
CachedWrite submit->done 61 -> 11 ms, DB done -> Notify.Send 108 -> 0.1 ms. The tracking
queue stays (three fewer fsyncs per call, batched). Docs corrected to attribute the cost
to the flush wait, with the trap recorded in CLAUDE.md and Component-SiteRuntime.md.
Tests: ForwardAsync returns while the audit commit is still pending (never-completing
writer task) and still records tracking; a late commit fault is swallowed.
Claude-Session: https://claude.ai/code/session_01NcMG6xXdsJbmD2wYAgkK29
A Flow Debugger live run (execution 1130c01f) spent 108 of a 176 ms site script run
between the cached DB write completing and Notify.Send starting: the immediate-
completion telemetry awaited two autocommit SQLite transactions on the consolidated
LocalDb file (RecordAttemptAsync + RecordTerminalAsync), on the script thread, each a
WAL fsync on a Docker volume; the submit row was a third. The audit half of the same
forwarder was already a channel write, so the tracking half was the whole cost.
OperationTrackingStore is now a single-writer queue: Record*Async enqueue an ordered
work item and return; one background pump drains the bounded channel (8192, wait
backpressure - tracking never drops) and applies each batch (<=256) in ONE transaction,
falling back to per-row apply when a batch fails so a poison row cannot take its
neighbours. Preserved invariants: FIFO across enqueue/attempt/terminal; read-your-
writes (GetStatusAsync and ReadChangedSinceAsync barrier on every write queued before
them, skipped when idle) so Tracking.Status, the telemetry actor's status join and
central's PullSiteCalls never see a stale row; timestamps stamped when recorded;
PurgeTerminalAsync returns after the purge lands; DisposeAsync drains the queue before
closing; sync Dispose keeps its documented in-flight-writes-fail semantics. New public
FlushAsync() is the explicit barrier. Accepted: a queued-but-unapplied row is lost on a
hard crash (ms window); S&F buffer and audit rows are unaffected.
No interface, forwarder or ScriptRuntimeContext change. Tests: write returns while
another connection holds the DB write lock and lands once released (fails on the old
store: 31 s); reads see 100 ordered attempts + terminal; DisposeAsync drains 200
queued rows; write after dispose throws ObjectDisposed. Existing tests that poke the
SQLite file right after a store call now FlushAsync first.
Claude-Session: https://claude.ai/code/session_01NcMG6xXdsJbmD2wYAgkK29
Found in a browser Live run on the rig: the run drew API, DB and Notify nodes from
its inline rows; the 5 s pending-refresh re-read the tree through Replay before the
site's cached-call telemetry (12 s) had reached central, swapped the graph wholesale,
and the resolved DB node vanished (7 -> 6 steps) until a manual refresh. Polling had
already stopped because nothing was Pending.
MergeRefreshedGraph now unions the re-read rows into the run's known rows by EventId
(fresh copy wins), stores the union back on _raw and rebuilds through
FlowGraphBuilder, so a refresh only ever adds nodes. The union is exact: the inline
rows are the same AuditEvent objects the site writer persists (FlowTraceCollector).
Tests: new PendingRefresh_KeepsStepsTheLiveRunAlreadyHad_WhenReplayLagsBehindTelemetry
(fails on the old code). Two existing fixtures were internally inconsistent and only
passed under the old replace semantics: ParkedThenResolved built its Resolved outcome
with the Parked raw rows, and OutboundOutcome minted fresh event/correlation ids per
call, so no row could ever match across live and replay. Both now share rows per run.
Claude-Session: https://claude.ai/code/session_01NcMG6xXdsJbmD2wYAgkK29
Rig-only posture, set in the tracked docker/central-node-{a,b}/appsettings.Central.json
so it survives deploy.sh / container recreates (it used to be a throwaway compose
override on central-a alone). Both nodes so Traefik on :9000 is login-free
regardless of which node is active. POST /auth/login still answers 302, so the
Playwright fixture login is unaffected; LoginTests will fail on this rig by design.
Documented in the scadabridge-cluster-ops skill.
Claude-Session: https://claude.ai/code/session_01NcMG6xXdsJbmD2wYAgkK29
Notify.Send mints NotificationId as Guid "N" (no dashes) and the outbox parses it back into
the audit row's CorrelationId, so the Flow Debugger's Notify node linked and relayed a "D"
string the Notifications table never matches (found live: the report opened empty). The
link and RetryNotificationRequest now use "N", and the Notification report normalises any
Guid-shaped ?correlationId= to "N" so Audit Log and Flow Debugger callers both land.
Claude-Session: https://claude.ai/code/session_01NcMG6xXdsJbmD2wYAgkK29
An ephemeral inbound method (created and deleted through the CLI, with a never-taken
Route.To branch so prediction resolves a route target) is run live in a real browser through
the fixture's /auth/login; the reload test drives the OnAfterRenderAsync localStorage restore
and then proves the page is still interactive. Both fail on the pre-fix image at the
'Predicted' wait (the circuit deadlock) and pass on the fixed one.
Claude-Session: https://claude.ai/code/session_01NcMG6xXdsJbmD2wYAgkK29
The page wires FlowStepDetail's OnRetry/OnDiscard to IFlowDebugService.RetryOutboundAsync /
DiscardOutboundAsync behind the same site-scope re-check and confirm wording the Site Calls /
Notification report pages use, toasts the relay outcome, and on success re-polls Replay (same
5 s / 2 min loop) until the acted node leaves Parked. Also drops a duplicated ?correlationId=
test block that a concurrent index-only commit left in QueryStringDrillInTests.
Claude-Session: https://claude.ai/code/session_01NcMG6xXdsJbmD2wYAgkK29
FlowDebugService.PredictMethodAsync resolved route targets with a sync-over-async
GetAwaiter().GetResult() inside the predictor's synchronous resolver — on the circuit's
single-threaded renderer sync context that blocked the dispatcher forever the moment a
method whose script routes to a site script was chosen (every later click and JS-interop
reply queued behind it; the page looked inert with a healthy websocket and no error).
Misdiagnosed on 2026-09-06 as a login-disabled localStorage stall. Now a resolve-and-rewalk
loop: each pass records the targets it could not answer, they are awaited between passes,
and the walk converges once a pass asks for nothing new (bounded by MaxPredictionPasses).
Regression test drives the service under a single-thread pump sync context (fails in 10 s
on the old code, passes now) plus a nested Scripts.Call resolve-across-passes test.
Claude-Session: https://claude.ai/code/session_01NcMG6xXdsJbmD2wYAgkK29
Rig run 2026-09-06 (image built from this branch, containers recreated from
main's compose file): full drill A-H YES (active-central kill takeover 27 s,
rejoin as standby in 3 s), then with FLOW_ONLY=1:
H live+replay -> YES
live: YES root=8e628d0a-428b-48df-b937-f4c2ac631fc3 events=8 api=Delivered db=Delivered notify=Submitted
replay: YES root=8e628d0a-428b-48df-b937-f4c2ac631fc3 events=10 api=Delivered db=Delivered notify=Delivered
I outbound live+replay -> YES
Two rig facts the drill established: (1) with the DB reachable the site's
immediate-completion path resolves a CachedWrite INSIDE the run, so the DbCall
node is already Delivered inline (Submit/Attempted/Resolve share a tick — the
check now ranks statuses instead of taking list order); (2) cached-call
telemetry reaches central on its own ~20-30 s cadence, later than the 5 s audit
push, so replay needs the bounded retry. New FLOW_ONLY=1 switch runs H + I
without the notifier build or the docker kill.
Claude-Session: https://claude.ai/code/session_01NcMG6xXdsJbmD2wYAgkK29
After a successful Live run whose graph has Pending outbound steps the page
re-reads the tree through the existing Replay command every 5 s for up to
2 min (page constants), merging by replacing the graph and keeping the
selection by event id; never in Replay mode; cancelled by a new run, a mode
switch or Dispose. Status strip gains a Pending count, a 'Refresh outcomes'
button (both modes) and an 'outcomes still arriving' note while polling. The
run's own timing and the Completed chip are left alone — pending outbound
work is not a failed run. Plan 2026-09-06 D5.
Claude-Session: https://claude.ai/code/session_01NcMG6xXdsJbmD2wYAgkK29
A folded outbound step shows a Lifecycle tab (time · kind · status · node ·
error, oldest first; a Parked step adds the 'retry or discard from Site Calls /
Notification report' hint), replaces the never-populated Writes tab for those
steps, labels the Input tab Request / SQL + parameters / Subject + body by kind,
and 'Where this ran' names the System / Connection / List plus 'Delivered by'
(the terminal row's node). Pending/Parked outcome badges.
Claude-Session: https://claude.ai/code/session_01NcMG6xXdsJbmD2wYAgkK29
Four lanes (LaneCount 4; External sublabel = distinct targets, three then +n);
ApiCall/DbCall/Notify nodes carry a status pill (Ok/Pending/Parked/Failed —
pending pulses, reduced-motion aware) plus a sync/cached/notify mode; folded
lifecycles show '+4.1 s' submit→terminal wall time. Async edges are dashed
and routed through the inter-lane strip with a gutter tag; spawn edges use a
bolt marker. Predicted async edges (Task 6's PredictedEdge.Async) render the
same way. de-DE height pinned at 319.2 = (4×110+16)×0.7.
Claude-Session: https://claude.ai/code/session_01NcMG6xXdsJbmD2wYAgkK29
Review of Task 3: the sink invoke sat before WriteAsync/ForwardAsync inside the
best-effort try, so a throwing sink would have silently dropped the durable
row. GuardTraceSink wraps the delegate in every helper ctor (and Collect
guards EmitTrace); pinned by a throwing-sink test. ScriptRunLauncher no longer
allocates a collector for a traced run with no reply target (Nobody) — the
list would never be read. FlowTraceCollector.Complete pinned at unit level;
Snapshot documented as a non-closing peek.
Claude-Session: https://claude.ai/code/session_01NcMG6xXdsJbmD2wYAgkK29
InboundDatabaseHelper gains trailing optional auditWriter / nodeIdentity /
executionId / parentExecutionId / methodName / traceSink parameters; every
QuerySingle/Query/Execute writes one DbOutbound/DbWrite row (Delivered/Failed,
Target = connection name, Actor = 'api:{method}', DurationMs, SQL + parameters
as RequestSummary, Extra in the site's {op, rowsAffected|rowsReturned}
vocabulary, SourceNode) via ICentralAuditWriter — best-effort, never into the
script; the same instance goes to the RouteHelper trace sink first. Closes the
audit-scope gap Component-AuditLog.md already claimed (plan D7). Redaction and
the 8 KB cap apply at write time in the shared redactor, as for site rows.
Claude-Session: https://claude.ai/code/session_01NcMG6xXdsJbmD2wYAgkK29
FlowShapePredictor emits ExternalSystem.Call/CachedCall → ApiCall ('{s}.{m}'),
Database.Execute/Query/QuerySingle/CachedWrite → DbCall ('{conn} · {member}'),
Notify.Send(list) and Notify.To(list).Send() → Notify ('Notify {list}'), all on
FlowLane.External; Async-suffixed spellings map identically. PredictedEdge gains
an additive Async flag; an async edge (cached call, notify) does not advance the
chain, mirroring FlowGraphBuilder.IsAsync (D4/D8). Database.Connection() raw
handles no longer predict a node (command count is not statically knowable).
Claude-Session: https://claude.ai/code/session_01NcMG6xXdsJbmD2wYAgkK29
ScriptRuntimeContext caches a trace sink (null on an untraced run, so the
production path is byte-identical) and threads it into the ExternalSystem /
Database / Notify helpers and AuditingDbConnection→AuditingDbCommand (onAudit).
All eight outbound emit sites hand the SAME AuditEvent instance to the sink
first, then to the writer/forwarder. FlowTraceCollector gains Completed +
Complete(): ScriptRunLauncher closes the collector where it builds the reply
snapshot, so un-awaited calls finishing after the run returned never mutate a
list the reply was built from. Cap (500) unchanged. Plan 2026-09-06 D5.
Claude-Session: https://claude.ai/code/session_01NcMG6xXdsJbmD2wYAgkK29
Review of Task 2 (2026-09-06 outbound-calls plan): (1) an outbound step that
precedes the first chain step of its execution — the default Replay shape when
a run's ScriptTrace rows are absent — was orphaned or given a solid spawner
edge; leading async steps now hang off the spawner on Async edges and the
first non-async step gets the normal link. (2) IsAsync keyed on the first row
being CachedSubmit; it now mirrors the projector's sync rule (first row not
ApiCall/DbWrite) so a group missing its submit row still branches. (3) Same-
tick rows tie-broke by GUID; new LifecycleRank (submit < attempt < resolve,
non-terminal < terminal) makes first/latest attribution order-independent.
(4) DbSubtitle used GetInt32 (FormatException on out-of-range/fractional) —
now TryGetInt32, never throws. Also: External-lane edges never count as
central↔site crossings; DurationMs clamped; FlowStep doc updated.
Claude-Session: https://claude.ai/code/session_01NcMG6xXdsJbmD2wYAgkK29
GetTraceEventsAsync widens its channel filter to ScriptTrace ∪ ApiOutbound ∪
DbOutbound ∪ Notification ∪ (ApiInbound & InboundRequest) (plan D6); SecuredWrite,
Cluster and InboundAuthFailure rows in the same tree stay excluded. The site
SqliteAuditWriter.ReadByExecutionIdsAsync already had no channel predicate — pinned
by a test. Interface doc updated to match.
Claude-Session: https://claude.ai/code/session_01NcMG6xXdsJbmD2wYAgkK29
Fold rule: outbound rows sharing (kind, CorrelationId) become one FlowStep with
a Lifecycle list — first row's identity/time/input, latest row's outcome/
subtitle/output/error, DurationMs = sync row's own or submit→terminal wall time
(null while pending). Chain skips async steps (Notify, or first row
CachedSubmit) and links them with Async edges from the preceding chain step.
Spawner edges from a non-RouteCall are Spawn: true. The Response links from the
last non-async step of the inbound execution (the RouteCall), not the globally
last cascaded step (D9). FlowGraphView places a Response at max column + 1 so
the D9 origin does not pull it left (pulled forward from Task 7 to keep the
layout test green).
Claude-Session: https://claude.ai/code/session_01NcMG6xXdsJbmD2wYAgkK29
ResolveKind maps ApiOutbound/DbOutbound/Notification to ApiCall/DbCall/Notify;
ResolveLane puts the three on FlowLane.External; outbound outcomes map
Delivered→Ok, Failed→Failed, Parked/Discarded→Parked, else Pending; subtitles
'{status} · sync|cached', '{op} · n rows' from a DbWrite Extra, '{status}' for
Notify. New FlowStepProjector.IsOutbound helper.
Claude-Session: https://claude.ai/code/session_01NcMG6xXdsJbmD2wYAgkK29
Gap verified 2026-09-06: the projector and predictor already know the three outbound
channels, but the live FlowTraceCollector is fed only by EmitTrace (writes/waits) and
the replay query filters to ScriptTrace + the inbound row, so a run leaves every
predicted outbound node as a ghost. Central method-script Database.* calls are not
audited at all.
Plan (11 tasks): three step kinds ApiCall/DbCall/Notify on a fourth External lane;
one node per operation with the cached/notify lifecycle folded in (Pending/Parked
outcomes, Lifecycle tab); sync calls in the chain, async ones on dashed branch edges;
site rows ride the existing collector, replay filter widens, central DB helper gets
DbOutbound rows; the page refreshes pending outcomes via the existing Replay command;
spawn edges styled and Response linked from the route call (the "two parallel paths"
question). Mockup: docs/plans/…-mockup.html, published as a Claude artifact.
Claude-Session: https://claude.ai/code/session_01GSLkkHKz9JrAtmJjmQK3cP
The swimlane's node text was clipped by character count (20/24), so dotted attribute
paths showed only their shared head ("DelmiaReceiver.Reci…" five times in a row) and
the subtitle spilled past the box. It was also unstyled: the SVG <text> elements are
emitted as raw markup (Razor reserves the <text> tag), so they never carried the CSS
isolation scope attribute and every .fg-* rule silently missed.
- Node body is now HTML inside an SVG foreignObject: the browser measures the text,
the title wraps to two lines and ellipsizes by width, the subtitle by width on one
line, nothing can overflow the box, and the div's title attribute carries the full
unstripped text as a tooltip. Node height 78 -> 84.
- A lane whose real titles share a dotted head (CommonDottedPrefix) shows it once in
the lane gutter ("DelmiaReceiver.*") and each node shows only the tail that tells
it apart; the write subtitle strips the same head per item. Predicted-only nodes
are never stripped.
- WrapHints inserts zero-width spaces at camel-case and ./_/ boundaries so a wrapped
title breaks at "DelmiaRecipe|Download" rather than mid-word.
- Gutter widened 130 -> 160 with the sublabel clip raised to 22 so "site-a · DelmiaSim01"
fits; the remaining SvgText gutter labels are styled through ::deep.
Verified live on the rig (replay of the Task 22 run): all three lanes readable, no
overflow, gutter shows the factored prefix. CentralUI suite 1199 pass.
Claude-Session: https://claude.ai/code/session_01GSLkkHKz9JrAtmJjmQK3cP
Rig recreated from the branch-built image through the main checkout's compose file
(deploy.sh refuses a worktree, register row 68). Fresh key delmia-flowdbg-0906 scoped to
DelmiaRecipeDownload. Full drill: A/B/E/F YES, C/G NO, takeover 29 s, rejoin 3 s, then
H live+replay -> YES
live: 11 events (InboundRequest, RouteCall, ScriptRun, 7x AttributeWrite, AttributeWait)
replay: 17 events on the first attempt — the six extra rows are the simulator's two
cascaded ValueChange runs (2x ScriptRun + 4x AttributeWrite): inline TraceEvents
carry the routed run's own steps, replay walks the whole execution tree.
Recorded as row H in Delmia-Recipe-Download-API.md §8; ledger marks Task 22 completed
(all 24 tasks done). Note: the rig still runs main's appsettings, so the ScriptTrace
30-day retention override lands only after merge + a deploy from main.
Claude-Session: https://claude.ai/code/session_01GSLkkHKz9JrAtmJjmQK3cP
Both central nodes gain a root AuditLog:PerChannelRetentionDays:ScriptTrace=30 (validator
floor). verify-delmia-notifier.sh step H: finds DelmiaRecipeDownload via api-method list,
runs flow-debug run-method with the drill parameters and asserts completed=true plus the
kind set {InboundRequest, RouteCall, ScriptRun, AttributeWrite, AttributeWait} from each
event's detailsJson, then flow-debug replay on the root ExecutionId with the same check,
retried up to 40 s for the 5 s site telemetry push; prints 'H live+replay -> YES'.
VERIFICATION PENDING: the image scadabridge:latest was built from this branch
(docker/build.sh, exit 0) but the containers have not been recreated and step H has not
run yet — the next step is a recreate through the MAIN checkout's compose file (deploy.sh
refuses a worktree, register row 68: bind-mounted appsettings would rehome), a fresh
DelmiaRecipeDownload-scoped key, and the drill.
Claude-Session: https://claude.ai/code/session_01GSLkkHKz9JrAtmJjmQK3cP