LOGISTICS · TERMINAL INTENT IDENTITY

Send One Terminal Transfer Without Losing Its Exact Operation Identity

CURATED PATH

These links are maintained for a concrete prerequisite, failure mode, debugging step, or next lesson instead of generic topic similarity.

Market / Industry

Recommended next steps

Handle Terminal contention with market.dealConflict boundaryReserve Storage Energy before sendingResource budgetConnect cross-room logistics to Factory inputsIndustry path
Verification statusChinese source article: Reviewed in full · Official engine: Checked at 80977824199a596d174d392fd0cf8c458c21fcbd — send uses one set intent per Terminal, a later same-tick send replaces it, direct sends process before market deals, destination capacity may reduce amount, and stored descriptions omit empty strings and escape less-than signs · Technical correction: Added immutable transfer confirmation, shared Terminal reservation, bounded next-tick evidence, partial-amount states and ledger-description normalization

VERIFICATION

Evidence and test status

Chinese source article
Reviewed in full
Official engine
Checked at 80977824199a596d174d392fd0cf8c458c21fcbd — send uses one set intent per Terminal, a later same-tick send replaces it, direct sends process before market deals, destination capacity may reduce amount, and stored descriptions omit empty strings and escape less-than signs
Technical correction
Added immutable transfer confirmation, shared Terminal reservation, bounded next-tick evidence, partial-amount states and ledger-description normalization
JavaScript syntax
Required by the dedicated editorial simulation on the current branch
Repository integration
Pending — TypeScript, ESLint, build and production smoke checks have not completed on this branch
Screeps Console test
Pending
Live overwrite, deal collision, destination-capacity, description and receiver test
Pending
Genuine Console or room screenshots
Pending
Request shape validation
Checked — request ID, revision, Terminal ID, resource type, amount, destination and non-negative finite Energy reserve must be valid before budget calculation or send()
Last verified
August 30, 2026

Start with the set-intent boundary

StructureTerminal.send() writes one send intent for the exact Terminal ID. The official runtime uses a set operation, so another send() on the same Terminal later in the same JavaScript tick replaces the earlier payload. Both callers can see OK, but only the final send intent remains.

Market deals use a separate global intent channel and may reserve the same Terminal unless your code coordinates them. The engine processes direct sends before terminal market deals. Use one shared Terminal dispatcher rather than independent logistics and market schedulers.

Freeze one transfer revision

function buildTerminalSendConfirmation(request) {
  return [
    'TERMINAL_SEND',
    request.requestId,
    request.revision,
    request.terminalId,
    request.resourceType,
    request.amount,
    request.destination,
    request.description ?? '',
    request.energyReserve
  ].join('|');
}

Validate the request ID, positive revision, exact Terminal ID, resource, integer amount at least TERMINAL_MIN_SEND, different destination room, description length and reserve. Any payload edit requires a new confirmation.

Normalize the description that appears in the transaction ledger

function normalizeSendDescription(value) {
  if (value == null || value === '') {
    return {
      apiDescription: undefined,
      ledgerDescription: ''
    };
  }

  const text = String(value);
  if (text.length > 100) return null;

  return {
    apiDescription: text,
    ledgerDescription: text.replace(/</g, '&lt;')
  };
}

function readLedgerDescription(transaction) {
  return transaction.description ?? '';
}

Tick processing omits an empty description and escapes each literal less-than sign in a non-empty description. Comparing the raw request string directly with transaction.description incorrectly rejects both cases. The ledger normalization belongs in the frozen pending record.

Plan the source resource and Energy ceiling

function buildTerminalSendBudget(
  request,
  terminal
) {
  const estimatedCost =
    Game.market.calcTransactionCost(
      request.amount,
      terminal.room.name,
      request.destination
    );
  const energy = terminal.store.getUsedCapacity(
    RESOURCE_ENERGY
  );
  const resource = terminal.store.getUsedCapacity(
    request.resourceType
  );
  const requiredEnergy =
    request.resourceType === RESOURCE_ENERGY
      ? request.amount + estimatedCost
      : estimatedCost;

  if (
    request.resourceType !== RESOURCE_ENERGY
    && resource < request.amount
  ) {
    return { ready: false, status: 'resource-insufficient' };
  }
  if (
    energy - requiredEnergy
      < request.energyReserve
  ) {
    return { ready: false, status: 'energy-reserve' };
  }

  return {
    ready: true,
    status: 'budget-ready',
    estimatedCost,
    requiredEnergy,
    energyBefore: energy,
    resourceBefore: resource
  };
}

PWR_OPERATE_TERMINAL can reduce actual transfer cost and cooldown. The ordinary calculation is a conservative preflight estimate, not exact post-processing evidence.

Reserve the exact Terminal across send and deal

function createTerminalOperationDispatcher() {
  const terminalIds = new Set();

  return {
    reserve(terminalId, operationId) {
      if (terminalIds.has(terminalId)) {
        return {
          ready: false,
          status: 'terminal-already-reserved'
        };
      }
      terminalIds.add(terminalId);
      return {
        ready: true,
        status: 'terminal-reserved',
        operationId
      };
    },
    release(terminalId) {
      terminalIds.delete(terminalId);
    }
  };
}

Every direct send and every ordinary-resource market deal must use the same dispatcher instance. A reservation hidden inside only the send module cannot stop a deal module from using the same Terminal later in the tick.

Submit one frozen transfer

function submitTerminalTransfer(request, dispatcher) {
  const description = normalizeSendDescription(
    request?.description
  );
  if (!description) {
    return { status: 'description-invalid' };
  }
  if (
    !request
    || request.enabled !== true
    || typeof request.requestId !== 'string'
    || request.requestId.length === 0
    || !Number.isInteger(request.revision)
    || request.revision < 1
    || typeof request.terminalId !== 'string'
    || request.terminalId.length === 0
    || typeof request.resourceType !== 'string'
    || request.resourceType.length === 0
    || !Number.isInteger(request.amount)
    || request.amount < TERMINAL_MIN_SEND
    || typeof request.destination !== 'string'
    || request.destination.length === 0
    || !Number.isFinite(request.energyReserve)
    || request.energyReserve < 0
    || request.confirmation
      !== buildTerminalSendConfirmation(request)
  ) {
    return { status: 'request-invalid' };
  }

  const terminal = Game.getObjectById(request.terminalId);
  if (
    !terminal
    || terminal.structureType !== STRUCTURE_TERMINAL
    || terminal.my !== true
    || terminal.isActive() !== true
    || terminal.cooldown > 0
    || terminal.room.name === request.destination
  ) {
    return { status: 'terminal-not-ready' };
  }

  const budget = buildTerminalSendBudget(request, terminal);
  if (!budget.ready) return budget;

  const reservation = dispatcher.reserve(
    terminal.id,
    request.requestId + ':' + request.revision
  );
  if (!reservation.ready) return reservation;

  const outgoingIdsBefore =
    Game.market.outgoingTransactions
      .map(transaction => transaction.transactionId)
      .filter(Boolean);

  request.enabled = false;
  const pending = {
    requestId: request.requestId,
    revision: request.revision,
    confirmation: request.confirmation,
    submittedAt: Game.time,
    terminalId: terminal.id,
    sourceRoom: terminal.room.name,
    destination: request.destination,
    resourceType: request.resourceType,
    requestedAmount: request.amount,
    apiDescription: description.apiDescription,
    ledgerDescription: description.ledgerDescription,
    estimatedCost: budget.estimatedCost,
    outgoingIdsBefore
  };

  const result = terminal.send(
    pending.resourceType,
    pending.requestedAmount,
    pending.destination,
    pending.apiDescription
  );

  request.lastResult = result;
  request.lastAttemptAt = Game.time;
  request.status = result === OK
    ? 'accepted-pending-outgoing-transaction'
    : 'send-rejected';

  if (result === OK) {
    Memory.pendingTerminalSends ??= {};
    Memory.pendingTerminalSends[request.requestId] = pending;
  } else {
    dispatcher.release(terminal.id);
  }

  return { status: request.status, result };
}

Disabling before the call prevents automatic repetition. The pending record freezes both the API description and the normalized ledger form.

Treat destination capacity as an execution-time limit

The runtime cannot prove that a destination Terminal exists or has enough free capacity. During processing, the engine can reduce the sent amount to the receiving Terminal's free space. Therefore an exact new transaction with a smaller positive amount is a partial transfer, not automatically a mismatch.

If no valid destination Terminal exists, the accepted intent can produce no outgoing transaction. The next-tick verifier must report that absence rather than remain pending forever.

Verify the exact outgoing record on the next tick

function verifyTerminalTransfer(pending) {
  if (Game.time < pending.submittedAt + 1) {
    return { status: 'waiting-for-next-tick' };
  }
  if (Game.time > pending.submittedAt + 1) {
    return { status: 'verification-window-missed' };
  }

  const before = new Set(pending.outgoingIdsBefore);
  const candidates = Game.market.outgoingTransactions
    .filter(transaction =>
      transaction.transactionId
      && !before.has(transaction.transactionId)
      && transaction.time === pending.submittedAt
      && transaction.from === pending.sourceRoom
      && transaction.to === pending.destination
      && transaction.resourceType
        === pending.resourceType
      && transaction.amount > 0
      && transaction.amount
        <= pending.requestedAmount
      && readLedgerDescription(transaction)
        === pending.ledgerDescription
      && !transaction.order
    );

  if (candidates.length === 0) {
    return {
      status: 'accepted-no-outgoing-transaction-observed'
    };
  }
  if (candidates.length > 1) {
    return {
      status: 'outgoing-transaction-ambiguous',
      candidateIds: candidates.map(
        transaction => transaction.transactionId
      )
    };
  }

  const transaction = candidates[0];
  return {
    status: transaction.amount
      === pending.requestedAmount
      ? 'full-transfer-observed'
      : 'partial-transfer-observed',
    transactionId: transaction.transactionId,
    requestedAmount: pending.requestedAmount,
    actualAmount: transaction.amount
  };
}

Requiring !transaction.order distinguishes a direct send from a market transaction that happens to share the same rooms, resource and amount.

Preserve overwritten and absent states

StatusMeaning
terminal-already-reservedAnother send or deal owns this Terminal this tick.
accepted-pending-outgoing-transactionThe send intent was accepted but not processed yet.
accepted-no-outgoing-transaction-observedNo exact direct-send record appeared next tick.
outgoing-transaction-ambiguousSeveral exact new records matched.
partial-transfer-observedOne exact record sent a destination-limited amount.
full-transfer-observedOne exact record sent the complete request.

Sender evidence is not receiver workflow completion

An outgoing transaction proves server-side transfer settlement from the sender account. It does not prove that destination haulers moved the resource, Labs or a Factory consumed it, or another player's automation acknowledged receipt. Those are separate systems and may require destination-side evidence.

Production integration boundary

Use one dispatcher per tick for direct sends and terminal-backed deals. Save one immutable pending operation per request revision, verify it exactly on the next tick, and never allow a later request to overwrite an unresolved operation record for the same request ID.

Official documentation and engine source

SOURCE AND SCOPE

Review the source, evidence, or next system

This English guide keeps the technical scope and verification boundaries of its Chinese source. The verification record shows which checks were completed and which runtime observations are still pending.