Use for ab methodology recipes when optimizing a Coworld player.
日本語の概要は準備中です。原文の説明を表示しています。
Use for harness sdk protocol recipes when optimizing a Coworld player.
インストールする前に、エージェントに与えられる指示の中身を確認できます。
On-demand recipes (57). Each is a trigger→action heuristic; pull the relevant one when its situation arises. Tier tags and ⚠ (unverified) as in AGENTS.md.
crewrift
When you have the server/source, treat code as ground truth and docs as drift-prone. Pin gameplay constants to the live coworld_manifest.json the runtime loads, not a plan: one plan listed voteTimer=600/killCooldown=1200 while the authoritative manifest had voteTimerTicks=6000/killCooldownTicks=900, and the manifest interleaves config_schema defaults with per-variant game_config overrides so auditing one block gives the wrong value (kill-cooldown is now 500 ticks, not a stale 900). For Crewrift the Nim simulator (sim.nim) is authoritative over docs -- re-derive constants from it: 24 FPS (TargetFps=24, convert tick configs by /24); proximity gates on SQUARED distance (KillRange=20 dist^2<=400, VentRange=16 <=256, ReportRange=20 <=400); rewards completed task +1, imposter kill +10, win +100 to every player of the winning role; win conditions (alive imposters==0 or all crew tasks done => crew; alive imposters>=alive crewmates => imposters; MaxTicks during active phase => DRAW, no reward); a single A-press is processed report-body -> emergency-button -> kill/task and can short-circuit; inputs only take effect in Playing/Voting. AST-parse cited source files for constants and overwrite extracted/prose values, failing the doc on disagreement (prose archaeology produced 5 of 6 wrong button payloads and invented spurious actions like north/south/interact). Verify packet sizes, bit masks, HTTP endpoints (/healthz, /control/restart), and auth fields against the actual game SERVER (crewrift routes chat purely on isChatPacket with no follow-up button press; among_them demotes late /player connections to spectators). <sub>sources: auggie:5d1ff8d5-2505-4dca-af45-56d3070363e2, claude-code:067e7439-cbd1-4b26-9b3f-d054bde3aa40, claude-code:33356b24-de19-46ae-ad01-810557f4d9b1, claude-code:f70c9801-7ee7-499b-97f9-4fd0848a6b8e (+5)</sub>
loop
Coworld does NOT define the player wire protocol -- each game does, so read the URI in the manifest's game.protocols.player BEFORE writing any player code, link it from the README, and resolve transport discrepancies before assuming the SDK JSON bridge applies. Examples differ wildly: Among Them/BitWorld speaks binary bitscreen_v1 (8192-byte 128x128 4-bit-packed frames in; 2-byte button packets or [0x01]+ascii chat out) and MUST NOT reuse the SDK's coworld_json_bridge; crewrift speaks binary Sprite-v1 screen-streaming; Cogs vs Clips uses JSON coworld.player.v1 (player_config then observation, reply with action by index/name); Paint Arena uses custom JSON. A scripted policy and a runtime-client policy are different architectures even when they share a name (a stale among_them screen-space scripted AgentPolicy for an in-process mettagrid engine was dead under the Docker/WebSocket spec -- grep the whole repo to confirm nothing imports a stale adapter before deleting it). Binary-wire games must own their own websocket bridge (e.g. coworld/policy_player.py invoked directly by the image CMD). Support multiple inbound formats at the entrypoint (detect raw binary on the first frame), pin protocol handling to a specific runner.py commit SHA, and re-verify at each phase boundary because drift is real. The config_schema must require 'tokens' as a string array with equal minItems/maxItems = slot count (variants omit tokens; the runner injects them). <sub>sources: auggie:c5674179-5e48-4584-a7a9-f75d2cd3c61b, claude-code:52cdf7a0-e04b-4105-8678-39efc85c46da, claude-code:b952ba45-d32f-4ce0-a227-c691de8c588f, claude-code:ca5e32ea-3dad-42a9-8991-0f07d28b1060 (+6)</sub>
crewrift · negative result
The AmongThem/Coworld wire perception is bitscreen-only: the server sends a 128x128 palette-indexed framebuffer packed into 8192 bytes at two pixels per byte (ProtocolBytes = ScreenWidth*ScreenHeight/2), the LOW nibble holding the even-x pixel; there is NO structured game state on the wire. This nibble packing is shared across the Python wrapper, the Go UnpackFrame, and the Nim sim's packFramebuffer, so any reimplementation must match nibble order exactly. Output is a button-mask packet sent only when the mask changes (held buttons behave like held buttons). Convert a Bitscreen button mask to a cogames action index via mettagrid.bitworld.bitworld_action_index (with BITWORLD_ACTION_NAMES) rather than hand-mapping. The global/replay viewer uses sprite v1 while the player websocket uses bitscreen v1. NEGATIVE: when a packaged Coworld policy's actions do nothing in-game, suspect wrong packet encoding -- re-check the bitscreen_v1 spec and send only valid Bitscreen v1 packets. <sub>sources: bitworld/among_them/players/how_to_make_a_bot.md, bitworld/among_them/players/how_to_submit_coworld_policy.md, bitworld/among_them/players/lively_lecun/ROADMAP.md, bitworld/among_them/players/lively_lecun/lively_policy.py (+1)</sub>
crewrift · negative result
Crewrift policies speak the shared Bitworld Sprite-v1 protocol over a WebSocket; the runner starts each policy with COWORLD_PLAYER_WS_URL and the policy connects, plays to game end, and exits. Connect with NO frame-size cap (websockets.connect(url, max_size=None); sprite payloads are large), expect always-binary frames, and treat the ~24 Hz stream as incremental RETAINED deltas -- keep retained tables and apply each message. Client->server input is byte 0x84 followed by one button bitmask byte ([0x84, mask & 0x7f]): up/down/left/right=0x01/0x02/0x04/0x08, A=0x20, B=0x40, Select=0x10 (unused), bit 7 reserved=0; send a packet ONLY when the held mask changes (omitted bits mean released). Chat input is 0x81 + little-endian u16 length + ASCII payload (or packet [0x01]+ascii), kept separate from the movement mask so characters never leak in; chat routes purely on isChatPacket with NO follow-up button press (the common sendChat(); sendInput(0) sequence is just an idle release). The server emits exactly one binary message per socket per 24Hz tick = one complete frame: block for one message, apply it, run one perceive->...->resolve cycle, send input only if the mask changed. Frame coalescing (acting only on the freshest queued frame) is a latency optimization NOT a correctness requirement -- a sub-ms step self-corrects. There is no semantic action API: 'do task/kill/vote' are all movement plus A/B at the right place and time. NEGATIVE: mouse messages (0x82/0x83) are NOT read by game logic, so never rely on mouse input; treat the socket closing as episode-over (exit cleanly, do not retry). <sub>sources: coworlds/coworld-crewrift/README.md, personal_labs/crewrift_lab/crewrift/crewborg/AGENTS.md, personal_labs/crewrift_lab/crewrift/crewborg/design.md, personal_labs/crewrift_lab/docs/crewrift-player.md (+5)</sub>
crewrift
Crewrift/BitWorld/Persephone all sample RISING-EDGE button presses, not held buttons: a continuously held action bit registers exactly one press then nothing, and re-sending the same mask every tick wastes bandwidth and the server treats a held mask as a held button -- so 'resend only on change' is CORRECTNESS not optimization (it breaks edge-triggered UI like a voting cursor that advances per press). To repeat a one-shot (report/kill/vote-cursor-step/menu nav) release the bit for one tick then re-set it (2-tick minimum cycle); directional movement is exempt (holding continues acceleration). Emit a packet only when the mask changes via one helper (step_button_press(button) returning the button on press and 0 on release, toggling pressed_last_tick) so every task gets release-edge handling free. Treat an all-bits-set mask (0xFF) as a HAZARD: some servers reserve a full mask as a reset/re-register signal, and the high bit (0x80) must never be set -- audit encoding against the server's input handler and mask only intended buttons. Keep the policy's internal action representation separate from the wire and spell out lowering: tasks return an internal ActCommand (buttons + optional chat_text + optional reset_input) and a transport adapter lowers it (one input packet per tick, then at most one chat packet), so tasks never write raw sockets. When a binary/serialized format is in play, treat the producing tool as the byte-layout source of truth and match the parser byte-for-byte (a baker emitted 10-byte edge records: u32 offset, u16 count, u16 src, u16 dst, signed i16 points), validating decoded blobs by byte size and header counts so a version-skewed blob fails loudly. <sub>sources: claude-code:27659fc3-8e40-469f-beda-31ddd5c6ceca, claude-code:4c297fe5-08a0-4973-85ac-1b8824e638ac, claude-code:cb446cb3-1370-4f0b-9de6-34f05b9e5880, claude-code:ebd6cd36-8956-4a1e-b039-841683ff7792 (+8)</sub>
loop · ⚠ session-derived, unverified
Before any strategy, prove the perceive-act loop with a 'random action' loop: connect, decode the latest frame, pick a random valid action, send it, repeat -- strategy is meaningless until wire protocol, frame decode, and action encoding work end-to-end. Detect joins by liveness, not handshake: the server may be silent on connect with no ack, so log the bot's own observed frame rate because absence of frames (not a connection error) is how a failed join shows up. Use wall-clock time for human-facing pacing (interstitial dwell, voting windows) and ticks only for sim-relative logic, because nominal 24fps and observed ~22fps drift. Verify connection endpoints in client source not prose docs (global view defaults to /global, player perspective to /player; how_to_make_a_bot.md was stale). Real-time perception/policy bots fall behind silently: if a step exceeds the per-frame budget the harness drops frames and the bot only sees the early, useless part of the episode -- at ~55ms/agent x 8 agents (~440ms/batched step vs a ~42ms tick) one bot processed only ~130-164 of ~1147 frames and NOOPed; compute throughput = frames_processed/wall_seconds vs tick rate. A receive loop that drains the whole socket FIFO (while ws.recv()) instead of taking only the latest frame makes the policy act on stale state when the client is slower than the server (~9.4fps client vs 24fps server); use the runner's latest-frame drainer (_receive_player_frame, 0.0 timeout, returns newest frame plus frame_advance count). Smoke-test cogames via cogames play -m <mission> -p <policy> -r <log> -s <step_limit> with a small -s for fast iteration.
<sub>sources: opencode:ses_224eeb40bffeZqmBRcrjFgPFul, opencode:ses_2251f2219ffe0h3Sjj47eVLHrt, opencode:ses_225c247a2ffeNgrsANZfMMyih3, opencode:ses_21ab38af3ffeCyRfclXzAswGm1 (+2)</sub>
loop · negative result
A Coworld player container sees ONLY a fully-formed engine WebSocket URL (host, slot, token, game params like role=scout embedded), its declared public manifest env, and per-version secrets -- never the manifest, game config, variant id, slot count, other players, or the Observatory backend. Read the runner-set COWORLD_PLAYER_WS_URL first then fall back to the generic COGAMES_ENGINE_WS_URL (full ws://...:8080/player?slot=<slot>&token=<token>), and use the URL EXACTLY as given (never strip/reorder query params, rewrite host, or hardcode slot/token) or the game rejects the connection; derive your slot from the URL ?slot=N, never from assignment/position arrays (four distinct numbering schemes with no documented mapping). COGAME_LOG_URI is optional and may not be injected by local Docker or hosted k8s (re-check runner source). Use stdout STRICTLY as the wire-protocol channel and route ALL logs/traces to stderr (audit deps for stray print()). Exit 0 cleanly on game-end -- there are no start/end messages, so detect a {"type":"final"}-style discriminator (the field is literally named 'final') or the socket closing; a hang or non-zero exit fails the WHOLE run (10s exit timeout), so do no expensive end-of-episode work. The protocol tolerates long first-frame stalls (a crewrift player took ~13s yet games ran), so a heavy synchronous startup dependency is acceptable behind a flag. Pin the protocol-authority runner.py to a commit SHA and auto-detect the first message shape (isinstance str => JSON player_config branch, else binary), because a runner version bump can instantly break a working player via a first-message mismatch -- symptom: all containers connect then disconnect at game start and coworld play hangs forever; check the manifest version before debugging your own logic.
<sub>sources: claude-code:3bdbae7f-58b2-4e0e-880b-2a2e03cc0aad, claude-code:63658f66-83c7-482e-a805-d58527e10975, codex:019e18fd-64ea-7040-a62b-f54856a9e6d8, claude-code:0a2634a1-19b7-402e-841e-084ce80c015f (+12)</sub>
loop
Factor a player into durable layers wired per-tick (perceive -> update_belief -> strategy.decide -> mode.decide -> resolve_action -> wire packets), reused across live and replay, enforced by an acyclic import chain (perception parses only, memory stores only, state reads memory, brain decides, pathfinding executes, telemetry records). Perception turns the raw frame (e.g. 128x128) into belief_state -- raw scene/sprite pixels must NEVER enter belief, the only interface strategy and modes see; when adding an LLM feed it this belief/evidence, not raw frames, and do not invent new perception primitives to justify the integration (localization, task detection, navigation, voting-UI parsing are already solved). Modes emit high-level SYMBOLIC intents only (navigate-to-X, complete-task; never button presses or multi-step interactions); make inner-loop granularity a single-tick action intent and push planning/pathing into declarative memory. Task primitives are pure frozen dataclasses with no mutable state; ALL multi-tick sequencing state lives on a separate ActionMemory. Leaf modes must NEVER read env/global config -- they receive typed params; the strategy reads config ONCE at the assembly boundary (build_runtime) and stamps params onto each directive (load-bearing where the framework rebuilds a fresh mode instance per activation: constructor injection silently breaks across the rebuild). Funnel state through one WorldSnapshot (co-equal to_dict()/to_prompt()) and decisions through one output command type the executor consumes blind to its source (making the LLM trivially optional). Keep the per-tick entry point thin and wire transport (send_input/send_chat) as injected callables, not an owned socket, so the core is testable without a live server. Model SDK-facing types as pydantic: FREEZE immutable per-tick values (percept, intent, command), leave the loop-mutated world model and action-execution state non-frozen, and use a plain dataclass only for numpy/bytes buffers that never reach the strategy. This layering recurs across players including the Orpheus/persephone framework. <sub>sources: auggie:ac491a46-e7f7-4271-a1c0-f0e69987d424, auggie:c5674179-5e48-4584-a7a9-f75d2cd3c61b, claude-code:48add98e-a83d-4348-97bf-0079c48c42d6, claude-code:77de2491-bdb8-4277-b759-175ea5203c28 (+12)</sub>
loop · ⚠ session-derived, unverified
Make a curated typed belief snapshot (not raw observations) the API between loops, and never let the strategy/LLM outer loop emit raw game actions: its only output is a typed mode/directive selection while a local symbolic mode owns tick-by-tick action and movement. Scope the LLM to macro decisions only (mode, target, risk tolerance, companion/alibi target, kill permission, when to abandon a plan); this keeps the slow, latency-prone, unreliable LLM out of the per-tick hot path, confines its failures to mode selection, decouples per-game perception (OCR pixels vs grid vectors) from strategy so the outer loop and modes port across games, and cuts cost and game-losing structural mistakes. Distinguish durable gameplay directives (persist many ticks) from short situational meeting/burst actions and model them as different output types. Structure tunable prompts as three independent tiers selected at call time from context.self.role -- shared wire/output boilerplate, per-role goals, per-role strategy. <sub>sources: codex:019e18b2-4478-78d0-8882-752b7b0d55c9, codex:019e18d7-532e-7540-82ea-9abe1726aec7, codex:019e196e-93c2-7523-8da7-96f782ea578c, codex:019e2886-d576-7770-be1b-6d92ca97761e (+1)</sub>
loop · ⚠ session-derived, unverified
Define the directive as a frozen Pydantic model carrying mode/params/source/issued_at_tick/ttl_ticks/reason/metadata with typed bounded knobs (suspicion_threshold float [0,1], report_eagerness low|normal|high, voting_style evidence|majority|contrarian|skip_default), so invalid modes or wrong params are rejected before installation rather than blowing up mid-episode. Validate every ModeDirective in the ModeRegistry before installing it. Keep mode params as frozen extra-forbidden Pydantic ModeParams (never raw dicts); the inner loop and ModeRegistry must validate JSON into typed params before any mode sees it. Give parameterless modes (e.g. 'idle') a default-constructed params object via field(default_factory=ParamsType) -- on a frozen dataclass field(default_factory=...) is the correct pattern (a bare mutable default is rejected); give framework-managed objects a sane class-level default (params: ModeParams = ModeParams() on Mode); preserve positional-caller compatibility when adding a constructor arg by placing the new optional arg AFTER existing positional params, and prefer keyword args at policy decision points. Give the inner loop explicit reflexes plus a safe default and a TTL on every directive: the async/threaded strategy loop is slow, so if it stalls, errors, or goes silent the inner loop must degrade to a safe default rather than freeze on a stale directive. Feed the LLM enough context (mode params plus a scratch summary, not just mode name/source/ticks) to judge whether the current behavior is working and decide to abandon a failing plan. <sub>sources: codex:019e18b2-4478-78d0-8882-752b7b0d55c9, codex:019e18d7-532e-7540-82ea-9abe1726aec7, codex:019e1986-219e-70e1-8302-fd943e1280ff, codex:019e196e-93c2-7523-8da7-96f782ea578c (+4)</sub>
loop · negative result
Keep exactly one active mode per agent: a deterministic symbolic local policy that reads belief, owns its own scratch, and emits a symbolic INTENT (navigate_to, complete_task, hunt, meeting) -- never raw transport commands. When a new directive matches the current mode's name AND params, REAFFIRM (preserve the live instance and scratch); only on a true change call on_exit/new instance/on_enter -- re-instantiating or resetting scratch on every reaffirmation destroys in-progress plans (a policy recognized 'we created this key whisper' on entry then recomputed and lost it next tick; PRESERVE the live instance when a directive re-affirms the same mode/params). Make mode switching a deliberate ordered sequence: validate directive against registry, run switch callbacks (which may override/cancel), clean up the old mode, activate the new, then run entry logic; doing these out of order (cleanup before validate) leaves the policy inconsistent. Give modes lifecycle methods mode_enter (setup only -- select_task runs immediately after) and mode_switch_cleanup (teardown by the outgoing mode). On finish/block return ModeDecision.complete()/.stalled(reason) so the runtime lowers the intent, installs the default directive, and wakes strategy with a reason; never let a stalled mode spin silently. Belief is the ONLY interface the strategy inspects -- keep raw frames/pixels out -- split into four owned namespaces (world truth: written only by perceive; mode scratch; action state; strategy inferences: written only via a named inference namespace) so there is exactly one owner of each mutation. Store all per-mode cross-tick state in the designated scratch container rather than recomputing each tick; drive time-based behavior from a per-activation tick counter (action_memory.ticks_active); evolve internal sub-states BENEATH the existing top-level mode name rather than churning the public mode enum (the outer controller addresses the policy by mode name) but emit a trace event on every internal transition or a phase machine inside one mode becomes a black box in replay. <sub>sources: players/player_sdk/docs/metta_cogames_framework/README.md, players/player_sdk/docs/metta_cogames_framework/PYTHON_FRAMEWORK.md, players_checkouts/players/users/james/personal_cogs/among_them/guided_, codex:019e08bb-a4ea-7c83-a8d6-2652eaa1a3b8 (+4)</sub>
loop · ⚠ session-derived, unverified
Define task identity as structural equality of (task_type, params) via frozen dataclasses, not object identity, and clear per-task scratch state (action memory: A* waypoints, button history) only when the task actually changes, never on mode switch -- action memory must survive strategic re-evaluation while inferences are replaced wholesale on switch (but on a strategy-mode switch decide per-state whether memory survives; defaulting to 'reset everything' silently discards useful cross-mode history). Make set_task idempotent on (task type, target): a no-op when both match the active task, but a different target overrides; otherwise re-issuing constructs a fresh instance that restarts a multi-frame motion from frame 0 and loops in a tiny arc. Ensure typical task durations fit the re-planning interval (a planner every ~2s/~48 frames truncates a motion task needing 72+ frames) or make tasks resumable, not restartable. Make task/action selection an explicit first-class return value (select_task(...) -> Task | None, None means keep current) over a delta the framework must parse. Build modes as self-contained registry-keyed subclasses each returning Task objects per tick; wire in stub (idle) modes early and replace stage-by-stage, keeping a new stage in its own module (advanced_modes.py) rather than extracting shared internals. Fold related sub-behaviors into one mode rather than over-fragmenting (a mining mode includes drop-off, targets a resource TYPE not a specific extractor). Route menu/multi-step UI navigation through a single shared navigator (MenuNavigator(steps).next_command(...)) rather than open-coding step counters per task, because multi-step sequences (category->item->target) are where off-by-one and missing-release-tick bugs hide. Order the act handler with fixed precedence: reset, then buttons, then text/chat. <sub>sources: opencode:ses_1fed7bbaaffe4qARc9KvRdpGAC, opencode:ses_200bab964ffe5MjqDy3HjInDmF, opencode:ses_209613a85ffeqO1Gq9NEDobBYi, opencode:ses_20a41d230fferPYcSsh385tuSR (+4)</sub>
loop
Encode decision precedence as named, ordered gates rather than nested conditionals, documenting why each bypass exists (phase overrides bypass per-role stubs; mode-completion bypasses the minimum-duration gate). Separate 'build derived StrategicState from belief' (role, team, room, urgency, objective, tick counters) from 'decide a directive off that state'. An outer-loop decision function that runs every tick will THRASH modes (the framework applies any differing directive), so implement hysteresis: a minimum dwell (>=48 ticks / 2s) unless a critical override fires (phase change, key objective completing, forced whisper), never interrupt an in-progress whisper, and store the FULL held directive (with strategic params) plus the last non-whisper directive in persistent belief_state.ext (not the wholesale-replaced inferences dict, which erases params); test both (no hysteresis dithers; no override loses to phase changes). Drive a mode's sub-protocol with an explicit FSM (named states ENTER/ASSESS/EVALUATE/EXIT, each mapping to exactly one symbolic task per tick) and treat 'no longer in the expected view but I did not initiate the exit' as a forced-ejection signal -> EXIT immediately, rebuild knowledge, complete with reason 'forced_ejection', since the environment can yank a mode out of a sub-state. Add action cooldowns and response timeouts as first-class state fields (an N-tick cooldown between menu ops, an M-tick peer-response timeout), because untethered per-tick interaction sub-protocols spam the game and stall. Make reflex-driven no-LLM switches revocable: give each reflex a TTL and a per-reflex cooldown (~96 ticks / ~4s at 24 FPS) so the LLM can override within the window without immediate re-firing. Build explicit stuck-state recovery (>120 idle ticks during Playing -> force scout; role-detection failure -> default to grunt and keep retrying). <sub>sources: codex:019e0301-2d9b-7a33-9b5e-f584ff046932, codex:019e05ef-a042-79c3-aa22-fd78a9e2333d, codex:019e067b-643a-7100-abdb-8d6a0c79d242, codex:019e0817-8cba-79c2-aed4-d8bb6a4261d0 (+4)</sub>
loop
Guarantee liveness with TTLs, default directives, reflexes, and watchdogs. Reflexes are priority-ordered symbolic overrides running INSIDE the inner loop for urgent events that can't wait for the slow strategy pass (phase transitions, hazards, deadlines); higher priority wins, equal priority preserves registration order, and you must trace which rules were checked/won/skipped (emit a reflex_evaluated trace; implicit first-match-wins ordering is a hidden contract that breeds subtle bugs). Separate automatic reactive reflexes from LLM-directed strategic choices by time scale: a reflex ('body in view -> flee', fires automatically even for bodies you didn't make) versus a strategic follow-up ('after a confirmed kill, go be seen near a crewmate for an alibi'). On directive TTL expiry or active-mode is_legal(belief)==false, install the default directive -- but a silent fallback hides design bugs, so ALWAYS emit a trace reason. Add a watchdog/heartbeat counter so a wedged player (stuck mode, infinite task, dead WebSocket) is detectable rather than silently idle, and put the watchdog on the loop/pipeline object, not on belief state, so it survives resets. Add a ring buffer of the last N action commands to the standard inspectable fields so a mode can detect 'I'm sending the same command repeatedly' without bespoke per-task tracking. Give each reflex/interrupt an explicit cooldown (a concrete tick count pinned in the design doc) to prevent rapid re-firing and mode thrash. <sub>sources: players/player_sdk/docs/metta_cogames_framework/PYTHON_FRAMEWORK.md, players/player_sdk/docs/metta_cogames_framework/README.md, personal_cogs/persephone/agents/eurydice/DESIGN.md, players_checkouts/players/users/james/personal_cogs/persephone/TODO.md (+4)</sub>
loop
Keep ALL actuation -- movement, pathfinding, adjacent-cell interaction, button-pulse/cursor timing, menu open/close mechanics, multi-tick holds, movement-failure detection, transport packet/Action encoding -- in ONE action-resolver layer, and keep strategic priorities, long-horizon target choice, and social reasoning out of it: a mode emits a high-level discipline hint (DisciplineReport, DisciplineKillStrike vs DisciplineNormal, DisciplineTaskHold) and the action layer decides when to press (OR in button A only within action range), so button-timing logic lives in one place. Make the action layer stateful and intent-diffing: on unchanged intent continue executing (advance nav route, keep holding A, step the vote cursor), on changed intent discard in-progress execution and start fresh. Separate transport/platform config (runner env like COGAMES_ENGINE_WS_URL, read in policy_player.py) from behavior config (read in from_env) -- never mix them. Config leaks into behavior when an agent polls os.environ inside its decision loop (BE_DUMB/DICK_MODE every decision) or builds LLM clients from env at constructor time: resolve ALL configuration once at the composition root (build_runtime), reading and validating everything (fail-fast and loud rather than silently defaulting mid-run), and inject constructed clients/profiles/flags; leaves receive only piece-specific config (e.g. trace level), never raw config. Use a ModeRegistry.register_factory(name, factory, params_type=) seam so the composition root closes over injected dependencies (e.g. an llm_client) and builds composite modes, with register(cls) delegating to it; enforce factory return contracts in code (mode.name == registered_name plus a directive-match check inside create()), not just documented as MUST. Recognize an SDK DI seam does not by itself move strategy flags (dumb/dick) out of behavior logic; that needs a follow-up in the player's own config/strategy layer. <sub>sources: players/player_sdk/docs/metta_cogames_framework/README.md, players/player_sdk/docs/metta_cogames_framework/PYTHON_FRAMEWORK.md, players/player_sdk/docs/handoff-mode-registry-factory.md, players_checkouts/players_2/players/crewrift/crewborg/docs/designs/con (+3)</sub>
loop · ⚠ session-derived, unverified
Scope player hooks at two levels: agent-level (always run) and mode-level (run only when a mode is active), with deterministic order (agent-level FIFO first, then current-mode FIFO, no interleaving). Wire explicit hook points at the loop's phase boundaries (e.g. post-belief-update) from the start so modes/logging/experiments observe or mutate state at known seams without forking the pipeline, making A/B instrumentation cheap. Make hook code defensively isolated: deep-copy the belief/game state before each call, and if a hook raises, restore the LIVE state in place (mutate fields back; do NOT rebind a fresh local copy, because callers and prior hooks hold references to the live instance), log, and keep the pipeline running so a buggy experimental hook never crashes the player mid-episode (assert by registering a deliberately crashing hook alongside a real one). Apply outer-loop inferences BEFORE firing mode-switch callbacks and let those callbacks override/veto the directive; keep mode-switch callbacks as a SEPARATE dispatch path from per-tick hooks because their signature differs (they may return an override directive). Order of operations inside a frame is load-bearing for observability: read transient per-frame wake/trigger flags BEFORE the step that clears them, because clearing wake-reason flags at frame end makes any tracing/consumer log empty/stale. Resolve ambiguous hook/callback signatures by deferring to the design doc's signature table verbatim rather than guessing. <sub>sources: codex:019e02f7-0f59-7f43-ad7b-f4e3c2d37a2f, codex:019e0301-2d9b-7a33-9b5e-f584ff046932, opencode:ses_1fed7bbaaffe4qARc9KvRdpGAC, opencode:ses_200bab964ffe5MjqDy3HjInDmF (+3)</sub>
loop · negative result · ⚠ session-derived, unverified
A player can lose all per-tick state continuity if its policy object is rebuilt every observation. MettaPolicyPlayer.action_for_observation() called self.policy.agent_policy(slot) per tick and agent_policy() returned a brand-new policy each call, rebuilding a fresh InnerLoop, Memory, and OuterLoop thread every step and discarding memory, mode, and strategy results. Fix: cache the per-slot agent_policy instance instead of constructing it per observation. <sub>sources: claude-code:3c13aeb0-5617-452d-a8d7-aff376e3d4ff</sub>
loop · ⚠ session-derived, unverified
Make the LLM advisory over a deterministic floor, never the per-tick actuator. Always compute the deterministic recommendation as fallback; enable the LLM for social/meeting chat and voting while keeping movement/mode transitions on the symbolic path, gated by explicit env flags (LLM_DISABLE, LLM_GAMEPLAY_DIRECTIVES). Never let it pick from the raw legal set: a deterministic ranker pre-ranks and passes only the top selectable action ids plus a briefing; validate the response against those ids under strict JSON; fall back to the pure ranker on disable/error/timeout/illegal-id. Enforce legality in ONE function reused by the menu, validator, and submit-time re-check so an illegal move (imposter voting out a teammate) is structurally impossible; on the degenerate empty legal set fall back to skip/no-op. Per-tick calls are untenable (~2s/call, ~160 calls/game across 8 agents) and a 1-3s blocking call finishes after a ~1s meeting phase closes even when the WS loop queues backlog 1:1 -- run the LLM on a long cadence (~every 500 steps) setting a few soft enumerated knobs (resource_bias, role override, objective). Treat 'enabled' as 'available AND working', not 'key is set': latch permanent errors (401/403/404) immediately and transient errors after a small threshold, never calling the broken client again that episode (one agent used neither LLM nor fallback because key presence made enabled=True while every call 404'd). The player must never decline or crash. <sub>sources: auggie:ac491a46-e7f7-4271-a1c0-f0e69987d424, claude-code:307f1069-21fb-469e-bcd2-8a07f2dba0d8, claude-code:33356b24-de19-46ae-ad01-810557f4d9b1, claude-code:3837ef5e-3e9c-41bc-b290-df5865581698 (+4)</sub>
loop
Default to a HYBRID strategy: deterministic code computes affordances and hard constraints; the LLM selects among legal options using structured evidence and never presses buttons, never invents modes, falling back to deterministic policy when rejected. The LLM owns ONLY socially-complex surfaces (probe-target choice, reveal decisions, chat/deception wording and timing, coalition/vote choice). Make the boundary schema-driven with six pieces: (1) a context builder (belief -> compact JSON-safe context: identity, objective, player records with confidence/sources, recent messages, legal actions, affordances, cooldowns, failures, hard constraints); (2) a surface-specific prompt citing structured data, not raw frames; (3) a replaceable provider so deterministic fakes swap in; (4) a validator; (5) an executor mapping validated decision -> ModeDirective; (6) a full trace (context hash, raw decision, validation result, accepted directive, rejection reasons, latency, fallback). Move along the spectrum shadow -> advisory -> constrained -> full control one surface at a time, off by default, only after replay and trace review. Pair the LLM with a deterministic ranking DOCTRINE ported faithfully from the game image's own external-agent scorer (e.g. starter-framework.mjs rankLegalActions/actionScore) as a pure function so priorities match what the engine rewards; that doctrine both seeds the LLM's shortlist/briefing and serves as the fallback. A view-forced mode (e.g. in-whisper) can't be steered by a top-level decision alone -- expose it as a mode-local control surface consulting the LLM at specific decision points while it keeps its own timeouts/forced-exit safety. <sub>sources: players/player_sdk/docs/metta_cogames_framework/README.md, personal_cogs/persephone/agents/eurydice/DESIGN.md, personal_cogs/persephone/agents/eurydice/IMPLEMENTATION_PLAN.md, agents/eurydice/LLM_CONTROL.md (+5)</sub>
loop
Insert a deterministic validator between LLM output and game actions, using a CLOSED decision schema: a fixed allowed-action enum, required confidence (in 0-1) + short rationale, and typed optional payloads (target as [color,shape], destination as [x,y], hostage list), with message text capped (e.g. 48-60 ASCII chars). Gate on schema closure (no extra fields), view/phase legality, a per-mode is_legal check, target validity (exists, reachable, not self), message safety (ASCII, length, no spam), and reveal safety; on rejection or parse/transport failure fall back deterministically to a hold/no-op so model output never becomes arbitrary button presses or false high-confidence claims. Put legality in code, not in the prompt. When validating LLM JSON in Python, explicitly reject bool for numeric fields (confidence, coordinates) because Python treats bool as a subtype of int so true/false slip through as 1/0. Block mechanical-fact claims unless the belief state has evidence (constrain over-revealing models that publicly shout true-and-false role/mechanical claims). Never rely on LLM memory across ticks: require ALL decision state present in the per-tick context packet, feed the model an event buffer logging every task lifecycle event (started/fired/succeeded/failed/replaced) then flush it. Reject a STALE decision (and have action-execution modes self-complete into idle) when the view/phase changed, the target moved out of feasibility, whisper occupants changed, a pending offer disappeared, or the decision age exceeds a surface-specific budget -- tracking each decision's consumed/applied/staleness tick -- so stale responses can't send whisper text in global chat or move during a hostage exchange. <sub>sources: agents/eurydice/LLM_CONTROL.md, agents/eurydice/LLM_STRATEGY_MIGRATION.md, players_checkouts/players/users/james/personal_cogs/persephone/agents/, players_checkouts/players/users/james/personal_cogs/persephone/TODO.md (+5)</sub>
loop · ⚠ session-derived, unverified
A deterministic scripted safety net behind the LLM is a hard requirement, not a nice-to-have, and must be designed FIRST: when no API key is set, the call fails, or the action queue is empty, fall back to scripted behavior (vote-skip; imposter -> bandwagon; crewmate -> silence/no-accusation). Pair each LLM call (hypothesis, accusation, belief-update, vote) with a rule-based fallback that fires on timeout so a slow/flaky provider only degrades the policy and never breaks it; a player that hard-depends on the LLM being reachable hangs or crashes in CI and offline runs. The bot must pass validation and play competently with ANTHROPIC_API_KEY unset. Provide a deterministic 'stub'/'hold' no-op provider that cycles a couple of safe modes for end-to-end runs with no key or network, make the provider swappable (Anthropic Messages/OpenAI Chat/stub) via flags/env, and design 'hold' to defer to the always-computed deterministic fallback so the model can decline control without mode churn. Caveat: disabling the LLM shifts the whole burden onto reflex machinery that may still need localization/role detection, so validate the reflex fallback alone can localize, detect role, and act. For a tournament submission the LLM is a mandatory in-process component (per-call fallback is fine; absence of the LLM is not). <sub>sources: opencode:ses_20aad554dffe3dzhaUQ8zuBnZT, opencode:ses_21ab38af3ffeCyRfclXzAswGm1, opencode:ses_21af0e707ffeWhoJeouK9agHhn, opencode:ses_21b1e7074ffeYT9RjQFHNPlNKX (+2)</sub>
loop · ⚠ session-derived, unverified
Use the LLM only as a high-level steerer via three-level indirection: an LLM (on a dedicated worker thread, ~every 5s, cap ~120 calls/match, 15s timeout) picks a short-term symbolic mode (call it 'task'/'mode', not 'superaction'); the mode's select_task(belief) sets a multi-tick objective; a per-task select_action(belief, action_memory) chooses low-level control. The LLM sets only mode + target/params + meeting chat/votes, never frame buttons, navigation, or action timing. The fast symbolic loop must NEVER depend on the slow loop being alive: run the strategy loop as a daemon thread, catch all exceptions in both the decision step and the LLM-call wrapper so failure degrades to 'keep last mode' not a crash; initialize to a safe no-op IDLE mode and default to a safe exploration mode when the buffer is empty. Make the policy emit a non-NOOP movement mask from the FIRST valid tick (idle/bootstrap mode before role/localization are known) by re-evaluating defaults on new state (when role becomes known, transition immediately the tick after via stale-default re-evaluation, with NO LLM round-trip) rather than waiting on the slow brain. <sub>sources: opencode:ses_1f7b6dc30ffeDmQtlUZI25HAjh, opencode:ses_209613a85ffeqO1Gq9NEDobBYi, opencode:ses_20aad554dffe3dzhaUQ8zuBnZT, opencode:ses_21ab38af3ffeCyRfclXzAswGm1 (+1)</sub>
loop · negative result · ⚠ session-derived, unverified
Observed LLM latency was avg ~9.9s, p95 ~19.1s, max ~27.8s per call plus occasional historical inference timeouts, which directly limits how many sequential LLM decisions a tick-bounded phase can afford. Collapse multi-step intents into one: when an LLM turns one decision into a serialized select->navigate->confirm sequence, each step costs a round trip and the sequence blew the budget (one vote took ~456 ticks of a 600-tick window; the bottleneck was action choreography, not model latency since calls were sub-second). A 'vote' should mean select AND confirm. NEGATIVE: latch terminal LLM intents instead of discarding them -- a confirm_vote issued before the cursor reached the target was consumed as a navigate step and forgotten, so persist a terminal intent and auto-execute when its precondition is met rather than requiring the model to re-issue it. Multi-tick UI interactions (open-menu -> offer -> accept; navigate -> confirm) silently fail when the policy emits the action for one tick then resets to idle: when traces show an action initiated far more often than completed, make the interaction stateful/persistent across ticks. Key safety-net fallbacks on the exact missing outcome, not on 'the LLM did something': a no-LLM auto-vote fallback disabled after ANY LLM meeting action meant a single early 'speak' permanently killed it even when the bot never voted; gate it on 'no vote/confirm yet'. <sub>sources: codex:019e1d2c-7707-7223-bd6c-d2c41104a10a, codex:019e131a-1bb0-7400-b54a-f208202b291d</sub>
loop
At 24 FPS a tick is ~42ms while an LLM call takes 500-1500ms, so never call the model every frame. Throttle with a per-cooldown-ticks setting (e.g. EURYDICE_LLM_COOLDOWN_TICKS default 48) and run the LLM only at meaningful decision points: role/team/room becomes known, round or urgency band changes, the current mode completes/fails, target disappears, entering a whisper, occupant join/leave, an entry request, an incoming offer, parseable new chat, chat cooldown available with something to say, hostage selection -- plus a periodic heartbeat every 48-96 ticks. Use an AsyncStrategyRunner integrating with the existing event loop (not a threading.Thread runner) when the strategy is an async LLM call, because most LLM SDKs are async-first and a thread forces a bad choice (block on asyncio.run(), spin a second event loop, or abandon the runner). Connect the loops with latest-value (latest-snapshot) buffers, not queues, so the inner loop publishes the newest belief and consumes the newest strategy result and stale intermediate outputs are dropped rather than backlogged; better still avoid deep-copying snapshots back and forth (wasteful, creates staleness windows) and use one lock-protected shared object with snapshot.read()/write(). Bridge an async LLM worker to the synchronous per-tick loop by pumping every available guidance message into a per-tick pending-actions queue and draining one action per tick. If your provider already spawns a subprocess for the call, the subprocess itself can be the non-blocking primitive: poll its exit code each frame (peekExitCode/tryGather) instead of waitForExit. Give the LLM a compact derived mode SUMMARY (phase, reason, active/preferred target, last sighting, cover/strike state) rather than raw scratch fields. <sub>sources: agents/eurydice/LLM_CONTROL.md, agents/eurydice/LLM_STRATEGY_MIGRATION.md, players_checkouts/players/users/james/personal_cogs/persephone/agents/, players_checkouts/players/users/james/personal_cogs/persephone/GAME_AP (+5)</sub>
generic · ⚠ session-derived, unverified
Before any runtime handoff, run the full LLM decision loop (build context -> ask model -> validate -> log accept/reject -> compare to the rule stack -> aggregate metrics) beside the deterministic bot WITHOUT pressing buttons, to confirm the model picks legal/valid/safe choices. Add per-controller cadence control (fire the model only every N ticks) first: a heuristic provider produced 8,000+ decisions in one short match, and a real model on that path would issue thousands of API calls per game. Commit to this generalist controller over an ever-more-complex symbolic policy, because a symbolic policy that assumes opponents share your policy is fragile in a mixed tournament with unknown opponents. NEGATIVE: 'wire the LLM in' can be a misnomer -- an action the LLM is supposed to own may still be decided deterministically (the executor translated offer_role/accept_role into tasks, but the FSM never asked the LLM and made them itself). When delegating a decision class to an LLM, check EVERY state that can produce that action, not just the obvious entry point, and verify the hook actually fires with a trace; triage formatting glitches separately from call success (malformed/concatenated output like 'yetlet's' is a prompt/sanitization bug distinct from whether the call worked). <sub>sources: codex:019e131a-1bb0-7400-b54a-f208202b291d, claude-code:aae25940-ade3-41bc-b817-517dc86ccebf, codex:019e14a5-e574-7770-953b-17019f2f5e74</sub>
generic · native LLM client contract
Factor the LLM provider behind an interface early so models/providers swap without touching game logic (a monolithic ~4720-line bot was hard to evolve; its successor split into chat/llm/llm_dispatch/llm_provider/types and generalized a single OpenAI client into a multi-provider Anthropic Messages/OpenAI Chat one). Use the same native client boundary for advisor, analyst, and editor calls. Hosted clients read COWORLD_LLM_ENDPOINT and use placeholder credentials; local clients use OPENROUTER_API_KEY. Prefer provider-level structured output (register one output schema per call kind as a provider tool/function definition with tool_choice set to it, translating one Anthropic input_schema into the OpenAI function.parameters shape) over schema-in-prompt, keeping prompt-embedded hints only as fallback. Implement the native Messages tool-use loop as append-assistant-content -> execute tool calls -> append results as a user turn -> repeat to a max_rounds cap, returning token-usage totals for cost budgeting. Default the small game-control model to claude-haiku-4-5 (temp 0.2, max ~512 tokens, ~12s timeout), honoring fallback env vars COGAMES_LLM_MODEL/ANTHROPIC_SMALL_FAST_MODEL/ANTHROPIC_MODEL. Verify platform capability claims against docs/code before designing around an assumed limitation (an agent wrongly assumed LLMs couldn't run in the cogames tournament; API keys CAN be passed as runtime secrets so in-process LLM calls ARE supported). For vision-language calls a 1x1 PNG was rejected ('Could not process image') while 64x64 succeeded, and cache paid vision-inference on a content key (frame plus request) so reruns with a fresh run_id reuse prior labels -- spend-idempotency keys to inputs, not run identity. Use a native SDK wrapper when a compiled core needs HTTP inference; keep provider credentials outside hosted player code. <sub>sources: opencode:ses_219efd206ffe6azQSiSZsvnxX4, opencode:ses_21e58aef7ffeoJ7uZeDgGBCfIh, opencode:ses_21f89f53effe65xlVIKX1hvVv3, codex:019e13e1-e883-7fe0-93b4-54adeac75285 (+6)</sub>
loop
For a non-Python core, use a Python native Messages client pointed at the platform endpoint. Architecture: the core exports a request slot ('I need an LLM call with this context JSON'), Python drains it, calls the provider, and feeds the JSON response back through FFI (avoids crossing the FFI boundary mid-HTTP and reuses Python's credential handling); chat reaches the game via the standard Action(name=..., talk=...) in Python, not through the core. Gate the optional LLM layer behind a compile-time flag (Nim's -d:modTalksLlm / MODULABOT_LLM=1) so one source tree produces a baseline build that is BIT-FOR-BIT identical to the prior bot (parity 500/500) and an LLM-augmented build, bump the FFI ABI version when adding entry points, make new FFI functions no-op on non-LLM builds, and provide an explicit runtime kill-switch env var (MODTALKS_LLM_DISABLE=1) plus credential-detection fallback at policy init so the same build runs with or without the LLM. NEGATIVE: a bot's LLM layer may activate only on the Python-wrapper code path, not a standalone CLI binary that speaks the protocol directly (the CLI ran with LLM code present but inert because the enable-FFI call was never made); to exercise the LLM end-to-end locally, drive the bot through the same Python policy path the tournament uses (metta's bitworld_runner with a policy URI). <sub>sources: opencode:ses_21f67d3f5ffeBF92agcd0cbOV0, opencode:ses_21b008642ffeCM6Q48G44aM6k0, bitworld/among_them/players/mod_talks/README.md, bitworld/among_them/players/mod_talks/cogames/README.md (+1)</sub>
loop
Have the LLM brain issue high-level strategic directives (where to go, who to target, what to say) that override the scripted controller's goal selection -- it does NOT control frame-by-frame inputs. Constrain output to a single strict JSON object (no markdown fencing) with a fixed schema: strategy (closed enum blend_in/hunt/accuse/defend/fake_tasks/self_report), target_player, target_room, vote_target, chat_message, reasoning, hold, until. Give directives an expiry (hold/until and a TTL): hold the current directive locked until its 'until' condition is met so successive calls cannot thrash, and fall back to a default directive on TTL expiry. Trigger evaluation immediately at startup, then only by EVENT detection (not polling): each cycle diff consecutive snapshots, fire exactly one highest-priority non-debounced trigger, debounce each trigger TYPE independently (default ~60 ticks), keep a low-priority PERIODIC fallback (~every 100 ticks). Trigger priority: meeting/round-start/game-over and body-discovered > kill opportunities > player-missing and chat-accusations > low-value movement/idle/periodic. Give the agent three memory tiers with distinct lifetimes: Tier 1 volatile per-frame working memory; Tier 2 an episodic ring buffer (~200 events, categorized) where landmark events (body discovered, ejected, kill executed, self accused, game result) are PROTECTED from eviction ('landmarks are sacred'); Tier 3 a strategic key-value fact store with temporal supersession (newer facts override but do NOT delete older ones, keeping the trajectory). Before each call run a narrator step compressing all tiers into a compact (~800-token) prompt with fixed sections (WORKING MEMORY, RECENT EVENTS with landmarks marked, PLAYER MODEL, STRATEGIC SUMMARY) rather than dumping raw memory. Structure the agent so the LLM emits TASK LISTS (clear+append) feeding a queue executed frame-by-frame in three categories ONCE/SEQUENCE/LOOP, letting the deterministic executor handle phase-appropriate routing so the LLM never reasons about phase transitions. When chat is rendered by a limited pixel font, transliterate UTF-8 punctuation (smart quotes, em-dash, ellipsis) to ASCII and drop unmapped characters rather than rendering a literal '?'. <sub>sources: archive/cogames_playground/bulbacog/designs/LOOP.md, bitworld/among_them/bot-policies/sidecar/prompts/system.md, bitworld/among_them/bot-policies/sidecar/triggers.py, bitworld/among_them/players/SMART_BOT_GUIDE.md (+3)</sub>
loop
When a Coworld/cogames player's core is native (Nim) but league tooling loads a Python policy class, the load-bearing artifact is a ctypes/FFI bridge file (<player>_policy.py subclassing MultiAgentPolicy); no documentation makes the player submittable without it, so copy the bridge from a sibling that already submits rather than inventing the pattern. Build the Nim core to a ctypes-loadable shared library: compile from the repo root with nim c --app:lib -d:<libraryDefine> --out:<lib> plus --path flags for repo common/src dirs and the nim paths from nimby.lock (the library define gates the FFI exports; without it the same source compiles to a standalone CLI). Stamp each built library with an ABI version (a <lib>.abi file beside it) kept in sync on BOTH sides -- Nim source (ModulabotAbiVersion) and the Python build script (MODULABOT_ABI_VERSION) -- bump both when the FFI/observation contract changes, and have the ctypes wrapper refuse a load-time mismatch. Compile the library ON DEMAND at policy init only when the cached binary is missing or its .abi stamp mismatches (the tournament image Dockerfile.episode_runner ships Nim and nimby so the build runs in the worker). BUT read the actual exported FFI symbol name and current signature in source rather than assuming parity with the copied sibling: real drift was evidencebot_v2 still exporting nottoodumb_step_batch (inherited, never renamed) whose step_batch took 9 params with no frameAdvances arg. Observation tensors arrive in inconsistent shapes/encodings (2D/3D/4D ndarrays plus 4-bits-per-cell packed data), so the wrapper needs an explicit normalization step that unpacks and reshapes before the policy sees them. To drive a native (Go) bot, spawn the binary in stdio mode, pipe one 8192-byte packed frame per step, read back one button-mask byte, one subprocess per batch row lazily (safe because the runner iterates agents in stable player-index order). To support both a native websocket build and a Python/FFI build from one core, expose a 'take_chat'-style drain (host polls each frame, copies the message, clears the slot) with identical semantics across both transports, and keep a rule-based FFI wrapper in lockstep with its sibling reference wrapper.
<sub>sources: opencode:ses_219e032f9ffeqy8OzInbZDP27R, opencode:ses_219efb8a0ffetZ1X4BweDZQCca, bitworld/among_them/players/build_evidencebot_v2.py, bitworld/among_them/players/evidencebot_v3/build_evidencebot_v3.py (+2)</sub>
loop · negative result · ⚠ session-derived, unverified
A grader reads the input episode bundle from env var COGAME_EPISODE_BUNDLE_URI, inspects manifest.json/results.json, computes a scalar 0.0-1.0 'interestingness' heuristic over result fields (not a semantic replay analysis), and writes {"grader_id":..., "score":<number>} to COGAME_GRADE_URI (shared flow in graders/common/grader_runtime.py); the older COGAME_RESULTS_URI input / COGAME_GRADE_OUTPUT_URI output names are stale and silently fail or write nowhere -- this exact bug was in the among_them grader. A grader is a thin score-emitting runnable, not a rules judge: model new ones on among_them, keep formulas explainable, and fail clearly when the replay config is unavailable. A real commissioner is a per-round long-lived server, not a per-episode descriptor writer: expose GET /healthz and WEBSOCKET /round on 0.0.0.0:8080, exchange type-discriminated JSON (receive round_start/episode_result/episode_failed, send schedule_episodes/round_complete), and persist only opaque state passed back through the platform; the old COGAME_COMMISSIONER_OUTPUT_URI/COGAME_MANIFEST_URI/COGAME_LEAGUE_STATE_URI descriptor-file-and-exit pattern is obsolete. Runtime is 'contract defined, runtime pending': manifest.commissioner[] entries are declared but not yet invoked as containers and leagues still run in-process commissioners. <sub>sources: codex:019e6696-698a-7953-b210-3f541350ce2a, codex:019e66a2-b825-7181-9a3d-90435088068b, codex:019e66c3-638e-7e70-afdf-8f6df9299f6a, codex:019e74bf-7764-7fd2-82e5-8f66c5ff9e1d (+1)</sub>
loop · ⚠ session-derived, unverified
To check a manifest, load it through CoworldManifest.model_validate(json.load(...)) -- it surfaces exact failing errors and is the authoritative gate; do not eyeball JSON against the schema doc, and sync satellite repos to metta's authoritative sources (OVERVIEW.md, role design docs, spec 0045, EPISODE_BUNDLE_README.md, MANIFEST_README.md). Required-shape rules: (1) a 'reporter' array (minItems=1) MUST exist even with no custom reporter -- point a stub at ghcr.io/metta-ai/reporters-default:latest. (2) game.docs.pages MUST contain exactly one page id=='rules.md' and exactly one matching ^play_[a-z0-9][a-z0-9_-].md$ (e.g. play_crewrift.md); id:'play' does NOT satisfy it. (3) certification.players count must equal the fixed token count from config_schema.properties.tokens (equal minItems/maxItems), and variant/certification game_config blocks must OMIT 'tokens' (the runner injects them). Coworld also rejects a run manifest where multiple player slots share the same id, so when one policy fills all 8 slots the materializer must generate unique per-slot ids (crewborg, crewborg-1, crewborg-2, suffixing the first duplicate with -1) AND rewrite the certification.players[].player_id entries to match, or the episode fails manifest validation with a stale player_id reference. Spec 0045 prescribes a per-implementation layout (top-level README + CATALOG.yaml) that conflicts with a game-nested layout; a test referencing the game-nested path reflects current state, not the spec -- flag the misalignment. <sub>sources: auggie:dd690e14-4ba5-4e08-9415-8d6ecb76ff1d, auggie:f0d3adc1-1e28-4400-a8fb-94959c6722b5, claude-code:a5973ebb-5fd5-4158-8d61-0480948c75a9, codex:019e8fc7-ebad-7b00-bc95-df1727641ce0 (+1)</sub>
crewrift
Create crewborg/crewrift experience requests via the live route POST /v2/experience-requests (the API was renamed from /v2/episode-requests*; the published CLI ships behind the server and 404s old paths) using coworld.api_client.CoworldApiClient.from_login. Use Bearer auth only (Authorization: Bearer TOKEN), never X-Auth-Token; the token comes from ~/.softmax (softmax.auth.load_current_token / get_api_server) and must not be printed (fetching the Observatory spec unauthenticated returns 403). Always read the live OpenAPI (/openapi.json, schema V2CreateExperienceRequestRequest) before POSTing because the schema drifts (additionalProperties:false; a later drift replaced requester/opponents/rotate_seats with a single roster field of per-seat entries each a policy_ref/top_n/random selector plus a slot pin or -1 for round-robin, and removed backfill while adding a top_n opponent selector); re-check on any 4xx. To force your policy into a role, pin its roster slot AND supply the FULL game_config_overrides.slots array as OBJECTS {"role":"crew"|"imposter", "color"?, "token"?} (one per seat) -- NOT bare role strings (docs showing ["crew","imposter",...] are wrong and yield 400); the override SHALLOW-merges over variant config ({**variant_config, **game_config_overrides}) as whole-key replacements and validates against the GAME's own config_schema (Draft202012Validator), so verify shapes against the coworld manifest schema not the XP-request docs. ownership splits roster control: caller-owned explicit policy_version_ids are owner-gated (a non-owner token cannot pin a specific competitor's policy; competitor control is then only the backfill pool active_high_performers); non-owned tournament opponents go through requester.policy_version_id plus opponents/top_n; the assignments field is valid only with the direct policy_version_ids path. num_episodes 1-100; address policies by Metta policy_version_id UUID or playerName/topN; satisfy requester+opponents+topN == coworld agent count; resolve the canonical coworld version first (one canonical=True row per name) and don't reuse a coworld_id from an old episode tag.
<sub>sources: codex:019e8afb-efcd-7ca0-820f-0320501ef2fa, codex:019e8fc7-ebad-7b00-bc95-df1727641ce0, codex:019e93a0-50c3-71a0-a0dd-c1935af4add0, codex:019e941c-8eb9-7402-a77f-8fe75a5d19bf (+6)</sub>
loop · negative result · ⚠ session-derived, unverified
NEGATIVE: POST /v2/experience-requests is not atomic -- it persists parent xreq_ and child ereq_ rows first, then dispatches jobs synchronously. A POST can return HTTP 404 yet have SUCCEEDED (create commits in its own session, then the route reads back through a lagging read replica) -- do NOT blindly retry (you'll duplicate hosted jobs); GET or list recent requests first. Two failure modes strand children pending with job_id/job_request_id NULL: (1) /jobs/batch rate-limiting (429, surfaced as 500) leaves the parent persisted with null-job children; (2) a read-after-write replica race where dispatch reloads child rows via a read-only session hitting a lagging replica (observed lag ~216ms), sees no pending specs, and silently returns. After a successful POST children are ALSO intentionally left pending with no job_id because dispatch is deferred to the round-runner maintenance loop -- so 'pending with no job_id' is ambiguous between deferred-normal and stuck. There is NO public retry/dispatch endpoint and requests cannot be cancelled or deleted (only POST and GET exist), so a mistargeted request runs all its episodes -- get the target right, verify via list/detail readback before retrying, and report the request ID plainly with that caveat. A batch request (xreq_...) fans out into one child episode-request (ereq_...) per episode; inspect the CHILDREN not the parent (the parent only carries roster/config + rolled-up status). Per-league submission concurrency must stay under the shared MAX_OUTSTANDING_JOBS cap (raising a per-driver default to 100 while the scheduler starts one task per league let leagues exceed the global cap, returning 429s that driver.py reported as EpisodeFailed; the fix was raising the shared cap to 1000, not the per-league limit). <sub>sources: codex:019e8afb-efcd-7ca0-820f-0320501ef2fa, codex:019e8fc7-ebad-7b00-bc95-df1727641ce0, codex:019e93a0-50c3-71a0-a0dd-c1935af4add0, codex:019e941c-8eb9-7402-a77f-8fe75a5d19bf (+1)</sub>
loop
A Coworld/Among-Them runner injects URIs via environment variables: COGAME_CONFIG_URI (config JSON in), COGAME_RESULTS_URI (per-player final scores written as JSON, only when maxGames>=1), and optional COGAME_SAVE_REPLAY_URI / COGAME_LOAD_REPLAY_URI; CLI flags override env values. Per-slot connection tokens (one per slot, indexed by slot) are runner-managed and injected at runtime -- do not hardcode them in the manifest. To fill all N (e.g. 8) slots from a single operator, POST /v2/coworlds/play/session to create, then call /v2/coworlds/play/session/<sid>/join N times ANONYMOUSLY (no X-Auth-Token): the join route uses OPTIONAL_AUTH and skips the existing-claim idempotence check when principal is None, so each anonymous call lands on a fresh sequential slot; authenticated join is idempotent and returns the SAME slot so it cannot fill many slots with one policy. A player MAY reconnect to the same slot with the same token while the episode runs (in-game state survives short disconnects via a no-op/default during the gap) but the runner NEVER restarts an exited container. NEGATIVE: the Coworld play API has no per-episode results/replay endpoint, and the lobby/session endpoint keeps reporting status: ready even after the episode terminates, so there is no programmatic way to confirm a natural end versus early termination -- plan verification around container logs and step counts, not an API result query. A player container's non-zero exit fails the WHOLE run as a run error (not a low score), with a 10-second exit timeout. <sub>sources: archive/cogames_playground/bulbacog/COGS_V_CLIPS.md, archive/cogames_playground/bulbacog/designs/COWORLD_MIGRATION.md, archive/cogames_playground/docs/coworld_cli.md, bitworld/among_them/README.md (+3)</sub>
loop
Observability must be strictly subordinate to the agent staying alive. Build a per-tick trace dict in a local variable, deep-copy mutable working-memory snapshots (copy.deepcopy so later in-tick mutation cannot poison the snapshot), serialize pydantic args via .model_dump(mode="json"), run a json.dumps serializability check, and assign to the policy's info slot atomically only at the very end -- on failure fall back to a minimal {"version":..., "trace_write_failed": true} dict. Wrap every public trace-writer call in try/except with a one-shot stderr warning. Flush JSONL trace writers per line (flush() after each line) because low-volume event/mode logs are silently lost on crash when buffered, and any per-tick log you inspect post-mortem must survive a segfault or hard kill during cleanup. Don't rely on a single stream to diagnose multi-agent behavior: cross-check the highest-volume stream first -- when a SIGSEGV during harness cleanup left events.jsonl/modes.jsonl empty for most agents, the auto-flushed high-volume decisions.jsonl still revealed which agents entered an imposter mode. Budget trace overhead against the per-frame deadline (~42ms at 24fps, target under 10%) and ship synchronous flushes, moving to async/batching only if measured frame times degrade. <sub>sources: bitworld/among_them/players/mod_talks/TRACING.md, alpha_cog/docs/trace-format-design.md, opencode:ses_2051bd225ffefNc9y2aOEEFjHW, bitworld/among_them/players/modulabot/TRACING.md</sub>
loop · negative result
When N players run in one process and the trace dir comes from one process-wide env var, every instance writes the same path; a truncate-mode writer means the last instance to init silently destroys all others' data, and even a coarse ISO8601-to-seconds+PID scheme collides for writers built in the same wall-clock second. Fix at the source: each instance generates a unique session subdir at init using a process-wide monotonic instance counter plus PID (<root>/<ISO8601>-<PID>-<N>), making multi-instance tracing safe by default with zero launch-script changes, plus an opt-in explicit per-instance override. A runner docstring claiming per-agent isolation is NOT proof for your bot (one runner isolated only the reference bot while a new bot read a different env var and got none); verify isolation empirically per bot type. Symptoms of a clobbered trace: a manifest end_tick/frame count matching exactly one agent, and internally inconsistent role fields (manifest says crewmate but the decision tail shows impostor hunting) -- treat internal inconsistency as clobbered output, not a logic bug. NEGATIVE: if a manifest is only rewritten at end-of-round, a runtime feature toggled MID-round (e.g. an FFI call enabling the LLM layer after the initial manifest write) leaves the on-disk manifest stale even though the event stream is correct -- rewrite the manifest on shutdown, periodically, or fold counters into every Nth snapshot. Raw frame dumps are huge (~115MB uncompressed but ~5-10MB gzipped per 5-minute game) so plan retention up front (keep last K with a RETAIN sentinel). <sub>sources: opencode:ses_2044035bcffeRp9M35307uA4gR, opencode:ses_204495c66ffe7u8C8SOiEX1zdH, opencode:ses_2051bd225ffefNc9y2aOEEFjHW, opencode:ses_20be14977ffeIxGxctKKzW9s1y (+3)</sub>
loop
The clean SDK extension point for game-specific (domain) tracing is a SINGLE on_step_complete observer receiving a StepContext carrying the produced intent, command, finalized belief, action_state, and an emit callable (context.emit auto-applies the 'domain.' prefix, the correct tick, and configured sinks). Events keyed on button-edge 'attempts' MUST use on_step_complete (which sees the produced command), NOT Mode.emit. NEGATIVE: do not emit domain events by calling sink.record() from a mode or pure function (perceive, update_belief, resolve_action, Mode.decide) -- the runtime holds the sink and those callees get no reference; the fix belongs in the SDK. Thread trace_sink/metrics_sink EXPLICITLY into SynchronousStrategyRunner; it defaults to Null sinks and silently drops strategy_evaluated and cyborg.strategy.decide_ms telemetry (build_runtime()-style wiring can silently drop the SynchronousStrategyRunner trace/metrics hookup -- verify each runner's callbacks are actually connected; to serialize true mode params thread the active directive through StepContext and run the SDK validation tests when its signature changes). Build counter/histogram/gauge sinks plus a metrics adapter, use a stable namespace prefix (domain.), never attach high-cardinality labels like per-tick values to metrics (tick belongs on the trace event), and keep instrumentation changes strictly additive. Structure a per-game trace as four append-only streams separated by change-rate: a one-shot manifest, sparse edge-triggered events.jsonl, policy-branch decisions.jsonl, and periodic belief snapshots.jsonl; let the writer DETECT events by diffing current state against a shadow copy of the previous frame (a PULL model) rather than requiring policy code to emit them. Version schemas additively keeping the validator accepting all prior versions (v1/v2/v3) and reserve a schema field before implementing its producer (speaker:null reserved). Add counterfactual annotations (selected_tier/tier_candidates/tier_rejected, parity-safe) so the outer loop can reason about roads not taken, version with two counters (a per-tick program-version-id and a separate trace_format_version), and ground every non-trivial decision with a stable branch-ID <file_stem>.<category>.<specific> via one hook, enforcing in CI that every code path fires it exactly once.
<sub>sources: claude-code:4430a077-5411-426b-a87a-75512ec4300f, claude-code:c6965c58-2cb9-46fb-8b29-bad3f3e9e77f, claude-code:ee5aaed4-70d2-4597-b012-9d348f4f1e2b, codex:019e05e2-eadc-70a1-a58f-add2c62f1046 (+7)</sub>
loop
A trace meant to feed an outer-loop self-improvement LLM harness must be sourced ONLY from the agent's own experienced/belief state, NEVER server-side ground truth, so the harness reasons about the same partial-observation world the agent saw and the learned policy stays honest about what it could perceive at decision time. Make traces LLM-friendly by emitting only symbolic data (color/room/task names, stable branch IDs) and never raw pixels, sprite scores, or A* node lists -- this keeps a 5-minute game trace compact (~150-400 KB) and directly readable. Maintain a canonical decision-point taxonomy auto-generated from source so it cannot drift, keep forked bots' ID sets in lockstep, and enforce that EVERY code path through the decision function fires the hook exactly once before returning. Read crewborg's structured intent/command from decision_snapshot.data.intent and .command in trace logs, NOT from the framework's action_intent/act_command events, whose payloads are Python repr() strings rather than parseable structured data. <sub>sources: bitworld/among_them/players/mod_talks/TRACING.md, bitworld/among_them/players/modulabot/BRANCH_IDS.md, bitworld/among_them/players/modulabot/TRACING.md, personal_labs/crewrift_lab/crewrift/crewborg/docs/designs/suspicion-le (+1)</sub>
loop · tool: expand_corpus.py
Do not invent a separate trace format -- piggyback on the mettagrid/cogames replay (version 4), which already change-encodes ground truth (a ~100-step replay is ~60KB compressed). A cogames policy exposes belief/thinking by writing into the per-step policy_infos dict (AgentPolicy._infos during step()); the rollout harvests it, the replay persists it, mettascope's Policy Info panel shows it. For long-form per-step text use the magic key __monologue_transcript_tail. The replay change-encodes policy_infos at WHOLE-top-level-key granularity, so do NOT put the ever-changing tick inside _infos (forces a full re-write each tick, bloating to ~50MB raw); store only fields that actually change and exclude data recoverable elsewhere (raw obs, inventory, position, score, spatial memory reconstructable from observations). Budget ~1-3KB uncompressed per agent per tick. For episode-level metadata write a small uncompressed sidecar. To capture a trace by driving mettagrid.Simulator in-process, construct the env exactly as cogames play does: build the mission with its default max_steps=10000 (per-tick reward weight 1/10000), then mutate env_config.game.max_steps to the shorter length AFTER make_env() -- passing the reduced max_steps into mission construction bakes it into the reward handler and inflates scores by (10000/steps), making traces incomparable; assert trace scores match an A/B reference run on the same seed. A Crewrift .bitreplay is NOT stored frames -- it is per-tick 8-bit input masks plus Join/Leave/TickHash records and an embedded config JSON which the server RE-SIMULATES via stepReplay; expand replays with a game-version-matched expander binary keyed by git ref (a replay recorded under a different build HASH-FAILS), treat a hash_failed manifest status as 'came from a different build' (rebuild that version's expander, --ref <sha>), and classify per-episode status ok/hash_failed/error.
<sub>sources: alpha_cog/docs/trace-format-design.md, alpha_cog/experiments/README.md, opencode:ses_223c540d9ffe8nF6kcU1RXf0wp, players_checkouts/players/players/crewrift/crewborg/docs/crewrift-repl (+1)</sub>
crewrift · ⚠ session-derived, unverified
When adding a simEvents observation buffer to crewrift's SimServer for tooling, treat it as runtime-only state with hard invariants: it is NOT replay data, NOT game state, NOT included in gameHash, and must NOT influence player observations or policy behavior. Because SimServer is serialized into replay keyframes, clear simEvents at the start of every step (before inputs are applied) AND strip it before keyframe serialization (in serializeReplaySim), or seeking restores stale events from a previous tick. As long as gameHash does not mix in simEvents, the replay file and hash stream stay byte-unchanged. <sub>sources: codex:019eb8c0-1b42-7a92-843b-c5db259d9ac3, opencode:ses_20af98d9cffeVpWpOGzDAc35WZ</sub>
loop
Agent independence is a hard, game-specific scoring constraint -- verify it before adding ANY shared state: in Cogs-vs-Clips/Cogamer tournaments each of your agents runs as a totally independent process with no shared state/memory/out-of-band messages (shared state can score 0.00); coordination is only via in-game talk (bounded char limit) and a short role-draft phase. At least 2 of 8 Cog agents are opponents' submissions (never assume the whole team is yours) but at least one is yours, so opportunistic non-dependent coordination is worth attempting. Whether shared state is allowed is game-specific (some siblings use a softer shared map), so check and record it in game-info docs. Give each tournament agent PRIVATE memory and a private outer loop with no team blackboard, shared role allocator, or process-global coordination; delete any team-wide shared blackboard even for LOCAL play (a TeamBlackboard-style object is unavailable in isolated tournament play and keeping it locally only hides the real coordination problem -- make local play use the tournament coordination model: visible observations plus the talk channel only; removing it dropped an honest seed-42 3k baseline from blackboard-inflated numbers, but the lower number is the only relevant signal). Suppress redundant mode directives in two independent layers so a buggy outer-loop directive cannot reset mode state: (1) the inner loop publishes its current mode name and key params into the shared fact store and the outer loop emits NOTHING when its computed directive matches; (2) the inner loop independently rejects any incoming directive matching its current mode via a matches_directive check. <sub>sources: claude-code:73fa0a3d-37fb-4408-bae8-245db6e62c56, claude-code:7db8d397-c6dc-47b6-9b95-8f90675dd1b8, claude-code:829e80cb-6031-4b81-af72-52b3a12289f3, codex:019e05ef-a042-79c3-aa22-fd78a9e2333d (+4)</sub>
crewrift · ⚠ session-derived, unverified
Before building coordination on an agent talk channel, confirm its semantics from the runtime, not memory: in Cogs-vs-Clips talk is hard-limited to one message per agent per cooldown window (read policy_env_info.talk.cooldown_steps, do not hardcode 50); a sent talk becomes an ActiveTalk visible for the cooldown window (~50 ticks, not 1 tick and not forever) with proximity-based visibility that follows the speaker's CURRENT location. The real coordination problem is wasted bandwidth from every agent broadcasting on cooldown regardless of relevance -- send talk only to agents who care and are within text-visibility range, firing on events (role/resource/target coordination, raid recruitment) rather than every N ticks; one fix made talk event-based after ~100 ticks, compressed the status update, and used a 'status|message' delimiter. Note lowering talk volume does not by itself fix strategy. NEGATIVE: a hard transport char cap (observed 60 and 80 chars) silently clips LLM speech mid-word -- prompt for a target comfortably under the cap (~34-55 chars for an 80-char cap) and/or add a deterministic clean-shortening step in the action layer; verify post-run no line hits the limit. Coordination is capability-gated: different variants disable different action sets (bulbacog's talk variant disables change_vibe), so keep a talk_enabled gate and fall back to the text channel for role announcements instead of assuming vibes are available. <sub>sources: codex:019e194d-3a17-7eb2-9df9-7bae24112862, codex:019e19a7-8605-7992-9b28-cb4d2ca9132e, codex:019e1591-004e-7040-b768-062bd8e6acd4, codex:019e1593-c00d-7a32-9dfa-1cf6b16b01a4</sub>
loop · ⚠ session-derived, unverified
Before implementing a coworld policy, inspect the INSTALLED framework interfaces for exact signatures rather than guessing (cogames wraps the local Metta checkout run via uv): mettagrid Action, AgentObservation, ObservationToken, MultiAgentPolicy/AgentPolicy, PolicyEnvInterface. Build the observation parser DEFENSIVELY rather than assuming token order, and resolve tag/team/territory semantics through PolicyEnvInterface (obs_features, tags, action_names, vibe_action_names, egocentric_shape) since the env interface is the source of truth for feature ids and window shape. Do not assume a clean Gym-style observation/action/reward contract: real coworld interfaces have a fixed agent-to-game protocol (WebSocket clients, latched/asynchronous actions where the server REUSES the last submitted action each tick, sequential same-tile resolution) -- document the actual protocol and timing. When a policy chooses legal actions but never achieves the goal, the bottleneck is usually mechanics/timing/coordination, not legality: read the engine source for the exact acceptance condition (range, pending-state, ordering, timing window) -- e.g. an entry request is accepted only when another player in the same room is already whispering within 20px, a second press cancels, GRANT needs a pending entrant, auto-complete fires only when both sides offered. Before building a player for an unfamiliar coworld, generate a structured knowledge base first (orientation -> interface contract -> rules/mechanics -> observation decoding -> action semantics -> strategy/MVP-agent). The package is player_sdk (singular) at players/players/player_sdk; if a path doesn't resolve, ls the parent dir rather than looping on the broken path.
<sub>sources: codex:019e05c3-544b-7153-8a0f-cfd681fa5345, codex:019e181f-6289-7332-9837-05f0e9cadded, claude-code:c7a8b339-b61e-4359-95e4-7701c6a548a3, claude-code:cb446cb3-1370-4f0b-9de6-34f05b9e5880 (+1)</sub>
loop · ⚠ session-derived, unverified
Structured-observation and pixel-reading policies solve perception at different layers; know which contract your player targets. The metta cogames-agents baseline reads role/phase directly from a STATE_FEATURES observation header (PHASE_ROLE_REVEAL flag, role flags) with no pixel detection, while screen-reading bots derive everything from a 128x128 framebuffer -- pixel-perception lessons apply only to framebuffer players. The training/eval harness and the live tournament can feed different observation modes (an in-process training harness exposed structured-state observations while the tournament's network runner hardcoded pixel-only), so verify which the tournament uses BEFORE building perception: run one real episode through the actual tournament code path (network/runner, not the trainer) and log observation shape, dtype, and value range on the first frame (here shape=(4,128,128) uint8 indexed-palette frames). Before writing perception/action code that reads observation fields, run one baseline episode with bundled players, capture an actual decision_request/observation payload, and port against the OBSERVED shape (one session confirmed fields like economicValue, borderDistance, buildPlacementReason, tacticalAffordances existed before coding against them); likewise build replay parsing against a real replay file or authoritative schema (packages/mettagrid/replay_schema.py). When you can read the server source, replicate its exact interaction geometry instead of approximating with tolerance margins: an interaction may be an exact point-in-rect test (x in [rect.x, rect.x+w), y in [rect.y, rect.y+h), collision size 1x1) requiring the player's single position point literally inside the rectangle, and some servers gate interactions on the player being STATIONARY (a task-completing button press is accepted only when movement input is zero on both axes, so emit the action button alone on the settle frame). <sub>sources: opencode:ses_1f6f76557ffeCfmMTG7BPJwGRE, opencode:ses_22033c323ffeaukf7T2ZfJOoMR, auggie:79765dce-4e98-40f6-8606-5cd33f51d4c5, claude-code:3bdbae7f-58b2-4e0e-880b-2a2e03cc0aad (+1)</sub>
loop · ⚠ session-derived, unverified
For a framebuffer-driven player, handle desync types separately: phase desync (state machine disagrees with server phase), position desync (tracked position drifts, especially after votes/teleports), and player-index shifting (slot index changes when players join/leave). The blunt-but-reliable recovery for severe desync is full RE-REGISTRATION on a detected game reset, not incremental patching. Plan from the start for mid-game joining and unexpected frame sizes: a robust player needs a connection-loss retry loop, a path for joining a game in progress, a fallback when view localization fails, and code that drops/ignores unparseable frames instead of crashing (treat an unexpected frame as normal). Perception-only task completion is too rigid: tasks need explicit status/failure/timeout, not just visual confirmation, because many actions fail silently or leave no reliable confirmation (rate-limited chat, menu toggles, stale target pickers, interrupted sequences). NEGATIVE: a perception-driven player that emits a no-op on uncertainty can get KICKED -- when localization flips true->false mid-game and the pipeline maps localized=false to mask 0, the bot emits one identical no-op tuple for many ticks and the websocket closes, because many WS runners kick a slot sending no meaningful action for ~5-10s; emit some benign action or recover localization rather than falling silent. But do NOT assume the server enforces an inactivity timeout -- the real constraints are the game timers and per-frame action budget, so build your own frame-budget watchdog and edge-action timing. <sub>sources: claude-code:4c297fe5-08a0-4973-85ac-1b8824e638ac, codex:019dfe49-9c5e-76c0-86b8-99fbc2cf01b3, claude-code:5f9e8287-da92-4dff-bf39-2511bcaf0700</sub>
loop · negative result · ⚠ session-derived, unverified
When a player is dying or underperforming, first disambiguate in-game death (killed/voted out) from process/transport death (container exits, socket closes); scan bot logs for Traceback/Exception/panic first -- if there are none and the bot emitted decisions every tick up to failure, it is a transport/lifecycle problem, not a policy crash, so do not spelunk in strategy code. A clean disconnect with no traceback at a consistent tick, especially when all players drop simultaneously at the same wall-clock time, signatures a shared-transport timeout/keepalive issue. A clean repeatable ~40s WS cutoff is the Python websockets default ping watchdog (ping_interval=20 + ping_timeout=20): in a high-throughput binary socket (frames every few ms) the protocol-level keepalive ping times out and makes the CLIENT close (code 1011 'keepalive ping timeout', peer sees 1006) even while game data flows continuously; a suspiciously round failure time is a tell of a fixed library timeout. Disable client keepalive pings for high-frequency binary sockets and for one-off long-running re-sim requests (poll the running request instead of firing a second one). Instrument the websocket boundary to record close PROVENANCE instead of one generic 'websocket closed' line: close code/reason, whether close happened during receive vs action-send vs chat-send, frames/actions/chats processed, uptime, seconds since last receive/send, malformed-message counts -- the close code/reason is usually the single most decisive clue. The sim clock does not wait for slow bots (the frame-limiter caps tick rate but sim.step consumes whatever input is in shared state, so latency yields stale/NOOP actions, not a slower clock); a dropped slot is NOT re-promoted ('docker start' does not rejoin, the run continues degraded 7/8); hosted-play (proxy) sessions are NOT the tournament path (hosted-play WS goes bot -> FastAPI proxy -> in-cluster TCP pipe, tournament containers connect directly via ws://<game-service>:8080/player?...). Player-container connection/acceptance order is a red herring: slot assignment is stable (each connects to /player?slot=N, the game applies 'for slot in range(len(tokens))'). <sub>sources: codex:019e2819-d56b-72f3-9349-60fc965b6ed2, codex:019e2a05-c607-7f53-b4f7-cb5ec3bdbb3e, codex:019e8ae0-d849-7d12-9a7c-2b5e09ba75cf, codex:019e28e9-6b45-7c02-b4c7-bb9b0f44e022 (+4)</sub>
loop · ⚠ session-derived, unverified
The websocket bridge must close the runtime in a finally path: if runtime.close() only runs after a normal receive-loop exit, exceptions or close races bypass cleanup, so wrap connect -> receive-loop -> close so cleanup always runs. When swallowing ConnectionClosed to treat end-of-game as normal, keep connect/handshake failures OUTSIDE the swallowed path; only post-connect receive/send closes should be treated as benign. When equip/action requests become silent noops on a stalled zero-score player, suspect the Coworld wrapper discarding action fields, not only bad targets -- enumerate candidate boundaries (landmark-projection localization, equip/path target selection, or the wrapper silently dropping fields) rather than assuming a pure strategy problem. <sub>sources: codex:019e7174-3079-7351-b8ea-b1bfcb917ff4, codex:019e28e9-6b45-7c02-b4c7-bb9b0f44e022</sub>
loop · negative result · ⚠ session-derived, unverified
Two parallel index->action lookup tables silently drift: a native action table grouped direction-first and a Python table grouped direction+modifier disagreed on 22 of 27 entries, garbling actions with no loud failure. Derive both from one canonical source or add a build-time assertion that fails if their orderings diverge. A 'fall back to index 0 / noop' default on an unrecognized action mask is a silent-corruption trap; prefer raising or logging on an unmapped value so corruption surfaces instead of looking like the bot chose to wait. Do not hand-roll pieces the episode runner already exposes: debug/watch scripts that copy-paste their own mask encoder (index->name.split('+')->encode_buttons) and their own socket receive loop are the two drift-prone spots; prefer the runner's direct mask-table lookup (BITWORLD_ACTION_MASKS[action]) over the name round-trip, and its latest-frame drainer over a hand-rolled loop. In a finite-state-machine player, a mode-name enum whose integer values back trace logs and index static dispatch tables is schema-stable: appending is safe, but reordering or deleting a mid-list entry shifts every later value and silently corrupts old trace interpretation and external mode-name mappings (an LLM's directive vocabulary) -- prefer leaving a dead stub in place over reordering, and re-export shared enums (View/Room/PlayerShape) rather than duplicating them (duplicate enum definitions drift and cause subtle equality bugs). <sub>sources: opencode:ses_21ab38af3ffeCyRfclXzAswGm1, opencode:ses_21eb30b06ffeL6353Ov27zhdA1, opencode:ses_1fed7bbaaffe4qARc9KvRdpGAC, opencode:ses_2067f7b41ffe0wSvJ4MZlE58Q1</sub>
loop · ⚠ session-derived, unverified
Ship a minimal end-to-end P0 player first and keep it as a regression smoke test: it connects, receives valid observations, always emits a valid action every tick (button mask 0 = noop), registers a single always-legal IdleMode, and runs a trivial idle strategy through the REAL strategy runner -- validating every boundary at zero strategy risk. Build the inner loop as stubs first that run end-to-end producing a no-op before adding behavior, validated by a smoke test asserting observable side effects not internals (tick advanced 0->1, act returned a no-op, exactly one ('in', 0) byte sent); stub missing belief fields with explicit TODO markers rather than fabricating extraction. Re-verify the no-op smoke player before debugging perception or strategy so transport/lifecycle bugs stay isolated. An auto-generated player should also ship a dumb-but-safe scripted baseline (fixed right/down/left/up cycle held N frames) plus action-allowlist validation falling back to noop. Build deterministic fixtures: a 'hello world' episode (one player, zero imposters, zero tasks, instant timers) lets a no-op policy complete an episode; for a multi-slot game where the server blocks until all slots fill, run the candidate in self-play on EVERY slot -- an 8-slot game never starts with a one-bot test, while filling all slots exercises both roles and yields a full replay plus per-slot traces. Register every mode the decision layer can emit, including unimplemented ones, by stubbing gaps with idle/no-op so the mode-name->behavior lookup stays total and a directive for an unbuilt mode degrades to idle instead of crashing. Keep mode/registry construction in ONE shared builder (_build_registry()) used by both the live runner's policy.run() and the integration tests -- if tests build their own registry they can pass while the live agent is missing a mode. Build a new player by copying the exact runner shape of a known-good sibling (logger setup, send_input/send_chat closures, pipeline with initial_mode + fallback=idle, hook registration, websocket connect/frame loop, SIGINT/SIGTERM handling) rather than from scratch. <sub>sources: auggie:c5674179-5e48-4584-a7a9-f75d2cd3c61b, claude-code:27659fc3-8e40-469f-beda-31ddd5c6ceca, claude-code:2ed48615-a956-43ec-a15a-b92eb2c8f879, claude-code:7b66497b-650c-4bba-b410-6c854f59163d (+5)</sub>
loop · ⚠ session-derived, unverified
Integration-test a policy against REAL fixture frames through the real perception path while mocking only the transport (append emitted action masks/chats to lists and assert on them); this exercises decode plus decision logic for real while staying offline and fast. Reuse ONE canonical frame/IO protocol everywhere -- the live agent and the capture/replay harness must share the exact frame-unpack and input-send pattern (ws.recv() 8192-byte frames -> unpack_frame -> (128,128) uint8; ws.send(struct.pack('BB', 0x00, mask))) -- because divergent protocol code between live and test harnesses is a common source of drift, and normalize frame shapes so raw protocol frames and pre-unpacked fixtures both feed the same parse_frame path. Build a non-LLM rule-based reference agent (idle/wander/approach-nearest plus a simple meta_decide) that exercises the full perception->belief->decide->act pipeline and mode/task plumbing end to end against a live server, with no LLM cost or nondeterminism, giving a baseline opponent and a testable harness; copy the repo's existing baseline-agent policy entry point as the template. In a fire-and-forget action layer (no acknowledgment channel), do not return SUCCESS/FAILED from the action/task; record only 'I sent command X' in action memory and let the controller infer success from later perception. A live smoke test cannot run as a pytest unit test (it needs the real game server plus a Node/tsx runtime), so keep environment-coupled live-validation harnesses out of the fast suite and invoke them manually: launch a minimal Crewrift episode with nim r src/crewrift.nim --address:0.0.0.0 --port:2000 --config:'{"minPlayers":1,"imposterCount":0,"tasksPerPlayer":1}' and point the player via COGAMES_ENGINE_WS_URL. Judge a test suite by behavioral contracts not construction: high-value tests cover belief-update logic (chat dedup, room reassignment, role-reveal set-once), A* pathfinding, hook crash-recovery with belief rollback, thread-safety of the perceive->decide loop, and rising-edge button sequencing; the most common real gap is missing NEGATIVE-path coverage (handlers tested only on happy-path, never with None subfields, malformed data, or contradictory state like a whisper frame with chatroom=None). Avoid low-value tests (exact tick counts/threshold arithmetic break on correct behavior; replace hard time.sleep() with poll-until-condition helpers).
<sub>sources: claude-code:f196c6ef-01ee-461d-b240-e21e96b7c038, codex:019e038f-b411-7a01-b409-b9c4a04270c6, codex:019e04ef-5a71-7851-8ea6-58a19f29b42f, codex:019e0850-a17c-7021-b7fc-31f3ece88326 (+3)</sub>
loop · negative result · ⚠ session-derived, unverified · tool: play_match.py
NEGATIVE: play_match.py does NOT reset into fresh games after game_over -- a '600s session' kept the server alive but produced only ONE real game (ended ~tick 3500, two meetings), not a multi-game batch. For many independent games the harness needs real game-reset/restart, not just a longer wall-clock timeout; do not treat session duration as a proxy for game count. <sub>sources: codex:019e1591-004e-7040-b768-062bd8e6acd4</sub>
loop · ⚠ session-derived, unverified
When building a new coworld player on an existing perception/agent framework, read the base layer's types.py and perception/types.py FIRST and import shared domain types (e.g. Orpheus Room/View/PlayerShape) rather than redefining them -- redefinition creates type-identity mismatches across the perception/decision boundary. Make the foundational stage pure type contracts: re-export existing perception enums/constants (duplicating an enum creates two drifting sources of truth), keep tasks/modes as abstract base classes, make the belief object a mutable schema with explicit reset semantics. Watch for two parallel copies of a perception/policy package in one repo (a top-level 'perception/' vs a namespaced 'orpheus.perception/', or a tool-local vs core copy); they diverge silently and 'the same import' resolves differently by PYTHONPATH -- identify the canonical package before editing, and patch perception at the pipeline's IMPORT SITE (orpheus.pipeline.parse_frame), not the perception module. Avoid a backwards dependency where the core agent depends on a TUI tool: extract a shared observation parser (ParsedObservation, NameMaps, parse_observation()) into the core package and re-wire the TUI to import it. For a fork-and-learn STARTER player, optimize for simplicity over the sophistication of production bots (2000+ line Nim, compiled binaries are useless as a starting point) -- 'serviceable, simple, documented' beats 'optimal' for a template others fork. Construct policy configuration as a frozen PolicyConfig dataclass built once per policy instance from class attributes, and have skills/programs read only the injected cfg at runtime; model a new game capability as a new world-space intent plus an action-layer resolver (the crewrift ghost mode emits a navigate_to_noclip intent while the action layer owns the straight-line controller) rather than direct device control inside a mode. <sub>sources: claude-code:7bca307c-7349-423e-90c7-544957a43935, claude-code:829e80cb-6031-4b81-af72-52b3a12289f3, codex:019e012e-626f-7081-91fb-fccc04e39010, codex:019e0138-b924-7210-98fd-3fb3ada47fe9 (+6)</sub>
crewrift · ⚠ session-derived, unverified
Reset all per-meeting scratch (vote confirmed, action queue, vote target, cursor counters, last-LLM-action tick = -1) on mode entry and flush the meeting conversation buffer when leaving the Voting phase so chat context does not leak into the next meeting. Sanitize and rate-limit outbound chat: restrict to printable ASCII, hard-cap length (e.g. 60 chars), enforce a minimum tick gap between chat packets, and store one pending line that the C FFI export drains and the Python policy polls after the step call. Clear per-action working memory on task change so prior-task state can't leak. Do not maintain two parallel paths to derive the same state: when a contract designates a canonical source (e.g. player_knowledge), delete the alternate fallback that rebuilds the same knowledge from belief_state.players, because duplicate reconstruction is a maintenance trap. Check whether a capability already exists on another role's code path before writing new control flow: Crewrift's crewmate report path (body visible + in report range + low velocity -> press A) already existed and the impostor branch just never invoked it, but the impostor branch returned BEFORE the shared holdTaskAction/taskHoldTicks logic so a dedicated impostor-local timer field was the clean fix. <sub>sources: personal_cogs/among_them/guided_bot/MEETING_DESIGN.md, codex:019e05ef-a042-79c3-aa22-fd78a9e2333d, codex:019e14de-cdca-7482-88fa-81716ee830ae, opencode:ses_21b02c437ffeor4lV5Nb4Cec7y (+2)</sub>
loop · negative result
Keep a policy's emitted action/skill-args dict in exact sync with its strict Pydantic args model (ConfigDict(extra='forbid', strict=True)): extra='forbid' rejects unknown keys and strict=True prevents silent coercion ('5'->5). Adding a key without a matching field makes validation reject EVERY tick; if a per-stage try/except then falls through to no-op, the agent silently no-ops the whole episode and scores 0.00 with no error -- recognizable from episode stats showing only energy/hp/solar and zero gear/hearts. Validate in a thin wrapper around skill invocation (not inside the skill), expose schemas via cls.model_json_schema(), and treat an unexpected 0.00 as a likely silent validation/no-op; the mettagrid simulator's set_action raises eagerly (KeyError on unknown action/vibe, AttributeError on a None/int argument) so the wrapper must catch from the set_action dispatch, not only skill.step(). mettagrid supports dual-buffer actions: Action(name="move_north", vibe="change_vibe_heart") both moves and changes vibe in one tick (a dual-buffer Action is still one Action), but a bad vibe name raises AFTER the primary move buffer is written -- on ARG_VALIDATION_FAILED the handler must explicitly set_action("noop") for that agent. Mode params silently fall back to defaults when a directive carries the base params type instead of the mode's specific type (ProbeSystematicMode getting ModeParams() not ProbeSystematicParams). Thread params through their REAL consumer: alpha_cog's ExploreSkill takes ZERO args and direction rotation comes from explore_direction_index in working memory, so a new explore bias on the bare explore path must be threaded into ExploreSkill.step()/navigation.explore_action() directly, not set on the explore dispatch (silently dropped). If a policy stage runs in a fixpoint/interpret loop (called up to N times per tick until working memory stabilizes), every update must be idempotent within a tick (guard a non-idempotent counter with a per-tick sentinel storing the last-updated tick). A PRIMARY sort key must represent missing data as ZERO (no contribution), never a 'sort-last' sentinel that dominates the true signal and produces byte-identical A/B results. <sub>sources: archive/cogames_playground/alpha_cog/docs/agent-architecture.md, archive/cogames_playground/alpha_cog/docs/cogames-verification-report., archive/cogames_playground/alpha_cog/experiments/notebook/hypotheses/H, alpha_cog/AGENTS.md (+4)</sub>
loop · negative result
Variants alter the action space and a tag's ID is its index into policy_env_info.tags (differs across missions/variants), so reference actions by name (Action(name="move_north")) and NEVER hardcode numeric tag IDs or flat indices; numeric ids change across mission variants so indexing by id silently breaks on a different variant -- fall back to noop on a name miss but keep a strict lookup so a vocabulary mismatch is detectable. A scripted policy subclasses MultiAgentPolicy and returns a per-agent AgentPolicy from agent_policy(agent_id); at init build name->id maps once (tag_name_to_id from policy_env_info.tags, feature_name_to_spec from obs_features) then reference everything by name, and fail loudly with init-time assertions when a required action or tag is missing (intentional contract-drift detection). Honor the scripted-navigation per-tick caller contract IN ORDER: update_position (fold the prior move into position/blocked) -> observe (fold visible walls/entities) -> next_move -> record_step (push position onto the tabu trail); skipping update_position silently DESYNCS the tracked local position with no detection. Structure each agent's tick as Sense -> Decide -> Act and prefer a scored GOAL-TREE (a forest of composable Goal objects each scored every tick) over a monolithic state machine. Assign roles from policy-URI query params (buggy?miner=4&scout=0&aligner=2&scrambler=4); omitted counts make 'stem' agents that pick roles from goal evaluation. Persist per-agent intent by wrapping a per-agent policy in StatefulAgentPolicy under a MultiAgentPolicy. NEGATIVE: a move with insufficient energy fails SILENTLY (the simulator runs noop); detect by comparing intended last_action against observed last_action_executed. <sub>sources: archive/cogames_playground/alpha_cog/docs/cogames-policy-api.md, archive/cogames_playground/alpha_cog/docs/cogs-v-clips-strategy.md, alpha_cog/AGENTS.md, optimizers/.cursor/skills/scripted-navigation/SKILL.md (+4)</sub>
generic
To invoke Claude Code non-interactively, run claude -p "<prompt>" (processes the prompt, prints to stdout, exits); add --allowedTools "Read,Edit,Bash" to pre-approve tools (granular patterns like "Bash(git *)"), --add-dir <path> for access outside the cwd, --bare to skip hooks/plugins/CLAUDE.md/memory for deterministic side-effect-free runs, and --output-format json for structured output (result, session_id, usage, total_cost_usd); piped stdin is capped at 10MB. To invoke Codex non-interactively, use codex exec "<prompt>" (alias e): set the working dir with -C <dir>, write permissions with -s <mode> (read-only default / workspace-write / danger-full-access), --skip-git-repo-check (Codex requires a git repo by default), --ephemeral to avoid persisting sessions; the final message goes to stdout and progress to stderr, -o <file> writes the final message, --json gives a JSONL event stream. NEGATIVE: when Codex is authenticated via a ChatGPT account rather than an API key, model selection is limited to the account tier and requesting an unsupported model (e.g. o4-mini) returns 400; supplying CODEX_API_KEY unlocks the full roster.
<sub>sources: players_checkouts/players/tools/cogbase/docs/cli_runners.md</sub>
loop · ⚠ session-derived, unverified · tool: FLAS GPU worker
Split the architecture so the expensive credentialed LLM path covers only what needs it: keep cheap deterministic work (e.g. hidden-attribute classification) local and use a hosted LLM only for generation (e.g. writing answers) -- with a native Messages forced tool call -- and defer the LLM backend choice until the cheap path is proven. Parse a natural-language 'instructions' string into a typed config model at agent-creation time behind ONE return type, with a deterministic fallback when no LLM is available (an LLM parser emitting JSON matching a Pydantic schema plus a lossy keyword/regex fallback, both returning the same model, and raw config kwargs override instruction-derived values). The FLAS GPU worker is a plain HTTP service with a narrow surface: only GET /health, POST /generate, POST /choice-logprobs exist (root and mistyped paths 404); /health returns model id, device, and queue_depth so poll it to gauge load before firing a batch; no auth for normal-priority requests, request signing only buys tournament priority. Respect strict answer-format validation in generated game answers: answers must be printable keyboard characters only, single spaces (no repeated/leading/trailing), at least 3 non-space characters when non-empty, and under a whitespace-token cap (max_answer_tokens default 12); an empty answer is a permitted decline that scores 0; long or oddly-formatted answers are rejected. <sub>sources: claude-code:b952ba45-d32f-4ce0-a227-c691de8c588f, claude-code:eb751ed4-c198-4b0d-819e-1a3002f4f341</sub>
まだレビューはありません。使ってみた感想をお寄せください。
概要と使いどころ
Use for ab methodology recipes when optimizing a Coworld player.
日本語の概要は準備中です。原文の説明を表示しています。
Use for ab methodology recipes in scripted Coworld policy optimization.
日本語の概要は準備中です。原文の説明を表示しています。
Use for reusable ab methodology recipes in software and coding-agent work.
日本語の概要は準備中です。原文の説明を表示しています。
Use for Crewrift-specific ab methodology recipes when optimizing a player.
日本語の概要は準備中です。原文の説明を表示しています。
Use for Crewrift-specific agent workflow collab recipes when optimizing a player.
日本語の概要は準備中です。原文の説明を表示しています。
Use for reusable agent workflow collab recipes in software and coding-agent work.
日本語の概要は準備中です。原文の説明を表示しています。