Skip to content

feat(core): add durable server and indexing activation - #114

Closed
kunkunGames wants to merge 6 commits into
tumourlove:masterfrom
kunkunGames:jules/codex/core/persistent-service-activation
Closed

feat(core): add durable server and indexing activation#114
kunkunGames wants to merge 6 commits into
tumourlove:masterfrom
kunkunGames:jules/codex/core/persistent-service-activation

Conversation

@kunkunGames

@kunkunGames kunkunGames commented Jul 26, 2026

Copy link
Copy Markdown

Goal

Control MCP-server activation and source/project index-writer activation independently and persistently, while preserving the same fail-closed contract across failures, external edits, restarts, and Editor crashes.

Plain-language explanation

Selections made through Monolith.StartServer / Monolith.StopServer and Monolith.StartIndexing / Monolith.StopIndexing persist into the next Editor run and take effect immediately. Server and indexing settings do not overwrite one another, and neither can bypass the project's hard policy gates.

Improvements

  • Project defaults and generated per-user overrides are composed independently.
  • Malformed or unreadable activation state fails closed.
  • A bounded cache and the core ticker revalidate external configuration changes.
  • Only the Monolith HTTP route is unbound; Unreal Engine's process-shared listener remains intact.
  • Project/source reindex results and Settings-button eligibility now match the writer's actual acceptance predicate.
  • Active writers drain safely after Stop, while existing database reads remain available.

Before → After

Before After
Service-activation choices could diverge between processes and code paths Independent durable server and indexing state backed by shared predicates
A fail-closed cache could persist forever if an unreadable config recovered with the same timestamp After the one-second revalidation interval, the timestamp fast path is bypassed until a read succeeds
An Editor that started with activation off could leave a crashed-owner sentinel behind Current-process and dead-process sentinels are reclaimed safely before activation evaluation
A reindex handler could report success after the writer rejected the start The writer's real acceptance result or error is returned

Side-effect analysis

  • The bMcpServerEnabled, bEnableIndex, and bEnableSource hard gates always take precedence.
  • Unreadable activation remains fail closed within the normal cache interval and does not busy-loop on disk probes.
  • Sentinels owned by another live Editor, malformed sentinels, and sentinels replaced during validation are preserved.
  • Stop does not forcibly abort an in-progress transaction.
  • A live ServerPort change is rejected without dropping the working route and returns restart guidance.
  • StopAllListeners() is never called, so listeners owned by other plugins remain untouched.

Resolved review findings

Commit 62aba1b1f10948bae0e9cabb7f6c19150f5f5d2b addresses the two remaining review findings:

  1. Activation resolution now propagates an unreadable-input state. After the normal one-second interval, it retries the read even when the timestamp is unchanged and clears retry state after recovery.
  2. Non-commandlet startup validates the sentinel's numeric PID and removes it only for a current-process reload or a dead process. Immediately before deletion, it rereads both the owner and the complete serialized content to prevent a replacement race.

Regression tests deterministically cover unreadable-to-readable recovery with an unchanged timestamp and dead, live, current-process, malformed, and replaced sentinels.

Verification

  • Protected UE 5.7 Editor build: changed MonolithSettings.cpp, MonolithCoreModule.cpp, and activation tests were recompiled; UnrealEditor-MonolithCore.dll was relinked; wrapper exit 0.
  • UE 5.7 Monolith.Activation: 6/6 PASS, with 0 test warnings, 0 errors, and process exit 0. Report: D:\P4\MonolithPR114ReviewUE57Host\Saved\Automation\PR114ReviewRound5FinalUE57\index.json.
  • Protected UE 5.8 Editor build: full 436-action compile and link completed successfully; protected wrapper exit 0.
  • UE 5.8 Monolith.Activation: 6/6 PASS, with 0 test warnings, 0 errors, and process exit 0. Report: D:\P4\MonolithPR114ReviewUE58Host\Saved\Automation\PR114ReviewRound5FinalUE58\index.json.
  • The UE 5.8 build snapshot and public commit source had identical blob hashes for all five changed source and test files.
  • Differential run with the same current static-check configuration: base and branch each reported 36 blockers and 802 advisories; 0 findings were introduced or resolved by this PR. The target base does not contain the latest hosted checker and configuration, so the same checker and configuration were applied to both sides, with only incompatible offline-executable freshness checks disabled.
  • git diff --check: PASS. Monolith source contains 0 calls to FHttpServerModule::StopAllListeners(). The branch was 0 commits behind the latest tumourlove/master.
  • Verification record: Docs/testing/2026-07-26-persistent-service-activation.md.

