51 Commits
Author SHA1 Message Date
max bittkerandClaude Opus 5.5 54f3e62084 feat(webclient): publish nearby players' combat targets
NearbyPlayer gains combatTargetType/combatTargetIndex/inCombat, populated only
while the player's combatCycle corroborates the faced entity. BotState carries
combatTargetTelemetryVersion: 1 so consumers can tell supporting clients apart.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 09:30:48 -04:00
max bittkerandClaude Opus 5.5 df3d8888ae feat(sdk,mcp): cancellable execute_code, bot.mineRock, bounded walkTo
execute_code
- Cancellation now reaches inside the SDK via an AsyncLocalStorage scope:
  timeout, MCP cancel, or a newer call rejects pending waits and blocks
  further action packets, including those inside walkTo().
- One execution per bot: a new call supersedes the running one and starts
  once it has unwound; disconnect_bot does the same.
- Cancelled user code parks on bot/sdk calls instead of throwing, so a
  catch-all loop can't spin and starve the server.
- Console capture is routed per execution, so concurrent bots no longer
  clobber each other's output; leftover logs go to stderr.
- Stale world state warns and still runs, so a stale client can be diagnosed.
- Credentials are redacted from logs, results and launch messages.

SDK
- bot.mineRock() waits for ore plus Mining XP; interactLoc docs note that
  success can mean the action only started.
- walkTo() takes options.timeout (default 120s), refuses stale state, and
  reports the real last position.
- runScript uses the same cancellation scope for timeouts and signals.

Docs trimmed of duplicated or stale execute_code text.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 14:30:19 -04:00
max bittker 8b7be3de4b fix(sdk): reject text dialogue clicks after the page changes 2026-10-11 01:10:51 -04:00
max bittkerandClaude Fable 5.1 4c2069cc0c fix(sdk,content): tradeoff-free fixes from the 2026-09-21 bug-report inbox
- walkTo: honor the caller's tolerance on the final waypoint (was hardcoded
  2, so tolerance 0/1 could never be met) and judge distance in tiles
  (Chebyshev) so a diagonal neighbour counts as adjacent
- interactLoc: a same-named fallback loc must offer the requested op,
  otherwise dispatch the caller's own loc instead of a different id
- talkTo: refuse reachable:false targets instead of queueing a talk the
  server retries forever
- eatFood: target optional; no target eats the first item with Eat, not slot 0
- depositItem(pattern, -1): keep going past a per-id failure, report aggregate
- closeBank: also close when bank.isOpen outlives interface.isOpen by a frame
- openShop: return success when a shop is already open
- pickupItem: baseline the inventory count before dispatch (static map
  spawns never leave the ground mirror, so the count is the only signal)
- pickpocketNpc: match Steal-from; craftJewelry: dragonstone slot + reject
  unknown gems; craftLeather: hard/dragon leathers, named product wins
- create-bot --local: write localhost:8888 + GATEWAY_URL; cli hint names
  the lite runner; getInventory docs say it is compacted
- cheat_help: every debugproc listed with its required ::~ prefix

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-22 14:04:08 -04:00
max bittkerandClaude Code 002303875c feat(market): add sell orders view and default to 7-day window
- New "Sell orders" tab / sort=sellRecent: items with open asks, newest
  listing first, showing ask, sell qty, sell value and last listed time.
- MarketOverview and item quotes now include sellValue and lastSellAt.
- SDK MARKET_SORTS (and so the get_market MCP tool) accept sellRecent.
- Market list and API default to days=7 instead of 24h.

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-16 15:05:09 -04:00
max bittker 69d5b709c8 feat: ship Grand Exchange with durable Fly market storage 2026-09-07 23:28:51 -04:00
max bittkerandClaude Fable 5 9cd3d7019a fix(sdk,mcp,gateway): market-bench audit fixes - trade binding, serve window, walkTo guards
From the market-split-n24 audit (24 bots, 64 sdk-kind problems):

- tradeWith verifies the partner: declines a session open with a different
  player (a pending request racing yours used to be silently adopted -
  bots completed trades with the wrong counterparty) and re-pursues the
  requested one until the deadline; trade() gains retryOnBusy
- serveTrades maxTrades counts only completed trades (a lowballer's
  declined session used to consume a maxTrades:1 window); new rejectAfterMs
  declines offers that stay unacceptable; accept() predicate evaluated on
  offer change + every ~2s instead of every tick (console-spam fix)
- trade completion waits for the inventory delta to land before reporting
  received items as missing; TradeResult items carry an `amount` alias
