From 0bea574e6101ee37507f86c2462b75b216520e0b Mon Sep 17 00:00:00 2001 From: EHotwagner Date: Sun, 18 Oct 2026 02:07:41 +0200 Subject: [PATCH 1/7] Bind terminal follower expiry expectations to actual producer due and clock --- docs/roadmaps/wasm-shared-01.md | 36 +++++++++++++++++++ tests/Wasm.Lifecycle.Tests/LifecycleTests.fs | 32 +++++++++++++++++ tests/Wasm.PackageConsumer/Program.fs | 8 +++++ .../browser/package-consumer.mjs | 21 ++++++----- .../browser/package-consumer.spec.mjs | 19 ++++++++-- 5 files changed, 106 insertions(+), 10 deletions(-) diff --git a/docs/roadmaps/wasm-shared-01.md b/docs/roadmaps/wasm-shared-01.md index a5ed4478..f0bf8add 100644 --- a/docs/roadmaps/wasm-shared-01.md +++ b/docs/roadmaps/wasm-shared-01.md @@ -38,6 +38,42 @@ installed binder still selects that exact publication and refuses the unpublishe This source assignment selects no publication, installed operation, consumer activation, image/native workload or product outcome. +### First 0.6 source qualification and exact follower-expiry proof + +[PR722](https://github.com/FS-GG/FS.GG.Game/pull/722) delivered the opcode change +at `bc78722f7a62a1ce5f0351d7969a26c307ec4fe7`, exact tested/delivered tree +`9df5fea131983d4d9ec98176106df62f143ad529`. Its original PR qualification +39079271365/attempt1 passed the parser controls, 36 package-browser cases and +source/candidate-installed cohorts. The first protected qualification +[39080719438/attempt1](https://github.com/FS-GG/FS.GG.Game/actions/runs/39080719438) +failed with35 package-browser cases passed and one inherited terminal-mode11 +assertion failed: the undispatched follower retained `DeadlineExpired`, while the +fixture required `HostDisposed`. The new66,524-byte dense fixture passed all four +operations and its94 complete stage observations. This failed original retained +model evidence only; it produced no eligible package custody and selects no +publication or unchanged rerun. The successful PR result is not protected-current +acceptance or proof of an earlier timeout's cause. + +The producer's unchanged `DefaultCompatibility` update expires queued requests +at strict `original due < producer Clock` before active invalidation/whole-session +retirement. A real late timer can therefore expire both nearby250ms requests; +first cause must not be relabeled as disposal. The connected test correction reads +the follower's actual admitted queue due and actual producer clock at settlement, +then requires exactly the reason selected by that strict comparison. Both default +terminal families retain no ABI outcome, discarded authority, no follower Post, +distinct identity, once-only settlement, follower-before-terminal order and actual +whole-worker retirement before the terminal callback. No additional clock sample +sets the expected result. Missing actual queue admission remains a setup failure. + +Six direct Lifecycle controls independently exercise before/exact/after follower +due for shutdown and terminal-Process followers, including actual phase setup, +original due, duplicate-timer refusal and complete retirement effect order. The +separate compatible SC2 queue-head `>=` boundary and held-snapshot semantics remain +unchanged; Default expiry is not generalized to that profile. Producer code, +parser, deadlines/caps/version and all36 real-browser cases remain unchanged. +The repaired current source still requires independent review, exact native +qualification, complete custody and fit under the existing35-minute profile. + ## Accepted 0.5 closed Worker and installed qualifier The accepted source successor selected coherent `0.5.0` for the pair and SDK. diff --git a/tests/Wasm.Lifecycle.Tests/LifecycleTests.fs b/tests/Wasm.Lifecycle.Tests/LifecycleTests.fs index 41775366..bd78ed3c 100644 --- a/tests/Wasm.Lifecycle.Tests/LifecycleTests.fs +++ b/tests/Wasm.Lifecycle.Tests/LifecycleTests.fs @@ -547,6 +547,38 @@ let terminalProcess = let _, resumed = update 3L (ResumeRequested "unused") state Expect.isEmpty resumed "resume cannot revive retired held work" + for shutdownFollower in [ false; true ] do + for observed in [ 4L; 5L; 6L ] do + testCase (sprintf "Default follower strict due at %d shutdown=%b" observed shutdownFollower) <| fun _ -> + let reset = identity "worker-1" 1UL 1UL + let follower = identity "worker-1" 2UL 1UL + let candidate = identity "worker-2" 3UL 2UL + let state = loadedSelected DefaultCompatibility + let state = next 0L (LoadRequested(candidate, PrepareCandidate("due-candidate", Some 1UL), [| 0uy |])) state + let state = limited 0L (LimitedInvokeAndRetire(reset, [| 1uy |])) { terminalLimits with MaximumDeadlineMilliseconds = 3 } state + let following = if shutdownFollower then LimitedShutdown follower else LimitedInvokeAndRetire(follower, [| 2uy |]) + let state = limited 0L following { terminalLimits with MaximumDeadlineMilliseconds = 5 } state + let state = next 1L (WorkerObserved("worker-1", PhaseObserved(reset, tokenFor reset, AllocateInput))) state + let state = next 2L (WorkerObserved("worker-1", PhaseObserved(reset, tokenFor reset, Process))) state + let before = Lifecycle.project state + Expect.equal before.Current.DeadlineMilliseconds 3L "phase callbacks keep original active due" + Expect.equal before.Ordinary.Head.Id follower.Request "follower is actually queued" + Expect.equal before.Ordinary.Head.DeadlineMilliseconds 5L "original admitted follower due" + let state, effects = update observed (TimerObserved { Identity = reset; Correlation = tokenFor reset; Phase = Process }) state + expectRetired state effects reset [ "worker-1"; "worker-2" ] + Expect.equal (Lifecycle.project state).Clock observed "settlement uses exact producer clock" + let results = terminalResults effects + let destroyed = results |> List.find (fun result -> result.Identity = follower) + Expect.equal destroyed.Outcome None "queued follower never invokes an ABI" + Expect.equal destroyed.Disposition Discarded "no follower delivery authority" + Expect.equal destroyed.Reason (Some(if 5L < observed then DeadlineExpired else HostDisposed)) "strict queued expiry precedes terminal retirement; exact due is unexpired" + Expect.equal (results |> List.filter (fun result -> result.Identity = follower)).Length 1 "follower settles once" + let actual = results |> List.last + Expect.equal actual.Identity reset "actual terminal follows follower settlement" + Expect.isTrue (actual.Outcome |> Option.exists (fun outcome -> outcome.State = TimedOut && outcome.Dispatch = Dispatched)) "active Process timeout keeps actual ABI dispatch" + let _, duplicate = update (observed + 1L) (TimerObserved { Identity = reset; Correlation = tokenFor reset; Phase = Process }) state + Expect.isEmpty duplicate "duplicate timer cannot settle or dispatch again" + for observed, expired in [ 4L, false; 5L, true; 6L, true ] do testCase (sprintf "queued terminal head boundary %d" observed) <| fun _ -> let a = identity "worker-1" 1UL 1UL diff --git a/tests/Wasm.PackageConsumer/Program.fs b/tests/Wasm.PackageConsumer/Program.fs index 06dabdf4..76ecb82e 100644 --- a/tests/Wasm.PackageConsumer/Program.fs +++ b/tests/Wasm.PackageConsumer/Program.fs @@ -105,6 +105,14 @@ let createHost path digest outputCap deadline (transport: HostTransport) receive Host.CreateConnected(settings, transport, settle) |> unwrap +// Test observation of actual producer state; never samples/advances its clock. +let producerClock (host: Host) = string host.Projection.Clock +let queuedDeadline (host: Host) request = + host.Projection.Ordinary + |> List.tryFind (fun pending -> string pending.Id = request) + |> Option.map (fun pending -> string pending.DeadlineMilliseconds) + |> Option.defaultValue "" + let load (host: Host) now wire artifact = host.Load(now, identity wire, artifact) // The cold-start controls use the same real, pre-create enclosing deadline as diff --git a/tests/Wasm.PackageConsumer/browser/package-consumer.mjs b/tests/Wasm.PackageConsumer/browser/package-consumer.mjs index 0ac890b6..0e485357 100644 --- a/tests/Wasm.PackageConsumer/browser/package-consumer.mjs +++ b/tests/Wasm.PackageConsumer/browser/package-consumer.mjs @@ -4,7 +4,7 @@ const bytes = async name => new Uint8Array(await (await fetch(`./modules/${name} const digest = async value => Array.from(new Uint8Array(await crypto.subtle.digest("SHA-256", value)), byte => byte.toString(16).padStart(2, "0")).join(""); async function session(artifact, prefix, outputCap = 65536, deadline = 250, inject = false, observer, holdLoaded = false) { const transport = createHostTransport(new URL("./_content/FS.GG.Wasm.Browser/", import.meta.url),observer?{observe:observer}:undefined); - const waiting = new Map(), results = [], posts=[], terminations=[], deliveryStates=[]; + const waiting = new Map(), results = [], posts=[], terminations=[], deliveryStates=[], admissions=[]; const terminate=transport.TerminateWorker; transport.TerminateWorker=name=>{terminations.push(name);terminate(name);}; let ignored=0, host;const held=[]; @@ -27,15 +27,19 @@ async function session(artifact, prefix, outputCap = 65536, deadline = 250, inje transport.PostCommand=(name,command)=>{posts.push({name,Kind:command.Kind,request:command.Identity.Request});post(name,command);}; const artifactDigest=await digest(artifact); host = bridge.createHost(prefix === "barc" ? "bar-protected" : "sc2-imported-strict", artifactDigest, outputCap, deadline, transport, result => { - deliveryStates.push({request:result.Request,projection:bridge.project(host),terminations:[...terminations]}); + deliveryStates.push({request:result.Request,clock:bridge.producerClock(host),projection:bridge.project(host),terminations:[...terminations]}); results.push(result); const pending = waiting.get(result.Request); waiting.delete(result.Request); pending?.(result); }); let request = 0; const send = (method, value, owner = "installed-worker", generation = "1") => { const id = String(++request), identity = {WorkerInstance: owner, Request: id, Generation: generation}; - return new Promise(resolve => { waiting.set(id, resolve); bridge[method](host, transport.Now(), identity, value); }); + return new Promise(resolve => { + waiting.set(id, resolve); + bridge[method](host, transport.Now(), identity, value); + admissions.push({request:id,queuedDeadline:bridge.queuedDeadline(host,id)}); + }); }; - return { host, send, results, posts, terminations, deliveryStates, transport, + return { host, send, results, posts, terminations, deliveryStates, admissions, transport, heldCount:()=>held.length, releaseHeld:()=>{for(const row of held.splice(0))row.receive(row.wire);}, ignored:()=>ignored, @@ -227,18 +231,19 @@ window.runTerminalProcess = async (mode=18, follower="invokeAndRetire") => { const following=s.send(follower,new Uint8Array([3])); const [a,r,f]=await Promise.all([active,reset,following]); const retirement=s.deliveryStates.find(item=>item.request===r.Request); - return {active:a,reset:r,follower:f,posts:s.posts.filter(item=>item.Kind==="process"||item.Kind==="shutdown").map(item=>({request:item.request,kind:item.Kind})),retirement,deliveries:s.results.map(item=>item.Request),projection:s.projection()}; + return {active:a,reset:r,follower:f,followerDue:s.admissions.find(x=>x.request===f.Request)?.queuedDeadline,followerClock:s.deliveryStates.find(x=>x.request===f.Request)?.clock,posts:s.posts.filter(item=>item.Kind==="process"||item.Kind==="shutdown").map(item=>({request:item.request,kind:item.Kind})),retirement,deliveries:s.results.map(item=>item.Request),projection:s.projection()}; }finally{s.dispose();} }; window.runTerminalRaw = async mode => { const artifact=await bytes("control-ordinary.wasm"),s=await session(artifact,"sc2c"); try { - await s.send("load",artifact);await s.send("initialize",new Uint8Array([mode])); - await s.prepare(artifact,"terminal-candidate"); + const loaded=await s.send("load",artifact),initialized=await s.send("initialize",new Uint8Array([mode])); + const candidate=await s.prepare(artifact,"terminal-candidate"); + if([loaded,initialized,candidate].some(x=>x.State!=="succeeded"))throw new Error("terminal raw setup did not succeed"); const reset=s.send("invokeAndRetire",new Uint8Array([1])); const follower=s.send("shutdown"); const [r,f]=await Promise.all([reset,follower]); - return {reset:r,follower:f,posts:s.posts.filter(x=>x.Kind==="process"||x.Kind==="shutdown").map(x=>x.Kind),retirement:s.deliveryStates.find(x=>x.request===r.Request)}; + return {reset:r,follower:f,followerDue:s.admissions.find(x=>x.request===f.Request)?.queuedDeadline,followerClock:s.deliveryStates.find(x=>x.request===f.Request)?.clock,deliveries:s.results.map(x=>x.Request),posts:s.posts.filter(x=>x.Kind==="process"||x.Kind==="shutdown").map(x=>x.Kind),retirement:s.deliveryStates.find(x=>x.request===r.Request)}; }finally{s.dispose();} }; diff --git a/tests/Wasm.PackageConsumer/browser/package-consumer.spec.mjs b/tests/Wasm.PackageConsumer/browser/package-consumer.spec.mjs index e51f70c5..747fc8f0 100644 --- a/tests/Wasm.PackageConsumer/browser/package-consumer.spec.mjs +++ b/tests/Wasm.PackageConsumer/browser/package-consumer.spec.mjs @@ -190,12 +190,27 @@ test("unsafe output after guest rejection preserves status and removes dead Work expect(value.active).toBe(""); }); +// DefaultCompatibility expires queued work before active retirement, using the +// strict original due < producer Clock boundary. Read the actual queue/settlement +// state instead of assuming that a late timer fires between two nearby deadlines. +function expectTerminalFollower(value) { + expect(value.followerDue,"actual follower must have entered the producer queue").toMatch(/^[0-9]+$/); + expect(value.followerClock,"actual producer settlement clock").toMatch(/^[0-9]+$/); + const expired=BigInt(value.followerDue){ const value=await page.evaluate(follower=>window.runTerminalProcess(18,follower),follower); expect(value.reset).toMatchObject({State:"succeeded",Phase:"Process",Dispatched:true}); expect(Array.from(value.reset.Output)).toEqual([83,67,50,51]); expect(value.posts.map(item=>item.kind)).toEqual(["process","process"]); - expect(value.follower).toMatchObject({HasOutcome:false,Disposition:"Discarded",Reason:"HostDisposed"}); + expectTerminalFollower(value); expect(value.retirement.projection).toMatchObject({Disposed:true,ActiveWorker:"",CandidateWorker:""}); expect(new Set(value.retirement.terminations)).toEqual(new Set(["installed-worker","candidate-worker"])); expect(value.deliveries.indexOf(value.follower.Request)).toBeLessThan(value.deliveries.indexOf(value.reset.Request)); @@ -210,5 +225,5 @@ test(`terminal Process preserves actual installed ABI and cleanup evidence mode expect(r.posts).toEqual(["process"]); expect(r.retirement.projection).toMatchObject({Disposed:true,ActiveWorker:"",CandidateWorker:""}); expect(new Set(r.retirement.terminations)).toEqual(new Set(["installed-worker","candidate-worker"])); - expect(r.follower).toMatchObject({HasOutcome:false,Disposition:"Discarded",Reason:"HostDisposed"}); + expectTerminalFollower(r); }); From 5930fb06915206d20f1611a228fb87a73b3a9314 Mon Sep 17 00:00:00 2001 From: EHotwagner Date: Sun, 18 Oct 2026 02:09:42 +0200 Subject: [PATCH 2/7] Retain actual terminal receipt metadata before any contract assertion --- .../Wasm.PackageConsumer/browser/package-consumer.spec.mjs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/tests/Wasm.PackageConsumer/browser/package-consumer.spec.mjs b/tests/Wasm.PackageConsumer/browser/package-consumer.spec.mjs index 747fc8f0..523711af 100644 --- a/tests/Wasm.PackageConsumer/browser/package-consumer.spec.mjs +++ b/tests/Wasm.PackageConsumer/browser/package-consumer.spec.mjs @@ -193,11 +193,14 @@ test("unsafe output after guest rejection preserves status and removes dead Work // DefaultCompatibility expires queued work before active retirement, using the // strict original due < producer Clock boundary. Read the actual queue/settlement // state instead of assuming that a late timer fires between two nearby deadlines. +function reportTerminal(value) { + const complete=/^[0-9]+$/.test(value.followerDue??"")&&/^[0-9]+$/.test(value.followerClock??""); + console.log(JSON.stringify({stage:"terminal follower original deadline",resetRequest:value.reset.Request,resetState:value.reset.State,resetPhase:value.reset.Phase,resetDispatched:value.reset.Dispatched,followerRequest:value.follower.Request,due:value.followerDue,settledAt:value.followerClock,reason:value.follower.Reason,complete,expired:complete?BigInt(value.followerDue){ const value=await page.evaluate(follower=>window.runTerminalProcess(18,follower),follower); + reportTerminal(value); expect(value.reset).toMatchObject({State:"succeeded",Phase:"Process",Dispatched:true}); expect(Array.from(value.reset.Output)).toEqual([83,67,50,51]); expect(value.posts.map(item=>item.kind)).toEqual(["process","process"]); @@ -220,6 +224,7 @@ for(const follower of ["invokeAndRetire","shutdown"])test(`terminal Process reti for(const [mode,state,status] of [[18,"succeeded",0],[19,"rejected",92],[16,"faulted",0],[17,"faulted",0],[11,"timeout",0]]) test(`terminal Process preserves actual installed ABI and cleanup evidence mode ${mode}`,async({page})=>{ const r=await page.evaluate(mode=>window.runTerminalRaw(mode),mode); + reportTerminal(r); expect(r.reset).toMatchObject({State:state,GuestStatus:status,Dispatched:true}); if(mode===16||mode===17)expect(r.reset.CleanupDiagnostic).toContain("unreachable"); expect(r.posts).toEqual(["process"]); From 10769bd90ca7801d0054c5fe9b88c72f50ae83f2 Mon Sep 17 00:00:00 2001 From: EHotwagner Date: Sun, 18 Oct 2026 02:16:00 +0200 Subject: [PATCH 3/7] Capture terminal decision clock before connected termination reentry --- docs/roadmaps/wasm-shared-01.md | 19 ++++-- tests/Wasm.Lifecycle.Tests/LifecycleTests.fs | 65 +++++++++++++++++++ .../browser/package-consumer.mjs | 20 ++++-- .../browser/package-consumer.spec.mjs | 25 +++++-- 4 files changed, 112 insertions(+), 17 deletions(-) diff --git a/docs/roadmaps/wasm-shared-01.md b/docs/roadmaps/wasm-shared-01.md index f0bf8add..e675195b 100644 --- a/docs/roadmaps/wasm-shared-01.md +++ b/docs/roadmaps/wasm-shared-01.md @@ -58,17 +58,24 @@ The producer's unchanged `DefaultCompatibility` update expires queued requests at strict `original due < producer Clock` before active invalidation/whole-session retirement. A real late timer can therefore expire both nearby250ms requests; first cause must not be relabeled as disposal. The connected test correction reads -the follower's actual admitted queue due and actual producer clock at settlement, -then requires exactly the reason selected by that strict comparison. Both default +the follower's actual admitted queue due and actual producer clock at the first +retirement effect, before native termination and connected Host reentry, then +requires exactly the reason selected by that strict comparison. Both default terminal families retain no ABI outcome, discarded authority, no follower Post, distinct identity, once-only settlement, follower-before-terminal order and actual -whole-worker retirement before the terminal callback. No additional clock sample -sets the expected result. Missing actual queue admission remains a setup failure. +whole-worker retirement before the terminal callback. The later follower-callback clock is retained separately: connected termination +reenters `Observe(Now)` and can advance it beyond the original decision. Queued +expiry must settle before the first termination, disposal after it. No additional +clock sample sets the expected result. Missing actual queue admission or retirement +batch remains a setup failure. Six direct Lifecycle controls independently exercise before/exact/after follower due for shutdown and terminal-Process followers, including actual phase setup, -original due, duplicate-timer refusal and complete retirement effect order. The -separate compatible SC2 queue-head `>=` boundary and held-snapshot semantics remain +original due, duplicate-timer refusal and complete retirement effect order. Three additional actual connected-Host controls reproduce mechanical termination +reentry with declared inert ports: before/equal/after decision clocks, captured +actual timer identity/delay, and a later callback clock beyond the queued due. +They are logical interpreter controls, not browser timing or ABI execution proof. +The separate compatible SC2 queue-head `>=` boundary and held-snapshot semantics remain unchanged; Default expiry is not generalized to that profile. Producer code, parser, deadlines/caps/version and all36 real-browser cases remain unchanged. The repaired current source still requires independent review, exact native diff --git a/tests/Wasm.Lifecycle.Tests/LifecycleTests.fs b/tests/Wasm.Lifecycle.Tests/LifecycleTests.fs index bd78ed3c..7a31d2ab 100644 --- a/tests/Wasm.Lifecycle.Tests/LifecycleTests.fs +++ b/tests/Wasm.Lifecycle.Tests/LifecycleTests.fs @@ -579,6 +579,71 @@ let terminalProcess = let _, duplicate = update (observed + 1L) (TimerObserved { Identity = reset; Correlation = tokenFor reset; Phase = Process }) state Expect.isEmpty duplicate "duplicate timer cannot settle or dispatch again" + // Actual connected interpreter with declared inert mechanics. Termination + // advances only this injected port clock, reproducing Observe(Now) reentry. + // This is not a browser timing or ABI-execution claim. + for observed in [ 4L; 5L; 6L ] do + testCase (sprintf "connected retirement decision clock survives reentry at %d" observed) <| fun _ -> + let mutable portClock = 0L + let mutable connected: Host option = None + let timers = System.Collections.Generic.Dictionary unit)>() + let events = ResizeArray() + let results = ResizeArray() + let posts = ResizeArray() + let transport: HostTransport = + { Now = fun () -> portClock + CreateWorker = fun _ _ -> () + PostCommand = fun _ command -> posts.Add command + TerminateWorker = fun worker -> + events.Add(("terminate:" + worker, connected.Value.Projection.Clock)) + portClock <- 6L + ArmTimer = fun key delay callback -> timers[key] <- (delay, callback) + CancelTimer = fun key -> timers.Remove key |> ignore } + let receive (result: BrowserResult) = + let clock = connected.Value.Projection.Clock + events.Add(("settle:" + string result.Identity.Request, clock)) + results.Add((result, clock)) + let host = Host.CreateConnected(settings Sc2ImportedStrict, transport, receive) |> Result.defaultWith (fun issues -> failtestf "%A" issues) + connected <- Some host + let load = identity "worker-1" 100UL 1UL + let initialize = identity "worker-1" 101UL 1UL + let reset = identity "worker-1" 1UL 1UL + let follower = identity "worker-1" 2UL 1UL + host.Load(0L, load, [| 0uy |]) + host.Observe(0L, "worker-1", AbiVersionObserved(load, tokenFor load, 0x10000u)) + host.Initialize(0L, initialize, [| 0uy |]) + host.Observe(0L, "worker-1", InvocationObserved(success Initialize initialize, tokenFor initialize)) + host.SubmitLimited(0L, LimitedInvokeAndRetire(reset, [| 1uy |]), { terminalLimits with MaximumDeadlineMilliseconds = 3 }) |> fun result -> Expect.equal result (Ok ()) "terminal admitted" + host.SubmitLimited(0L, LimitedShutdown follower, { terminalLimits with MaximumDeadlineMilliseconds = 5 }) |> fun result -> Expect.equal result (Ok ()) "follower admitted" + for at, phase in [ 1L, AllocateInput; 2L, Process ] do + portClock <- at + host.Observe(at, "worker-1", PhaseObserved(reset, tokenFor reset, phase)) + Expect.equal host.Projection.Current.DeadlineMilliseconds 3L "active due remains original" + Expect.equal host.Projection.Ordinary.Head.DeadlineMilliseconds 5L "actual queued due" + Expect.equal timers.Count 1 "only actual active phase timer remains" + let timer = timers |> Seq.exactlyOne + Expect.isTrue (timer.Key.EndsWith(":Process")) "actual Process timer selected" + let delay, fire = timer.Value + Expect.equal delay 2 "actual strict timer uses due-now+1" + events.Clear(); results.Clear(); posts.Clear() + portClock <- observed + fire () + let firstTermination = events |> Seq.find (fun (kind, _) -> kind.StartsWith("terminate:")) + Expect.equal (snd firstTermination) observed "capture outer producer clock before termination reentry" + let destroyed, callbackClock = results |> Seq.find (fun (result, _) -> result.Identity = follower) + Expect.equal destroyed.Reason (Some(if 5L < observed then DeadlineExpired else HostDisposed)) "reason binds decision, not later callback clock" + Expect.equal destroyed.Outcome None "no follower ABI" + Expect.equal destroyed.Disposition Discarded "no follower authority" + Expect.equal callbackClock 6L "connected termination advances callback clock even when queue was unexpired" + let kinds = events |> Seq.map fst |> Seq.toList + Expect.equal ((kinds |> List.findIndex ((=) "settle:2")) < (kinds |> List.findIndex ((=) "terminate:worker-1"))) (5L < observed) "queued expiry precedes termination; disposal follows it" + Expect.equal kinds.[kinds.Length - 1] "settle:1" "terminal result follows retirement/follower" + Expect.isTrue host.Projection.Disposed "whole session retired" + Expect.isEmpty posts "no following post" + let count = results.Count + fire () + Expect.equal results.Count count "stale real timer callback cannot settle twice" + for observed, expired in [ 4L, false; 5L, true; 6L, true ] do testCase (sprintf "queued terminal head boundary %d" observed) <| fun _ -> let a = identity "worker-1" 1UL 1UL diff --git a/tests/Wasm.PackageConsumer/browser/package-consumer.mjs b/tests/Wasm.PackageConsumer/browser/package-consumer.mjs index 0e485357..e9627cba 100644 --- a/tests/Wasm.PackageConsumer/browser/package-consumer.mjs +++ b/tests/Wasm.PackageConsumer/browser/package-consumer.mjs @@ -4,9 +4,14 @@ const bytes = async name => new Uint8Array(await (await fetch(`./modules/${name} const digest = async value => Array.from(new Uint8Array(await crypto.subtle.digest("SHA-256", value)), byte => byte.toString(16).padStart(2, "0")).join(""); async function session(artifact, prefix, outputCap = 65536, deadline = 250, inject = false, observer, holdLoaded = false) { const transport = createHostTransport(new URL("./_content/FS.GG.Wasm.Browser/", import.meta.url),observer?{observe:observer}:undefined); - const waiting = new Map(), results = [], posts=[], terminations=[], deliveryStates=[], admissions=[]; + const waiting = new Map(), results = [], posts=[], terminations=[], deliveryStates=[], admissions=[], terminationStates=[]; const terminate=transport.TerminateWorker; - transport.TerminateWorker=name=>{terminations.push(name);terminate(name);}; + transport.TerminateWorker=name=>{ + // Observe the producer decision BEFORE mechanical termination and the + // connected Host's subsequent WorkerTerminated/Now reentry. + terminationStates.push({worker:name,clock:bridge.producerClock(host),disposed:bridge.project(host).Disposed,settledRequests:results.map(x=>x.Request)}); + terminations.push(name);terminate(name); + }; let ignored=0, host;const held=[]; const create=transport.CreateWorker; transport.CreateWorker=(name,receive)=>create(name,wire=>{ @@ -27,7 +32,7 @@ async function session(artifact, prefix, outputCap = 65536, deadline = 250, inje transport.PostCommand=(name,command)=>{posts.push({name,Kind:command.Kind,request:command.Identity.Request});post(name,command);}; const artifactDigest=await digest(artifact); host = bridge.createHost(prefix === "barc" ? "bar-protected" : "sc2-imported-strict", artifactDigest, outputCap, deadline, transport, result => { - deliveryStates.push({request:result.Request,clock:bridge.producerClock(host),projection:bridge.project(host),terminations:[...terminations]}); + deliveryStates.push({request:result.Request,clock:bridge.producerClock(host),projection:bridge.project(host),terminations:[...terminations],terminationStates:[...terminationStates]}); results.push(result); const pending = waiting.get(result.Request); waiting.delete(result.Request); pending?.(result); }); let request = 0; @@ -39,7 +44,7 @@ async function session(artifact, prefix, outputCap = 65536, deadline = 250, inje admissions.push({request:id,queuedDeadline:bridge.queuedDeadline(host,id)}); }); }; - return { host, send, results, posts, terminations, deliveryStates, admissions, transport, + return { host, send, results, posts, terminations, deliveryStates, admissions, terminationStates, transport, heldCount:()=>held.length, releaseHeld:()=>{for(const row of held.splice(0))row.receive(row.wire);}, ignored:()=>ignored, @@ -226,12 +231,13 @@ window.runTerminalProcess = async (mode=18, follower="invokeAndRetire") => { await s.send("load",artifact);await s.send("initialize",new Uint8Array([mode])); const candidate=await s.prepare(artifact,"terminal-candidate"); if(candidate.State!=="succeeded")throw new Error("candidate preparation failed"); + const priorTerminations=s.terminationStates.length; const active=s.send("invoke",new Uint8Array([1])); const reset=s.send("invokeAndRetire",new Uint8Array([2])); const following=s.send(follower,new Uint8Array([3])); const [a,r,f]=await Promise.all([active,reset,following]); const retirement=s.deliveryStates.find(item=>item.request===r.Request); - return {active:a,reset:r,follower:f,followerDue:s.admissions.find(x=>x.request===f.Request)?.queuedDeadline,followerClock:s.deliveryStates.find(x=>x.request===f.Request)?.clock,posts:s.posts.filter(item=>item.Kind==="process"||item.Kind==="shutdown").map(item=>({request:item.request,kind:item.Kind})),retirement,deliveries:s.results.map(item=>item.Request),projection:s.projection()}; + return {active:a,reset:r,follower:f,retirementBatch:retirement.terminationStates.slice(priorTerminations),followerDue:s.admissions.find(x=>x.request===f.Request)?.queuedDeadline,followerClock:s.deliveryStates.find(x=>x.request===f.Request)?.clock,posts:s.posts.filter(item=>item.Kind==="process"||item.Kind==="shutdown").map(item=>({request:item.request,kind:item.Kind})),retirement,deliveries:s.results.map(item=>item.Request),projection:s.projection()}; }finally{s.dispose();} }; @@ -241,9 +247,11 @@ window.runTerminalRaw = async mode => { const loaded=await s.send("load",artifact),initialized=await s.send("initialize",new Uint8Array([mode])); const candidate=await s.prepare(artifact,"terminal-candidate"); if([loaded,initialized,candidate].some(x=>x.State!=="succeeded"))throw new Error("terminal raw setup did not succeed"); + const priorTerminations=s.terminationStates.length; const reset=s.send("invokeAndRetire",new Uint8Array([1])); const follower=s.send("shutdown"); const [r,f]=await Promise.all([reset,follower]); - return {reset:r,follower:f,followerDue:s.admissions.find(x=>x.request===f.Request)?.queuedDeadline,followerClock:s.deliveryStates.find(x=>x.request===f.Request)?.clock,deliveries:s.results.map(x=>x.Request),posts:s.posts.filter(x=>x.Kind==="process"||x.Kind==="shutdown").map(x=>x.Kind),retirement:s.deliveryStates.find(x=>x.request===r.Request)}; + const retirement=s.deliveryStates.find(x=>x.request===r.Request); + return {reset:r,follower:f,retirementBatch:retirement.terminationStates.slice(priorTerminations),followerDue:s.admissions.find(x=>x.request===f.Request)?.queuedDeadline,followerClock:s.deliveryStates.find(x=>x.request===f.Request)?.clock,deliveries:s.results.map(x=>x.Request),posts:s.posts.filter(x=>x.Kind==="process"||x.Kind==="shutdown").map(x=>x.Kind),retirement}; }finally{s.dispose();} }; diff --git a/tests/Wasm.PackageConsumer/browser/package-consumer.spec.mjs b/tests/Wasm.PackageConsumer/browser/package-consumer.spec.mjs index 523711af..ff6cd66b 100644 --- a/tests/Wasm.PackageConsumer/browser/package-consumer.spec.mjs +++ b/tests/Wasm.PackageConsumer/browser/package-consumer.spec.mjs @@ -191,17 +191,32 @@ test("unsafe output after guest rejection preserves status and removes dead Work }); // DefaultCompatibility expires queued work before active retirement, using the -// strict original due < producer Clock boundary. Read the actual queue/settlement +// strict original due < producer decision Clock boundary. Read actual queue/effect // state instead of assuming that a late timer fires between two nearby deadlines. function reportTerminal(value) { - const complete=/^[0-9]+$/.test(value.followerDue??"")&&/^[0-9]+$/.test(value.followerClock??""); - console.log(JSON.stringify({stage:"terminal follower original deadline",resetRequest:value.reset.Request,resetState:value.reset.State,resetPhase:value.reset.Phase,resetDispatched:value.reset.Dispatched,followerRequest:value.follower.Request,due:value.followerDue,settledAt:value.followerClock,reason:value.follower.Reason,complete,expired:complete?BigInt(value.followerDue)=BigInt(decision.clock)).toBe(true); + } expect(value.follower.Request).not.toBe(value.reset.Request); expect(value.deliveries.indexOf(value.follower.Request)).toBeGreaterThanOrEqual(0); expect(value.deliveries.indexOf(value.follower.Request)).toBeLessThan(value.deliveries.indexOf(value.reset.Request)); From fae6b000131f3e5f718c4294844c73d767150db4 Mon Sep 17 00:00:00 2001 From: EHotwagner Date: Sun, 18 Oct 2026 02:18:16 +0200 Subject: [PATCH 4/7] Join connected timer control to exact active and queued identities --- docs/roadmaps/wasm-shared-01.md | 6 ++++-- tests/Wasm.Lifecycle.Tests/LifecycleTests.fs | 5 ++++- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/docs/roadmaps/wasm-shared-01.md b/docs/roadmaps/wasm-shared-01.md index e675195b..e62402bb 100644 --- a/docs/roadmaps/wasm-shared-01.md +++ b/docs/roadmaps/wasm-shared-01.md @@ -63,7 +63,8 @@ retirement effect, before native termination and connected Host reentry, then requires exactly the reason selected by that strict comparison. Both default terminal families retain no ABI outcome, discarded authority, no follower Post, distinct identity, once-only settlement, follower-before-terminal order and actual -whole-worker retirement before the terminal callback. The later follower-callback clock is retained separately: connected termination +whole-worker retirement before the terminal callback. The later follower-callback +clock is retained separately: connected termination reenters `Observe(Now)` and can advance it beyond the original decision. Queued expiry must settle before the first termination, disposal after it. No additional clock sample sets the expected result. Missing actual queue admission or retirement @@ -71,7 +72,8 @@ batch remains a setup failure. Six direct Lifecycle controls independently exercise before/exact/after follower due for shutdown and terminal-Process followers, including actual phase setup, -original due, duplicate-timer refusal and complete retirement effect order. Three additional actual connected-Host controls reproduce mechanical termination +original due, duplicate-timer refusal and complete retirement effect order. Three +additional actual connected-Host controls reproduce mechanical termination reentry with declared inert ports: before/equal/after decision clocks, captured actual timer identity/delay, and a later callback clock beyond the queued due. They are logical interpreter controls, not browser timing or ABI execution proof. diff --git a/tests/Wasm.Lifecycle.Tests/LifecycleTests.fs b/tests/Wasm.Lifecycle.Tests/LifecycleTests.fs index 7a31d2ab..0b1e234e 100644 --- a/tests/Wasm.Lifecycle.Tests/LifecycleTests.fs +++ b/tests/Wasm.Lifecycle.Tests/LifecycleTests.fs @@ -618,11 +618,14 @@ let terminalProcess = for at, phase in [ 1L, AllocateInput; 2L, Process ] do portClock <- at host.Observe(at, "worker-1", PhaseObserved(reset, tokenFor reset, phase)) + Expect.equal host.Projection.Current.Id reset.Request "actual current terminal identity" + Expect.equal host.Projection.Current.Worker reset.WorkerInstance "actual current worker" Expect.equal host.Projection.Current.DeadlineMilliseconds 3L "active due remains original" + Expect.equal host.Projection.Ordinary.Head.Id follower.Request "actual queued follower identity" Expect.equal host.Projection.Ordinary.Head.DeadlineMilliseconds 5L "actual queued due" Expect.equal timers.Count 1 "only actual active phase timer remains" let timer = timers |> Seq.exactlyOne - Expect.isTrue (timer.Key.EndsWith(":Process")) "actual Process timer selected" + Expect.equal timer.Key $"{reset.WorkerInstance}:{reset.Generation}:{reset.Request}:{OperationToken.value (tokenFor reset)}:Process" "exact current request/correlation/phase timer selected" let delay, fire = timer.Value Expect.equal delay 2 "actual strict timer uses due-now+1" events.Clear(); results.Clear(); posts.Clear() From 5d77b79f790303883a18ec931519fb4cbceafe28 Mon Sep 17 00:00:00 2001 From: EHotwagner Date: Sun, 18 Oct 2026 02:19:56 +0200 Subject: [PATCH 5/7] Capture original terminal expiry failure and decision-clock observation boundary --- docs/atlas/catalog.json | 2 +- ...63db90d4b6d3fea2d226a7a58b08b88cf748e619ddb407ea83911e9.json | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 docs/atlas/records/7eba935f363db90d4b6d3fea2d226a7a58b08b88cf748e619ddb407ea83911e9.json diff --git a/docs/atlas/catalog.json b/docs/atlas/catalog.json index e223299c..8611ece2 100644 --- a/docs/atlas/catalog.json +++ b/docs/atlas/catalog.json @@ -1 +1 @@ -{"records":["049f1d0d8155f65b6f7675ebdb200d4595570cc7d585dd74876e0285eceffed6","0863093e69a16355fa9b90f8e7e29156ea4ad88c4523bc4883568bde831bdf35","2056fa8b653c2240afdad2a16f572a07019109254847829610a9468bb2f95248","231df3792931ad2333d00970a5993b3e13fa9dc38f687899f9fa2c966ed9fbfa","24fdc3980640ba58bedcae6f16d5b467eb7383c31161fb9b6ff10fd2e667b0e5","264a2f7887a9b6473cbc24102e1bf9be70e57f6d7ba224138140efa094d541f4","2b7dfb3a2f76368cb603161cdb6f8fa8c2b8afc3e96659dd37c975921ba5ba66","33df463263533529cca24a0b09937573f836d197cc1a739e9960b1cdd4deb1fd","343077d02ddd266b21d8677f7c256854134f7e3e2d18137b55ebe3f32564c5c0","34391bda3fb56135a2bddf74ab790958c175bc6a69c38175e6f8db792cfd50a0","36a19fefd7024f54f3391ffed19019c10222a514719edd7a44f0cde2a1c48258","3d3a0a302c02cd0bfee86715d09b0211baa8dad3e47402c638fc95c7cfa02e87","3de8718fb3ca9b15212e8bd84fba205995d24636087090a61c63c28a96a745b1","3df4f31fda99ad137c651322ad29809e36380400516399274870b4a4332b4a92","3f09ee199e3c2e808e0f6e74f197f2fdd8158ae099a7195c498ce9466bb78b03","44bda82d6f9a9625a83283bf56a15a1831f287037214dc134eeeae87f1faa869","6c5b38ee260d1f15f8029d699ec1cebceafb5e1407a6341dc7a4b96f4d9d3521","71bc29a175940d22dd1d85148ee49f47aed714395fbd51d07b8751147815a55f","72742cbaa561237f10251e94aa5c61153ab5c7f14fc5771b7a5ebdece1e1cb11","85f06991da74fa4e59ba73c7034dc53a35a4e90c5b64d83cc8956d3afd61d376","87d6de6f3730cecd75d9a4411bf4dbac4e058127a3ce9f46b07f6de110bc26de","8b1cf51c5f03c0ae19c46ca3ea4a1158acbb6e47bf109ca8c506c5417b51cf9f","8c514796f190fe6d4307be57983c84f92208f5d2f00fcbe3093b72618f2d7bc1","95115e1052b5ea4fcd393f5c9b9c6f0298657f0028eee84e16215ede97576cfa","9b6c05923a43ea37b10504396105d97ba51c0430e82fb3bb4953ee4256cdc1aa","a38737476856236f33e0aef1b97e2b900677cf99f1dbcfd381c708ceeca8b7f3","ab4502eff76606ac8e58b623878f3d76e1e0b18f30796aec7c446e2aab8b57c8","ab69ebe00eac40c68aec6131ef0559967b399266aae89f9673d3c30ce12f36b0","c01c2c98f13a65b49113916a2f295e46a03fc4c185ece880267dd86c3f3dee8b","d557525e3ebfad227c8d6db9bb6350d42506fb02b1de8058cb443f4f008c0f67","d9718eb12e9ae9ea3a29a9904f131f53e99c584ef1b297aa05f1f8f6cdc6ab2a","fa8cd29fe962d7e853457e829c2d5927685f0ff374e6450fff5529f785db442d"],"schema":"fsgg.atlas/1"} +{"records":["049f1d0d8155f65b6f7675ebdb200d4595570cc7d585dd74876e0285eceffed6","0863093e69a16355fa9b90f8e7e29156ea4ad88c4523bc4883568bde831bdf35","2056fa8b653c2240afdad2a16f572a07019109254847829610a9468bb2f95248","231df3792931ad2333d00970a5993b3e13fa9dc38f687899f9fa2c966ed9fbfa","24fdc3980640ba58bedcae6f16d5b467eb7383c31161fb9b6ff10fd2e667b0e5","264a2f7887a9b6473cbc24102e1bf9be70e57f6d7ba224138140efa094d541f4","2b7dfb3a2f76368cb603161cdb6f8fa8c2b8afc3e96659dd37c975921ba5ba66","33df463263533529cca24a0b09937573f836d197cc1a739e9960b1cdd4deb1fd","343077d02ddd266b21d8677f7c256854134f7e3e2d18137b55ebe3f32564c5c0","34391bda3fb56135a2bddf74ab790958c175bc6a69c38175e6f8db792cfd50a0","36a19fefd7024f54f3391ffed19019c10222a514719edd7a44f0cde2a1c48258","3d3a0a302c02cd0bfee86715d09b0211baa8dad3e47402c638fc95c7cfa02e87","3de8718fb3ca9b15212e8bd84fba205995d24636087090a61c63c28a96a745b1","3df4f31fda99ad137c651322ad29809e36380400516399274870b4a4332b4a92","3f09ee199e3c2e808e0f6e74f197f2fdd8158ae099a7195c498ce9466bb78b03","44bda82d6f9a9625a83283bf56a15a1831f287037214dc134eeeae87f1faa869","6c5b38ee260d1f15f8029d699ec1cebceafb5e1407a6341dc7a4b96f4d9d3521","71bc29a175940d22dd1d85148ee49f47aed714395fbd51d07b8751147815a55f","72742cbaa561237f10251e94aa5c61153ab5c7f14fc5771b7a5ebdece1e1cb11","7eba935f363db90d4b6d3fea2d226a7a58b08b88cf748e619ddb407ea83911e9","85f06991da74fa4e59ba73c7034dc53a35a4e90c5b64d83cc8956d3afd61d376","87d6de6f3730cecd75d9a4411bf4dbac4e058127a3ce9f46b07f6de110bc26de","8b1cf51c5f03c0ae19c46ca3ea4a1158acbb6e47bf109ca8c506c5417b51cf9f","8c514796f190fe6d4307be57983c84f92208f5d2f00fcbe3093b72618f2d7bc1","95115e1052b5ea4fcd393f5c9b9c6f0298657f0028eee84e16215ede97576cfa","9b6c05923a43ea37b10504396105d97ba51c0430e82fb3bb4953ee4256cdc1aa","a38737476856236f33e0aef1b97e2b900677cf99f1dbcfd381c708ceeca8b7f3","ab4502eff76606ac8e58b623878f3d76e1e0b18f30796aec7c446e2aab8b57c8","ab69ebe00eac40c68aec6131ef0559967b399266aae89f9673d3c30ce12f36b0","c01c2c98f13a65b49113916a2f295e46a03fc4c185ece880267dd86c3f3dee8b","d557525e3ebfad227c8d6db9bb6350d42506fb02b1de8058cb443f4f008c0f67","d9718eb12e9ae9ea3a29a9904f131f53e99c584ef1b297aa05f1f8f6cdc6ab2a","fa8cd29fe962d7e853457e829c2d5927685f0ff374e6450fff5529f785db442d"],"schema":"fsgg.atlas/1"} diff --git a/docs/atlas/records/7eba935f363db90d4b6d3fea2d226a7a58b08b88cf748e619ddb407ea83911e9.json b/docs/atlas/records/7eba935f363db90d4b6d3fea2d226a7a58b08b88cf748e619ddb407ea83911e9.json new file mode 100644 index 00000000..d6cf1113 --- /dev/null +++ b/docs/atlas/records/7eba935f363db90d4b6d3fea2d226a7a58b08b88cf748e619ddb407ea83911e9.json @@ -0,0 +1 @@ +{"citations":[{"path":"docs/roadmaps/wasm-shared-01.md","repository":"FS-GG/FS.GG.Game","revision":"fae6b000131f3e5f718c4294844c73d767150db4","sha256":"1b18d63e3dd7633818f39357336378230101cf52aa745b3d8f0bcf59fda4f5f5"},{"path":"tests/Wasm.PackageConsumer/Program.fs","repository":"FS-GG/FS.GG.Game","revision":"fae6b000131f3e5f718c4294844c73d767150db4","sha256":"3c53d61db88ebe7eddbdce897b3976cfe7f75f714eeac4e28bc037c5e9fb244c"},{"path":"tests/Wasm.PackageConsumer/browser/package-consumer.mjs","repository":"FS-GG/FS.GG.Game","revision":"fae6b000131f3e5f718c4294844c73d767150db4","sha256":"d2994c129805a73a23505237dc8582be386e7a63ca14771ebb2f393a2024e966"},{"path":"tests/Wasm.PackageConsumer/browser/package-consumer.spec.mjs","repository":"FS-GG/FS.GG.Game","revision":"fae6b000131f3e5f718c4294844c73d767150db4","sha256":"8c35dcb9ab07d91aa631c85bf7ff60539ac0ad98d67b929627fdd1d8d3387c2b"},{"path":"tests/Wasm.Lifecycle.Tests/LifecycleTests.fs","repository":"FS-GG/FS.GG.Game","revision":"fae6b000131f3e5f718c4294844c73d767150db4","sha256":"757cedc77d5c9c4e55c4bb9ee489e31a92f6ab5df11ccecd3344d7dadd9d74f5"},{"path":"src/Wasm.Browser/Lifecycle.fs","repository":"FS-GG/FS.GG.Game","revision":"bc78722f7a62a1ce5f0351d7969a26c307ec4fe7","sha256":"3eeb9466880a9cc8db0284a3422d3e93d8d0634c128ceb32fc8aa456c98c0437"},{"path":"src/Wasm.Browser/Lifecycle.fsi","repository":"FS-GG/FS.GG.Game","revision":"bc78722f7a62a1ce5f0351d7969a26c307ec4fe7","sha256":"d263e8dd7535335751fd5480e47babce440f8ca0975c0d3cd8159bf29bc7f477"},{"path":"src/Wasm.Browser/Host.fs","repository":"FS-GG/FS.GG.Game","revision":"bc78722f7a62a1ce5f0351d7969a26c307ec4fe7","sha256":"c502f8f9c55ec78dada7908241ae2f65242f442b91d14821bb83aef2c2319957"},{"path":"src/Wasm.Browser/Admission.fs","repository":"FS-GG/FS.GG.Game","revision":"bc78722f7a62a1ce5f0351d7969a26c307ec4fe7","sha256":"0bb3799d2ee00ddbbbfa413db6b616a692e3228943d1fb43cf3c370f1e0475dc"},{"path":"eng/wasm-shared/version.props","repository":"FS-GG/FS.GG.Game","revision":"bc78722f7a62a1ce5f0351d7969a26c307ec4fe7","sha256":"6b50423f8f30da7d52305ee527baaddd91f77cc422f7585cf61c063bcad209e1"}],"conflictsWith":[],"id":"game/closed-worker-build-boundary","kind":"lesson","recordedAt":"2026-10-18T00:18:37Z","repository":"FS-GG/FS.GG.Game","schema":"fsgg.atlas/1","status":"recorded","summary":"Original0.6 source PR722/run39079271365/attempt1 passed at5d39; native merged producerbc78722f7a62a1ce5f0351d7969a26c307ec4fe7 has exact tested/delivered tree9df5fea131983d4d9ec98176106df62f143ad529. Source/candidate-installed CLR/Fable/numeric and all36 package-browser cases passed, including the public instruction-dense history. This is exact PR acceptance, not protection against a different subsequent result or proof of earlier timeout CPU cause.\nFirst protected original39080719438/attempt1 failed SDK browser qualification with35PASS/1FAIL8.3s: inherited terminal Process mode11 expected followerHostDisposed, but actual no-outcome/discarded follower retainedDeadlineExpired. The new66524-byte dense module passed allfour operations and94 complete observations across its three fresh Workers. The failed original retained model artifact12119445244/SHA549a7767f21cb8e01b66b2a3207c791d544006fc8fb93518e83464ba37f1466e/27974bytes; no eligible package custody/publication acceptance resulted. Neither the passed dense control nor failed follower assertion settles prior timing causes.\nDefaultCompatibility expireAt partitions and settles queued due= and held semantics remain distinct and unchanged.\nThe source delta is test/owning documentation only; parser/lifecycle/clock/caps/ABI/version/workflow code remains unchanged, with unpublished0.6 and the same36 real-browser cases/35-minute profile. Existing clean-source inert preflight and syntax checks passed; actual compilation/Fable, repaired metadata observation, boundary cohort/real browser behavior/cleanup and fit remain pending. Original failures and all prior source/publication/installed evidence stay preserved. No unchanged rerun,0.6publication/installed/consumer/image/native operation or whole-product acceptance is selected by this correction.","supersedes":["9b6c05923a43ea37b10504396105d97ba51c0430e82fb3bb4953ee4256cdc1aa"],"tags":["wasm","expiry","deadline","terminal","first-cause","qualification","current-only"],"title":"Bind Default terminal follower cause to original due and actual producer clock"} From ed35ba5affc25efcb38213ad6e2f568d542ccd49 Mon Sep 17 00:00:00 2001 From: EHotwagner Date: Sun, 18 Oct 2026 02:36:37 +0200 Subject: [PATCH 6/7] Compare actual terminal follower bytes without changing receipt semantics --- docs/roadmaps/wasm-shared-01.md | 13 +++++++++++++ .../browser/package-consumer.spec.mjs | 5 ++++- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/docs/roadmaps/wasm-shared-01.md b/docs/roadmaps/wasm-shared-01.md index e62402bb..5fbd7ae4 100644 --- a/docs/roadmaps/wasm-shared-01.md +++ b/docs/roadmaps/wasm-shared-01.md @@ -83,6 +83,19 @@ parser, deadlines/caps/version and all36 real-browser cases remain unchanged. The repaired current source still requires independent review, exact native qualification, complete custody and fit under the existing35-minute profile. +The first corrected source attempt, [39082520529/attempt1](https://github.com/FS-GG/FS.GG.Game/actions/runs/39082520529), +compiled and passed all50 Lifecycle controls, including the six new reducer and +three connected-host boundaries. Its browser cohort failed29PASS/7FAIL: the new +shared assertion compared an actual Fable `Uint8Array` receipt to plain `[]`. +All seven first failures were this representation mismatch, with matched reason +and no-outcome/discarded/undispatched fields. The reports retain the actual clocks; +mode11 recorded due458, decision457 and callback459. Assertions after that shared +comparison remained blocked and are not accepted. The repair compares actual byte +content with `Array.from`, as existing ABI assertions already do, while preserving +all first-cause, identity, ordering, no-post and retirement checks. Runtime code +and all36 browser cases remain unchanged. Fresh exact-head full qualification +is required; this failed original yielded no eligible package custody. + ## Accepted 0.5 closed Worker and installed qualifier The accepted source successor selected coherent `0.5.0` for the pair and SDK. diff --git a/tests/Wasm.PackageConsumer/browser/package-consumer.spec.mjs b/tests/Wasm.PackageConsumer/browser/package-consumer.spec.mjs index ff6cd66b..dd3a46c5 100644 --- a/tests/Wasm.PackageConsumer/browser/package-consumer.spec.mjs +++ b/tests/Wasm.PackageConsumer/browser/package-consumer.spec.mjs @@ -206,7 +206,10 @@ function expectTerminalFollower(value) { expect(decision.disposed).toBe(true); expect(decision.settledRequests).not.toContain(value.reset.Request); const expired=BigInt(value.followerDue) Date: Sun, 18 Oct 2026 02:36:55 +0200 Subject: [PATCH 7/7] Retain original typed-output fixture failure and exact qualification boundary --- docs/atlas/catalog.json | 2 +- ...e34990113bb43cb8cd58cf251d06083cf812398d6da9fdebb98e0f5.json | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 docs/atlas/records/0b92e4db5e34990113bb43cb8cd58cf251d06083cf812398d6da9fdebb98e0f5.json diff --git a/docs/atlas/catalog.json b/docs/atlas/catalog.json index 8611ece2..b83c2bfe 100644 --- a/docs/atlas/catalog.json +++ b/docs/atlas/catalog.json @@ -1 +1 @@ -{"records":["049f1d0d8155f65b6f7675ebdb200d4595570cc7d585dd74876e0285eceffed6","0863093e69a16355fa9b90f8e7e29156ea4ad88c4523bc4883568bde831bdf35","2056fa8b653c2240afdad2a16f572a07019109254847829610a9468bb2f95248","231df3792931ad2333d00970a5993b3e13fa9dc38f687899f9fa2c966ed9fbfa","24fdc3980640ba58bedcae6f16d5b467eb7383c31161fb9b6ff10fd2e667b0e5","264a2f7887a9b6473cbc24102e1bf9be70e57f6d7ba224138140efa094d541f4","2b7dfb3a2f76368cb603161cdb6f8fa8c2b8afc3e96659dd37c975921ba5ba66","33df463263533529cca24a0b09937573f836d197cc1a739e9960b1cdd4deb1fd","343077d02ddd266b21d8677f7c256854134f7e3e2d18137b55ebe3f32564c5c0","34391bda3fb56135a2bddf74ab790958c175bc6a69c38175e6f8db792cfd50a0","36a19fefd7024f54f3391ffed19019c10222a514719edd7a44f0cde2a1c48258","3d3a0a302c02cd0bfee86715d09b0211baa8dad3e47402c638fc95c7cfa02e87","3de8718fb3ca9b15212e8bd84fba205995d24636087090a61c63c28a96a745b1","3df4f31fda99ad137c651322ad29809e36380400516399274870b4a4332b4a92","3f09ee199e3c2e808e0f6e74f197f2fdd8158ae099a7195c498ce9466bb78b03","44bda82d6f9a9625a83283bf56a15a1831f287037214dc134eeeae87f1faa869","6c5b38ee260d1f15f8029d699ec1cebceafb5e1407a6341dc7a4b96f4d9d3521","71bc29a175940d22dd1d85148ee49f47aed714395fbd51d07b8751147815a55f","72742cbaa561237f10251e94aa5c61153ab5c7f14fc5771b7a5ebdece1e1cb11","7eba935f363db90d4b6d3fea2d226a7a58b08b88cf748e619ddb407ea83911e9","85f06991da74fa4e59ba73c7034dc53a35a4e90c5b64d83cc8956d3afd61d376","87d6de6f3730cecd75d9a4411bf4dbac4e058127a3ce9f46b07f6de110bc26de","8b1cf51c5f03c0ae19c46ca3ea4a1158acbb6e47bf109ca8c506c5417b51cf9f","8c514796f190fe6d4307be57983c84f92208f5d2f00fcbe3093b72618f2d7bc1","95115e1052b5ea4fcd393f5c9b9c6f0298657f0028eee84e16215ede97576cfa","9b6c05923a43ea37b10504396105d97ba51c0430e82fb3bb4953ee4256cdc1aa","a38737476856236f33e0aef1b97e2b900677cf99f1dbcfd381c708ceeca8b7f3","ab4502eff76606ac8e58b623878f3d76e1e0b18f30796aec7c446e2aab8b57c8","ab69ebe00eac40c68aec6131ef0559967b399266aae89f9673d3c30ce12f36b0","c01c2c98f13a65b49113916a2f295e46a03fc4c185ece880267dd86c3f3dee8b","d557525e3ebfad227c8d6db9bb6350d42506fb02b1de8058cb443f4f008c0f67","d9718eb12e9ae9ea3a29a9904f131f53e99c584ef1b297aa05f1f8f6cdc6ab2a","fa8cd29fe962d7e853457e829c2d5927685f0ff374e6450fff5529f785db442d"],"schema":"fsgg.atlas/1"} +{"records":["049f1d0d8155f65b6f7675ebdb200d4595570cc7d585dd74876e0285eceffed6","0863093e69a16355fa9b90f8e7e29156ea4ad88c4523bc4883568bde831bdf35","0b92e4db5e34990113bb43cb8cd58cf251d06083cf812398d6da9fdebb98e0f5","2056fa8b653c2240afdad2a16f572a07019109254847829610a9468bb2f95248","231df3792931ad2333d00970a5993b3e13fa9dc38f687899f9fa2c966ed9fbfa","24fdc3980640ba58bedcae6f16d5b467eb7383c31161fb9b6ff10fd2e667b0e5","264a2f7887a9b6473cbc24102e1bf9be70e57f6d7ba224138140efa094d541f4","2b7dfb3a2f76368cb603161cdb6f8fa8c2b8afc3e96659dd37c975921ba5ba66","33df463263533529cca24a0b09937573f836d197cc1a739e9960b1cdd4deb1fd","343077d02ddd266b21d8677f7c256854134f7e3e2d18137b55ebe3f32564c5c0","34391bda3fb56135a2bddf74ab790958c175bc6a69c38175e6f8db792cfd50a0","36a19fefd7024f54f3391ffed19019c10222a514719edd7a44f0cde2a1c48258","3d3a0a302c02cd0bfee86715d09b0211baa8dad3e47402c638fc95c7cfa02e87","3de8718fb3ca9b15212e8bd84fba205995d24636087090a61c63c28a96a745b1","3df4f31fda99ad137c651322ad29809e36380400516399274870b4a4332b4a92","3f09ee199e3c2e808e0f6e74f197f2fdd8158ae099a7195c498ce9466bb78b03","44bda82d6f9a9625a83283bf56a15a1831f287037214dc134eeeae87f1faa869","6c5b38ee260d1f15f8029d699ec1cebceafb5e1407a6341dc7a4b96f4d9d3521","71bc29a175940d22dd1d85148ee49f47aed714395fbd51d07b8751147815a55f","72742cbaa561237f10251e94aa5c61153ab5c7f14fc5771b7a5ebdece1e1cb11","7eba935f363db90d4b6d3fea2d226a7a58b08b88cf748e619ddb407ea83911e9","85f06991da74fa4e59ba73c7034dc53a35a4e90c5b64d83cc8956d3afd61d376","87d6de6f3730cecd75d9a4411bf4dbac4e058127a3ce9f46b07f6de110bc26de","8b1cf51c5f03c0ae19c46ca3ea4a1158acbb6e47bf109ca8c506c5417b51cf9f","8c514796f190fe6d4307be57983c84f92208f5d2f00fcbe3093b72618f2d7bc1","95115e1052b5ea4fcd393f5c9b9c6f0298657f0028eee84e16215ede97576cfa","9b6c05923a43ea37b10504396105d97ba51c0430e82fb3bb4953ee4256cdc1aa","a38737476856236f33e0aef1b97e2b900677cf99f1dbcfd381c708ceeca8b7f3","ab4502eff76606ac8e58b623878f3d76e1e0b18f30796aec7c446e2aab8b57c8","ab69ebe00eac40c68aec6131ef0559967b399266aae89f9673d3c30ce12f36b0","c01c2c98f13a65b49113916a2f295e46a03fc4c185ece880267dd86c3f3dee8b","d557525e3ebfad227c8d6db9bb6350d42506fb02b1de8058cb443f4f008c0f67","d9718eb12e9ae9ea3a29a9904f131f53e99c584ef1b297aa05f1f8f6cdc6ab2a","fa8cd29fe962d7e853457e829c2d5927685f0ff374e6450fff5529f785db442d"],"schema":"fsgg.atlas/1"} diff --git a/docs/atlas/records/0b92e4db5e34990113bb43cb8cd58cf251d06083cf812398d6da9fdebb98e0f5.json b/docs/atlas/records/0b92e4db5e34990113bb43cb8cd58cf251d06083cf812398d6da9fdebb98e0f5.json new file mode 100644 index 00000000..2d690e15 --- /dev/null +++ b/docs/atlas/records/0b92e4db5e34990113bb43cb8cd58cf251d06083cf812398d6da9fdebb98e0f5.json @@ -0,0 +1 @@ +{"citations":[{"path":"docs/roadmaps/wasm-shared-01.md","repository":"FS-GG/FS.GG.Game","revision":"ed35ba5affc25efcb38213ad6e2f568d542ccd49","sha256":"1d14f1661b2857c3cc52df5e3004c3c73add5ba15362a079e08556acbb72bf09"},{"path":"tests/Wasm.PackageConsumer/Program.fs","repository":"FS-GG/FS.GG.Game","revision":"fae6b000131f3e5f718c4294844c73d767150db4","sha256":"3c53d61db88ebe7eddbdce897b3976cfe7f75f714eeac4e28bc037c5e9fb244c"},{"path":"tests/Wasm.PackageConsumer/browser/package-consumer.mjs","repository":"FS-GG/FS.GG.Game","revision":"fae6b000131f3e5f718c4294844c73d767150db4","sha256":"d2994c129805a73a23505237dc8582be386e7a63ca14771ebb2f393a2024e966"},{"path":"tests/Wasm.PackageConsumer/browser/package-consumer.spec.mjs","repository":"FS-GG/FS.GG.Game","revision":"ed35ba5affc25efcb38213ad6e2f568d542ccd49","sha256":"8971b6aa2eeab82d5be20bc2e9bad6dc9f82c76ce86cee903aeff3a00401fbc4"},{"path":"tests/Wasm.Lifecycle.Tests/LifecycleTests.fs","repository":"FS-GG/FS.GG.Game","revision":"fae6b000131f3e5f718c4294844c73d767150db4","sha256":"757cedc77d5c9c4e55c4bb9ee489e31a92f6ab5df11ccecd3344d7dadd9d74f5"},{"path":"src/Wasm.Browser/Lifecycle.fs","repository":"FS-GG/FS.GG.Game","revision":"bc78722f7a62a1ce5f0351d7969a26c307ec4fe7","sha256":"3eeb9466880a9cc8db0284a3422d3e93d8d0634c128ceb32fc8aa456c98c0437"},{"path":"src/Wasm.Browser/Lifecycle.fsi","repository":"FS-GG/FS.GG.Game","revision":"bc78722f7a62a1ce5f0351d7969a26c307ec4fe7","sha256":"d263e8dd7535335751fd5480e47babce440f8ca0975c0d3cd8159bf29bc7f477"},{"path":"src/Wasm.Browser/Host.fs","repository":"FS-GG/FS.GG.Game","revision":"bc78722f7a62a1ce5f0351d7969a26c307ec4fe7","sha256":"c502f8f9c55ec78dada7908241ae2f65242f442b91d14821bb83aef2c2319957"},{"path":"src/Wasm.Browser/Admission.fs","repository":"FS-GG/FS.GG.Game","revision":"bc78722f7a62a1ce5f0351d7969a26c307ec4fe7","sha256":"0bb3799d2ee00ddbbbfa413db6b616a692e3228943d1fb43cf3c370f1e0475dc"},{"path":"eng/wasm-shared/version.props","repository":"FS-GG/FS.GG.Game","revision":"bc78722f7a62a1ce5f0351d7969a26c307ec4fe7","sha256":"6b50423f8f30da7d52305ee527baaddd91f77cc422f7585cf61c063bcad209e1"}],"conflictsWith":[],"id":"game/closed-worker-build-boundary","kind":"lesson","recordedAt":"2026-10-18T00:36:55Z","repository":"FS-GG/FS.GG.Game","schema":"fsgg.atlas/1","status":"recorded","summary":"Original0.6 source PR722/run39079271365/attempt1 passed at5d39; native merged producerbc78722f7a62a1ce5f0351d7969a26c307ec4fe7 has exact tested/delivered tree9df5fea131983d4d9ec98176106df62f143ad529. Source/candidate-installed CLR/Fable/numeric and all36 package-browser cases passed, including the public instruction-dense history. This is exact PR acceptance, not protection against a different subsequent result or proof of earlier timeout CPU cause.\nFirst protected original39080719438/attempt1 failed SDK browser qualification with35PASS/1FAIL8.3s: inherited terminal Process mode11 expected followerHostDisposed, but actual no-outcome/discarded follower retainedDeadlineExpired. The new66524-byte dense module passed allfour operations and94 complete observations across its three fresh Workers. The failed original retained model artifact12119445244/SHA549a7767f21cb8e01b66b2a3207c791d544006fc8fb93518e83464ba37f1466e/27974bytes; no eligible package custody/publication acceptance resulted. Neither the passed dense control nor failed follower assertion settles prior timing causes.\nDefaultCompatibility expireAt partitions and settles queued due= and held semantics remain distinct and unchanged.\nThe source delta is test/owning documentation only; parser/lifecycle/clock/caps/ABI/version/workflow code remains unchanged, with unpublished0.6 and the same36 real-browser cases/35-minute profile. Existing clean-source inert preflight and syntax checks passed; actual compilation/Fable, repaired metadata observation, boundary cohort/real browser behavior/cleanup and fit remain pending. Original failures and all prior source/publication/installed evidence stay preserved. No unchanged rerun,0.6publication/installed/consumer/image/native operation or whole-product acceptance is selected by this correction.\nThe first corrected original PR723 qualification39082520529/attempt1 at5d77 compiled, passed all50 Lifecycle controls (including the nine new reducer/connected-host boundaries), and then failed29PASS/7FAIL in the36-case browser cohort. All seven first failures were the new shared helper comparing actual Fable Uint8Array empty output to plain Array[]; matching first-cause/no-outcome/discarded/undispatched fields agreed. Seven original scalar reports were complete; mode11 due458/decision457/callback459 demonstrates the intended distinction. Assertions after the shared byte comparison were blocked, not passed; first-family retirement assertions after it were also unqualified. The narrow repair uses Array.from(actualOutput).toEqual([]), matching existing real ABI assertions without defaulting missing bytes or changing the receipt. Full terminal caller/clock/effect and byte-projection siblings were swept; no production defect was identified. Exact native synthetic d578b7f636c09d1b6573427cf4ed55b6519b8e69 has ordered bc/5d parents and equal candidate treeb36d03d7e1975e9f228e0d37cbfbde3c1a2e4025. Original model artifact12119227914 retained SHA6ec92956d407ad526ca8e1046907b961a98e42b5007192e248b662aa60c2fcaf/27975bytes; no eligible package custody resulted. Node syntax and diff checks pass for this repair; fresh exact-head full hosted qualification remains mandatory, with all original failures/budgets and separate unpublished0.6 gates unchanged.","supersedes":["7eba935f363db90d4b6d3fea2d226a7a58b08b88cf748e619ddb407ea83911e9"],"tags":["wasm","expiry","deadline","terminal","first-cause","qualification","current-only"],"title":"Bind Default terminal follower cause to original due and actual producer clock"}