WorkFingerprint

  • agent: Codex
  • category: lifecycle / reliability / activation
  • module: MonolithCore, MonolithIndex, MonolithSource, MonolithEditor
  • component/action/helper: durable activation resolver/cache, core lifecycle reconciler, sentinel ownership, project/source writer acceptance
  • intended files: MonolithSettings.{h,cpp}, MonolithCoreModule.{h,cpp}, MonolithHttpServer.{h,cpp}, MonolithSentinelFile.h, MonolithCoreTools.cpp, Index/Source subsystem and indexer activation surfaces, Settings customization, matching specs/tests/config
  • risk type: unintended service restart, stale readiness marker, shared-listener damage, false reindex acceptance, writer teardown race
  • public API impact: yes, persistent console-command behavior and honest reindex-result contract
  • docs/spec impact: yes

Duplicate check

On 2026-07-27, open PRs #104, #112, #113, and #114, their related remote branches, and their actual changed files were checked again. #104 targets updater selection, #112 targets discovery filtering, and #113 targets project search and FTS repair. #112 overlaps physically in MonolithCoreTools.cpp, and #113 overlaps in the Index subsystem and shared documentation, but they own discovery/read-search contracts while this PR owns activation and write acceptance. No other open PR provides the same persistent activation and sentinel-reclamation implementation. Common files may require rebasing depending on merge order, and that conflict risk remains explicit.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 1cdd55b192

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread Source/MonolithIndex/Private/MonolithIndexSubsystem.cpp Outdated
Comment thread Source/MonolithSource/Private/MonolithSourceSubsystem.cpp Outdated
Comment thread Source/MonolithCore/Private/MonolithHttpServer.cpp Outdated
@kunkunGames kunkunGames changed the title ⚙️ Codex: add persistent server and indexing activation feat(core): add persistent server and indexing activation Jul 26, 2026
@kunkunGames
kunkunGames force-pushed the jules/codex/core/persistent-service-activation branch from 1cdd55b to 5c5fe7a Compare July 26, 2026 15:26

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 5c5fe7a6a9

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread Source/MonolithCore/Private/MonolithCoreModule.cpp Outdated
Comment thread Source/MonolithCore/Private/MonolithHttpServer.cpp Outdated
Comment thread Source/MonolithCore/Private/MonolithCoreTools.cpp
@kunkunGames
kunkunGames force-pushed the jules/codex/core/persistent-service-activation branch from 5c5fe7a to bebc7b1 Compare July 26, 2026 16:14

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: bebc7b13ce

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread Source/MonolithCore/Private/MonolithSettings.cpp Outdated
Comment thread Source/MonolithCore/Private/MonolithHttpServer.cpp
Comment thread Source/MonolithCore/Private/MonolithCoreModule.cpp Outdated
@kunkunGames
kunkunGames force-pushed the jules/codex/core/persistent-service-activation branch 4 times, most recently from 9d3e222 to 996b6f6 Compare July 26, 2026 17:57

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 996b6f63c5

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread Source/MonolithCore/Private/MonolithSettings.cpp Outdated
@kunkunGames
kunkunGames force-pushed the jules/codex/core/persistent-service-activation branch from 996b6f6 to 8fc4f8e Compare July 26, 2026 19:08

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 8fc4f8e1a6

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread Source/MonolithEditor/Private/MonolithSettingsCustomization.cpp Outdated
@kunkunGames
kunkunGames force-pushed the jules/codex/core/persistent-service-activation branch from 8fc4f8e to 2232c7c Compare July 26, 2026 19:39
…reconciler state

Three defects found in a follow-up self-review of this branch.

`ProbePort` returned false whenever `FSocket::SetNonBlocking(true)` failed,
because the whole result was gated on `bNonBlocking`. Probe *setup* failure was
therefore indistinguishable from "port not listening": on a platform that
cannot switch the socket to non-blocking, `Start()` would exhaust every attempt
against a listener that had actually bound, and the server could never start.
The bounded non-blocking path stays as the fast path; a blocking `Connect` —
which is what master did — is now the fallback.

`HandleReindex` read a bool out of a `ProcessEvent` parameter buffer without
checking the reflected signature. `MonolithCore` reaches `MonolithIndex` only
through reflection and has no compile-time dependency on it, so if
`StartFullIndex` / `StartIncrementalIndex` ever stopped returning bool, the
zeroed buffer would read false and the action would report `reindex_not_started`
for work that actually started. It now requires an `FBoolProperty` return and
reports an explicit module-sync error otherwise, rather than guessing from an
untyped buffer.

