Skip to content

Harden hot bot party reliability - #77

Merged
pmbstyle merged 1 commit into
mainfrom
agent/hot-bot-party-reliability
Jul 30, 2026
Merged

Harden hot bot party reliability#77
pmbstyle merged 1 commit into
mainfrom
agent/hot-bot-party-reliability

Conversation

@pmbstyle

@pmbstyle pmbstyle commented Jul 30, 2026

Copy link
Copy Markdown
Owner

Summary

This change hardens hot-bot party lifecycle and companion behavior so persisted parties, native invitations, loot rules, and emergency combat movement remain consistent after activation and restarts.

What changed

  • clean up dissolved or orphaned background parties before their members are activated
  • reserve hot-bot activations before asynchronous party cleanup to prevent duplicate concurrent spawns
  • keep bots already assigned to a persisted party out of ambient party recruitment
  • route party invitations through the native C4 accept/refuse flow, allowing a bot to reject an invitation normally
  • safely detach companions when the hunt command changes their autonomous behavior
  • respect By Turn loot ownership when companions collect party drops
  • make critically wounded non-tanks retreat without repeatedly aborting their active retreat route
  • add focused regression coverage for party cleanup, concurrent activation, native invite responses, loot ownership, and repeated low-HP AI ticks

Why

Hot-bot activation previously had an asynchronous gap where the same character could be activated more than once, while stale persisted party state could survive long enough to produce invalid membership or recruitment decisions. Some companion actions also bypassed native party semantics, and emergency retreat logic restarted itself every AI tick instead of allowing movement to complete.

These fixes keep party state authoritative across cold/hot transitions and make companion behavior match the game-facing C4 flow more closely.

Player impact

  • parties should remain coherent when bots activate or the server restarts
  • invited bots can accept or refuse through the normal party protocol
  • party loot distribution no longer gets bypassed by companion pickup logic
  • low-health non-tanks can complete their retreat instead of stuttering in place
  • background bots already belonging to a party are not silently recruited elsewhere

Validation

  • npm test
  • npm run check
  • git diff --check origin/main..HEAD

Summary by CodeRabbit

  • New Features
    • Party companions now support additional “By Turn” loot distribution options.
    • Critically wounded non-tank companions retreat from threats instead of continuing combat.
    • Bot party invitations now provide clearer acceptance and failure responses.
  • Bug Fixes
    • Improved recovery of bots from dissolved background parties.
    • Prevented party-affiliated bots from being incorrectly selected for nearby activation.
    • Hardened bot activation against incomplete cleanup and simultaneous activation attempts.
  • Tests
    • Added coverage for loot pickup, companion retreat, invitation flows, party recovery, and activation concurrency.

@coderabbitai

coderabbitai Bot commented Jul 30, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Party companion invitations, loot pickup, detachment, background-party lifecycle recovery, activation cleanup, near-player eligibility, and critical-health retreat behavior were updated with corresponding regression coverage.

Changes

Party companion flow

Layer / File(s) Summary
Native party invitation acknowledgement
src/GameServer/World/World.js, tests/test_party_companion_rest_follow.js
Bot invitations now use pending invite state, validate acceptance responses, and return JoinParty success or failure packets.
Companion detachment and loot behavior
src/GameServer/Bot/AI/PartyCompanionService.js, src/GameServer/Bot/BotManager.js, tests/test_party_bot_loot.js
Hunt commands detach managed companions, and distributions 1–4 enable automated ground-loot pickup before turn resolution.

Background-party lifecycle

Layer / File(s) Summary
Activation cleanup and concurrency
src/GameServer/Bot/Population/HotActivation.js, tests/test_bot_background_party_recruitment.js
Hot activation releases background parties before spawning, verifies cleanup, and rejects duplicate activations.
Startup recovery and activation eligibility
src/GameServer/Bot/Population/BotLifeState.js, src/GameServer/Bot/Population/PopulationService.js, tests/test_bot_population_state.js
Startup recovery releases dissolved-party members, while near-player activation excludes party-affiliated states.

Companion survival behavior

Layer / File(s) Summary
Critical-health retreat
src/GameServer/Bot/AI/States/FollowingState.js, tests/test_party_companion_rest_follow.js
Critically wounded non-tank companions clear combat state, retreat from threats, and preserve active retreat routes across ticks.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant PlayerSession
  participant World
  participant BotSession
  participant PartyCompanionService
  PlayerSession->>World: askForTeamUp
  World->>BotSession: store pendingPartyInvite
  BotSession->>World: answerForTeamUp id=1
  World->>PartyCompanionService: inviteBotCompanion
  PartyCompanionService-->>PlayerSession: joinParty result
Loading
sequenceDiagram
  participant HotActivation
  participant PartyState
  participant LifeState
  participant BotManager
  HotActivation->>PartyState: dissolve background party
  HotActivation->>LifeState: clear and refresh party state
  HotActivation->>HotActivation: verify release and reserve activation
  HotActivation->>BotManager: loadAndSpawnBot
Loading

Possibly related PRs

  • pmbstyle/L2Solo#76: Related party-threat and companion combat coordination changes in FollowingState.js.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title is concise and captures the main goal of improving hot bot party behavior and reliability.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch agent/hot-bot-party-reliability

Comment @coderabbitai help to get the list of available commands.

@pmbstyle pmbstyle self-assigned this Jul 30, 2026
@pmbstyle
pmbstyle marked this pull request as ready for review July 30, 2026 14:43

@coderabbitai coderabbitai 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.

Actionable comments posted: 4

🧹 Nitpick comments (3)
tests/test_party_bot_loot.js (2)

304-311: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Avoid hard-coding pickupCalls[10].

Capture the call index immediately before reconcileGroundLoot; otherwise unrelated fixture changes can make this test inspect or complete the wrong pickup.

Proposed fix
+    const pickupCallIndex = pickupCalls.length;
     PartyCompanionService.reconcileGroundLoot(botSession);
     assert.deepStrictEqual(
-        pickupCalls[10] && { session: pickupCalls[10].session, actor: pickupCalls[10].actor, data: pickupCalls[10].data },
+        pickupCalls[pickupCallIndex] && { session: pickupCalls[pickupCallIndex].session, actor: pickupCalls[pickupCallIndex].actor, data: pickupCalls[pickupCallIndex].data },
         { session: botSession, actor: closestBot, data: { id: 500011 } },
         'By Turn loot should still be collected from the ground by an available companion'
     );
-    pickupCalls[10].onComplete();
+    pickupCalls[pickupCallIndex].onComplete();
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_party_bot_loot.js` around lines 304 - 311, Update the test around
PartyCompanionService.reconcileGroundLoot to capture the pickupCalls length
immediately before reconciliation, then inspect and complete the newly added
call using that captured index instead of hard-coded pickupCalls[10].

291-295: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Cover distribution 4 as well.

The production change enables distributions 3 and 4, but this regression only exercises 3. Parameterize the case over both modes or add a separate By Turn Including Spoil case.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_party_bot_loot.js` around lines 291 - 295, Extend the regression
test around leaderSession.partyCompanionSettings to cover distribution modes 3
and 4, preferably by parameterizing the existing case while preserving its
current assertions and setup for each mode.
src/GameServer/Bot/Population/HotActivation.js (1)

229-233: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

craft_recipe_sync_failed over-reports. The flag stays true for everything after the craft profile lookup, so a loadAndSpawnBot throw is also reported as a recipe-sync failure (and the reservation is dropped even though a spawn may have been requested). Scope the flag to the ensureRecipes call itself.

♻️ Narrow the flag to the recipe sync
-                const recipesReady = craftShop
-                    ? CraftShopService.ensureRecipes(state.characterId, craftShop)
-                    : Promise.resolve();
+                const recipesReady = craftShop
+                    ? CraftShopService.ensureRecipes(state.characterId, craftShop)
+                        .catch((error) => { craftActivation = true; throw error; })
+                    : Promise.resolve();

with craftActivation initialized to false and no longer set from !!craftShop.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/GameServer/Bot/Population/HotActivation.js` around lines 229 - 233, Scope
the craft-specific failure flag to the ensureRecipes call in the activation
flow: initialize craftActivation to false and do not set it from !!craftShop
during the profile lookup. Set it only while ensureRecipes is executing, so
loadAndSpawnBot failures retain activation_prepare_failed handling.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/GameServer/Bot/AI/States/FollowingState.js`:
- Around line 444-459: Update retreatFromThreat so the rooted case is handled
before the retreatInProgress early return: clear session.partyRetreatUntil,
abort active automation, and return false when rooted. Only preserve the
existing retreat route and return true for non-rooted retreatInProgress cases.

In `@src/GameServer/Bot/Population/HotActivation.js`:
- Around line 124-134: Update the cached-state fast path in the promise callback
to normalize refreshed.activity from 'grouped' to 'hunting' before returning it,
matching the constructed state path while preserving all other refreshed state
fields.
- Around line 118-136: Update releaseBackgroundParty to compensate when the
post-setStatus clearParty validation fails: restore the background party’s prior
active status or enqueue the established retry/reconciliation path before
propagating the release error. Keep the existing successful refresh and
state-clearing behavior unchanged, and ensure compensation covers both a zero
clear count and a remaining cached party link.

