Skip to content

After a creature died, unsummon pet.#6

Merged
billy1arm merged 2 commits into
mangoszero:masterfrom
zwisus:master
Feb 2, 2017
Merged

After a creature died, unsummon pet.#6
billy1arm merged 2 commits into
mangoszero:masterfrom
zwisus:master

Conversation

@zwisus

@zwisus zwisus commented Feb 2, 2017

Copy link
Copy Markdown
Contributor

No description provided.

@billy1arm billy1arm merged commit c07868a into mangoszero:master Feb 2, 2017
MadMaxMangos added a commit that referenced this pull request Jul 8, 2026
* feat(ah-sp2): wire contract - player-mutation + resolve opcodes/structs

Adds src/ipc/PlayerMutations.h (PlayerSell/Bid/Buyout/CancelPrepare/
CancelDecide intents, MutationFacts by-value snapshot, PlayerMutationResult,
ResolveApply/ResolveAck) in the AuctionIntents.h idiom (fixed WIRE_SIZE,
truncation-guarded Decode); appends opcodes 0x1040-0x1048; extends
IntentResult with itemGuid+auctionId for bot materialization; carries the
write-authority bit in IPC_HELLO_ACK ({runId, writeAuthority}, legacy
4-byte body => 0) via IpcServer::SetWriteAuthority / IpcClient::
WriteAuthority; bumps IPC_PROTOCOL_VERSION to 2. Default-off: no behavior
changes until the supervisor grants authority. Worker --selftest covers
every codec round-trip + a truncation guard.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(ah-sp2): worker journal DAO (ah_worker_journal outbox/recovery)

AhJournal DAO over ServiceDatabase::Character(): Insert/SetState append to
the caller's open checked txn (co-commit with book writes); Get/LoadActive
are synchronous reads; DeleteAppliedOlderThan is the standalone TTL prune.
The binary facts payload is stored as ASCII hex (printf PExecute and
Field::GetCppString both truncate at NUL) and decoded on read. Worker
--selftest exercises full CRUD against the character DB and self-skips when
none is configured.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(ah-sp2): custody ReleaseGoldToWallet + resolution applied-record

Adds ROLE_RESOLUTION=4 and three CustodyService primitives for the SP-2
finalize legs: ReleaseGoldToWallet silently re-credits a rejected/aborted
reservation (online ModifyMoney, offline in-txn characters.money UPDATE)
and flips the row CST_TERMINAL_BACK with no mail; WriteResolutionApplied /
ResolutionApplied record and detect the "resolve:<uuid>" applied-record for
at-least-once idempotency (DUPLICATE == APPLIED). Covered by mangosd -t
ahrelease (offline credit + applied-record write/duplicate/absent).

Fixes two latent bugs in the test itself found while making it pass: the
seeded offline character name exceeded characters.name's varchar(12), and
the applied-record cleanup LIKE pattern targeted the uuid's hex digits
instead of the decimal digits WriteResolutionApplied actually keys on
(left a stray row breaking every second run).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(ah-sp2): worker authoritative auction book (load gates + admission table)

AuctionBook: in-memory book keyed by auction id, loaded from
auction LEFT JOIN item_instance with the legacy LoadAuctions gates --
missing/undecodable items and invalid houses are REPORTED as orphans
(never repaired; spec 5.6), item-data drift is adopted + repaired
(worker owns book hygiene under WriteAuthority; spec 5.7). Active
journal rows re-mark BOOK_RESOLVING / BOOK_CANCEL_PREPARED on load and
the one-ACTIVE-per-auction invariant refuses a corrupt boot. Admit()
pins the spec 4.3b op-vs-state admission matrix with the legacy
AuctionError low bytes. ItemInstanceFields gains the entry word
(OBJECT_FIELD_ENTRY) for the gate-C cross-check. Pure-core
BuildFromRows + NULL-db mode keep it all selftest-reachable.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(ah-sp2): worker player-mutation commits - sell/bid/buyout

MutationHandler validates against the authoritative book in legacy
order (existence/lock admission, bid-own + same-account, price<=bid,
min-increment via the 1.12 (bid/100)*5-min-1c formula, below-startbid
silent reject) with the 50-owned-listings cap moved worker-side, then
commits auction row + JRN_COMMITTED journal row in ONE checked txn and
replies complete fact snapshots (effectiveBid = min(maxPrice, buyout)
on buyout; prior-bidder fields for the outbid refund; REJECTED results
carry the AUCTION_ERR_HIGHER_BID data). Checked-commit failure rolls
back the in-memory image and replies the legacy err-database shape.
Dispatch cases for 0x1040-0x1042 reply on IPC_PLAYER_RESULT; the book +
handler construct only when IPC_HELLO_ACK grants write authority.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(ah-sp2): worker cancel two-phase (prepare/confirm/abort + timeout unlock)