`ReconcileHttpServerActivation` carried an unreachable first-tick branch:
`StartupModule` sets `bHasResolvedServerActivation = true` before `AddTicker`,
so the ticker could never observe it false. Removed the branch and the flag; the
baseline is resolved before the ticker exists, which is what the code already
did.

Also documents, rather than changes, the fact that `Monolith.StopServer` cannot
release the OS listener — UE exposes no per-port teardown — so a port-based
liveness check is not a valid readiness signal after a Stop; the sentinel is.

Verified at this head on both engines:

- UE 5.7: editor build Succeeded, `Monolith.Activation` + `Monolith.Source`
  15/15 under `-RenderOffscreen`, 0 failed, exit 0.
- UE 5.8: editor build Succeeded, same suite 15/15, 0 failed, exit 0.
@kunkunGames

Copy link
Copy Markdown
Author

Self-review pass on this branch before it reaches you. Three defects found and fixed in c438e6ff, plus one behavior documented rather than changed.

ProbePort failed closed on probe setup failure. The result was gated on bNonBlocking, so if FSocket::SetNonBlocking(true) returned false the probe reported "not listening" regardless of the port's actual state. On such a platform Start() would exhaust every attempt against a listener that had genuinely bound, and the server could never start. The bounded non-blocking path stays as the fast path — it's what removes the ~2s blocking-connect stall on a closed Windows loopback port — and a blocking Connect, which is what master did, is now the fallback.

HandleReindex trusted an unverified reflected return. MonolithCore reaches MonolithIndex only through reflection, with no compile-time dependency, so nothing checks that StartFullIndex / StartIncrementalIndex still return bool. If that ever drifted to void, ProcessEvent would leave the parameter buffer zeroed and the action would report reindex_not_started for a re-index that actually started. It now requires CastField<FBoolProperty>(Func->GetReturnProperty()) and returns an explicit module-sync error when the contract doesn't hold, rather than reading an answer out of an untyped buffer.

Dead state in the reconciler. ReconcileHttpServerActivation had a first-tick !bHasResolvedServerActivation branch that could never run — StartupModule sets the flag before AddTicker. Removed the branch and the flag; the baseline is resolved before the ticker exists, which is what the code already did.

Documented rather than changed: Monolith.StopServer unbinds Monolith routes but cannot release the OS listener, because UE 5.7/5.8 expose no per-port teardown and this branch deliberately stopped calling process-wide StopAllListeners() (0 calls in Monolith source). So after a Stop the port still answers, it just serves no Monolith route — a port probe is not a valid readiness signal, the sentinel file is. That's now stated in SPEC_MonolithCore.md instead of implied.

One known residue I deliberately left alone: sentinel removal is ownership-gated, so a sentinel left behind by a crashed process is not cleaned up by a later editor that starts with activation off, because that editor never takes ownership. The gating is still right — it's what stops one editor deleting another live editor's sentinel — but a stale-sentinel reaper belongs with the startup path, not this change. Happy to add it here if you'd rather it ship together.

Verified at head c438e6ff on both engines:

UE 5.8 UE 5.7
Editor build Result: Succeeded Result: Succeeded
Monolith.Activation + Monolith.Source 15/15, 0 failed, exit 0 15/15, 0 failed, exit 0 (-RenderOffscreen)

Full record in Docs/testing/2026-07-26-persistent-service-activation.md §6.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c438e6ffd3

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread Source/MonolithCore/Private/MonolithSettings.cpp Outdated
Comment thread Source/MonolithCore/Private/MonolithHttpServer.cpp
…ned listener

Two defects from the review pass on the previous commit.

`ReadConfigFile` collapsed "file absent" and "file exists but unreadable" into
the same empty `FConfigFile`, because `FConfigFile::Read` returns void and its
result was never probed. Absent correctly means "no user override, inherit the
project default" — but the defaults are enabled, so an unreadable file silently
re-enabled a server the user had persistently stopped, and the activation ticker
would then start it mid-session. Readability is now probed explicitly and the
three states are distinct: an unreadable user file fails closed with both
services disabled, exactly like a malformed explicit value, and is neither
migrated nor deleted.

The same conflation was worse on the write path: `SetActivationInFileUnlocked`
read, set one key, and wrote. With an unreadable file that produced a config
containing only the key being set, dropping the other one and reverting that
service to its enabled default. It now refuses the write with an explicit error
rather than persisting a file that discards state it could not read.

Second: a `MonolithCore` unload/reload left the port unstartable for the rest of
the process. Shutdown unbinds routes but deliberately leaves UE's listener up,
since UE exposes no per-port teardown, while the replacement
`FMonolithHttpServer` has no router ownership — so the pre-bind check saw
Monolith's own retained listener and treated it as a foreign owner, refusing
persistent activation and `Monolith.StartServer` until the editor exited.
`FHttpServerModule` outlives the reload, so its listener map is the authority:
`GetHttpRouter` returns the existing in-process listener's router without
rebinding, and yields nothing for a port held by another process.

Telling those two apart costs one rejected bind on the foreign-owner path, which
UE logs at Error level. That only happens where startup was going to fail
anyway, so `Monolith.Activation.OccupiedServerPort` now expects it.

Adds `Monolith.Activation.ReloadReclaimsRetainedListener`, which starts an
instance, destroys it, confirms the UE listener survives, and asserts a fresh
instance reclaims that port.

Verified at this head on both engines: editor builds succeed and
`Monolith.Activation` (6) + `Monolith.Source` (10) report 16/16 Success with
0 failed and exit 0 on UE 5.7 (`-RenderOffscreen`) and UE 5.8.
@kunkunGames

Copy link
Copy Markdown
Author

Both new findings were real and are fixed in c7bf48e0. I also owe a correction: my previous batch resolve marked these two threads resolved before I had read them. That was my error, not a judgement that they were stale.

Unreadable activation file failed open. Confirmed and worse than reported. FConfigFile::Read returns void on both 5.7 and 5.8, so ReadConfigFile never had a result to check and collapsed "absent" and "exists but unreadable" into the same empty config. Absent legitimately means inherit the project default — and those defaults are enabled — so an unreadable file silently re-enabled a server the user had persistently stopped, with the activation ticker then starting it mid-session. Readability is now probed with FFileHelper::LoadFileToString and the three states are distinct; an unreadable user file fails closed with both services disabled, exactly like a malformed explicit value, and is neither migrated nor deleted.

The write path had the same conflation with a worse outcome, which the report didn't cover: SetActivationInFileUnlocked read, set one key, and wrote. Against an unreadable file that persisted a config containing only the key being set, dropping the other one and reverting that service to its enabled default. It now refuses the write with an explicit error rather than discarding state it could not read.

Retained listener after a module reload. Confirmed. Shutdown unbinds routes but deliberately leaves UE's listener up, and the replacement instance has no router ownership, so the pre-bind check treated Monolith's own listener as a foreign owner and refused every start for the rest of the process.

FHttpServerModule outlives a MonolithCore reload, so its listener map is the authority. GetHttpRouter(Port, /*bFailOnBindFailure=*/true) returns the existing in-process listener's router without rebinding, and yields nothing for a port held by another process — which is exactly the discriminator that was missing.

One honest cost: telling the two cases apart takes one rejected bind on the foreign-owner path, and UE logs that at Error level (HttpListener unable to bind to ...). It only happens where startup was going to fail anyway and the message is accurate, so Monolith.Activation.OccupiedServerPort now expects it. I verified this empirically rather than assuming — the occupied-port test binds a real socket, and it correctly fails closed.

New test Monolith.Activation.ReloadReclaimsRetainedListener covers the reload path directly: start an instance, stop and destroy it, confirm the UE listener is still reachable, assert a fresh instance reclaims that port. It fails without the fix.

Verified at c7bf48e0 on both engines:

UE 5.8 UE 5.7
Editor build Result: Succeeded Result: Succeeded
Monolith.Activation (6) + Monolith.Source (10) 16/16 Success, 0 failed, exit 0 16/16 Success, 0 failed, exit 0 (-RenderOffscreen)

One environment note worth recording, since it cost me a false failure: an automation host left on the default ServerPort=9316 will fight a developer's running editor for that port and attribute the engine's bind error to whichever test happens to be executing. Both of my hosts now pin ServerPort=19316 in their own Config/DefaultMonolith.ini. That is host configuration, not plugin behavior, so nothing in the branch changed for it.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c7bf48e000

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread Source/MonolithSource/Private/MonolithSourceSubsystem.cpp
Comment thread Source/MonolithSource/Private/MonolithSourceSubsystem.cpp Outdated
…uild a locked DB

Two defects surfaced by review of the previous commit.

