Written entirely by Claude, as a working record for agents more than for human readers: every number with its source file, and every claim since withdrawn. Some facts here may be out of date; where it disagrees with the post, trust the post. For the readable version, see the companion.

Stage two - introspection SFT and the 0.25 persona merge

Open Character Training's second stage, run bug-faithfully on all 134 traits - 12,000 self-generated transcripts per trait, a rank-64 SFT LoRA on the DPO-merged base, then a linear merge of DPO at 1.0 with SFT at 0.25.

currentverified 2026-09-16

Open Character Training's stage two takes the stage-one model, has it generate transcripts about itself, fine-tunes on them, and releases a linear merge of the two adapters. qwen35/oct_stage2.py runs it on the zoo's single-trait adapters instead of OCT's eleven hand-written personas. The source-paper description is in qwen35/paper_notes.md sections 1.3 and 2.4; see Open Character Training (Maiya et al., 2025) and Persona Cartography (Baines et al., 2026).

What is generated, per trait

Mapped from OCT's repository rather than its paper (qwen35/oct_stage2.py docstring):

  1. Self-reflection — 10 fixed introspective instructions x 1,000 samples = 10,000 rows, generated by the post-DPO model under OCT's appendix-A system prompt with the "reflective mood" line. The system prompt is dropped from the training rows.
  2. Self-interaction — 1,000 conversations x 10 turns, run twice (free and --leading), the model talking to a copy of itself by role-swapped re-rendering. Interaction rows keep a simplified system prompt at training time.
  3. Assembly — the three files concatenated and shuffled into one sft_data/<trait>.jsonl of 12,000 rows.

Sampling: temperature 0.7, top_p 0.95, top_k -1, min_p 0.0 (qwen35/oct_stage2.py:GEN). Generation engine is vLLM if it can load the merged snapshot, else transformers.generate, and which one ran is recorded — the 34-trait Lexicon run records "engine": "vllm:0.27.1".

The SFT step

Recorded per trait, e.g. qwen35/phase10_runs/results_oct2_34traits_v1-n1000-ni1000-k10-bugsfaithful.json#stages.sft[0] for effeminate:

field value
base merged(Qwen/Qwen3.5-4B + stage1 effeminate)
LoRA rank 64, alpha 128, dropout 0.0
targeted modules 248
learning rate 5e-05
train batch / micro batch 32 / 2
epochs 1
max_len 3072
warmup ratio 0.1, max_norm 1.0, min_lr_ratio 0.1
adam betas (0.9, 0.98)
seed 123456
optimizer steps 375
n_rows_in / n_rows_trained / n_dropped_at_max_len 12000 / 12000 / 0
loss_first / loss_last 1.45979642868042 / 1.0129634388287863
train_seconds 24029.058915376663

Note max_len 3072 here against stage one's 1024: paper_notes.md section 3.11 had flagged that "max_len 1024 is the DPO value... Phase 10 must not inherit 1024", and it did not.

loss_last from these records is unreliable for any run that resumed; see Run provenance - runmeta.json and its defects.

Bug-faithful mode

The default is to reproduce OCT's released code including five discrepancies against its own paper, following the project's sq_approx_kl precedent of reproducing the code rather than the prose. --fix-oct-bugs flips all five at once and is off in every batch unit. From qwen35/oct_stage2.py:

Deliberate deviations, each recorded in runmeta: the persona is one trait and {TRAITS} in OCT's template is filled with that trait's constitution verbatim; enable_thinking=False on every apply_chat_template, because Qwen3.5's template opens a reasoning block that leaks <think> into generations and breaks the prompt-prefix slice; the DPO merge is applied by patching the base snapshot's safetensors in place (W += s·B@A on the 248 targeted tensors) rather than merge_and_unload and re-save, because the base is a composite vision-language checkpoint; the SFT step is a transformers.Trainer reimplementation of OpenRLHF's exact row semantics.

Checkpointing

Everything is resumable and keyed by a done-marker with a hash and counts (<path>.done.json), under the phase-10 volume: /oct/merged/<trait>/, /oct/self_reflection/<trait>.p<i>.jsonl, /oct/self_interaction/<trait>[-leading].{state.json,jsonl}, /oct/sft_data/<trait>.jsonl plus .diagnostics.json, /oct/loras_introspection/<trait>/, /oct/personas/<trait>/. See modal-volumes.

The merge

MERGE_WEIGHTS = {"dpo": 1.0, "sft": 0.25}, applied with PEFT's add_weighted_adapter(..., combination_type="linear"), reproducing tools/merge_loras.py lines 37-46 of OCT's repository. paper_notes.md section 2.5 notes Persona Cartography phrases the 0.25 as a shrink: "The SFT LoRA is shrunk by multiplying all weights by 0.25 following Maiya et al. 2025, and then combined with the DPO LoRA."

What PEFT's "linear" combination actually computes is not the weighted sum of the two deltas. That was measured and corrected; see The persona merge and its correction.

The batches

The systemd units record which traits ran in each batch, with the phase budget each was given. All use --n-reflection 1000 --n-interaction 1000 --k-turns 10 --minutes-per-trait 431.

unit traits PC_PHASE_BUDGET description in the unit
pilot (no unit) 3: extraverted, warm, organized — results_oct2_3traits_...json
zoo40-oct.service 40 900 "OCT stage-2 zoo (40 traits), phase 10"
zoo-batch2.service 10 300 "10 traits, balanced 1+/1- per Big Five factor"
zoo-batch3.service 10 235 "10 traits, balanced 1+/1- per Big Five factor"
zoo-batch4.service 39 940 "39 traits: completes the Big Five 100"
zoo-lex.service 34 750 "Lexicon validation set (held-out traits, never define the factor axes)"

The five units name 133 distinct traits. The 134th is warm, which went through in the three-trait pilot alongside extraverted and organized (both of which recur in the 40). All 134 appear in qwen35/analysis/merge_audit.json.

Two operational notes are recorded in the units themselves. zoo-batch3.service carries a comment explaining that StartLimit* is only honoured in [Unit], and that with it in [Service] the burst cap could never fire — "that is how the uploader restarted 232 times" — which matters more here because ExecStart is modal run --detach, so a restart while the previous detached app is still alive launches a second app on the same traits and doubles the burn.

The second-seed stage-two run

/etc/systemd/system/zoo-oct2seed1.service ran the full two-stage pipeline again from a second initialisation on 15 traits — helpful, cold, harsh, organized, disorganized, careful, relaxed, anxious, fretful, extraverted, quiet, assertive, intellectual, simple, unimaginative — described in the unit as "three per Big Five factor", against the matched seed-1 stage-one adapters (--adp-subdir data_null_seedpaired_s40_matched --oct-ns seed1 --sft-seed 1), PC_APP_NAME=pc-qwen35-phase11-oct2seed1, PC_PHASE_BUDGET=240. Its results file is results_oct2_15traits_v1-n1000-ni1000-k10-bugsfaithful.json. What it shows is Stage two at a second seed.

Known confound: the 3,072-token drop

qwen35/zoo_page/MODEL_CARD.md:

Trait verbosity is a confound. Transcripts over 3,072 tokens were dropped before SFT. The median trait lost 28 rows of 12,000; the worst lost 2,000 (16.7%), and 14 of 45 lost more than 5%. The heavy losers are the verbose traits. Adapters therefore differ slightly in how much data they saw, which matters if you compare them in weight space. Per-trait counts are in each runmeta.json. It does not affect convergence (correlation with final loss +0.01).

Those figures are stated over 45 adapters and were not restated when the set grew to 134. Note also the card's phrasing: the drop happens at the SFT tokenizer, not at assembly — the 34-trait Lexicon run records n_rows_in 12000 and n_dropped_at_max_len 0 for effeminate, and qwen35/analysis/hf_dataset_audit.md measured four published sft_data files at exactly 12,000 rows, contradicting the dataset card's claim that "sft_data is often smaller than 12,000".

Repetition in the generated corpora, and how it differs by keying, is Repetition in the stage-two corpora. Content review of the transcripts is in Hugging Face artefacts.

Sources

  • qwen35/oct_stage2.py
  • qwen35/paper_notes.md
  • qwen35/phase10_runs/results_oct2_34traits_v1-n1000-ni1000-k10-bugsfaithful.json
  • qwen35/analysis/merge_audit.json
  • qwen35/zoo_page/MODEL_CARD.md
  • qwen35/zoo_page/DATASET_CARD.md
  • /etc/systemd/system/zoo40-oct.service
  • /etc/systemd/system/zoo-batch2.service
  • /etc/systemd/system/zoo-batch3.service
  • /etc/systemd/system/zoo-batch4.service
  • /etc/systemd/system/zoo-lex.service
  • /etc/systemd/system/zoo-oct2seed1.service

Linked from