OnCancelPrepare validates ownership (legacy err-database + cheater log),
durably journals JRN_CANCEL_PREPARED (standalone checked commit BEFORE
the reply), locks the row BOOK_CANCEL_PREPARED and replies MUT_PREPARED
carrying {curBid, curBidder, deposit}. OnCancelDecide answers EVERY
confirm: CONFIRM commits DELETE + journal COMMITTED in one checked txn
-> MUT_OK with the facts snapshot; ABORT retires the journal row and
unlocks; unknown/expired uuids get an explicit MUT_REJECTED_STALE. The
prepare-timeout sweep (T=10s, serializer thread) unlocks via a
journal-anchored RESOLVE_CANCELLED_UNLOCK: prepare row -> JRN_APPLIED +
new worker-minted JRN_RESOLVING row (facts = encoded ResolveApply) in
ONE txn, preserving the one-ACTIVE-per-auction invariant; the queued
ResolveApply is the resolve-send driver's seam (PopQueuedResolve).
Boot re-arms CANCEL_PREPARED locks via AdoptActiveJournal. Dispatch
cases for 0x1043/0x1047/0x1048 reply on IPC_PLAYER_RESULT.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(ah-sp2): worker expiry/win tick + resolve outbox (Task 7)

Tick scans LIVE book rows past expireTime and mints resolutions (WON when
bid!=0, EXPIRED_NOBID otherwise) bounded by a 16/tick budget and a 64
un-acked window (decision 10): each is journaled JRN_RESOLVING as its own
checked commit BEFORE the IPC_RESOLVE_APPLY send (spec M1 -- AhJournal::
Insert only appends to an open txn, so the mark is wrapped in its own
BeginTransaction/CommitTransactionChecked for real durability), then marks
terminal-kind rows BOOK_RESOLVING and tracks the frame against the window.

OnResolveAck: APPLIED/DUPLICATE on a terminal kind (WON, EXPIRED_NOBID)
commits journal-APPLIED + auction-row DELETE in one txn and drops the book
row via a new AuctionBook::RemoveMemoryOnly (Remove() already issues its
own DB DELETE for OnSell/OnBuyout/OnCancelDecide's already-open
transactions, so it can't be reused here without a redundant out-of-txn
DELETE); non-terminal kinds (CANCELLED_UNLOCK, REPAIR_RETURN) are
journal-only and the listing persists; FAILED acks and failed local txns
keep the entry RESOLVING for the resend cadence; unknown uuids/statuses are
loud protocol-fault logs. ResendStaleResolving re-sends RESOLVING entries
older than 30 game-seconds verbatim from the cached wire blob, with a stuck
alarm past 10 attempts. PrimeResolvingFromJournal replays every
JRN_RESOLVING row immediately on boot (at-least-once; mangosd dedupes via
the resolve:<uuid> applied-record).

Tick also drains Task 6's CheckPrepareTimeouts m_resolveQueue seam into the
same outbox tracking (already journaled by CheckPrepareTimeouts's own txn,
so no re-journal here) so a RESOLVE_CANCELLED_UNLOCK ack actually reaches
OnResolveAck's non-terminal branch instead of going unsent forever -- that
queue was produced by Task 6 but never drained until now.

Main.cpp wires IPC_GAMETIME into MutationHandler::SetGameTime, adds the
IPC_RESOLVE_ACK dispatch case, and runs Tick + ResendStaleResolving on an
AH.Service.TickMs cadence (default 1000ms) only when write authority was
granted and a gametime is known; handler-queued frames drain to the
reliable lane every pass. Reuses the existing ahBook/ahHandler construction
site and NextWorkerUuid's uuid counter (both pre-dating this task under
those names, so NextUuid() forwards to it); AuctionBook's and
MutationHandler's constructors were left exactly as Tasks 3-6 landed them
(ServiceDatabase* pointer, book-then-db order) rather than the contract's
assumed reference/order, to avoid touching their already-merged call sites.
New selftest RunResolveOutboxSelfTest() covers mark-before-send ordering,
WON/EXPIRED_NOBID kind selection, journal-failure suppression, the
budget/window clamps, all ack transitions, and the resend/boot-replay
cadence.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(ah-sp2): keep non-terminal resolutions BOOK_LIVE across worker restart

F1 (code review): AuctionBook::BuildFromRows re-marked EVERY JRN_RESOLVING
journal row's book row to BOOK_RESOLVING at boot, regardless of ResolveKind.
Only TERMINAL kinds (RESOLVE_WON, RESOLVE_EXPIRED_NOBID) should freeze the
row while awaiting the terminal apply; NON-terminal kinds
(RESOLVE_CANCELLED_UNLOCK, RESOLVE_REPAIR_RETURN) leave the listing LIVE
while resolving (spec 4.3 v3 I2/I3). A crash mid prepare-timeout-unlock (or
a future repair-return resolution) followed by a worker restart froze that
live listing BOOK_RESOLVING permanently -- unbiddable, unbuyable, and
uncancellable until the NEXT restart. Reachable today via Task 6's
CANCELLED_UNLOCK path.

Fix has two parts:
 - Root cause: BuildFromRows now only marks BOOK_RESOLVING when the
   journal row's kind is RESOLVE_WON or RESOLVE_EXPIRED_NOBID; non-terminal
   kinds are left alone (the row is already BOOK_LIVE from the load loop).
 - Defensive: MutationHandler::OnResolveAck's non-terminal branch now
   resets a BOOK_RESOLVING row back to BOOK_LIVE on ack, so a listing
   mis-frozen by a pre-fix boot self-heals the next time its resolution
   is acknowledged, even without another restart.

Added an F1 regression case to the book selftest: seeds a JRN_RESOLVING
row with kind = RESOLVE_CANCELLED_UNLOCK plus a matching LIVE book row and
asserts the row stays BOOK_LIVE after BuildFromRows. Verified red before
the fix (book selftest FAILED: F1 regression...) and green after (book
selftest OK, all suites OK).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(ah-sp2): bot fold-in - in-process book writes + sell materialization

Under WriteAuthority the bot no longer sends IPC_INTENT_BID/BUYOUT: the
worker calls MutationHandler directly with bot identity (bidder=0 per the
cross-check matrix) - simple bids apply straight to the book+journal
(JRN_APPLIED), player-displacing bids co-commit a non-terminal
RESOLVE_REPAIR_RETURN refund, buyouts queue a terminal RESOLVE_WON
(bot-win destroy branch). Bot sells use the retained IPC_INTENT_SELL as a
materialization round-trip (JRN_INTENT_PENDING -> send -> commit listing on
IntentResult{itemGuid,auctionId}) with boot re-send + resend/abandon cadence.
Legacy wire path unchanged with WriteAuthority off.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(ah-sp2): MutationPendingMap - apply-all consume-once pending map

Spec 5.5: per-player (8) + total (4096) caps checked before any reserve,
consume-once Take that also serves late replies against tombstones,
cancel phase-2 RearmConfirm in the same cap slot, and a TTL sweep that
tombstones in-doubt entries emitting each newly-in-doubt uuid exactly
once. Plus AhMintMutationUuid (boot-second high word, disjoint from
worker (runId<<32)|seq). Covered by mangosd -t ahmutpending.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(ah-sp2): boot-latched AH.Service.WriteAuthority world flag

CONFIG_BOOL_AH_WRITE_AUTHORITY latched via configNoReload (spec decision
7: .reload config explicitly does not re-read it), startup invariant
WriteAuthority => Custody with error + force-off (spec I2), advisory
when no worker is configured, IsAhWriteAuthority() accessor beside
IsAhCustodyEnabled(), and the world-thread MutationPendingMap member +
GetMutationPending() accessor for the Task 10/11 forward/finalize seams.
Default-off: the key is absent from every shipped conf (the conf task
adds the documented entry), so behavior is unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(ah-sp2): pass write-authority bit to the IPC handshake

mangosd arms IpcServerHandler::SetPendingWriteAuthority with the
boot-latched sWorld.IsAhWriteAuthority() before WorkerSupervisor::Start,
so IPC_HELLO_ACK carries the authority grant to the worker (spec
decision 7). Static set-once covers every child respawn.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(ah-sp2): bid-forward classifier + forward-reserve selftest

AhClassifyBidForward maps CMSG_AUCTION_BID onto the SP-2 wire from
mangosd's own ledger (the book is worker-owned): proven same-bidder
raise -> IPC_PLAYER_BID with delta reserve (spec I9); everything else
-> IPC_PLAYER_BUYOUT with the full submitted price reserved as maxPrice
(worker computes effectiveBid = min(maxPrice, buyout), spec 4.1);
price <= own live bid -> inline legacy AUCTION_ERR_HIGHER_BID. DB-level
coverage via mangosd -t ahforwardreserve (reserve row shapes + pending
registration, no live worker).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(ah-sp2): sell forward branch - reserve then PlayerSellIntent