The completion handler captured raw `this`. The indexer broadcasts from its
worker thread and the handler hops to the game thread, so `Deinitialize()` could
run in between: it closes the database and deletes the indexer, and the queued
task then called `ReopenDatabase()` on a torn-down subsystem — reopening a closed
database at best, a use-after-free or module-unload crash at worst. This was
survivable before only because indexing started on explicit request; this branch
starts a catch-up run from `Initialize()`, so a fast editor close now lands
squarely in that window.

The handler now holds a `TWeakObjectPtr` and re-checks a `bIsShuttingDown` flag
set at the top of `Deinitialize()`, and `Deinitialize()` clears `OnComplete`
before destroying the indexer so no further broadcast can reach it at all.

`StartPreferredIndex` also treated "database file missing" and "database file
present but not open" identically, falling through to `TriggerReindex()` on an
explicit activation. That is a CLEAN build: it calls `ResetDatabase()` and drops
the existing engine index. A transient lock — another editor holding the file —
would therefore be silently converted into a destructive multi-minute rebuild,
leaving a partial database if interrupted. Full bootstrap is now reserved for a
genuinely absent file; an existing-but-unopenable database reports an explicit
error and leaves the index intact.

Verified at this head on both engines: editor builds succeed and
`Monolith.Activation` (6) + `Monolith.Source` (10) report 16/16 with 0 failed
and exit 0 on UE 5.7 (`-RenderOffscreen`) and UE 5.8.
@kunkunGames

Copy link
Copy Markdown
Author

Both findings from the pass on c7bf48e0 were real and are fixed in d977baa2.

P1 — completion landing on a torn-down subsystem. Confirmed, and this branch is what made it reachable. The handler captured raw this; the indexer broadcasts from its worker thread and the handler hops to the game thread, so Deinitialize() — which closes the database and deletes the indexer — can run in between, after which the queued task called ReopenDatabase() on a dead subsystem.

The pre-existing code survived this because indexing only ever started on explicit request. This branch starts a catch-up run from Initialize(), so closing the editor shortly after launch now lands squarely in that window. Three changes:

  • the handler holds a TWeakObjectPtr instead of raw this and bails when it cannot be resolved;
  • Deinitialize() sets bIsShuttingDown before touching anything, and the handler rejects on it — a live UObject that has already been deinitialized is not caught by the weak pointer alone;
  • Deinitialize() calls Indexer->OnComplete.Clear() before delete Indexer, so no further broadcast can reach the handler at all.

P2 — destructive rebuild on a locked database. Confirmed. StartPreferredIndex treated "file missing" and "file present but not open" identically and fell through to TriggerReindex(), which is a clean build: it calls ResetDatabase() and drops the existing engine index. A transient lock — another editor holding the file — would have been silently converted into an expensive destructive rebuild, leaving a partial database if interrupted.

Full bootstrap is now reserved for a genuinely absent file. An existing-but-unopenable database returns an explicit error and leaves the index intact:

if (bDatabaseFileExists && Database.IsValid() && Database->IsOpen())
{
    return TriggerProjectReindex();
}
if (bDatabaseFileExists)
{
    // refuse the clean rebuild, report, keep the index
    return false;
}
if (bAllowFullBootstrap) { return TriggerReindex(); }

Verified at d977baa2 on both engines:

UE 5.8 UE 5.7
Editor build Result: Succeeded Result: Succeeded
Monolith.Activation (6) + Monolith.Source (10) 16/16, 0 failed, exit 0 16/16, 0 failed, exit 0 (-RenderOffscreen)

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d977baa2df

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread Source/MonolithIndex/Private/MonolithIndexSubsystem.cpp Outdated
Comment thread Source/MonolithIndex/Private/MonolithIndexSubsystem.cpp Outdated
…on any outcome

Startup hard-coded `bExplicitRequest=false` when applying `bDeferFirstTimeIndex`,
so a persisted `Monolith.StartIndexing` was treated like an inherited default.
An explicitly enabled first-time index that did not finish before the editor
exited was therefore re-deferred on every subsequent launch, even though the
durable activation stayed enabled. It now passes `Activation.bIndexingUserSet`,
matching what the source subsystem already does for its bootstrap decision.

Live Asset Registry callbacks were re-armed only when a full index finished
successfully. `StartFullIndex` unregisters them for the duration of the run, so a
cancelled or failed run left them off while `bAutomaticIndexingEnabled` stayed
true: the subsystem reported itself active while silently dropping every later
asset change until a successful reindex, another Start command, or an editor
restart. They are now re-armed on every outcome. `RegisterLiveCallbacks()` is
already self-guarding on effective activation, active-run state, and database
readiness, and is idempotent, so a run completing after indexing was genuinely
deactivated still leaves them off.