- walkTo rejects non-numeric coordinates (walkTo(undefined, undefined)
  used to march bots into scorpions/wolves) and aborts when the player
  dies en route; NearbyPlayer gains worldX/worldZ aliases
- interactLoc/interactNpc reject null targets (used to hit a random loc);
  find* patterns get a clear error for non-string/RegExp input
- formatter no longer flags the open trade window (3323) as a blocking
  modal to close - that advice cost a bot a 100gp advance
- Spells component ids (HIGH_ALCHEMY etc.) exported from sdk/spells.ts
  and injected as an execute_code global
- MCP execute_code transpiles TS (the tool advertised TS but rejected
  type syntax), formats Errors in console capture (was '{}'), and warns
  about ~60s MCP client caps with code continuing to run
- gateway serializes HTTP says per bot: concurrent PMs clobbered
  currentActionId, so delivered messages reported false "Say timed out"

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-18 19:47:32 -04:00
max bittkerandClaude Fable 5 042ad988c0 feat(sdk,webclient,gateway): private message (DM) chat between players
The engine already supported MESSAGE_PRIVATE end-to-end; this adds the
send path and tooling:

- sendPrivateMessage on browser Client + LiteClient (userhash + WordPack
  payload, local type-6 echo), new 'privateMessage' bot action
- sdk.sendPrivateMessage(name, msg) + sdk.dm(name, text) (chunked like say)
- gateway: observe mode whitelists privateMessage alongside say; new
  POST /dm/:username endpoint - a separate path from /say on purpose, so
  old gateways' non-JSON banner makes the CLI fall back to the WS path
  (cleanly rejected) instead of leaking a DM as public chat
- chat CLI: bun sdk/chat.ts <bot> --to <player> "msg"
- engine: FriendServer PM handler logs private_chat best-effort instead of
  letting a missing account row (local easy-startup) kill the relay

PMs deliver to any online player - no friends list needed. One social
packet (say or PM) per tick; dm() paces its chunks. Delivery requires
FRIEND_SERVER=true (prod fly.toml already sets it; local quickstart
defaults off and drops PMs silently).

Verified E2E on the local stack: both directions, 3-chunk long message,
observe-mode WS path, non-chat actions still rejected in observe mode.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-18 16:37:12 -04:00
max bittkerandClaude e960b3a8d2 fix(sdk,webclient): classify every trade close as completed vs declined
- acceptTrade/trade/serveTrades: a closed screen is now settled from the
  'Accepted trade.' message or the inventory delta, including closes that
  race our own timeout decline; report possiblyDropped overflow.
- trade(): separate requestTimeout (30s) from the open-session timeout.
- declineTrade retries once across the offer->confirm blip.
- closeBotModal always sends CLOSE_MODAL (local modal ids can lag the server).

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-18 15:33:56 -04:00
max bittkerandClaude Fable 5 d80066c78a fix(sdk,clients): bug-report harvest 2026-08-16 - dialog choice regression, false failures, Windows paths
78 new reports since 8/12 triaged; the tradeoff-free fixes:

- dismissBlockingUI no longer clicks multi-option choice dialogs. The 8/12
  fix (9afd6d961) made it click options[0].index, which ANSWERS a p_choice
  as option 1 before every bot action - the wrong quest branch ran and the
  caller's intended click was silently refused as stale. Root-caused via
  live repro; the published index/componentId mapping was never wrong.
  Blocks-quests class (Restless Ghost, Rune Mysteries, Al Kharid gate).
- interactLoc/useItemOnLoc/useItemOnNpc/equipItem accept real evidence:
  same-level teleports (dungeon trapdoors, z +/-6400), inventory deltas
  (cow milking, flour bin), late equipment arrival. No more false failures
  on actions that worked.
- 'interactLoc froze the event loop' root cause was lazy synchronous
  initPathfinding() (~30s on slow hosts) killing the socket mid-action:
  runner + MCP server now warm it before connecting. Runner also exits
  non-zero on signals (128+n), bounds disconnect, force-exits a wedged
  event loop, and honors a second signal immediately.
- ground items on blocked tiles: browser client now mirrors the authentic
  OP_OBJ handler (retry route with 1x1 footprint, dispatch regardless);
  lite ground-item op dispatches unconditionally too. Unblocks Monk's
  Friend / Plague City / Witch's House pickups.
- interactLoc/useItemOnLoc re-find an entity target by (id,x,z) identity,
  not name - same-named neighbours (Varrock cart crates) can't win.
- lite runner/swarm repoRoot via fileURLToPath - Windows and
  space-containing clones can run headless again (4 reports).
- smithAtAnvil: longest-key product match ('mithril longsword' no longer
  smiths a sword), metal-prefix bar selection, honest smithed-product
  report; castSpell resolves known spell names, rejects unknown ones, and
  no longer reports a never-dispatched cast as a splash; buyFromShop fails
  fast with out_of_stock/no_inventory_space; depositItem -1 with an
  alternation pattern deposits every matching id; openShop falls back to
  the nearest Trade-option NPC when called with no target; cli.ts honors
  blank SERVER= as local instead of diagnosing against the demo gateway.
- NearbyNpc publishes tileX/tileZ (authoritative route tile) and size;
  x/z are interpolated render coords, off by 1-3 tiles on movers and
  floor(size/2) on multi-tile NPCs. Also lands the isSkulled/
  autoRetaliateEnabled collector for the already-published type decls.
- Melzar's Maze funexit doors added to ONE_WAY_DOORS (verified in
  collision data) so walkTo/attack stop stranding bots.

Checked: imp red-bead drop table is fair (5/128, reachable branch) - the
report was 1-in-18 variance, no fix.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-16 23:51:08 -04:00
max bittkerandClaude Fable 5 98b28b1ca3 feat(chat): HTTP chat endpoints on gateway; chat CLI bounded budgets, no more silent hangs
The chat CLI opened a fresh observer WebSocket per invocation. Over a TLS
tunnel (split-topology benchmark runs) that handshake is flaky, and a send
against a dead game-client relay burned the SDK's 60s actionTimeout per
80-char chunk - multi-chunk sends stalled 120s+ with zero output.

- gateway: GET /chat/:bot serves recent chat from a per-bot accumulated
  ChatHistory (fed from state snapshots); POST /say/:bot relays through the
  bot's game client with a hard per-chunk deadline (default 10s) and aborts
  remaining chunks on first failure. Same authenticateSDK gate as sdk_connect.
- sdk/chat.ts: HTTP-first with per-attempt timeouts + one retry on network
  failure, a 90s force-exit backstop on every non-watch path, and clear
  non-zero exits. Falls back to the observer WebSocket (now with bounded
  connectTimeout/actionTimeout) for gateways without the endpoints; --watch
  keeps the long-lived observer connection.
- chunkMessage moved to sdk/chunking.ts so the gateway can share it without
  importing the full SDK; re-exported from sdk/index.
- tests: sdk/test/gateway-http-chat.test.ts runs the real gateway subprocess
  with a scripted bot client, covering read/send/chunking/409/timeout paths
  and the CLI end-to-end.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-16 22:29:53 -04:00
max bittkerandClaude Fable 5 0fccd0cc77 feat(sdk): withdraw-as-note support in bot.withdrawItem
withdrawItem gains {asNote: true}: the bank_main note/item toggle
(varp 115 %bankcert, components 5386/5387) is synced to the requested
mode before the withdrawal - and synced back on the next plain call, so
a sticky server-side toggle can't silently change what arrives.
Completion is detected by item name in note mode, because the noted
arrival is the cert obj: a different id whose published name ObjType
copies from the linked item. Fleet hauling previously had to click raw
components and gave up on result verification (bug report msnqt2yt).
BankState now publishes noteMode for observability.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-12 12:52:52 -04:00
max bittkerandClaude Fable 5 47bf4ee5d1 fix(client): route non-inventory useInventoryItem to INV_BUTTON
Both clients sent OPHELD1-5 no matter which interfaceId was passed, but
the engine only wires OPHELD to the main inventory (3214) - item options
on any other component (tradeside:inv, bank_side:inv, ...) are
inv_button triggers. The packet passed the wire and the handler silently
dropped it, which broke trade offers for weeks with zero error surface
(bug report msplxq69). Non-3214 components now dispatch INV_BUTTON1-5
via clickInterfaceIop, interfaceId is plumbed through the gateway action
and sdk.sendUseItem, and an encoding test pins both packet families.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-12 12:49:26 -04:00
max bittkerandClaude Fable 5 035c64a9ad feat(sdk): publish plane on NearbyLoc, refuse stale cross-plane loc refs
Scans are already scoped to the player's plane, but a NearbyLoc had no
level field, so a reference carried across a climb/descend clicked
through to whatever occupies those coordinates on the wrong floor
(Black Knights' Fortress ladder report). Publish the observation plane
on every scanned loc, and make resolveLocation re-resolve a passed-in
loc whose plane no longer matches the player - failing loudly with
loc_not_found instead of interacting with the wrong floor.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-12 12:43:05 -04:00
max bittkerandClaude Fable 5 dfac800e21 feat(lite): in-process fast-KBD attack/prayer hooks in the gateway runner
Policy-gated fast path for King Black Dragon fights: the lite runner emits
the KBD attack op (and protect-prayer on/off flips) in-process on the same
publication that reveals the spawn/removal, instead of round-tripping
through the gateway controller. Enabled only when bots/logs/
fleet-runtime.mode contains the fast-kbd mode string; state publications
carry fastKbdAttackTick/fastKbdPrayerOn/fastKbdPrayerOff so the controller
can observe what was already dispatched.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-11 11:58:45 -04:00
max bittkerandClaude Fable 5 da424c7ab8 fix(sdk): pickupItem no longer reports success when someone else wins the item
The wait predicate treated the ground publication disappearing as
completion, then returned success (and handed back any inventory item of
that id the bot already carried) even when the calling account gained
nothing - exactly the trap chopTree already guards against. Contested
fleet loot had to diff exact item counts around every call to know who won
(bug report 2026-08-10).

Success is now gaining the item, tracked by exact id count around the
pickup; ground disappearance only stops the wait. A short grace covers the
inventory add landing a tick after the ground removal. Losing the race
returns a new 'taken_by_other' reason.

Verified live on the local engine: drop + re-pickup still reports success
with the gained item.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-11 11:15:45 -04:00
max bittkerandClaude Fable 5 c4c22b395c fix(sdk): allow bank withdraw/deposit amounts above 10k
validateActionQuantity's 10k cap exists to bound actions that expand into
repeated packets, but bank withdraw/deposit serialize the whole amount into
a single Withdraw-X/Deposit-X count-dialog packet. Applying the cap there
made withdrawItem(coins, 20000) return invalid_amount without sending
anything - an ordinary stackable withdrawal silently no-opped (bug report
msolats3, 2026-08-11).

Bank ops now validate against MAX_BANK_ACTION_QUANTITY (2^31-2; 2^31-1 is
the client's Withdraw-All encoding, so it stays unreachable).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-11 11:09:32 -04:00
max bittkerandClaude Fable 5 2fa65b8342 feat(sdk): player-to-player trade API
Three layers over the game's request/offer/accept/confirm trade flow:

Layer 0 - state and primitives. StateCollector publishes a `trade` slice
(screen, partner, both offers, both accept flags) read from the
trademain/tradeconfirm interfaces; accept flags come from the server-set
status text, the only place the game reports them. Sends reuse existing
packets: request is interactPlayer op 4 (requesting someone who already
requested you IS the accept), offers are INV_BUTTON with automatic
Offer-X count-dialog handling, accept is IF_BUTTON, decline is
close-modal. waitForTradeRequest surfaces chat type 4, which the default
chat readers filter out.

Layer 1 - bot.trade(target, {give, want, accept}). Opens the session,
offers, holds accept until the partner's offer satisfies the policy, and
re-verifies on the confirm screen before the irreversible second accept.
Offer changes reset both accepts server-side, so the confirm screen's
offers are final - re-checking there makes offer-switching unable to slip
past. Reports the observed inventory delta rather than assuming.

Layer 2 - bot.serveTrades(policy). The receiving half of a muling setup,
as an explicit loop that owns the bot rather than a background hook.

Two engine behaviours the porcelain has to work around, both found by
running it against two live bots:

- Accepts are gated on tick advancement. Every executed action publishes
  a snapshot with an unchanged tick, so a retry loop waiting on "any
  state change" spins at full speed, exhausts the engine's per-tick
  packet budget, and backlogs that client's own update stream by minutes
  while the trade advances server-side.

- "Trade closed" needs two ticks of confirmation. openMainModal writes an
  IfClose for the side modal before the IfOpenMain of the confirm screen,
  so a single closed-looking snapshot mid-trade is the offer->confirm
  transition, not a decline.

Verified end to end against a local server with two lite clients: gift,
mutual exchange with both sides enforcing `want`, and an unmet-want
decline. Adds 15 unit tests covering the pure helpers and the porcelain
flows including the confirm-screen re-verification.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-09 23:06:19 -04:00
max bittkerandClaude Fable 5 515f29ded5 feat(chat): observe-mode say, sdk/chat.ts CLI, 400-char messages
- Gateway: observe-mode sessions may send 'say' (and nothing else), so
  chat tools can talk through a bot without pre-empting its controller
- sdk/chat.ts: send / read / --watch chat over an observe connection
- maxMessageLength default 80 -> 400; engine WordPack caps follow the
  config instead of hardcoding 80
- 240-byte packed budget in both WordPack copies + byte-aware SDK
  chunking: all chat frames carry a 1-byte length and digit/punctuation
  chars pack at ~1 byte each, so long non-prose text would wrap the
  length byte and corrupt the stream
- chat handlers use Packet.alloc(1): the 100-byte alloc(0) tier threw
  RangeError on >~200-char messages and kicked the player with no log
- say() paces multi-chunk sends with wall-clock sleep in observe mode
  (a 'wait' action is rejected there and would occupy the bot executor)
- lite runner: GATEWAY_URL override (SERVER doubles as game origin and
  gateway address, which broke for localhost:8888)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-06 19:05:32 -04:00
max bittkerandClaude Opus 5 28acd81cb0 feat(sdk): surface server op rejections; reachability-filtered find*
Two silent failure modes cost bots a 10s timeout each and were
indistinguishable from "the action ran and did nothing".

Op rejections. When the server refuses an op (stunned, mid-action, target
out of view, modal open underneath) every Op*Handler answers with
UNSET_MAP_FLAG and nothing else - no message, no error. Client.ts and the
lite client now both count those packets, StateCollector turns unsets
that arrive while the player is standing still into
state.opFeedback.opRejectedCount, and ActionHelpers exposes the
snapshot/compare pair. interactNpc and pickpocketNpc use it to bail with
reason 'rejected' instead of waiting out the timeout. opFeedback is
optional throughout - an older connected client just goes quiet, and the
helpers report "no rejection".

Pickpocket acking. "You attempt to pick..." is printed by ~pick_pocket
before its first p_delay, so it is the earliest evidence the op actually
ran - an attempt earlier than xp or a stun. Without it, a lost op looked
identical to a slow one. Attempts that neither ack nor reject within 3
idle ticks (measured from the last step, since the client walks before
writing OPNPC) now return reason 'not_started'. Also fixes "you fail to
pick" being read as a timeout rather than a stun, and checks xp before
messages so a successful pick is never misreported.

find* reachability. All four find* lookups take {reachable: true}, which
returns null rather than a target the client cannot route to - the case
where the client silently writes no packet at all. Default behaviour is
unchanged: an unreachable match still wins if it is the only one.

PATCHES.md gains entries for the lite client, the ClientStream.isClosed
getter, the LoopCycle split, and the mapFlagUnsetCount counter, which is
now duplicated in both clients and would go quiet without a loud failure
if a sync reverted it. bun run test covers src/lite too.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 12:55:45 -04:00
max bittkerandClaude Fable 5 3f27a1b9e1 feat(state): publish reachable flag on nearby entities; fix result false-negatives
Silent cant_reach (ms2cczk7/ms2coo7g) killed at the source: StateCollector
now stamps `reachable` on nearbyNpcs/nearbyPlayers/nearbyLocs/groundItems
via a ReachProbe flood fill over the same collision map the dispatch paths
route over. find* helpers prefer reachable matches, the CLI formatter tags
[UNREACHABLE], and NPC dispatch routes with the NPC's real footprint so a
2x2 target is approachable from any side (probe and dispatch must agree).

Result false-negatives (ms7lmyi6, ms3cw2xr):
- interactLoc counts a player.level change (staircase/ladder) as success
- smithAtAnvil propagates the real sendUseItemOnLoc reason (cant_reach)
  instead of conflating every failure into no_anvil
- talkTo/interactNpc/interactLoc give late server responses a short grace
  window instead of failing one tick before the dialog arrives

Also lands the shared halves the lite client builds on: opFeedback
(UNSET_MAP_FLAG counter for silently-dropped ops), ClientStream.isClosed,
and StateCollector.setReachabilityEnabled for swarms.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-02 21:58:09 -04:00
max bittkerandClaude Fable 5 3c08ce1061 fix(sdk): shortest-name matching, drop/sell/fletch papercuts from bug reports
- find* methods now pick the shortest matching name (ties keep nearest-first),
  so /hopper/i resolves to "Hopper" instead of a nearer "Hopper controls"
- interactLoc/useItemOnLoc no longer rebuild a RegExp target from the matched
  name after walking, which silently dropped the caller's anchors
- sellToShop accepts -1 as "all" for parity with bank deposits
- fletchLogs treats a level-up "Click here to continue" chatbox as a blocking
  dialog: dismisses it and re-uses the knife instead of reporting the product
  missing; the skill-dialog helpers never count a continue button as a product
- new bot.dropItem(target, amount|'all') drops by pattern and waits for each
  drop to land, replacing slot loops that silently no-op on stale slots

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-02 21:18:21 -04:00
max bittker 160ffcbb50 combat improvements 2026-07-26 15:16:37 -04:00
Max Bittker aba0f4e344 Merge pull request #51 from MaxBittker/sdk/interface-close-and-fletching-fixes
fix(sdk): make interface closing work and fletching pick the right product
2026-07-24 19:56:29 -04:00
max bittkerandClaude Opus 5 8fb2390717 fix(sdk): make interface closing work and fletching pick the right product
Close-type components ("Close Window") have no server if_button trigger -
the real client handles them locally. clickComponent sent IF_BUTTON anyway,
so the server answered "No trigger for [if_button,shop_template:com_77]" and
the modal stayed open. Route close-type buttons to closeModal().

An open shop or bank replaces the inventory tab, so isComponentVisible()
rejects inventory packets sent underneath with no game message at all.
sendUseItemOnItem now fails up front with reason 'modal_open' naming the
close method, instead of appearing to do nothing.

fletchLogs searched dialog.options for buttons reading "Ok". skill_multi3
has none - its twelve buttons are Make X / Make 10 / Make 5 / <product> per
product - so that branch never fired and every call fell through to an
index guess: arrow shafts clicked Make X and hung on the count prompt,
'short' on regular logs made ten arrow shafts. Product labels also arrive
padded with layout newlines ("\n\n\n\n15 Arrow Shafts"), defeating string
matching.

Flatten component labels at the client, and resolve the product by name
via resolveSkillDialogProduct - only the make-1 button carries a name, so
the named options are the products in order. On no match, report the
available labels rather than fletching something else.

Also document closeInterface/closeShop/sendCloseModal and the dialog
option ordering in CLAUDE.md, which is where they were missing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-24 19:54:17 -04:00
max bittkerandClaude Opus 5 f58713223f fix(sdk): report dropped action dispatches instead of throwing
sendAction rejected on four paths - dispatch timeout, socket close
mid-flight, gateway sdk_error, and a missing result payload. Porcelain
actions dispatch outside their try blocks, so those rejections escaped
past the `if (!result.success)` branches written to handle them and
killed the caller's script. bot.pickpocketNpc() advertised a
reason: 'timeout' it could never actually return; a thieving run died on
the first stalled dispatch instead of retrying.

sendAction now resolves { success: false, reason, phase: 'dispatch' },
tagging each rejection site via ActionDispatchError so callers can tell a
timeout from a disconnect from a gateway error. The pre-flight
"Not connected" guard still throws: a dead connection should stop a
script, not let it spin.

pickpocketNpc reports the dispatch failure as 'dispatch_failed' rather
than 'timeout' - withDoorRetry retries on 'timeout', which would have
cost 3x actionTimeout per call against a stalled client.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-24 19:48:02 -04:00
max bittkerandClaude Opus 5 39e105aeb8 chore(sdk): remove dead code
- runner.ts: delete disconnectBot() and listConnectedBots(), which had
  no callers anywhere in the repo
- types.ts: delete SayResult, orphaned from an earlier say() design;
  say() returns ActionResult[]
- pathfinding.ts: un-export TemporaryDoorBlock (used only internally by
  TemporaryDoorBlocklist)
- generate-api-docs.ts: un-export MethodDoc, extractPublicClassMethods,
  renderApiMarkdown; only buildApiModel and generateApiMarkdown are
  consumed outside the module

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-24 18:56:32 -04:00
max bittkerandClaude Opus 5 9958f2b074 fix(sdk): correct interface-option clicks and make quantity actions honest
Audit of codex/porcelain-action-reliability, landing the changes that fix
real defects and leaving the speculative parts behind.

Interface options are published with a 1-based `index` label, but
`sendClickInterfaceOption` takes a 0-based array position. `fletchLogs`
and `craftLeather` matched an option by text and then fed its `index`
straight into that method, so both crafted the product *after* the one
requested — and matching the first option skipped it entirely. Adds
`sdk.clickInterfaceOption(selector)`, which resolves by object or visible
text and dispatches the option's `componentId`, and uses it at both call
sites. `sendClickInterfaceOption` keeps its behaviour with a renamed
parameter and a doc comment naming the trap.

`buyFromShop`/`sellToShop` decomposed the requested amount into an array
of 10/5/1 steps before sending anything, so `buyFromShop(item, 1e9)`
allocated a hundred-million-element array. Quantities are now validated
up front and the packets are streamed under a packet cap and deadline;
`sellAllToShop`'s unbounded `while (true)` is bounded the same way.

`sellToShop` reused the shop slot captured before the loop. Selling a
batch of non-stackables removes that slot, so later packets sold whatever
had slid into it. The slot is re-resolved by item id before each packet.

Shop and bank quantity actions returned `success: true` for short fills,
so a caller that asked for 28 proceeded believing it had 28. They now
report `requestedAmount`, the amount that actually moved, and
`partial: true`, and reserve `success` for an exact fill.

`chopTree` returned success when the tree merely despawned — which
happens when another player fells it — and then reported logs already in
the inventory as the ones just chopped. Success now requires the log
count to rise; the despawn still ends the wait, as a failure.

`withdrawItem` identified the withdrawn item by diffing inventory slots,
which names whatever landed in a changed slot rather than what was
withdrawn. It now tracks the bank item's id.

Also regenerates sdk/API.md, which was already stale on main (`bun run
check` fails there at docs:api:check), and adds unit coverage for option
resolution and quantity handling to test:focused.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-24 18:16:14 -04:00
Max BittkerandClaude Opus 5 e5fb1e9d07 Make the agent API self-describing and add regression CI (#43)
* Improve agent API docs and validation

* Drop the wiki search tool

Removes wiki/search.ts and its test. The index only covered names, aliases,
summaries and Sources lines, so it had worse recall than grep on body text
(e.g. "cow hide" missed skills/crafting.md), and normalize() stripped
underscores so map-square queries matched every neighbouring square
(m50_50 returned m50_53 NPCs). The computed mapSquareBounds were only
indexed as text, so coordinate lookups — the one thing grep can't do —
never worked.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Regenerate the wiki from server/content

Adds the coordinate-samples column to NPC location tables and picks up
content drift the checked-in pages had missed (807 NPC, 117 shop, 1573 item
pages; 335 of them new). Examples: Aubury now stocks 1000 chaos/death runes
rather than 500, and shop rows point at "Vial of water"/"Bolts" instead of
the stale "Vial"/"Bolt" names.

Also stops all three generators from crashing on their final step. Each one
read wiki/README.md unconditionally, but that file has never existed, so the
index update threw ENOENT on every run. Skip it when absent.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-07-24 17:52:51 -04:00
max bittker f3601ed8ac observe mode 2026-07-20 12:39:26 -04:00
max bittkerandClaude Fable 5 0f4e5fae85 SDK chat: 500-deep accumulated history + waitForChat
The SDK read chat only from the latest state-sync snapshot (50 messages),
so anything older was unrecoverable and getNewChat missed messages that
scrolled out of the window between polls. Also, chat cursors were
high-water *ticks* (Client.loopCycle), which restart near 0 on page
reload — permanently muting getNewChat and the MCP chat display.

- sdk/chat-history.ts: ChatHistory merges every snapshot into a deduped
  500-deep buffer. Handles the same-tick split (chat packet processed
  after the PLAYER_INFO that triggered the previous sync shares its
  loopCycle) and re-baselines on tick reset instead of dropping messages.
- getChat/getNewChat/getChatFrom read the accumulated history;
  getChat({limit: 0}) returns everything since connect. getNewChat's
  cursor is now a count into the history — immune to resets/eviction.
- sdk.waitForChat({from, matching, types, timeout}): await the next
  matching message (null on timeout), own private cursor so it doesn't
  consume getNewChat's. One-liner for bot<->bot coordination.
- mcp/server.ts: reset lastShownMessageTick when every visible tick is
  below it (client reloaded), instead of muting chat forever.
- learnings/chat.md usage guide, CLAUDE.md quick-ref rows, API.md
  regenerated, 24-case logic test (sdk/test/chat-history.test.ts).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 11:45:22 -04:00
max bittkerandClaude Opus 4.8 20e62a1133 Server: configurable chat length + ground-item drop timers
Add runtime-configurable tunables to make bot collaboration easier, all
defaulting to current behavior:

- node.maxMessageLength (default 80): max public/private chat length. Now the
  single source of truth for server validation, webclient truncation, and SDK
  chunking. Plumbed engine -> webclient (EJS) -> gateway -> SDK so a controlling
  SDK/bot learns the server's value over the existing handshake.
- node.objRevealTicks (default 100): ticks before a dropped item becomes visible
  to other players (was hardcoded Obj.REVEAL).
- node.objDespawnScale (default 100%): scales ground-item despawn timers,
  applied at the World.addObj choke point for DESPAWN-lifecycle objs only
  (respawns untouched), preserving relative per-drop durations.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 13:57:07 -04:00
max bittkerandClaude Opus 4.8 ccc80f8bce SDK chat API: deep player-chat buffer, cursor reads, send feedback
Fixes bot<->bot collaboration failures where a partner's message would
evaporate from the 5-deep, system-spam-interleaved chat window before the
partner polled. The underlying client chat ring is already 100-deep and
type-tagged; the SDK was just throwing it away.

- StateCollector.collectGameMessages: keep 50 most recent (was 5), return
  chronological order (ring is newest-first; .slice(-N) callers + formatter
  expect newest-last), strip @cr/@col codes from sender, set fromSelf.
- GameMessage.fromSelf flag (both type files). PLAYER_CHAT_TYPES=[1,2,3,6,7]
  + isPlayerChat() shared predicate (closes the type 1 crowned-public / type 7
  crowned-PM gap the old {2,3} filters had).
- sdk.getChat({limit,types,includeSelf}), sdk.getNewChat({...}) with internal
  cursor (promotes the MCP lastShownMessageTick idea to the SDK surface),
  sdk.getChatFrom(name) — removes hand-rolled baselines + sender regex.
- Client.say returns {ok,truncated,filtered,finalText}; sendSay surfaces it via
  ActionResult.data so agents learn when a message was clipped/censored.
- sdk.say(longText) auto-chunks past the 80-char cap on word boundaries.
- formatter/showChat use isPlayerChat + fromSelf; webclient debug dumps capped
  at last 10; API.md regenerated.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 18:28:43 -04:00
max bittkerandClaude Opus 4.8 2d9c0ffe52 Fix modal-interface tarpit: auto-close blocking modals, closeInterface(), API.md
From RuneBench arrav-duo run feedback (rs-bench3/SDK-FIXES.md): a quest book
modal blocked all input and the agent burned 27/30 min diagnosing it.

- dismissBlockingUI() now closes blocking modal interfaces (books, quest
  scrolls), except shop/bank sessions and a NEVER_AUTO_CLOSE list where
  closing is destructive: trade screens (3323/3443), duel_confirm (6412),
  macro_cube (6554), character design (3559)
- formatter: blocking modals now print an actionable hint (close with
  bot.closeInterface() / sdk.sendCloseModal()); fix stale char design id
  (269 -> 3559, per interface.pack player_kit)
- add generic bot.closeInterface(); closeBank() delegates to it
- generate-api-docs: zero-arg methods were dropped (empty params string is
  falsy) — getState(), openBank(), sendCloseModal() etc. were all missing
  from API.md; regenerated (56->84 SDK methods, 39->42 actions)
- formatter: clearer dialog option hints (sendClickDialog(N) numbering)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 13:35:03 -04:00
max bittker d37ae26080 fixxes 2026-05-07 17:35:41 -04:00
max bittker 669d937ec5 cleanup 2026-02-26 10:47:34 -05:00
max bittker cf383d7ce8 client fixes 2026-02-13 19:33:17 -05:00
max bittker ce76f19d06 jewerly api 2026-02-11 10:58:22 -05:00
max bittker e71cd5ff4a just put hp on player objects since claude thinks its there 2026-02-11 02:09:04 -05:00
max bittker d9b053f659 smithing and banking fix 2026-02-10 00:14:47 -05:00
max bittker 2c9a10fa7c fixes 2026-02-09 19:25:02 -05:00
max bittkerandClaude Opus 4.6 8b7409f8fc fix pickupItem failing on stackable items and false door retries
Two issues:
1. Success detection only checked inventory.length, which doesn't change
   for stackable items (coins, arrows, bones). Now also checks if the
   ground item disappeared and if total inventory count increased.
2. Door retry triggered on timeout, not just cant_reach. A timeout from
   stacking/despawn doesn't mean a door is blocking.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-09 17:48:58 -05:00
max bittker 1339ebd54d improved dungeon pathfinding + prayer sdk 2026-02-09 15:46:08 -05:00
max bittker 4abfa3c73f better pathfinding 2026-02-08 02:13:46 -05:00
max bittkerandClaude Opus 4.6 a56b802728 Add useItemOnNpc action across SDK and webclient
Implements the full pipeline for using an inventory item on an NPC
(OPNPCU packet), mirroring the existing useItemOnLoc pattern. Includes
a sheep-shearing integration test.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-06 10:06:47 -05:00
max bittker 5f155bda26 show dialog in sdk, also filter out plyaer mesages for safety 2026-02-04 00:57:44 -05:00
max bittker 86f50b7890 console warns for logouts 2026-02-03 20:38:42 -05:00
max bittker d99aeb9b33 useitem bot api 2026-02-03 17:08:17 -05:00
max bittker e17d7d2d6f bank methods 2026-02-03 00:52:46 -05:00
max bittker f419035284 refactor script runner 2026-02-02 22:57:15 -05:00