HandleAuctionSellItem under WriteAuthority: shared legacy validation +
deposit compute, then mangosd-minted auction ID (spec decision 8),
in-memory escrow (mAitems + MoveItemFromInventory, legacy debit timing),
ONE checked txn writing the item ownership move + item:<id> escrow +
dep:<id> reserve (spec decision 9) with full live-state rollback on
commit failure, pending registration, and IPC_PLAYER_SELL on the
reliable lane. Worker-down => unavailable message + ERR_DATABASE; send
failure => in-doubt tombstone (decision 10). Default-off: gated on the
boot-latched WriteAuthority flag.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(ah-sp2): bid/buyout forward branch - classify, reserve, forward

HandleAuctionPlaceBid under WriteAuthority: full-price guard at legacy
parity (silent return), AhClassifyBidForward against mangosd's own
ledger (same-bidder raise -> IPC_PLAYER_BID delta reserve per spec I9,
everything else -> IPC_PLAYER_BUYOUT with maxPrice fully reserved,
price <= own live bid -> inline ERR_HIGHER_BID), durable ReserveGold in
one checked txn with in-memory refund on rollback, pending registration
carrying bid:<id>:<seq>, and the reliable-lane forward. Worker-down =>
unavailable + ERR_DATABASE; send failure => in-doubt tombstone
(decision 10). Existence/min-increment/bid-own guards are worker-side
(spec I6). Default-off behind the boot-latched WriteAuthority flag.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(ah-sp2): worker commits a below-buyout IPC_PLAYER_BUYOUT as a normal bid

The mangosd bid classifier AhClassifyBidForward forwards every bid that is
not a provable same-bidder raise -- including a normal first/competing bid
below the buyout price -- as IPC_PLAYER_BUYOUT with maxPrice = the full
submitted price (intentional: mangosd does not own the book, so it fully
reserves and lets the worker adjudicate, spec I6). But the worker's
ValidateBuyout treated buyout == 0 || maxPrice < buyout as a forwarder defect
and SILENT-REJECTED it, so every first/competing below-buyout bid was
silently dropped -- normal bidding did nothing.

Spec 4.1: IPC_PLAYER_BUYOUT with maxPrice means "buy out if maxPrice >=
buyout, else place a NORMAL BID at maxPrice", effectiveBid = min(maxPrice,
buyout). ValidateBuyout now admits the win leg directly and, for the
below-buyout / buyout-less leg, applies the remaining legacy bid checks to
maxPrice (min-increment -> BID_INCREMENT, below-startbid -> silent). OnBuyout
branches on isWin = (buyout != 0 && maxPrice >= buyout): the win path is
unchanged; the below-buyout leg commits a normal bid at maxPrice via a new
shared CommitBidAt(auctionId, bidder, amount, op, uuid) helper (the exact
UpdateBid + JRN_COMMITTED + checked-commit + rollback idiom lifted from
OnBid), keeping op = 0x42 and leaving the row LIVE with prior* = the
displaced bidder.

Win vs bid is signalled purely by the facts (no new field): a WIN has
effectiveBid == buyout (buyout != 0); a BID has effectiveBid < buyout (or
buyout == 0). The mutation selftest is reworked: the below-buyout
ValidateBuyout/OnBuyout cases now assert a committed bid (plus a below-buyout
min-increment reject and a below-buyout outbid), and the genuine win case
(maxPrice >= buyout) still asserts the removing win. Full --selftest suite
green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(ah-sp2): player-mutation finalize + cancel phase-2 driver

AhHandlePlayerMutationResult applies gold/items/mail from the worker's fact
snapshot, value-only and cross-checked fail-closed against mangosd's own
custody ledger (no AuctionEntry/AuctionsMap access):
  - MUT_OK sell: confirm only (deposit+item stay escrowed);
  - MUT_OK bid/buyout: standing-bid update, displaced-prior refund, and -- for
    a genuine buyout WIN -- seller payout (effectiveBid+deposit-cut), item to
    winner, deposit return, and release of the (maxPrice-effectiveBid)
    remainder;
  - MUT_OK cancel: S5 replay (bidder refund, deposit forfeit, item return, cut
    sink) minus the book delete;
  - MUT_REJECTED: silent ReleaseGoldToWallet + legacy error result;
  - MUT_REJECTED_STALE: release the cut reservation + resolve the tombstone;
  - MUT_PREPARED: the cancel phase-2 driver (affordability gate -> reserve cut +
    CONFIRM, else ABORT; tombstone answers ABORT).
Failed checked commits go to a forward-only redrive queue (never rollback); the
same tick ages un-answered mutations into in-doubt tombstones and emits the
one-time AUCTION_ERR_DATABASE result. Adds MutationPendingMap::SetReserve and
the finalize entry-point declarations.

RECONCILIATION 1 (win-vs-bid): an op=0x42 (IPC_PLAYER_BUYOUT intent) MUT_OK is a
buyout WIN only when facts.buyout != 0 && facts.effectiveBid == facts.buyout
(HEAD 16e22c4e: the worker now commits a below-buyout price as a NORMAL bid and
keeps the row live). A below-buyout op=0x42 falls through the standing-bid path
(release = reservedAmount - effectiveBid == 0; no seller payout, no item
delivery, only the displaced-prior refund).

RECONCILIATION 2 (PREPARED abort gate): gate the cancel abort on facts.curBid !=
0 with an explicit offline-seller branch, not on the computed cut. AhCutFor
returns 0 with no DBC loaded (the -t harness), which would otherwise mask the
gate; an offline seller who owes a cut on cancel cannot be debited -> ABORT.

Seam reconciliations vs the brief: pending storage is m_map/unordered_map (Peek
already present, only SetReserve added); MutationPending.h/PlayerMutations.h
already included by Task 10; escrow keys are dep:<auc>/item:<auc>.

Covered by mangosd -t ahmutresult (both op=0x42 sub-cases, MUT_OK sell/bid,
REJECTED, REJECTED_STALE, PREPARED-abort, sweep, unknown-uuid). The test seeds
its own account-bearing recipient row: the AH mail path correctly skips a
recipient with no characters row (legacy account guard), so guids 1/2/3 absent
from the DB were surfacing as "mail missing" until seeded.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(ah-sp2): wire IPC_PLAYER_RESULT pump case + redrive cadence

World::HandleAhInbound gains the IPC_PLAYER_RESULT case (decode ->
AhHandlePlayerMutationResult), and World::Update pumps AhProcessRedriveQueue
each tick while the service is active (failed-finalize retry + in-doubt
tombstone sweep, forward-only). Inert under -t (no live supervisor) and with
WriteAuthority off (no frames arrive). IPC_RESOLVE_APPLY is left for Task 12.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(ah-sp2): finalize IPC_RESOLVE_APPLY + reconcile-on-reconnect

AhHandleResolveApply applies worker-initiated resolutions per kind (WON =
seller payout + item to winner incl. bot-win destroy branch; EXPIRED_NOBID =
deposit forfeit + item return; CANCELLED_UNLOCK = cut release; REPAIR_RETURN
= bot-outbid prior-bidder refund / item return) inside ONE checked txn with
the resolve:<uuid> applied-record, so DUPLICATE==APPLIED and RES_FAILED keeps
the worker row RESOLVING (never rolls the book back). The World pump acks each
IPC_RESOLVE_APPLY; AhReconcileOnReconnect walks in-flight pendings against the
shared ah_worker_journal on the service-active edge (committed/applied =>
forward, absent => release, cancel-prepared => abort+release). Reuses Task 11's
from-facts value helpers. Covered by mangosd -t ahresolve.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(ah-sp2): resolve-apply money-safety - cut double-credit + WON fail-closed

AhHandleResolveApply money/leak safety, mirroring the established X6
undo + AhFinalizeBidOk fail-closed patterns.

F1 [HIGH double-credit] RESOLVE_CANCELLED_UNLOCK: ReleaseGoldToWallet
credits an online owner via an immediate non-transactional ModifyMoney;
only the ledger flip is inside the checked txn. On CommitTransactionChecked
failure the code returned RES_FAILED WITHOUT undoing that in-memory
credit, so the worker retry re-releases the still-RESERVED cut and the
owner is credited twice. Capture the online owner + released amount and,
on the commit-failure branch, ModifyMoney(-amount) to undo it (the DB
write was transactional and already rolled back), exactly like the five
sibling finalize sites. Offline owners are an in-txn UPDATE that rolls
back -- nothing to undo.

F2 [MEDIUM fail-closed] RESOLVE_WON: a REAL winner (curBidderGuid != 0)
with zero or >1 live bid rows silently skipped terminalizing the bid
reservation yet STILL paid the seller, delivered the item, and wrote the
applied-record -> the winner's RESERVED bid leaked and it was recorded
done (never re-drives). Now fail-closed like AhFinalizeBidOk: log a
PROTOCOL FAULT, roll back, and return RES_FAILED (pay/deliver nothing).
A legit BOT win has curBidderGuid == 0 and correctly no bid row -> it
still proceeds; the guard fires ONLY when curBidderGuid != 0 && no row.

F3 [LOW] RESOLVE_CANCELLED_UNLOCK: release the cut to cutRow.ownerGuid
(the guid actually debited at reserve time -- the authoritative owner on
the reserved row) instead of the worker-supplied facts.sellerGuid, for
both the ReleaseGoldToWallet owner and the online-player lookup.

Test: -t ahresolve gains an F2 case (RESOLVE_WON, curBidderGuid != 0, no
live bid row) asserting RES_FAILED, no seller payout, no applied-record,
and dep/item rows rolled back. Red before the guard (5 assertions fail:
pays seller + writes applied-record + flips rows), green after.
-t ahresolve / ahmutresult / custody all pass. F1/F3 inspection-verified
(no commit-fail injector under -t).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(ah-sp2): bot-sell materialization mangosd-side + orphan sweep

Under WriteAuthority, IPC_INTENT_SELL becomes the materialization leg:
mint+persist+escrow the item, GenerateAuctionID, record a durable
botlist:<uuid> idempotency row (replays ids for a redelivered uuid), and
reply IntentResult{itemGuid, auctionId} for the worker to write the book -
mangosd no longer inserts the auction row. Stray IPC_INTENT_BID/BUYOUT are
logged+ignored (worker owns bids). SweepOrphanMaterializations reaps items
whose auction never reached the shared table. GM .auction item re-dispositions
onto the materialization shape (no reserve). Inert with WriteAuthority off.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(ah-sp2): reliable IPC lanes + residual-writer gating + Eluna + conf

Mutation-class opcodes (IPC_PLAYER_*/IPC_RESOLVE_*/IPC_INTENT_SELL/RESULT)
ride an unbounded lane both directions (IpcIsReliableOpcode classifier) so a
browse flood can never drop a value-bearing frame; drained before the bounded
queue each pass. Under WriteAuthority the residual mangosd-side auction writers
no-op (LoadAuctions repair, SetHighestGuids orphan-delete, WUPDATE_AUCTIONS ->
mail-sweep-only, in-process ahbot) since the worker owns the book, and Eluna
OnAdd/OnRemove fire at the finalize position on a synthesized AuctionEntry.
Adds AH.Service.WriteAuthority (default 0, requires Custody=1) + AH.Service.TickMs
with ConfVersion bumps and the doc/AuctionHouseBot.md WriteAuthority section.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(ah-sp2): exempt reliable IPC frames from the drop-newest drain clamp

Finding 1 (MEDIUM): Task 14 added an unbounded RELIABLE lane so mutation-
class IPC frames (IPC_PLAYER_RESULT / IPC_RESOLVE_APPLY / etc.) are never
dropped (SP-2 decision 10). But DrainInboundProtocol() staged reliable
frames into the SAME m_pendingFrames vector as bounded/browse frames, and
DrainInbound()'s over-cap clamp (m_pendingFrames.resize(IPC_INBOUND_QUEUE_CAP)
== 256) truncated that combined vector without regard to frame class. If
more than 256 frames were staged in one drain interval and the excess were
reliable, those value-bearing frames were silently dropped - defeating the
never-drop guarantee. This was latent only because the worker caps
in-flight resolves at RESOLVE_WINDOW=64 (< 256), so the guarantee was
accidental headroom, not a structural property.

Fix: give reliable frames their own staging container,
m_pendingReliableFrames, fully separate from the bounded/browse
m_pendingFrames. DrainInboundProtocol() stages PopReliable() output there
directly (still bypassing the app/browse budgets, as before). DrainInbound()
drains m_pendingReliableFrames to exhaustion (or the per-tick budget) BEFORE
touching m_pendingFrames, and the IPC_INBOUND_QUEUE_CAP clamp now applies
only to m_pendingFrames - so no reliable frame can ever be dropped by it,
regardless of how many are staged. ClearStagedFrames() now purges both
containers on child death/respawn so a dead child's staged-but-unconsumed
reliable frames can't survive into the next child. Updated the now-false
"cap enforced upstream" comment on DrainInbound() to describe the
reliable-exempt behavior.

Build: cmake --build server_build --config RelWithDebInfo --target mangosd
clean. Regressions: mangosd -t custody and -t ahmutresult both exit 0
(unaffected - the fix only changes the drain path's internal bookkeeping,
inert with no worker connected).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test(ah-sp2): crash seams + differential/crash operator procedures

Wires MaybeCrash at the four SP-2 seams (post-reserve-pre-forward,
worker-committed-pre-reply, resolving-pre-apply, finalize-fail), all inert
by default. Adds the sp2_differential.md A/B runbook (serialized workload,
allocator determinism, end-state + packet-sequence diff, spec section-12
divergences) and the doc/AuctionHouseBot.md crash-injection + whole-branch
acceptance checklist - the deterministic-gate substitute for a multi-realm
soak on a solo realm.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(ah-sp2): pre-enable hardening - resolve-uuid restart collision, reconcile fail-safe, guards

Whole-branch pre-enable hardening for AH worker write-authority
(AH.Service.WriteAuthority, default OFF). Four code fixes + one documented
divergence.