Also closes the same use-after-free class that was just fixed in the source
subsystem. The index worker queues `OnIndexingFinished` to the game thread
capturing raw `this`/`Owner`. `Deinitialize()` joins the worker, which is what
makes every worker-thread `Owner->` access safe, but a task queued immediately
before that join outlives it and can land on a destroyed subsystem. Both queue
sites now resolve the subsystem through a `TWeakObjectPtr` and drop the
completion if it is gone.

Verified at this head on both engines: editor builds succeed and
`Monolith.Activation` + `Monolith.Source` + `Monolith.Index` report 16/16 with
0 failed and exit 0 on UE 5.7 (`-RenderOffscreen`) and UE 5.8.
@kunkunGames

Copy link
Copy Markdown
Author

Both findings on d977baa2 were real and are fixed in 6de1d38f, along with one more of the same class I went looking for after the second.

Explicit activation lost to bDeferFirstTimeIndex. Confirmed — startup passed a hard-coded false, so a persisted Monolith.StartIndexing was indistinguishable from an inherited default and an unfinished first-time index was re-deferred on every launch. Now passes Activation.bIndexingUserSet, which is what the source subsystem was already doing for its bootstrap decision; the two paths agree again.

Callbacks not restored after an aborted full index. Confirmed and the worse half is the silence: bAutomaticIndexingEnabled stayed true, so the subsystem reported itself active while no asset change was being queued. Re-armed on every outcome rather than only bSuccess. Worth noting RegisterLiveCallbacks() is already self-guarding on effective activation, active-run state, and database readiness, and is idempotent — so an unconditional call is correct rather than merely tolerable, and a run that completes after indexing was genuinely deactivated still leaves the callbacks off. That also removes a duplicated condition instead of adding one.

Third, found by looking for the pattern rather than reported: the index worker queues OnIndexingFinished to the game thread capturing raw this/Owner, at two sites. Deinitialize() joins the worker — which is exactly what makes the pervasive worker-thread Owner-> access safe — but a task queued immediately before that join outlives it and can land on a destroyed subsystem. This is the same class you flagged as P1 in MonolithSourceSubsystem one round earlier, so fixing one and leaving the other would have been an odd place to stop. Both sites now resolve through a TWeakObjectPtr and drop the completion if the subsystem is gone.

I deliberately did not widen that to the rest of FIndexingTask. Every other Owner-> access happens on the worker thread inside the join, so it is already covered; converting those would be churn without a defect behind it.

Verified at 6de1d38f on both engines:

UE 5.8 UE 5.7
Editor build Result: Succeeded Result: Succeeded
Monolith.Activation + Monolith.Source + Monolith.Index 16/16, 0 failed, exit 0 16/16, 0 failed, exit 0 (-RenderOffscreen)

@kunkunGames

Copy link
Copy Markdown
Author

Offering a split before you spend review time on this, since you staged #113 rather than taking it whole and this branch is larger.

Four review rounds have now produced ten fixes on this branch. None were in the activation logic itself — they were in the listener lifecycle, the writer gating, and two use-after-free windows on shutdown. That pattern says the breadth is the problem, not any one piece, so here is how it separates. Dependencies are real, not cosmetic:

1. Listener lifecycle — independent, no activation needed. MonolithHttpServer.{h,cpp} (+142/−56, +33/−2). Four fixes that stand on their own against master:

  • Stop() and the failed-probe path called FHttpServerModule::StopAllListeners(), which is process-wide — in a host project with any other HTTP-server plugin, stopping Monolith stopped theirs too, on a different port. Monolith source now contains zero calls to it.
  • The startup probe used a blocking loopback connect, which stalls roughly two seconds on a closed Windows port. Bounded non-blocking connect, with the blocking call kept as the fallback.
  • A live ServerPort change ran Stop() before discovering the move was impossible, leaving MCP dead until an editor restart. Rejected before anything is torn down.
  • A MonolithCore reload left the port permanently unstartable, because the replacement instance saw its own retained listener as a foreign owner.

This is the slice I would land first if you only take one. It fixes shipped behavior and needs nothing else in the branch.

2. Durable activation state — foundation. MonolithSettings.{h,cpp} (+594, +75), Config/DefaultMonolith.ini (+5), MonolithSettingsActivationTests.cpp (+841). Project defaults, per-user overrides in generated Monolith.ini, one-time legacy migration, fail-closed parsing, GConfig reconciliation. Nothing consumes it yet, so it lands inert and is testable on its own — the 841 lines are the tests.

3. Server activation + console commands. MonolithCoreModule.{h,cpp} (+247/−21, +13), MonolithSentinelFile.h (+52). Monolith.StartServer / StopServer, ticker reconciliation, sentinel ownership. Needs 1 and 2.

4. Indexing activation. The two subsystems, MonolithSourceModule, the indexer, MonolithSourceActions, the reindex handler in MonolithCoreTools.cpp, and the Settings button predicates (~+860/−100 including tests). Monolith.StartIndexing / StopIndexing, writer gating, honest acceptance returns, drain-safe recovery. Needs 2, independent of 3.

So: 1 alone, then 23 and 4 in parallel.

Say the word and I will open them as separate PRs in that order, each with its own build and automation evidence, and close this one. If you would rather review it whole, it is verified as-is: UE 5.7 and UE 5.8 both build, Monolith.Activation + Monolith.Source + Monolith.Index pass 16/16 on each.

One scope note either way, since it is a policy call rather than a defect: Config/DefaultMonolith.ini ships bServerEnabledByDefault=True and bIndexingEnabledByDefault=True, so a fresh checkout behaves exactly as it does today and the console commands only ever write per-user overrides. If you would rather a new install started inert, that is a one-line change to that file and the tests already cover both defaults.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 6de1d38f40

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +417 to +421
if (bRequestMatches
&& UserStamp == Cache.UserStamp
&& LegacyStamp == Cache.LegacyStamp)
{
return Cache.Value;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Recheck fail-closed activation files after access recovers

When LoadFileToString fails transiently while the file can still be statted—for example during a Windows sharing lock or temporary permission failure—the new fail-closed result is cached together with the file's existing timestamp. If access later recovers without rewriting the file, this timestamp-only fast path returns the disabled value indefinitely, so the server ticker never restarts the server and indexing remains inactive despite the comment promising recovery once the file is readable. Avoid caching unreadable results as timestamp-stable, or record that state and force another read after the revalidation interval.

Useful? React with 👍 / 👎.

{
const FString Path = GetSentinelFilePath();
if (FPaths::FileExists(Path))
switch (MonolithSentinelFile::RemoveOwned(Path, bOwnsSentinelFile))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reclaim stale sentinels left by crashed editors

When an editor crashes after writing .monolith_running and the next editor starts with server activation or project policy disabled, bOwnsSentinelFile remains false and every stop or shutdown reaches NotOwned here. Unlike the previous unconditional cleanup, no later lifecycle path removes the dead process's sentinel unless this process successfully starts and overwrites it, so the file can continue advertising a nonexistent server indefinitely. Inspect the recorded PID and reclaim the sentinel when that owner is no longer alive while still preserving sentinels belonging to a live editor.

Useful? React with 👍 / 👎.

@kunkunGames kunkunGames changed the title feat(core): add persistent server and indexing activation 🛡️ Codex: add durable server and indexing activation Jul 27, 2026
@kunkunGames kunkunGames changed the title 🛡️ Codex: add durable server and indexing activation feat(core): add durable server and indexing activation Jul 27, 2026
tumourlove added a commit that referenced this pull request Aug 1, 2026
…ur lifecycle defects

Eight defect fixes salvaged from PR #114. The PR's durable-activation
feature is NOT taken -- it was declined on review: it persists a server-off
state to disk whose only recovery is typing a console command in the editor,
while Monolith's primary consumer is an agent driving the editor through the
very transport that state disables. These are the genuine bugs underneath it.

FHttpServerModule::StopAllListeners() is process-wide. Monolith called it on
stop and in the bind-retry loop, so it silently killed every OTHER plugin's
HTTP listener in the editor as a side effect of Monolith restarting. Replaced
with per-route unbinding; Restart() keeps a listener-recreating path so its
stated purpose survives.

Four fire-and-forget AsyncTask(GameThread) lambdas in FIndexingTask::Run
captured raw this. WaitForCompletion() joins the worker thread but does not
drain the game-thread task queue, so an already-queued lambda runs after the
task object is destroyed. PR #114 converted two of them -- the completion
callbacks -- and left the two OnProgress.Broadcast sites, which fire every
batch rather than once, and are therefore the more likely crash. All four now
capture a weak pointer, with the counter values copied into locals before the
lambda is constructed, since they cannot be read through a dangling this.

Live asset-registry callbacks are unregistered for the duration of a full
index and were only re-armed on success. After a failed or cancelled run the
subsystem reported itself active while silently dropping every subsequent
asset change until a successful reindex or an editor restart -- stale data
with no error. They are now re-armed on every outcome, the unregister path
resets its delegate handles, and register is idempotent so a double-bind
cannot accumulate.

Two latched-state bugs left indexing permanently refused until an editor
restart: a failed FRunnableThread::Create in StartAsync, and a failed
OpenForWriting in Run, both returned without broadcasting completion, so
bIsIndexing stayed true and every later request got "Indexing already in
progress".

Reindex results are now honest. TriggerReindex, TriggerProjectReindex,
StartFullIndex and StartIncrementalIndex all return bool and the handlers
reported unconditional success, so an agent received reindex_started for work
that never started.

ProbePort no longer blocks: a loopback connect to a closed port stalls the
game thread ~2s per attempt, during editor startup.

Reported and prototyped by @kunkunGames (#114).
@tumourlove

Copy link
Copy Markdown
Owner

Partly shipped in v0.22.0 — about 250 lines of it. I land contributor fixes as my own commits rather than merging the branch — I keep the shipped history single-author for release integrity, and credit you in the release notes instead. No reflection on the patch.

The bug fixes buried in this PR are real and I took all of them:

  • StopAllListeners() is process-wide, so Monolith stopping or restarting was taking down every other plugin's HTTP listener in the editor. Replaced with per-route unbinding.
  • Three use-after-frees on AsyncTask(GameThread) callbacks — WaitForCompletion() joins the worker but does not drain the game-thread queue. Worth flagging: there are four such sites in FIndexingTask::Run, not two. You converted the completion callbacks; the two OnProgress.Broadcast sites fire every batch rather than once, so they are the more likely crash. All four are converted now.
  • Live asset-registry callbacks never re-armed after a failed or cancelled run, so the subsystem reported itself active while silently dropping every asset change until an editor restart. That one was a genuinely nasty silent-staleness bug.
  • bIsIndexing latching on two indexer failure paths, so everything after got "Indexing already in progress" until restart.
  • Honest reindex results instead of unconditional success.
  • Non-blocking ProbePort — the blocking version stalls the game thread ~2s per attempt during startup.

Your MonolithSourceIndexerFailureTest came along too.

The durable-activation feature I have declined, and I want to give you the actual reason rather than a vague one. The disqualifier is the recovery path: Monolith.StopServer persists ServerEnabled=False to disk, and the only documented way back is typing a command in the editor console — while the primary consumer of this plugin is an agent driving the editor through the transport that state just disabled. That is a lockout an agent cannot recover from, and "reinstall the plugin" is not an acceptable answer for a settings toggle.

Two smaller things that fed into it: the PR describes making Monolith.StartServer/StopServer/StartIndexing/StopIndexing persistent, but none of those four commands exist on master — the PR introduces them and then builds persistence underneath, so there is no existing behaviour being repaired. And no issue asks for this, while the nearest real demand in the area (#117, index progress lost on crash) is untouched by it.

None of that reflects on the engineering, which is careful — the sentinel reclaim logic in particular is more thoughtful than most. It is a scope and blast-radius call on the most load-bearing module in the plugin.

@tumourlove tumourlove closed this Aug 1, 2026
mindmedicine added a commit to mindmedicine/monolith that referenced this pull request Aug 20, 2026
…ve-and-re-add

That string shipped in tool output from remove_map_parameter_pin and
set_script_parameter_type, and it is now false. Stale prose in a doc is one thing;
stale prose emitted to whoever calls the action is worse, because it is read at
exactly the moment someone is deciding what to do next.

It now points at apply_script_changes, states that it was measured with controls on
add, remove and retype, and says the thing that actually matters for the choice: it
preserves the caller's other override values, which remove-and-re-add destroys. So
re-adding is named as the worse option rather than the only one.

It also names the two residuals apply does NOT clean up, because a warning that
oversells a fix is the same defect in the other direction: removing an input leaves a
dead override pin (tumourlove#114) and retyping one leaves the old override so two same-named
inputs exist at different types (tumourlove#115), and a compile shows neither.

The block comment above it is corrected too, since it asserted that NO action
refreshes a placed caller. That was true when written -- tumourlove#62 tried saving, re-opening
the system editor and toggling the enabled flag, and all three failed. They failed
because none of them is Apply. Compile is not Apply, which is the whole point. What
survives from tumourlove#62 is the static-switch case, still untested, and that is now stated
as the remaining scope rather than left implied.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants