File size: 7,753 Bytes
cfb152f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
//! The free-agent loop cycle, composed from the proven parts (bigpickle asolaria-loop.mjs:26-103).
//!
//! One cycle: revolver -> room select + rename-before-load (`row=project_swap`) -> mock free agent ->
//! hookwall pass (the SCORE/Fischer lanes called through [`GateLanes`]) -> second prism PID +
//! `row=prism_in` -> GC emit. The live opencode spawn and the filesystem writes (the `ROOM.hbp` marker,
//! :43-46, and the prism append, :69-78) are the integrator's lanes; the rows they write are returned,
//! built with a supplied timestamp. `runLoop`'s per-verdict tally (:104-118) is kept by the caller from
//! each [`CycleOutcome`].

use std::path::Path;

use crate::dispatcher::run_free_agent_mock;
use crate::district_fabric::DISTRICTS;
use crate::gc_runtime::{CapStatus, GcRuntime, DEFAULT_GC_EVERY_MESSAGES};
use crate::hookwall::{pass, Envelope, GateLanes, PassOpts, Verdict};
use crate::revolver::PidChainRevolver;
use crate::room_router::{ProjectRoomRouter, RoomSwapPlan};
use crate::sha::sha8;

/// Rooms of the named district at `scale`, 2000 for a name outside the catalog (asolaria-loop.mjs:26-29).
pub fn district_room_count(name: &str, scale: u64) -> u64 {
    DISTRICTS
        .iter()
        .find(|d| d.name == name)
        .map_or(2000, |d| d.rooms)
        * scale
}

/// Loop context: the proven modules plus the rolling observation hash and the optional prism rotor.
pub struct LoopCtx {
    pub revolver: PidChainRevolver,
    pub router: ProjectRoomRouter,
    pub gc: GcRuntime,
    pub district: String,
    pub room_count: u64,
    pub prev_hash: String,
    pub prism_revolver: Option<PidChainRevolver>,
    pub prism_room_count: u64,
}

impl LoopCtx {
    /// The context `runLoop` builds (asolaria-loop.mjs:90-103): anchor `anchor` or
    /// `ASOLARIA-LOOP-<UPPER>`, prism rotor `ASOLARIA-PRISM-ROTATOR-<UPPER>`, room counts from
    /// [`district_room_count`] (prism rooms from `prism`), GC window `gc_every` or 2000, active room 0.
    /// `scale` is upstream's `opts.scale` (default `ASOLARIA_ROOM_SCALE` or 1, read by the caller).
    pub fn for_district(
        district: &str,
        anchor: Option<&str>,
        scale: u64,
        gc_every: Option<u64>,
    ) -> Self {
        let upper = district.to_uppercase();
        let anchor = match anchor {
            Some(a) if !a.is_empty() => a.to_string(),
            _ => format!("ASOLARIA-LOOP-{upper}"),
        };
        let prism_anchor = format!("ASOLARIA-PRISM-ROTATOR-{upper}");
        LoopCtx {
            revolver: PidChainRevolver::new(&anchor).expect("the anchor is non-empty"),
            router: ProjectRoomRouter::new(),
            gc: GcRuntime::with_gc_every(gc_every.unwrap_or(DEFAULT_GC_EVERY_MESSAGES)),
            district: district.to_string(),
            room_count: district_room_count(district, scale),
            prev_hash: "0000000000000000".to_string(),
            prism_revolver: Some(
                PidChainRevolver::new(&prism_anchor).expect("the anchor is non-empty"),
            ),
            prism_room_count: district_room_count("prism", scale),
        }
    }
}

/// Per-cycle options (asolaria-loop.mjs:32 `opts`). Under upstream `dryRun` both ledger paths are `None`.
pub struct CycleOpts<'a> {
    /// `opts.question`; `None` or `""` takes the default probe (:48 `||`).
    pub question: Option<&'a str>,
    /// `opts.observeOnly`, spread into the hookwall pass (:51-54, asolaria-hookwall.mjs:84).
    pub observe_only: bool,
    /// Row timestamp (upstream `new Date().toISOString()`).
    pub ts: &'a str,
    /// `ctx.ledgerPath`, the observation append (:53).
    pub ledger_path: Option<&'a Path>,
    /// `opts.fischerLedgerPath`, the Fischer row append (asolaria-hookwall.mjs:75-80).
    pub fischer_ledger_path: Option<&'a Path>,
}

/// The outcome of one cycle, including the two emitted rows and the observation row.
pub struct CycleOutcome {
    pub agent_pid: String,
    pub room_id: u32,
    pub rotation: u64,
    /// `!swap.noop`. Upstream's `renamed` (:42-46) is this when the `project_swap_row` append to an
    /// existing room's `ROOM.hbp` succeeds, and `false` under `dryRun`; that append is the caller's.
    pub would_rename: bool,
    pub mock: bool,
    pub verdict: Verdict,
    pub mark: String,
    pub score: String,
    pub l0_real: bool,
    pub gc_runs: u64,
    pub cap_status: CapStatus,
    pub project_swap_row: String,
    pub prism_in_row: String,
    pub observation_row: String,
}

/// Run one full loop cycle (asolaria-loop.mjs:32-86). The gate calls `lanes` for SCORE (pid, answer)
/// and Fischer (pid, envelope, score, `ctx.prev_hash`) as upstream's hookwall pass does.
pub fn loop_cycle(ctx: &mut LoopCtx, lanes: &mut dyn GateLanes, opts: &CycleOpts) -> CycleOutcome {
    // 1. PID emitter
    let agent_pid = ctx.revolver.next_pid();
    let rotation = ctx.revolver.counter - 1;
    // 2. room selection + rename-before-load marker
    let room_id = ctx
        .router
        .room_for_pid(&agent_pid)
        .expect("revolver PIDs are 16 hex digits")
        % (ctx.room_count as u32);
    let swap = ctx.router.plan_swap_to(room_id);
    let would_rename = !matches!(swap, RoomSwapPlan::Noop { .. });
    let ts = opts.ts;
    let project_swap_row = format!(
        "HBPv1|row=project_swap|agent_pid={agent_pid}|room={room_id}|rotation={rotation}|invariant=rename-before-load|ts={ts}|json=0"
    );
    // 3. free agent in the unique project (mock)
    let default_question = format!(
        "[{}] cycle {rotation}: produce one genius/mistake mark for this lane",
        ctx.district
    );
    let question = match opts.question {
        Some(q) if !q.is_empty() => q,
        _ => &default_question,
    };
    let agent = run_free_agent_mock("opencode-coder", question, &agent_pid);
    // 4. hookwall gate (SCORE/Fischer lanes)
    let env = Envelope {
        pid: Some(&agent_pid),
        actor: &ctx.district,
        verb: "answer",
        target: "prism",
        payload: Some(&agent.answer),
        payload_sha16: "",
    };
    let pass_opts = PassOpts {
        observe_only: opts.observe_only,
        prev_hash: &ctx.prev_hash,
        ts,
        ledger_path: opts.ledger_path,
        fischer_ledger_path: opts.fischer_ledger_path,
    };
    let gate = pass(&env, lanes, &pass_opts);
    ctx.prev_hash = gate.row_hash.clone();
    // 5. second prism PID + prism_in row
    let mut prism_segment = String::new();
    if let Some(prism_rev) = ctx.prism_revolver.as_mut() {
        if ctx.prism_room_count > 0 {
            let prism_rotation = prism_rev.counter;
            let prism_pid = prism_rev.next_pid();
            let prism_room_id = ctx
                .router
                .room_for_pid(&prism_pid)
                .expect("revolver PIDs are 16 hex digits")
                % (ctx.prism_room_count as u32);
            prism_segment = format!(
                "|prism_room={prism_room_id}|prism_pid={prism_pid}|prism_rotation={prism_rotation}"
            );
        }
    }
    let prism_in_row = format!(
        "HBPv1|row=prism_in|from_pid={agent_pid}|from_room={room_id}{prism_segment}|mark={}|score={}|gnn_lane=reverse_gain_gnn|answer_sha16={}|ts={ts}|json=0",
        gate.mark,
        gate.score,
        sha8(&agent.answer)
    );
    // 6. GC emit
    let gc_status = ctx.gc.emit();

    CycleOutcome {
        agent_pid,
        room_id,
        rotation,
        would_rename,
        mock: agent.mock,
        verdict: gate.verdict,
        mark: gate.mark,
        score: gate.score,
        l0_real: gate.l0_real,
        gc_runs: gc_status.runs,
        cap_status: gc_status.cap_status,
        project_swap_row,
        prism_in_row,
        observation_row: gate.observation,
    }
}