In `@src/GameServer/World/World.js`:
- Around line 317-336: Update the invitation-answer handling around
pendingPartyInvite to preserve the existing player-to-player response flow:
validate and consume the pending invite, then route human invitations through
the normal party-formation path instead of inviteBotCompanion. Call
inviteBotCompanion only when the pending invitation targets a bot, while
retaining the existing rejection behavior for invalid or declined answers.

---

Nitpick comments:
In `@src/GameServer/Bot/Population/HotActivation.js`:
- Around line 229-233: Scope the craft-specific failure flag to the
ensureRecipes call in the activation flow: initialize craftActivation to false
and do not set it from !!craftShop during the profile lookup. Set it only while
ensureRecipes is executing, so loadAndSpawnBot failures retain
activation_prepare_failed handling.

In `@tests/test_party_bot_loot.js`:
- Around line 304-311: Update the test around
PartyCompanionService.reconcileGroundLoot to capture the pickupCalls length
immediately before reconciliation, then inspect and complete the newly added
call using that captured index instead of hard-coded pickupCalls[10].
- Around line 291-295: Extend the regression test around
leaderSession.partyCompanionSettings to cover distribution modes 3 and 4,
preferably by parameterizing the existing case while preserving its current
assertions and setup for each mode.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 514dedaf-789f-415f-a1ed-f05fd88824a8

📥 Commits

Reviewing files that changed from the base of the PR and between 5bc9d81 and 3d1a134.

📒 Files selected for processing (11)
  • src/GameServer/Bot/AI/PartyCompanionService.js
  • src/GameServer/Bot/AI/States/FollowingState.js
  • src/GameServer/Bot/BotManager.js
  • src/GameServer/Bot/Population/BotLifeState.js
  • src/GameServer/Bot/Population/HotActivation.js
  • src/GameServer/Bot/Population/PopulationService.js
  • src/GameServer/World/World.js
  • tests/test_bot_background_party_recruitment.js
  • tests/test_bot_population_state.js
  • tests/test_party_bot_loot.js
  • tests/test_party_companion_rest_follow.js

Comment on lines +444 to +459
function retreatFromThreat(session, bot, threat, player, rooted) {
const retreatInProgress = Date.now() < Number(session.partyRetreatUntil || 0) &&
(!!session.moveTimer || bot.state?.fetchTowards?.());
session.currentTargetId = undefined;
bot.unselect();
bot.attack?.abortCast?.(session, bot);
bot.attack?.clearTimers?.();
bot.state?.setHits?.(false);

// Damage wakeups can run this state several times before a 500-unit route
// completes. Keep the existing escape movement instead of cancelling it
// and returning without a replacement route on every cooldown tick.
if (retreatInProgress) return true;

bot.automation?.abortAll?.(bot);
if (rooted) return false;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Abort an active retreat when root is applied.

retreatInProgress returns before the rooted check. If the bot is rooted during the 1.5-second retreat window, its existing route remains active and moved is reported as true. Check rooted first, clear partyRetreatUntil, and abort automation before preserving a route.

Proposed fix
 function retreatFromThreat(session, bot, threat, player, rooted) {
     const retreatInProgress = Date.now() < Number(session.partyRetreatUntil || 0) &&
         (!!session.moveTimer || bot.state?.fetchTowards?.());
     session.currentTargetId = undefined;
     bot.unselect();
     bot.attack?.abortCast?.(session, bot);
     bot.attack?.clearTimers?.();
     bot.state?.setHits?.(false);
 
+    if (rooted) {
+        session.partyRetreatUntil = 0;
+        bot.automation?.abortAll?.(bot);
+        return false;
+    }
+
     if (retreatInProgress) return true;
 
     bot.automation?.abortAll?.(bot);
-    if (rooted) return false;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
function retreatFromThreat(session, bot, threat, player, rooted) {
const retreatInProgress = Date.now() < Number(session.partyRetreatUntil || 0) &&
(!!session.moveTimer || bot.state?.fetchTowards?.());
session.currentTargetId = undefined;
bot.unselect();
bot.attack?.abortCast?.(session, bot);
bot.attack?.clearTimers?.();
bot.state?.setHits?.(false);
// Damage wakeups can run this state several times before a 500-unit route
// completes. Keep the existing escape movement instead of cancelling it
// and returning without a replacement route on every cooldown tick.
if (retreatInProgress) return true;
bot.automation?.abortAll?.(bot);
if (rooted) return false;
function retreatFromThreat(session, bot, threat, player, rooted) {
const retreatInProgress = Date.now() < Number(session.partyRetreatUntil || 0) &&
(!!session.moveTimer || bot.state?.fetchTowards?.());
session.currentTargetId = undefined;
bot.unselect();
bot.attack?.abortCast?.(session, bot);
bot.attack?.clearTimers?.();
bot.state?.setHits?.(false);
if (rooted) {
session.partyRetreatUntil = 0;
bot.automation?.abortAll?.(bot);
return false;
}
// Damage wakeups can run this state several times before a 500-unit route
// completes. Keep the existing escape movement instead of cancelling it
// and returning without a replacement route on every cooldown tick.
if (retreatInProgress) return true;
bot.automation?.abortAll?.(bot);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/GameServer/Bot/AI/States/FollowingState.js` around lines 444 - 459,
Update retreatFromThreat so the rooted case is handled before the
retreatInProgress early return: clear session.partyRetreatUntil, abort active
automation, and return false when rooted. Only preserve the existing retreat
route and return true for non-rooted retreatInProgress cases.

Comment on lines +118 to +136
function releaseBackgroundParty(state, reason) {
const partyId = state?.party?.partyId;
if (!partyId) return Promise.resolve(state);

return BackgroundPartyState.setStatus(partyId, 'dissolved')
.then(() => LifeState.clearParty(partyId, `hot_activation_${reason}`))
.then((cleared) => {
const refreshed = LifeState.cachedState(state.characterId);
if (refreshed && !refreshed.party?.partyId) return refreshed;
if (Number(cleared || 0) <= 0 || refreshed?.party?.partyId) {
throw new Error(`background_party_release_failed:${partyId}`);
}
return {
...state,
activity: state.activity === 'grouped' ? 'hunting' : state.activity,
party: { ...(state.party || {}), partyId: null, leaderId: null }
};
});
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

No compensation when clearParty fails after the party is already marked dissolved.

setStatus(partyId, 'dissolved') is committed before clearParty. If the clear returns 0 or the cache still shows the link, the thrown error aborts activation but the background party stays dissolved while member rows still reference partyId. Those members are then orphaned at runtime — nothing reconciles them until recoverDissolvedPartyMembers() runs on the next restart. Consider restoring the party status (or enqueuing a retry) in a failure handler here.

🛠️ Sketch of a compensating path
     return BackgroundPartyState.setStatus(partyId, 'dissolved')
         .then(() => LifeState.clearParty(partyId, `hot_activation_${reason}`))
         .then((cleared) => {
@@
             return {
                 ...state,
                 activity: state.activity === 'grouped' ? 'hunting' : state.activity,
                 party: { ...(state.party || {}), partyId: null, leaderId: null }
             };
-        });
+        })
+        .catch((error) => BackgroundPartyState.setStatus(partyId, 'active')
+            .catch(() => null)
+            .then(() => { throw error; }));
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
function releaseBackgroundParty(state, reason) {
const partyId = state?.party?.partyId;
if (!partyId) return Promise.resolve(state);
return BackgroundPartyState.setStatus(partyId, 'dissolved')
.then(() => LifeState.clearParty(partyId, `hot_activation_${reason}`))
.then((cleared) => {
const refreshed = LifeState.cachedState(state.characterId);
if (refreshed && !refreshed.party?.partyId) return refreshed;
if (Number(cleared || 0) <= 0 || refreshed?.party?.partyId) {
throw new Error(`background_party_release_failed:${partyId}`);
}
return {
...state,
activity: state.activity === 'grouped' ? 'hunting' : state.activity,
party: { ...(state.party || {}), partyId: null, leaderId: null }
};
});
}
function releaseBackgroundParty(state, reason) {
const partyId = state?.party?.partyId;
if (!partyId) return Promise.resolve(state);
return BackgroundPartyState.setStatus(partyId, 'dissolved')
.then(() => LifeState.clearParty(partyId, `hot_activation_${reason}`))
.then((cleared) => {
const refreshed = LifeState.cachedState(state.characterId);
if (refreshed && !refreshed.party?.partyId) return refreshed;
if (Number(cleared || 0) <= 0 || refreshed?.party?.partyId) {
throw new Error(`background_party_release_failed:${partyId}`);
}
return {
...state,
activity: state.activity === 'grouped' ? 'hunting' : state.activity,
party: { ...(state.party || {}), partyId: null, leaderId: null }
};
})
.catch((error) => BackgroundPartyState.setStatus(partyId, 'active')
.catch(() => null)
.then(() => { throw error; }));
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/GameServer/Bot/Population/HotActivation.js` around lines 118 - 136,
Update releaseBackgroundParty to compensate when the post-setStatus clearParty
validation fails: restore the background party’s prior active status or enqueue
the established retry/reconciliation path before propagating the release error.
Keep the existing successful refresh and state-clearing behavior unchanged, and
ensure compensation covers both a zero clear count and a remaining cached party
link.

Comment on lines +124 to +134
.then((cleared) => {
const refreshed = LifeState.cachedState(state.characterId);
if (refreshed && !refreshed.party?.partyId) return refreshed;
if (Number(cleared || 0) <= 0 || refreshed?.party?.partyId) {
throw new Error(`background_party_release_failed:${partyId}`);
}
return {
...state,
activity: state.activity === 'grouped' ? 'hunting' : state.activity,
party: { ...(state.party || {}), partyId: null, leaderId: null }
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Cached-state fast path skips the groupedhunting normalization.

Line 126 returns refreshed as-is, so a cache entry with partyId cleared but activity === 'grouped' propagates into activationPlan and backgroundActivity, unlike the constructed state at Line 132. Normalize both paths.

♻️ Proposed fix
-            const refreshed = LifeState.cachedState(state.characterId);
-            if (refreshed && !refreshed.party?.partyId) return refreshed;
+            const refreshed = LifeState.cachedState(state.characterId);
+            if (refreshed && !refreshed.party?.partyId) {
+                return {
+                    ...refreshed,
+                    activity: refreshed.activity === 'grouped' ? 'hunting' : refreshed.activity
+                };
+            }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
.then((cleared) => {
const refreshed = LifeState.cachedState(state.characterId);
if (refreshed && !refreshed.party?.partyId) return refreshed;
if (Number(cleared || 0) <= 0 || refreshed?.party?.partyId) {
throw new Error(`background_party_release_failed:${partyId}`);
}
return {
...state,
activity: state.activity === 'grouped' ? 'hunting' : state.activity,
party: { ...(state.party || {}), partyId: null, leaderId: null }
};
.then((cleared) => {
const refreshed = LifeState.cachedState(state.characterId);
if (refreshed && !refreshed.party?.partyId) {
return {
...refreshed,
activity: refreshed.activity === 'grouped' ? 'hunting' : refreshed.activity
};
}
if (Number(cleared || 0) <= 0 || refreshed?.party?.partyId) {
throw new Error(`background_party_release_failed:${partyId}`);
}
return {
...state,
activity: state.activity === 'grouped' ? 'hunting' : state.activity,
party: { ...(state.party || {}), partyId: null, leaderId: null }
};
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/GameServer/Bot/Population/HotActivation.js` around lines 124 - 134,
Update the cached-state fast path in the promise callback to normalize
refreshed.activity from 'grouped' to 'hunting' before returning it, matching the
constructed state path while preserving all other refreshed state fields.

Comment on lines +317 to +336
const pending = session.pendingPartyInvite;
session.pendingPartyInvite = null;

if (!pending?.requestorSession || !pending?.requestorActor) {
session.dataSendToMe(ServerResponse.actionFailed());
return false;
}

if (Number(data?.id) !== 1) {
pending.requestorSession.dataSendToMe(ServerResponse.joinParty(0));
return false;
}

return this.inviteBotCompanion(
pending.requestorSession,
pending.requestorActor,
session,
pending.distribution,
pending.source || 'invite'
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Restore player-to-player invitation answers.

The non-bot path still sends askForTeamUp without a pending invite, so its native answer reaches Line 320 and is rejected. Even if pending state were added, this method now always calls inviteBotCompanion, so accepted player invitations cannot form a normal party. Preserve the prior human-answer path and branch to bot attachment only for bot invitations.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/GameServer/World/World.js` around lines 317 - 336, Update the
invitation-answer handling around pendingPartyInvite to preserve the existing
player-to-player response flow: validate and consume the pending invite, then
route human invitations through the normal party-formation path instead of
inviteBotCompanion. Call inviteBotCompanion only when the pending invitation
targets a bot, while retaining the existing rejection behavior for invalid or
declined answers.

@pmbstyle
pmbstyle merged commit 3053bb7 into main Jul 30, 2026
4 checks passed
@pmbstyle
pmbstyle deleted the agent/hot-bot-party-reliability branch July 30, 2026 17:41
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.

1 participant