FIX A [MUST-FIX, silent value loss] worker resolve-uuid collision across a full
mangosd restart. The supervisor runId is not persistent (resets to 1 each
restart) and the worker minter (m_nextSeq) restarts at 0x80000000, but
PrimeResolvingFromJournal adopts surviving JRN_RESOLVING rows with their
original uuids without advancing the minter. A reused runId could re-mint an
already-in-flight uuid for a different auction -> mangosd ResolutionApplied is
already true -> RES_DUPLICATE -> that auction's value silently dropped while the
worker deletes its book row. Fix: for every adopted row whose uuid high-32 ==
this run's runId, advance m_nextSeq = max(m_nextSeq, low32(uuid) + 1). Added a
red->green selftest assertion in the resolve-outbox selftest.

FIX B [belt-and-suspenders] OnBotSellResult zero-id guard. On INTENT_OK with
itemGuid == 0 or auctionId == 0 (a first-party mangosd bug the only trigger),
fail safe: retire the pending + drop it + log, never build a book row with id 0.

FIX C [SHOULD-FIX, misconfig double-credit] reconcile table-missing vs
row-absent. (1) World.cpp boot preflight: probe SHOW TABLES LIKE
'ah_worker_journal'; if absent, force WriteAuthority OFF (same mechanism as the
Custody force-off). (2) AhReadWorkerJournal now returns a tri-state
(FOUND/ABSENT/QUERY_FAILED): on a NULL row query a SHOW TABLES probe
distinguishes a genuine absent row (release) from a query that could not run
(table missing / transient DB error). AhReconcileOnReconnect leaves a
QUERY_FAILED pending in-doubt (tombstone, reservation held) instead of releasing
a possibly-committed reservation; genuine-absent still releases.

FIX D [cheap hardening] duplicate MUT_PREPARED state guard. In
AhHandleCancelPrepared, after the tombstone check, ignore any slot not in
PMUT_AWAIT_RESULT so a duplicate PREPARE frame cannot re-run the affordability
gate / re-reserve the cut on an already-confirmed slot.

DOC (divergence, SP-3 deferred, NOT code-fixed): a current high bidder clicking
Buyout (price >= buyout) is silently dropped under WriteAuthority
(AhClassifyBidForward routes a same-bidder raise as IPC_PLAYER_BID; the worker
ValidateBid silent-rejects a bid at/over buyout). Gold conserved (no dupe/loss);
the buyout no-ops and the player wins at expiry at their bid. Documented at
AhClassifyBidForward, as divergence 6 in sp2_differential.md, and OPERATOR
WARNING 4 + a mandatory live-smoke checklist item in doc/AuctionHouseBot.md.

Build: ah-service + mangosd RelWithDebInfo clean. Regression: ah-service
--selftest all green (incl. FIX A red->green), mangosd -t ahmutresult / ahresolve
/ custody all exit 0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(ah-sp2): resolve bot owner GUID under WriteAuthority so the worker starts

Under AH.Service.WriteAuthority=1, World::SetInitialWorldSettings skipped
sAuctionBot.Initialize() entirely to keep the in-process bot agents from
writing the book. But that init is also what resolves the AH bot owner GUID
(SetAHBotId -> the AHBOT_SYSTEM_OWNER forged-owner name intercept). Skipping
it left the bot GUID 0, so WorkerSupervisor::Start() refused to spawn the
worker ("bot GUID is 0"), silently falling back to the in-process bot -- the
write-authority worker could never start with the forged (no-character) owner.

Fix: in the WriteAuthority branch, load the bot config (sAuctionBotConfig
.Initialize(), which resolves the forged owner GUID with no real character)
WITHOUT calling InitializeAgents() -- the in-process buyer/seller agents stay
off, the worker's supervisor gets the non-zero forged GUID via --botguid.
Caught by the pre-enable live smoke.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(ah-sp2): seed BOTH uuid minters past retained journal rows (cross-restart duplicate-PK)

Live WriteAuthority smoke surfaced a flood of duplicate-PRIMARY-KEY
`ah_worker_journal` INSERT errors. Root cause: the worker mints journal
PKs as (runId<<32)|seq, but the supervisor runId is NOT persistent -- it
resets to 1 on every mangosd restart -- so a minter that restarts low
re-mints uuids already persisted by the previous run. Terminal JRN_APPLIED
rows linger (the prune runs only in a selftest, never in production) and
are invisible to LoadActive (state IN 1,2,4,5), so they collide.

The worker has TWO independent journal-PK minters that share the runId
high word and partition the low-32 seq space by its high bit:
  - MutationHandler::m_nextSeq -- HIGH half [0x80000000,0xFFFFFFFF]:
    player mutations, resolves, bot buyout.
  - BotBrain::m_seq            -- LOW  half [1,0x7FFFFFFF]:
    bot sells (BotSellBegin) and simple bot bids (PersistBotBidSimple).
The pre-existing RESOLVING-only [FIX A] covered neither terminal rows nor
the second minter.

Fix -- seed EACH minter past its own half's retained max at boot:
  - AhJournal::MaxSeqForRunId(runId, highHalf, &out) -> bool: sargable
    PRIMARY KEY range scan (uuid BETWEEN runBase|lo AND runBase|hi) for
    the selected half; returns false on a query error (a no-GROUP-BY MAX
    always yields one row, so a NULL result means failure) -- boot fails
    closed rather than seed unsafely.
  - MutationHandler::SeedMinterPast (high half) + BotBrain::SeedSeqPast
    (low half); exhaustion sentinels keep neither minter from spilling
    into the other's half.
  - Main.cpp boot queries both halves under WriteAuthority; exits(1) if
    either query errors.
Selftests: high-half case in RunResolveOutboxSelfTest, low-half case in
RunBotFoldInSelfTest (via BotBrain::CurrentSeq()).

Found + completed under two rounds of adversarial multi-agent review
(round 1 caught the missed BotBrain minter; round 2 clean). Verified
against the live dirty journal: high-half max 0x80000087, low-half max
525 -> boot seeds player->0x80000088, bot->526, clear of all retained
rows. Worker-only change; --selftest all green.

Follow-up (SP-3): DeleteAppliedOlderThan is never wired into production
(journal grows unbounded); divergence #6 (high-bidder buyout no-op).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(ahbot): stand the ike3 AhBot down under AH.Service.WriteAuthority

The ike3 economy AhBot (src/modules/Bots/ahbot) writes the `auction` table
directly via sAuctionMgr/CharacterDatabase. Under SP-2 WriteAuthority the
out-of-process worker is the sole writer of the auction book, so running ike3
would double-write and corrupt it. ike3 is unwired in this tree (no startup
Init caller, no Update scheduler); the one live trigger is the `.ahbot` chat
command. This is a preventive safety-gate.

Three early-return guards on sWorld.IsAhWriteAuthority() (boot-latched, so
race-free from ike3's background AhbotThread): Init force-disables + logs,
ForceUpdate (economy funnel) returns, HandleCommand (.ahbot) returns with
operator feedback. Also define AhBotConfig::enabled = false in the ctor (it was
an uninitialized bool controlling auction writes) and document the
incompatibility in ahbot.conf.dist.in. Legacy (WriteAuthority=0) unchanged.

Full ike3-economy-through-the-worker integration is deferred (owner-model clash
with SP-2's single-owner materialization guard); see
Memory/Future_Work/AH_SubProcess/2026-07-03-ike3-ahbot-writeauthority-design.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(ah): recover committed cancel repairs

* fix(ah): route same-bidder buyouts via buyout intent

Under AH write-authority, the core could classify a current high bidder clicking Buyout as IPC_PLAYER_BID. The worker correctly rejects bids at or over buyout, so the click became a no-op. Route same-bidder top-ups through IPC_PLAYER_BUYOUT while keeping the delta reserve; below-buyout 0x42 remains a normal bid and at-buyout 0x42 resolves the win.

Tests: mangosd -t ahforwardreserve; mangosd -t ahmutresult; mangosd -t ahresolve; mangosd -t ahrepair

Co-authored-by: OpenAI Codex <noreply@openai.com>

* chore(ah): bump gates and remove custody runbook

Require the AH world and character DB updates from the core version gate, bump the AH-related config versions, and remove the local custody promotion runbook from the branch.

Co-authored-by: OpenAI Codex <noreply@openai.com>

* style(ah): align SP-2 changes with coding standard

Expand same-line bodies and compact test blocks, wrap the clearest long AH lines, adjust branch-added comments to Doxygen style, and make the crash-seam docs easier to read in source.

Co-authored-by: OpenAI Codex <noreply@openai.com>

* docs(ahbot): trim worker planning notes

Keep stable ah-service and worker write-authority operator documentation, but remove branch acceptance checklists, crash-injection runbook text, and SP-2 planning residue from AuctionHouseBot.md.

Co-authored-by: OpenAI Codex <noreply@openai.com>

* test(ah): seed mail receiver fixture

Make the mangosd mail harness self-contained by seeding offline character guid 1 only when the clone lacks it, then removing only that temporary fixture row after the test.

Co-authored-by: OpenAI Codex <noreply@openai.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: OpenAI Codex <noreply@openai.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants