Skip to content

feat(sim): reflexive world-state model for the calendar simulation - #6

Merged
linroger merged 19 commits into
mainfrom
claude-reflexive-world-state-model
Oct 17, 2026
Merged

linroger merged 19 commits into
mainfrom
claude-reflexive-world-state-model

Conversation

@linroger

Copy link
Copy Markdown
Owner

What changed

The calendar simulation now evolves an explicit world every round, shared by both OASIS platforms. Each round is observe → decide → act → world update:

  • Observe. Every active agent receives a WORLD STATE brief: tracked facts (seeded from the dossier's quantitative facts and the official-data snapshot, each with unit, as-of date and provenance), last period's moves and their consequences, alliance and rivalry shifts, and open commitments. Scenario probabilities never reach agents (herding guard).
  • Decide. Each deciding actor (principals first, then active named actors, capped by SIM_WORLD_DECISION_MAX_ACTORS) makes ONE structured decision from a menu grounded in its sealed role brief and the current world: responses to moves aimed at it, its documented plans, its agency class's catalogued levers within remaining capacity, and hold_position. The answer carries targets, magnitude, a public statement, a dated commitment, alliance moves and the scenario the move pushes toward. Invalid or failed answers degrade to typed holds, never to silence.
  • Act. The statement is published as that actor's post on both platforms (one decision per actor per round, shared across platforms); the reaction step is unchanged.
  • World update. When both platforms have delivered the round, exactly one update commits: the deterministic kernel consumes capacity, registers commitments (honoured by acting when due, lapsing otherwise), nudges relationships, fires due delayed effects and research events; one bounded arbiter call resolves contested moves, moves state variables (clamped by SIM_WORLD_MAX_CHANGE_RATIO), emits emergent events and delayed effects. The scenario-share WorldState steps from the actors' scenario commitments. A platform never starts round i+1 before update i is committed.

New modules: world_model.py (kernel), agent_decision.py (18 agency classes, action catalogue, option menus, decision contract), world_update.py (deterministic step + arbiter), world_model_seed.py (PREPARE-time world_model block). Runner: _ReflexiveWorldCoordinator in run_parallel_simulation.py (asyncio barrier, shared decisions, tagged ManualAction posts, USER memory notes, resume guards). Ontology: agency_class / levers metadata and normalisation. Downstream: run-state projection, run_summary.world_model, GET /api/simulation/<id>/world-state, report world-evolution block and world-variables chart (EN/ZH titles), orchestrator summary, frontend WorldStatePanel.vue.

Flags (all documented in .env.example and both READMEs): SIM_WORLD_MODEL (default true, presence-keyed on the sealed block), SIM_WORLD_ARBITER, SIM_WORLD_BARRIER_TIMEOUT_S, SIM_WORLD_DECISION_MAX_ACTORS, SIM_WORLD_DECISION_CONCURRENCY, SIM_WORLD_MAX_VARIABLES, SIM_WORLD_MAX_CHANGE_RATIO. SIM_WORLD_MODEL=false and hours mode keep the legacy in-band scenario evolution byte-identical.

Why

The previous "world state" was only a probability distribution over the forecast scenarios plus a 900-character digest of posts. Agents never observed consequences of anyone's actions, so the simulation measured discourse rather than an evolving world. Design and acceptance matrix: docs/handoff/reflexive-world-state/design.md; ledger and evidence: docs/handoff/reflexive-world-state/handoff.md.

Verification

  • New offline tests: test_world_model.py (18), test_agent_decision.py (27), test_world_update.py (6), test_world_model_seed.py (9), test_world_model_downstream.py (10), test_world_model_loop.py (6, drives the real round loop with a fake env and scripted LLM: reflexivity, two-platform barrier ordering, arbiter failure, off switches, resume). test_calendar_round_loop.py and test_sim_reaction_phase.py unchanged and green.
  • Full backend suite: 6229 passed, 22 skipped, 11 xfailed; the only 2 failures are the pre-existing test_export_demo_site_data demo-asset tests caused by deleted docs/demos files in the author's working tree (not part of this branch).
  • Frontend: 145/145 unit tests, npm run build OK. ruff (F,E9), check_env_drift --strict, compileall and init.sh pass.

For reviewers

  • No live LLM run has been done yet; feature_reflexive_world_state_model stays passes: false until the live validation (a follow-up session is prepared for it).
  • The Reddit canonical system messages are byte-attested, so all per-round context is injected as USER memory notes; the attestation path itself is unchanged.
  • The standalone run_twitter_simulation.py / run_reddit_simulation.py scripts never supported calendar mode and are out of scope.
  • A separate Codex planning branch (codex/reflexive-world-simulation) explores the same topic independently; this PR does not depend on it.

Roger Lin added 2 commits October 16, 2026 22:46
Every calendar round is now observe → decide → act → world update, shared by
both OASIS platforms:

- world_model.py: pure kernel — state variables with causal history and a
  per-round change-ratio clamp, actor states (capacity, plans, red lines,
  commitments), valenced relationships, delayed effects on a seeded RNG,
  events, public/private briefs that never carry scenario shares.
- agent_decision.py: 18 agency classes with word-boundary inference, an
  action catalogue per class, world-grounded option menus (responses,
  documented plans, class levers within capacity, hold), a JSON decision
  contract validated against menu/cast/scenarios with typed invalid/failed
  holds, private memory notes.
- world_update.py: deterministic kernel step, bounded arbiter prompt +
  validation + application, agent-facing digest, round record.
- run_parallel_simulation.py: _ReflexiveWorldCoordinator — asyncio barrier
  (no platform starts round r+1 before update r commits), one shared
  decision per actor per round, statements published as tagged CREATE_POST
  ManualActions, USER memory notes (sealed system messages untouched),
  world_state.json / world_state_history.jsonl / world_actions.jsonl plus
  the unchanged trajectory/digest/decisions contract, resume guards,
  world_update rows in actions.jsonl.
- PREPARE: world_model_seed.py builds the sealed world_model block from
  quantitative facts, official series, actor packs and relationships;
  AgentActivityConfig gains agency_class/archetype/outcome_power; ontology
  addendum + normalisation carry agency_class/levers.
- Downstream: run-state projection, run_summary.world_model,
  GET /api/simulation/<id>/world-state, report world-evolution block and
  world-variables chart (EN/ZH titles), orchestrator summary, frontend
  WorldStatePanel with pure helpers, flags SIM_WORLD_MODEL / SIM_WORLD_ARBITER /
  SIM_WORLD_BARRIER_TIMEOUT_S / SIM_WORLD_DECISION_MAX_ACTORS /
  SIM_WORLD_DECISION_CONCURRENCY / SIM_WORLD_MAX_VARIABLES /
  SIM_WORLD_MAX_CHANGE_RATIO, .env.example and README EN/ZH.

SIM_WORLD_MODEL=false and hours mode keep the legacy in-band scenario
evolution byte-identical. Offline tests drive the real round loop with a
fake env and scripted LLM (reflexivity, two-platform barrier, arbiter
failure, off switches, resume).
Design with acceptance matrix, continuity record with verification evidence
(full suite 6229 passed; only the two pre-existing demo-asset failures),
feature_list entry (passes pending a live run) and the progress log entry.
Copilot AI balanced review requested due to automatic review settings October 16, 2026 15:49
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 16, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-16T15:52:37.411129Z 4b2858f PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 4b2858fcc0

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +3965 to +3968
missing = [p for p in self._known_platforms()
if p != str(platform) and p not in self._done
and self._watermark.get(p, -1) < round_num - 1]
self._stalled.update(missing)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Mark never-registered platforms stalled after a timeout

When a two-platform run loses one platform before its first heartbeat or delivery, that platform is absent from _known_platforms(), so this timeout handler computes missing=[] and never adds it to _stalled. The surviving platform then cannot commit normally because the coordinator still expects two known platforms, causing another full SIM_WORLD_BARRIER_TIMEOUT_S wait before every subsequent round; with the default this can add 30 minutes per round after an initialization failure. Track the expected platform identities up front, or otherwise permanently reduce the live-platform set after the first timeout.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

@codex[agent] please fix

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Comment on lines +3879 to +3881
if self._ws is not None and self._world.scenario_shares:
# keep the mirrored scenario shares of a resumed run (the WorldState itself restarts from seed)
pass

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Restore scenario shares and trajectory when resuming

On a partial resume, the committed WorldModel is loaded with its saved scenario_shares, but _ws remains freshly initialized from the original base rates and this branch does nothing. If any new rounds are committed, their shares are therefore evolved from the seed rather than the pre-crash distribution, and finalize() overwrites world_state_trajectory.json with a new trajectory containing only seed plus post-resume rounds. This changes the final forecast and discards the earlier trajectory, so the saved shares and existing trajectory/decisions need to be restored before continuing.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

@codex[agent] please fix

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Comment on lines +5842 to +5844
if _world_coord is not None:
# REFLEXIVE WORLD: a dead round still advances the world (due effects, events)
await _world_coord.heartbeat(_ckpt_platform, round_num)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Deliver scheduled events on heartbeat-only rounds

When a calendar round has no active agents, this path calls heartbeat() without _scheduled_events_due(...); the heartbeat buffer initializes events as empty, unlike the normal deliver() path. Consequently, if all live platforms have a dead round—or a single-platform round falls into this branch—confirmed events scheduled for that period are silently omitted from register_scheduled_events, the arbiter prompt, and all later world briefs. Pass the due events through the heartbeat path so an inactive cast does not erase exogenous events.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

@codex[agent] please fix

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Changes recommended

Unresolved seed eligibility, decision constraints, update validation and resume defects can distort simulated outcomes.

13 open findings
What changed in this PR

Adds a shared, evolving world to calendar simulations so actors can observe consequences and adjust subsequent decisions.

Changes:

  • Introduces research-seeded world facts, structured actor decisions and bounded world updates.
  • Exposes world evolution through the API, dashboard, reports and run summaries.
  • Adds offline coverage, configuration flags and design documentation.
File Description
README.zh-CN.md Documents the workflow and settings in Chinese.
README.md Documents the workflow and settings in English.
frontend/​tests/​worldState.test.mjs Tests world-state formatting and summaries.
frontend/​src/​utils/​worldState.js Builds the panel view model.
frontend/​src/​utils/​vizManifest.js Adds the Chinese chart caption.
frontend/​src/​components/​research/​WorldStatePanel.vue Displays and polls world state.
frontend/​src/​components/​research/​SimulationView.vue Integrates the world-state panel.
frontend/​src/​api/​simulation.js Adds the world-state API client.
feature_list.json Records acceptance steps and pending live validation.
docs/​handoff/​reflexive-world-state/​design.md Defines architecture and acceptance criteria.
backend/​tests/​test_world_update.py Tests deterministic and arbiter updates.
backend/​tests/​test_world_model.py Tests world-model mechanics and persistence.
backend/​tests/​test_world_model_seed.py Tests seed generation and sealing.
backend/​tests/​test_world_model_loop.py Tests round coordination, fallback and resume.
backend/​tests/​test_world_model_downstream.py Tests API, reporting and summaries.
backend/​tests/​test_agent_decision.py Tests action menus and decision validation.
backend/​scripts/​action_logger.py Logs committed world updates.
backend/​app/​services/​world_update.py Coordinates mechanical and arbiter updates.
backend/​app/​services/​world_model_seed.py Builds research-grounded world seeds.
backend/​app/​services/​simulation_runner.py Projects updates and manages artifacts.
backend/​app/​services/​simulation_config_generator.py Adds actor metadata and sealed world seeds.
backend/​app/​services/​report_visualizer.py Charts simulated world variables.
backend/​app/​services/​report_agent.py Includes world evolution in reports.
backend/​app/​services/​pipeline_orchestrator.py Records world-model summaries.
backend/​app/​services/​ontology_generator.py Adds agency classes and lever metadata.
backend/​app/​services/​agent_decision.py Defines action catalogs and structured decisions.
backend/​app/​config.py Adds world-model configuration flags.
backend/​app/​api/​simulation.py Exposes committed state and bounded history.
agent-progress.txt Records implementation and verification status.
.env.example Documents world-model environment settings.

🧠 Review effort: Balanced


💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +191 to +195
row: Dict[str, Any] = {
"name": name,
"value": number if number is not None else _clean(fact.get("value"), 160),
"unit": unit if number is not None else "",
"kind": "numeric" if number is not None else "text",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Addressed in commit 4749466.

Comment on lines +224 to +228
if not isinstance(value, (int, float)) or isinstance(value, bool) or not math.isfinite(float(value)):
continue
if as_of and date and date > as_of:
continue
if latest is None or date > str(latest.get("date") or ""):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Addressed in commit 4749466.

problems.append(f"empty value for '{var.id}'")
continue
row["new_value"] = val
update["variable_changes"].append(row)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Addressed in commit 4749466.

Comment thread backend/app/services/world_update.py Outdated
except Exception as exc: # noqa: BLE001 — a failed arbiter must not break the round
logger.warning("world arbiter failed in round %s (status=failed): %s", round_num, exc)
return "failed", {}, [f"llm error: {type(exc).__name__}"]
update, problems = validate_arbiter_update(raw, world, scenarios)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Addressed in commit 4749466.

Comment on lines +3879 to +3881
if self._ws is not None and self._world.scenario_shares:
# keep the mirrored scenario shares of a resumed run (the WorldState itself restarts from seed)
pass

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Addressed in commit 4749466.

Comment on lines +3136 to +3142
seed = float(hist[0]["value"])
xs, ys, hover = [], [], []
for h in hist:
rnd = int(h.get("round") or 0)
xs.append(labels.get(rnd) or (f"period {rnd}" if rnd else "seed"))
raw = float(h["value"])
ys.append(raw / seed * 100.0 if seed not in (0.0, -0.0) else raw)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Addressed in commit 4749466.

if plotted == 0:
return None
_apply_layout(fig, "Simulated World Variables (indexed, seed = 100)", height=460)
fig.update_layout(xaxis_title="Simulated period", yaxis_title="Index (seed value = 100)")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Addressed in commit 4749466.

self.add_variable(nv["name"], nv.get("value"), unit=nv.get("unit") or "",
kind=nv.get("kind") or "numeric", as_of=str((self.period or {}).get("period_end") or ""),
provenance="arbiter", rationale=nv.get("rationale") or "", var_id=vid)
summary["variables_added"].append({"id": vid, "name": self.variables[vid].name,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Addressed in commit 4749466.

Comment thread backend/app/services/world_update.py Outdated
Comment on lines +167 to +175
def _num(value: Any) -> Optional[float]:
if isinstance(value, bool):
return None
if isinstance(value, (int, float)):
return float(value)
try:
return float(str(value).replace(",", "").replace("%", "").strip())
except (TypeError, ValueError):
return None

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Addressed in commit 4749466.

@Codex

Codex AI commented Oct 16, 2026

Copy link
Copy Markdown

Just as a heads up, I was blocked by some firewall rules while working on your feedback. Expand below for details.

Warning

Firewall rules blocked me from connecting to one or more addresses (expand for details)

I tried to connect to the following addresses, but was blocked by firewall rules:

  • ab.chatgpt.com
    • Triggering command: /opt/hostedtoolcache/node/24.21.0/x64/lib/node_modules/@openai/codex/node_modules/@openai/codex-linux-x64/vendor/x86_64-REDACTED-linux-musl/codex/codex (dns block)

If you need me to access, download, or install something from one of these locations, you can either:

Co-authored-by: linroger <136704361+linroger@users.noreply.github.com>
Roger Lin added 16 commits October 17, 2026 01:31
Owner-approved live run pipe_d569f211eb14 / sim_d6ebcc77e22d (glm-5.3, 9 monthly
rounds, 7 agents): arbiter applied 7/9, 50 valid decisions with 12 option ids by
agency class, one decision per actor per round on both platforms, caused fact
changes inside the clamp, rounds 39-140 s vs the 1800 s barrier; dashboard panel,
run_summary block and report chart verified. feature_reflexive_world_state_model
set to passes=true; follow-ups listed in the progress entry. The report stage
failed its publish gate on a provider content-filter rejection, unrelated to this
branch.
Force-added (the repo .gitignore matches every handoff.md): request, acceptance
matrix evidence, live-run scenario checks (pipe_d569f211eb14 / sim_d6ebcc77e22d),
verification totals, open issues and the follow-up list.
…el' into claude-reflexive-world-state-model
…ignal

Calendar mode ignores max_rounds by design (the forecast horizon is never
truncated), so the review-fix test could not stop after round 1. The run is
now interrupted by the runner's shutdown event right after the first world
update commits, which exercises the resume path that restores the committed
world and the scenario-share trajectory before the remaining rounds commit.
The ticks under the World State panel are now toggle buttons (role=group,
aria-pressed, descriptive aria-label). Selecting a past round shows that
round's headline, narrative, arbiter verdict and problems, decision counts,
changed and added facts, moves, consequences and agent-facing digest,
labelled "past period · round r / N" with a one-click return to the latest
round, which stays selected by default. A live refresh keeps the selection
unless the round vanished or the simulation changed. Tick hue follows the
arbiter verdict (green tint applied, red failed/invalid, amber skipped),
solid fill marks the selection and bold plus a ring marks the latest round.

GET /api/simulation/<id>/world-state history rows now carry bounded
arbiter_problems, the update error when present and the round's digest
joined from world_digest.jsonl under the same containment rules.

Tests: backend downstream 11 passed (new history-row test); frontend 156
passed (new SSR-driven panel test, extended view-model tests); build OK;
verified live on sim_d6ebcc77e22d. README EN/ZH stay line-aligned.
…efine truncation lint

Live run pipe_d569f211eb14 (glm-5.3) failed its publish gate twice over:

- The single 48k-character Part-1 binary-forecast extraction prompt was rejected
  by the provider's content filter (HTTP 400, code 1301). With no fallback
  provider configured the caller swallowed the error, binary_count stayed 0 and
  the structured-forecast contract failed. forecast_binary_resilience wraps the
  extractor: a content-filter rejection triggers a chunked retry (dossier split
  at H2 headings into at most four bounded chunks, filtered chunks skipped, rows
  merged and renumbered); when every chunk is filtered, binaries are derived
  deterministically from the finalised scenario partition (one proposition per
  scenario, P = scenario probability, membership declared, source labelled),
  which satisfies the proposition-consistency audit by construction and demotes
  confidence instead of blocking publication. The recovery record is kept in
  forecast.json (binary_extraction_recovery and binary_quality.recovery).
  Non-filter exceptions still propagate unchanged.

- A complete table cell ending after a parenthesis ("… pillar (soybeans, general
  licences, tariff-cut lists) demonstrably incomplete") was flagged as a width
  cut because the clause tail after ")" had two words. Balanced parentheticals
  are now stripped before the final clause segment is examined, so only
  dangling function words, orphan fragments, unbalanced brackets and
  machine-length tokens count. This commit also carries the dangling-tail
  refinement of detect_table_cell_truncation that was already present but
  uncommitted in the working tree; both changes are covered by
  test_wave9_report_focus.py and the new test module.
…it hygiene in the world loop

Live run sim_d6ebcc77e22d: two arbiter calls were rejected by the Zhipu content filter
(HTTP 400, code 1301) and world_state_history.jsonl only said "llm error: BadRequestError";
one of fifty public statements came out in Chinese in an English run; arbiter-created facts
carried their kind name as unit ("yes boolean").

- app/utils/llm_errors.py (new, pure): describe_llm_error / llm_error_details — exception
  class, HTTP status, provider code, content-filter / quota / auth class, bounded snippet and
  the failover note. llm_client's content-filter classification reuses it (Zhipu 1301 now
  counts toward the breaker) and LLMClient._try_fallback attaches the failover outcome to the
  primary error (not configured / skipped / failed), so records say why nothing took over.
  chat() already retries every non-retryable error once on LLM_FALLBACK_PROVIDER; the live
  deployment has none configured, so no second attempt is added.
- world_update.run_arbiter and agent_decision.elicit_decision record that description in
  arbiter_problems / AgentDecision.problems (new field, written to world_actions.jsonl).
- agent_decision: CJK-vs-Latin script check against the run language; a non-hold move whose
  model-written statement is in the wrong script becomes a typed hold
  (status="language_mismatch") with the rejected text in problems, so the actor falls back to
  its organic post. The coordinator passes the run language; round records and
  summarize_decisions count the new status.
- Units: validate_arbiter_update blanks the unit when the kind is non-numeric or equals the
  kind / a type word; StateVariable keeps units on numeric facts only (also on load and when
  a numeric fact becomes text); format_value, the report evolution block and the frontend
  formatter never print a kind name as a unit.
- Tests: test_world_update (precise record; applied round via the fallback provider through
  the real LLMClient with one primary attempt + one fallback retry; both-fail / unconfigured
  records; unit blanking; briefs never print "yes boolean"), test_agent_decision (script
  heuristic, typed language rejection, provider records, fallback recovery),
  test_world_model_loop (content-filter arbiter rejection over 3 rounds; Chinese statement in
  an English run → language_mismatch row, organic post), test_world_model, test_llm_errors
  (new), conftest helpers (content_filter_error, scripted_failover), frontend worldState test.
…ation.log

Close the three gaps found in the live validation of the reflexive world
model (pipe_d569f211eb14 / sim_d6ebcc77e22d):

- world_model_seed: research engine v3 actors.json rows carry goals /
  constraints / stated_vs_revealed / memory but none of the legacy plan or
  red-line keys, so every actor's option menu lacked documented plans.  When
  the legacy keys yield nothing, derive_v3_plans promotes the actor's own
  stated commitments, action-phrased goals and forward commitments in
  memory, and derive_v3_red_lines promotes constraints and statements
  phrased as non-negotiables.  Hedged wording, refusals, the revealed half,
  incentives, vulnerabilities and worldview are never promoted; explicit
  keys still win; MAX_PLANS / MAX_RED_LINES unchanged.  The live dossier
  yields plans for 8 of 11 actors and, faithfully, no red lines.
- report_visualizer: world_variable_series (pure) places the seed at the
  as-of date (index 100) and one point per committed period - the last
  value recorded in that period, carried forward when unchanged - so series
  start at the seed and never draw vertical duplicates; arbiter-created
  variables start at their first period; categorical x axis in run order.
- simulation.log: the backend hands the runner subprocess the file as
  stdout/stderr (mode w) while the runner's SimulationLogManager opens it
  with FileHandler mode w; two non-appending writers overwrote each other
  at private offsets (torn UTF-8, grep treating the file as binary, the
  print copies of the last rounds flushed only at exit).
  SimulationRunner._open_main_log truncates then appends; the FileHandler
  appends, truncates only when no standard stream already is the file and
  skips the console echo when stderr is; the runner's stdout is
  line-buffered on every platform (the existing reconfigure block was
  Windows-only).

Tests: three seed tests with a v3-shaped record (seed suite 11 passed), two
chart tests (downstream 12 passed) and
test_world_round_lines_reach_simulation_log, which reproduces the backend
capture on the same append handle and fails against the old writers (loop
suite 7 passed).  Full backend suite: 6195 passed, 41 skipped, 11 xfailed;
the 19 errors are test_legacy_official_data loading the deer-flow vendor
checkout, which this worktree does not have.  ruff F,E9 unchanged (16
pre-existing findings on untouched lines), compileall clean.  Hours mode
untouched: the seed block is only sealed in calendar mode and the logging
change alters no simulation artifact.
…accept floor-rejected drafts

Resume #1 of pipe_d569f211eb14 regenerated the report but the publish gate refused it
for one generation-failed chapter: the provider content filter (HTTP 400 code 1301)
rejected "The Actor Architecture" on all six attempts. The agent log shows why: the
model wrote a complete 13-15k-character Final Answer that passed the filter, the ReAct
loop rejected it for 1 < min tool calls, the extra retrieval dumps (48k + 22k chars)
then tripped the input-side filter, and every retry replayed the identical request.

- report_filter_safe.py (new): ladder levels, register directive, observation cap.
- report_agent.py: when a content filter interrupts an attempt that already holds a
  clean draft rejected only for the tool-call floor, return that draft (both generator
  paths, loop and forced-finish calls; recorded as filter_safe_sections=0). Otherwise
  the retry wrapper climbs a no-backoff ladder: L1 institutional-register directive +
  tool floor 1 + retrieved passages capped at 6k; L2 no retrieval tools. The native
  path no longer falls back to ReAct on a filter. _parse_tool_calls accepts flat and
  OpenAI-style arguments (the live resurrection sent an empty query). Report.to_dict
  discloses filter_safe_sections. REPORT_FILTER_SAFE_LADDER=false restores the old
  identical-retry behaviour.
- Rejected after live probes: rewriting leaders' names as office descriptors is
  itself filtered by this provider while the original brief passes.
- tests/test_report_filter_safe.py: 18 offline tests; full suite 6262 passed with the
  two pre-existing demo-asset failures.

Live: resume #2 completed with a published report (report_0f2e07d62a34, gate passed);
filter_safe_sections = {Forecast Table: 0, The Actors: 1, Legal Cliff Calendar: 2}.
…orld-state-model

# Conflicts:
#	agent-progress.txt
#	backend/app/services/world_update.py
#	docs/handoff/reflexive-world-state/handoff.md
…ld-state-model

# Conflicts:
#	agent-progress.txt
#	backend/app/services/report_visualizer.py
#	backend/app/services/world_model_seed.py
#	backend/tests/test_world_model_loop.py
…rld-state-model

# Conflicts:
#	agent-progress.txt
#	docs/handoff/reflexive-world-state/handoff.md
The World State panel's period strip is a real round selector (merged from the
side branch) and the feed now follows it: selecting a committed period shows that
period's posts and the threads replied to in it, instead of the newest 60 posts.

- backend/app/services/feed_rounds.py: per-round index of a platform's feed, joining
  <platform>/actions.jsonl (CREATE_POST / CREATE_COMMENT rows with their round,
  world_update rows with period labels) to the platform SQLite (posts by agent +
  normalised content consumed in log order, comments by id), with honest fallbacks
  for unmatched rows; read-only, bounded, cached on the artefacts' mtime/size.
- GET /api/simulation/<id>/feed-rounds; `round=` on /posts and /comments returns
  only that round's rows (parents of the period's replies included), each row
  carrying its round; the legacy paths are unchanged without the parameter.
- WorldStatePanel: `round` prop + `update:selectedRound` emit so the parent can
  follow and reset the selection; SimulationView loads the feed for the selected
  round, shows the period label and an "all periods" button, and marks older posts
  surfaced by this period's replies.
- Tests: test_feed_rounds.py (6), feedRounds.test.mjs (4), panel emit test; live
  check on the Brent crude run (17 half-month periods) recorded in
  docs/handoff/world-state-merge/handoff.md (force-added: the repo ignores handoff.md).
…m the Codex world engine

Studied codex/reflexive-world-simulation (world-v1: a separate opt-in engine that
replaces the social simulation and failed its own live gate) and kept the integrated
world model as the base; design.md §2.10 records what was ported and what was not.

- Consent rule: world_model.Interaction — a proposing move aimed at another actor
  (open_talks, form_alliance, signal_concession, sign_agreement, partnership_deal,
  bargain, convene, program_decision) opens an offer with exact `terms` (new decision
  field); the recipient's menu starts with accept:/counter:/reject:/defer:<id>;
  acceptance binds both parties with obligations and pulls the relationship closer,
  rejection pushes it apart, a counter opens a counter-offer, a deferral moves the
  deadline one period (twice at most), unanswered offers lapse after two periods; a
  lapsed obligation or break_alliance is a breach (reputation, relationship,
  obligations breached/released). Offers show in both briefs, the digest, the arbiter
  prompt (never assume consent), the round record, the run summary, the /world-state
  API, the panel card and the report block.
- Modelling gaps: world_model_seed.seed_gaps lists what the research could not ground
  (unit-less or undated facts, actors without goals/plans/class, no relationships);
  kept by the kernel, counted in the run summary, listed by the API, badged in the
  panel, named in the report block.
- Behaviour diagnostics: world_update.behaviour_metrics scores, per round, the moves
  that answered an offer or named a fact/actor that changed last period (textual, not
  a realism verdict); aggregated in the run summary, badged in the panel, reported.
- Chart: world_variable_series skips zero-seed variables again (regression from the
  quirky-lamport merge) and rounds indexed values; its test now pins the
  carried-forward one-point-per-period series.
- README EN/ZH (line-aligned) and design.md updated; tests:
  test_world_interactions.py (7), seed/update/downstream/visualizer additions,
  worldState.test.mjs (offers, gaps, behaviour).

Verified in an isolated worktree carrying exactly these hunks: full backend suite
6287 passed (19 errors are test_legacy_official_data fixtures needing the untracked
deer-flow tree); frontend 163 passed; build OK. A concurrent process is editing this
checkout (market price-history chart removal and related files); its changes are not
part of this commit.
@linroger
linroger merged commit f78397e into main Oct 17, 2026

This branch was successfully deployed

1 active deployment
github-pages — f78397e1 Deployed Oct 17, 2026 by linroger via deploy #51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants