mission_script_runtime_delivery.cpp 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595
  1. #include <cstdint>
  2. #include <limits>
  3. #include <string_view>
  4. #include "../../bap/runtime.h"
  5. #include "../../gameplay/squad_entity_retirement.h"
  6. #include "mission_script_runtime_internal.h"
  7. // The delivery state machine: the four stages and the timeout reconcilers. A
  8. // script owns one intent at a time, so every step reads and writes one instance. The intent
  9. // fan-out, the instance table and the service loop stay elsewhere.
  10. namespace sunrise::server::activity::mission {
  11. /** @return now plus delay, saturated at the maximum instead of wrapping. */
  12. [[nodiscard]] std::uint64_t deadline_after(std::uint64_t now, std::uint64_t delay) noexcept {
  13. return now > (std::numeric_limits<std::uint64_t>::max)() - delay
  14. ? (std::numeric_limits<std::uint64_t>::max)()
  15. : now + delay;
  16. }
  17. namespace {
  18. /** How long a cancelled Host output may take to confirm before the delivery faults. */
  19. constexpr std::uint64_t kCancelTimeoutMs = 2'000;
  20. /** Recheck delay while a cancel request could not be queued yet. */
  21. constexpr std::uint64_t kCancelRetryMs = 250;
  22. /** How long one intent may stay in delivery before it is refused. */
  23. constexpr std::uint64_t kIntentLifetimeMs = 60'000;
  24. /** Retired head events compact only past this count, so small queues never reallocate. */
  25. constexpr std::size_t kScriptEventCompactionThreshold = 64;
  26. /** Acknowledges only the exact durable head and Host output revision. */
  27. [[nodiscard]] bool acknowledge_delivery_state(RuntimeInstance& instance) noexcept {
  28. mission_state::Snapshot snapshot{};
  29. const mission_state::Status status =
  30. mission_state::acknowledge_intent_output(instance.view.binding,
  31. instance.programKey,
  32. instance.missionStateRevision,
  33. instance.durableIntentSequence,
  34. instance.expectedScriptableRevision,
  35. snapshot);
  36. if (status != mission_state::Status::ready) {
  37. // This attachment lost its compare. Leave authoritative State unchanged so an exact
  38. // reattach can reconcile the retained Host transport revision without rerunning Lua.
  39. lua_vm::fault(instance.vm, "durable mission intent acknowledgement compare was refused");
  40. instance.programStatus = ProgramStatus::programError;
  41. log_line(
  42. core::log::Level::warn, &instance, "intent_ack", mission_state::status_name(status));
  43. return false;
  44. }
  45. accept_mission_state(instance, snapshot);
  46. return true;
  47. }
  48. /**
  49. * Stages one effectResult event for the script that raised the intent.
  50. * The event replaces nothing already staged, because a program runs one delivery at a time. The
  51. * service tick is stamped when the event is dispatched, not here.
  52. */
  53. void queue_effect_result(RuntimeInstance& instance,
  54. const lua_vm::Intent& intent,
  55. host::EffectOutcome outcome) noexcept {
  56. if (intent.requestKey == mission_state::kAbsentIntentKey) {
  57. return;
  58. }
  59. host::Event event{};
  60. event.binding = instance.view.binding;
  61. event.sequence = intent.requestKey;
  62. event.sourceGeneration = instance.view.activityClientGeneration;
  63. event.missionSequence = instance.lastMissionSequence;
  64. event.effectRequestKey = intent.requestKey;
  65. event.effectAction = static_cast<std::uint8_t>(intent.kind);
  66. event.effectOutcome = outcome;
  67. event.kind = host::EventKind::effectResult;
  68. push_script_event(instance, event);
  69. }
  70. } // namespace
  71. /** Releases an exact unstaged Host revision while retaining the durable intent. */
  72. [[nodiscard]] bool release_delivery_state(RuntimeInstance& instance) noexcept {
  73. if (instance.expectedScriptableRevision == 0) {
  74. return true;
  75. }
  76. mission_state::Snapshot snapshot{};
  77. const mission_state::Status status =
  78. mission_state::release_intent_output(instance.view.binding,
  79. instance.programKey,
  80. instance.missionStateRevision,
  81. instance.durableIntentSequence,
  82. instance.expectedScriptableRevision,
  83. snapshot);
  84. if (status != mission_state::Status::ready) {
  85. lua_vm::fault(instance.vm, "durable mission intent release compare was refused");
  86. instance.programStatus = ProgramStatus::programError;
  87. log_line(core::log::Level::warn,
  88. &instance,
  89. "intent_release",
  90. mission_state::status_name(status));
  91. return false;
  92. }
  93. accept_mission_state(instance, snapshot);
  94. // The durable head owns no Host revision now, so a later step must not release it twice.
  95. instance.expectedScriptableRevision = 0;
  96. return true;
  97. }
  98. /** Returns the instance to the idle stage and clears delivery timing. */
  99. void clear_delivery(RuntimeInstance& instance) noexcept {
  100. instance.expectedScriptableRevision = 0;
  101. instance.deliveryDeadline = 0;
  102. instance.firstIntentAttempt = 0;
  103. instance.nextIntentAttempt = 0;
  104. instance.intentAttempts = 0;
  105. instance.lastIntentStatus = (std::numeric_limits<std::uint16_t>::max)();
  106. instance.deliveryStage = DeliveryStage::idle;
  107. }
  108. /** Retires one idempotently applied local effect and emits its terminal result. */
  109. bool complete_local_effect(RuntimeInstance& instance, std::string_view result) noexcept {
  110. lua_vm::Intent intent{};
  111. if (!lua_vm::pending_intent(instance.vm, intent)
  112. || instance.durableIntentSequence == mission_state::kAbsentIntentSequence
  113. || instance.durableHostOutputRevision != mission_state::kAbsentHostOutputRevision
  114. || instance.expectedScriptableRevision != 0) {
  115. fault_delivery(instance,
  116. "local_effect_mismatch",
  117. "local effect did not match the unassigned durable intent head");
  118. return false;
  119. }
  120. mission_state::Snapshot snapshot{};
  121. const mission_state::Status status =
  122. mission_state::acknowledge_intent(instance.view.binding,
  123. instance.programKey,
  124. instance.missionStateRevision,
  125. instance.durableIntentSequence,
  126. snapshot);
  127. if (status != mission_state::Status::ready) {
  128. fault_delivery(instance, "local_effect_ack", mission_state::status_name(status));
  129. return false;
  130. }
  131. accept_mission_state(instance, snapshot);
  132. lua_vm::consume_intent(instance.vm);
  133. ++instance.intentsTransportStaged;
  134. log_line(core::log::Level::info, &instance, "delivery", result);
  135. queue_effect_result(instance, intent, host::EffectOutcome::transportStaged);
  136. clear_delivery(instance);
  137. return true;
  138. }
  139. /** Faults the program and abandons the delivery. */
  140. void fault_delivery(RuntimeInstance& instance,
  141. std::string_view result,
  142. std::string_view reason) noexcept {
  143. fault_instance(instance, reason);
  144. instance.programStatus = ProgramStatus::programError;
  145. clear_pending_events(instance.view.binding);
  146. clear_delivery(instance);
  147. log_line(core::log::Level::warn, &instance, "delivery", result, {}, reason);
  148. }
  149. /**
  150. * Logs one adapter status, and only when it differs from the last one logged.
  151. * @param status Adapter status, biased so each adapter owns its own range.
  152. * @param name Diagnostic name of that status.
  153. */
  154. void report_intent_status(RuntimeInstance& instance,
  155. std::uint16_t status,
  156. std::string_view name) noexcept {
  157. if (instance.lastIntentStatus == status) {
  158. return;
  159. }
  160. instance.lastIntentStatus = status;
  161. log_line(core::log::Level::debug, &instance, "intent", name);
  162. }
  163. /**
  164. * @param now Current service tick.
  165. * @return Whether the intent has been in delivery longer than its lifetime allows.
  166. */
  167. [[nodiscard]] bool intent_lifetime_expired(const RuntimeInstance& instance,
  168. std::uint64_t now) noexcept {
  169. return instance.intentAttempts != 0
  170. && now >= deadline_after(instance.firstIntentAttempt, kIntentLifetimeMs);
  171. }
  172. namespace {
  173. /** Clears one VM and State head only after the same Host revision reached transport. */
  174. void complete_delivery(RuntimeInstance& instance) noexcept {
  175. lua_vm::Intent intent{};
  176. if (!lua_vm::pending_intent(instance.vm, intent)) {
  177. fault_delivery(instance, "missing_intent", "transport staged without a pending intent");
  178. return;
  179. }
  180. if (instance.expectedScriptableRevision == 0
  181. || instance.durableIntentSequence == mission_state::kAbsentIntentSequence
  182. || instance.durableHostOutputRevision != instance.expectedScriptableRevision) {
  183. lua_vm::fault(instance.vm, "transport stage did not match durable mission State");
  184. instance.programStatus = ProgramStatus::programError;
  185. log_line(core::log::Level::warn, &instance, "intent_ack", "durable_mismatch");
  186. clear_pending_events(instance.view.binding);
  187. clear_delivery(instance);
  188. return;
  189. }
  190. if (!acknowledge_delivery_state(instance)) {
  191. clear_pending_events(instance.view.binding);
  192. clear_delivery(instance);
  193. return;
  194. }
  195. lua_vm::consume_intent(instance.vm);
  196. const char* result = nullptr;
  197. switch (intent.kind) {
  198. case lua_vm::IntentKind::placeSquad:
  199. result = "squad_staged";
  200. break;
  201. case lua_vm::IntentKind::actorCommand:
  202. result = "actor_command_staged";
  203. break;
  204. case lua_vm::IntentKind::bindCombatantToSquad:
  205. result = "combatant_binding_staged";
  206. break;
  207. case lua_vm::IntentKind::activateAuthoredScene:
  208. result = "scene_staged";
  209. break;
  210. case lua_vm::IntentKind::setObjectActive:
  211. result = "object_staged";
  212. break;
  213. case lua_vm::IntentKind::setDeviceChannel:
  214. result = "device_staged";
  215. break;
  216. case lua_vm::IntentKind::applySlotAuth:
  217. result = "slot_auth_staged";
  218. break;
  219. case lua_vm::IntentKind::setLifetime:
  220. result = "lifetime_staged";
  221. break;
  222. case lua_vm::IntentKind::restartCheckpoint:
  223. result = "checkpoint_staged";
  224. break;
  225. case lua_vm::IntentKind::fireTrigger:
  226. result = "trigger_staged";
  227. break;
  228. case lua_vm::IntentKind::playSequence:
  229. result = "sequence_staged";
  230. break;
  231. case lua_vm::IntentKind::setCinematicActive:
  232. result = "cinematic_staged";
  233. break;
  234. case lua_vm::IntentKind::playPerformance:
  235. result = "performance_staged";
  236. break;
  237. case lua_vm::IntentKind::resetObjectives:
  238. result = "objective_reset_staged";
  239. break;
  240. case lua_vm::IntentKind::advanceTask:
  241. result = "task_staged";
  242. break;
  243. case lua_vm::IntentKind::playDialogueCue:
  244. result = "dialogue_staged";
  245. break;
  246. case lua_vm::IntentKind::selectMissionState:
  247. result = "state_selected";
  248. break;
  249. }
  250. ++instance.intentsTransportStaged;
  251. log_line(core::log::Level::info, &instance, "delivery", result);
  252. queue_effect_result(instance, intent, host::EffectOutcome::transportStaged);
  253. clear_delivery(instance);
  254. }
  255. /** Atomically excludes a refused queued Host output, then retires the intent exactly once. */
  256. void terminate_refused_delivery(RuntimeInstance& instance,
  257. std::uint64_t now,
  258. std::string_view result,
  259. std::string_view reason,
  260. host::EffectOutcome outcome) noexcept {
  261. const host::ScriptableWithdrawStatus withdrawn = host::withdraw_scriptable_output(
  262. instance.view.binding, instance.durableIntentSequence, instance.expectedScriptableRevision);
  263. if (withdrawn == host::ScriptableWithdrawStatus::transportStaged) {
  264. complete_delivery(instance);
  265. return;
  266. }
  267. if (withdrawn == host::ScriptableWithdrawStatus::committed) {
  268. instance.deliveryStage = DeliveryStage::awaitingTransport;
  269. instance.deliveryDeadline = deadline_after(now, kTransportTimeoutMs);
  270. log_line(core::log::Level::debug, &instance, "delivery", "commit_race_reconciled");
  271. return;
  272. }
  273. if (withdrawn == host::ScriptableWithdrawStatus::advanced
  274. || withdrawn == host::ScriptableWithdrawStatus::mismatch) {
  275. fault_delivery(instance,
  276. "withdraw_mismatch",
  277. "Host could not withdraw the exact queued mission output");
  278. return;
  279. }
  280. refuse_delivery(instance, result, reason, outcome);
  281. }
  282. } // namespace
  283. /**
  284. * Reports one refused request to the script and keeps the program running.
  285. * The durable head is dropped, so the outbox advances. A State compare that refuses the drop is a
  286. * real inconsistency and still faults.
  287. */
  288. void refuse_delivery(RuntimeInstance& instance,
  289. std::string_view result,
  290. std::string_view reason,
  291. host::EffectOutcome outcome) noexcept {
  292. lua_vm::Intent intent{};
  293. if (!lua_vm::pending_intent(instance.vm, intent)) {
  294. fault_delivery(instance, result, reason);
  295. return;
  296. }
  297. if (instance.expectedScriptableRevision != 0 && !release_delivery_state(instance)) {
  298. return;
  299. }
  300. if (instance.durableIntentSequence != mission_state::kAbsentIntentSequence) {
  301. mission_state::Snapshot snapshot{};
  302. const mission_state::Status status =
  303. mission_state::discard_intent(instance.view.binding,
  304. instance.programKey,
  305. instance.missionStateRevision,
  306. instance.durableIntentSequence,
  307. snapshot);
  308. if (status != mission_state::Status::ready) {
  309. fault_delivery(instance, "discard_refused", mission_state::status_name(status));
  310. return;
  311. }
  312. accept_mission_state(instance, snapshot);
  313. }
  314. lua_vm::consume_intent(instance.vm);
  315. queue_effect_result(instance, intent, outcome);
  316. if (intent.retirePlacedProps) {
  317. server::gameplay::squad_entity_retirement::cancel_placed_transition(
  318. instance.view.binding, instance.view.activityClientGeneration, intent.requestKey);
  319. }
  320. clear_delivery(instance);
  321. log_line(core::log::Level::warn, &instance, "intent_refused", result, {}, reason);
  322. }
  323. /**
  324. * Reconciles an exact transport stage whose Host event cursor was reset on reattach.
  325. * @return Whether the delivery was completed here.
  326. */
  327. [[nodiscard]] bool reconcile_transport_stage(RuntimeInstance& instance) noexcept {
  328. if (instance.deliveryStage == DeliveryStage::idle || instance.expectedScriptableRevision == 0) {
  329. return false;
  330. }
  331. host::InstanceSnapshot hostView{};
  332. if (host::instance_snapshot(instance.view.binding, hostView)
  333. && hostView.scriptableTransportRevision == instance.expectedScriptableRevision) {
  334. complete_delivery(instance);
  335. return true;
  336. }
  337. return false;
  338. }
  339. /**
  340. * Reconciles a lost stage event and cancels an exact unstaged Host body at hard expiry.
  341. * @return Whether the caller must stop, because the delivery was completed or left owned.
  342. */
  343. [[nodiscard]] bool reconcile_expired_delivery(RuntimeInstance& instance) noexcept {
  344. if (instance.deliveryStage == DeliveryStage::idle || instance.expectedScriptableRevision == 0) {
  345. return false;
  346. }
  347. const host::ScriptableWithdrawStatus withdrawn = host::withdraw_scriptable_output(
  348. instance.view.binding, instance.durableIntentSequence, instance.expectedScriptableRevision);
  349. if (withdrawn == host::ScriptableWithdrawStatus::transportStaged) {
  350. complete_delivery(instance);
  351. return true;
  352. }
  353. if (withdrawn == host::ScriptableWithdrawStatus::withdrawn
  354. || withdrawn == host::ScriptableWithdrawStatus::absent
  355. || withdrawn == host::ScriptableWithdrawStatus::canceled) {
  356. // Preserve the typed diagnostic head but remove ownership of an output that cannot run.
  357. return !release_delivery_state(instance);
  358. }
  359. if (withdrawn != host::ScriptableWithdrawStatus::committed) {
  360. return false;
  361. }
  362. if (server::bap::cancel_activity_scriptable_override(instance.view.binding,
  363. instance.expectedScriptableRevision)) {
  364. return !release_delivery_state(instance);
  365. }
  366. host::InstanceSnapshot reconciled{};
  367. if (host::instance_snapshot(instance.view.binding, reconciled)
  368. && reconciled.scriptableTransportRevision == instance.expectedScriptableRevision) {
  369. complete_delivery(instance);
  370. return true;
  371. }
  372. return false;
  373. }
  374. /**
  375. * Advances the delivery stage whose deadline has passed.
  376. * @param now Current service tick.
  377. * @return Whether a deadline fired, so no further work is owed this tick.
  378. */
  379. [[nodiscard]] bool service_delivery_timeout(RuntimeInstance& instance, std::uint64_t now) noexcept {
  380. if (instance.deliveryStage == DeliveryStage::idle || now < instance.deliveryDeadline) {
  381. return false;
  382. }
  383. host::InstanceSnapshot hostView{};
  384. if (!host::instance_snapshot(instance.view.binding, hostView)) {
  385. fault_delivery(instance, "host_missing", "Host instance vanished during delivery");
  386. return true;
  387. }
  388. if (hostView.scriptableTransportRevision == instance.expectedScriptableRevision) {
  389. complete_delivery(instance);
  390. return true;
  391. }
  392. if (hostView.scriptableRevision > instance.expectedScriptableRevision) {
  393. fault_delivery(instance, "revision_advanced", "Host advanced past the queued intent");
  394. return true;
  395. }
  396. if (instance.deliveryStage == DeliveryStage::awaitingHostCommit) {
  397. if (hostView.scriptableRevision == instance.expectedScriptableRevision
  398. && hostView.outputPending
  399. && hostView.outputKind == host::OutputKind::scriptableOverride) {
  400. instance.deliveryStage = DeliveryStage::awaitingTransport;
  401. instance.deliveryDeadline = deadline_after(now, kTransportTimeoutMs);
  402. log_line(core::log::Level::debug, &instance, "delivery", "commit_reconciled");
  403. } else if (hostView.scriptableRevision == instance.expectedScriptableRevision
  404. && !hostView.outputPending) {
  405. // The control applied and the output was then dropped. Nothing is left to stage.
  406. refuse_delivery(instance,
  407. "commit_canceled",
  408. "Host dropped the committed output before it staged",
  409. host::EffectOutcome::canceled);
  410. } else {
  411. terminate_refused_delivery(instance,
  412. now,
  413. "commit_missing",
  414. "Host did not commit the queued intent",
  415. host::EffectOutcome::refused);
  416. }
  417. return true;
  418. }
  419. if (instance.deliveryStage == DeliveryStage::awaitingTransport) {
  420. if (hostView.scriptableRevision != instance.expectedScriptableRevision
  421. || (hostView.outputPending
  422. && hostView.outputKind != host::OutputKind::scriptableOverride)) {
  423. fault_delivery(
  424. instance, "host_state_mismatch", "Host state no longer owns the queued intent");
  425. return true;
  426. }
  427. if (!hostView.outputPending) {
  428. refuse_delivery(instance,
  429. "stage_canceled",
  430. "Host dropped the committed output before it staged",
  431. host::EffectOutcome::canceled);
  432. return true;
  433. }
  434. if (server::bap::cancel_activity_scriptable_override(instance.view.binding,
  435. instance.expectedScriptableRevision)) {
  436. instance.deliveryStage = DeliveryStage::awaitingCancel;
  437. instance.deliveryDeadline = deadline_after(now, kCancelTimeoutMs);
  438. log_line(core::log::Level::debug, &instance, "delivery", "cancel_requested");
  439. } else {
  440. instance.deliveryDeadline = deadline_after(now, kCancelRetryMs);
  441. report_intent_status(instance, kIntentStatusCancelPending, "cancel_pending");
  442. }
  443. return true;
  444. }
  445. if (!hostView.outputPending) {
  446. refuse_delivery(instance,
  447. "cancel_reconciled",
  448. "Host canceled the timed-out intent",
  449. host::EffectOutcome::canceled);
  450. } else {
  451. fault_delivery(instance, "cancel_timeout", "Host did not confirm intent cancellation");
  452. }
  453. return true;
  454. }
  455. /** Retires the head event after its single delivery attempt. */
  456. void retire_script_event(RuntimeInstance& instance) noexcept {
  457. if (instance.scriptEventRead < instance.scriptEvents.size()) {
  458. ++instance.scriptEventRead;
  459. if (instance.scriptEventRead == instance.scriptEvents.size()) {
  460. instance.scriptEvents.clear();
  461. instance.scriptEventRead = 0;
  462. } else if (instance.scriptEventRead >= kScriptEventCompactionThreshold
  463. && instance.scriptEventRead >= instance.scriptEvents.size() / 2) {
  464. instance.scriptEvents.erase(
  465. instance.scriptEvents.begin(),
  466. instance.scriptEvents.begin()
  467. + static_cast<std::ptrdiff_t>(instance.scriptEventRead));
  468. instance.scriptEventRead = 0;
  469. }
  470. }
  471. instance.firstScriptEventAttempt = 0;
  472. instance.nextScriptEventAttempt = 0;
  473. instance.scriptEventAttempts = 0;
  474. }
  475. /**
  476. * Advances the delivery stage on one host event, or faults the delivery.
  477. * A revision that is zero or not the one the queued intent expects is a fault, never a skip.
  478. * @param now Service tick the next stage deadline is measured from.
  479. */
  480. void observe_delivery_event(RuntimeInstance& instance,
  481. const host::Event& event,
  482. std::uint64_t now) noexcept {
  483. if (instance.deliveryStage == DeliveryStage::idle) {
  484. return;
  485. }
  486. if (instance.deliveryStage == DeliveryStage::awaitingHostCommit) {
  487. if (event.kind == host::EventKind::scriptableOverrideCommitted) {
  488. if (event.scriptableRevision == 0
  489. || event.scriptableRevision != instance.expectedScriptableRevision) {
  490. fault_delivery(instance,
  491. "invalid_commit",
  492. "Host commit did not match the queued intent revision");
  493. return;
  494. }
  495. instance.deliveryDeadline = deadline_after(now, kTransportTimeoutMs);
  496. instance.deliveryStage = DeliveryStage::awaitingTransport;
  497. log_line(core::log::Level::debug, &instance, "delivery", "host_committed");
  498. } else if (event.kind == host::EventKind::scriptableOverrideTransportStaged) {
  499. if (event.scriptableRevision == 0
  500. || event.scriptableRevision != instance.expectedScriptableRevision) {
  501. fault_delivery(instance,
  502. "invalid_stage",
  503. "transport stage did not match the queued intent revision");
  504. return;
  505. }
  506. complete_delivery(instance);
  507. } else if (event.kind == host::EventKind::operatorRefused) {
  508. terminate_refused_delivery(instance,
  509. now,
  510. "host_refused",
  511. "Host refused the queued intent",
  512. host::EffectOutcome::refused);
  513. }
  514. return;
  515. }
  516. if (event.kind != host::EventKind::scriptableOverrideTransportStaged
  517. && event.kind != host::EventKind::scriptableOverrideCanceled) {
  518. return;
  519. }
  520. if (event.scriptableRevision == 0
  521. || event.scriptableRevision != instance.expectedScriptableRevision) {
  522. fault_delivery(instance, "revision_mismatch", "Host delivery revision did not match");
  523. return;
  524. }
  525. if (event.kind == host::EventKind::scriptableOverrideTransportStaged) {
  526. complete_delivery(instance);
  527. return;
  528. }
  529. if (instance.deliveryStage == DeliveryStage::awaitingCancel) {
  530. refuse_delivery(instance,
  531. "host_canceled",
  532. "Host canceled the timed-out intent",
  533. host::EffectOutcome::canceled);
  534. return;
  535. }
  536. refuse_delivery(instance,
  537. "host_canceled",
  538. "Host discarded the queued output before it staged",
  539. host::EffectOutcome::canceled);
  540. }
  541. /**
  542. * Reconciles an output assigned before a terminal program fault without ever starting a new
  543. * adapter request. The Host withdraw operation serializes queued-control removal against its
  544. * reducer; committed output is allowed to finish exactly once and staged output is acknowledged.
  545. */
  546. void reconcile_terminal_delivery(RuntimeInstance& instance) noexcept {
  547. if (instance.programStatus != ProgramStatus::programError || !instance.missionStateFaulted
  548. || instance.durableHostOutputRevision == mission_state::kAbsentHostOutputRevision
  549. || instance.durableIntentSequence == mission_state::kAbsentIntentSequence) {
  550. return;
  551. }
  552. instance.expectedScriptableRevision = instance.durableHostOutputRevision;
  553. const host::ScriptableWithdrawStatus status = host::withdraw_scriptable_output(
  554. instance.view.binding, instance.durableIntentSequence, instance.expectedScriptableRevision);
  555. if (status == host::ScriptableWithdrawStatus::transportStaged) {
  556. complete_delivery(instance);
  557. return;
  558. }
  559. if (status == host::ScriptableWithdrawStatus::withdrawn
  560. || status == host::ScriptableWithdrawStatus::absent
  561. || status == host::ScriptableWithdrawStatus::canceled) {
  562. if (release_delivery_state(instance)) {
  563. clear_delivery(instance);
  564. log_line(core::log::Level::debug, &instance, "delivery", "terminal_released");
  565. }
  566. }
  567. }
  568. } // namespace sunrise::server::activity::mission