@mcptoolshop/style-dataset-lab 3.4.0 → 3.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/README.es.md +111 -59
  3. package/README.fr.md +117 -67
  4. package/README.hi.md +109 -70
  5. package/README.it.md +111 -60
  6. package/README.ja.md +116 -76
  7. package/README.md +58 -2
  8. package/README.pt-BR.md +119 -64
  9. package/README.zh.md +108 -67
  10. package/bin/sdlab.js +50 -1
  11. package/lib/adapters/ai-toolkit.js +187 -30
  12. package/lib/adapters/comfyui-workflows.js +84 -9
  13. package/lib/asset-ingest.js +575 -0
  14. package/lib/asset-masks.js +323 -0
  15. package/lib/asset-source.js +1176 -0
  16. package/lib/canon-author.js +350 -0
  17. package/lib/canon-review-gate.js +205 -0
  18. package/lib/colab-launcher-guards.js +285 -0
  19. package/lib/export.js +1 -1
  20. package/lib/freeze-gate.js +3 -0
  21. package/lib/generated-provenance.js +178 -5
  22. package/lib/lane1-render.js +668 -0
  23. package/lib/memory-store.js +663 -0
  24. package/lib/model-licences.js +305 -0
  25. package/lib/npy-meta.js +130 -0
  26. package/lib/png-meta.js +512 -0
  27. package/lib/records.js +54 -8
  28. package/lib/reingest-selected.js +15 -1
  29. package/lib/reingest.js +27 -3
  30. package/lib/run-manifest.js +59 -11
  31. package/lib/train-runs.js +214 -0
  32. package/lib/verifier.js +3 -0
  33. package/lib/verify-chain.js +367 -0
  34. package/package.json +2 -2
  35. package/scripts/asset-ingest.js +160 -0
  36. package/scripts/canon-draft.js +82 -0
  37. package/scripts/canon-freeze.js +4 -0
  38. package/scripts/colab/REAL_SERVICE_ASSUMPTIONS.md +14 -0
  39. package/scripts/colab/ai-toolkit-override-py315.txt +45 -0
  40. package/scripts/colab/colab_window.sh +587 -0
  41. package/scripts/colab/lane1_render.py +970 -0
  42. package/scripts/colab/launch_guards.py +564 -0
  43. package/scripts/colab/launch_lane1_stub.sh +309 -0
  44. package/scripts/colab/launch_lane3_stub.sh +366 -0
  45. package/scripts/colab/remote_runner.py +59 -0
  46. package/scripts/doctor.js +1 -0
  47. package/scripts/generate.js +38 -12
  48. package/scripts/verify.js +67 -0
@@ -0,0 +1,45 @@
1
+ # CPython 3.15.1 dependency override file for ostris/ai-toolkit (Lane 3 LoRA training)
2
+ # Upstream tracking issue: https://github.com/ostris/ai-toolkit/issues/1087
3
+ # Reference: DESIGN-v3.md section 3.5 (CPython 3.15.1 wheel audit)
4
+ # Note: av==16.0.1 and torchcodec==0.9.1 are dropped for image LoRA lanes.
5
+ scipy==1.18.1
6
+ matplotlib==3.11.2
7
+ torchao==0.18.0
8
+ safetensors==0.7.0
9
+ transformers==5.5.3
10
+ lycoris-lora==1.8.3
11
+ flatten_json==0.1.14
12
+ pyyaml==6.0.3
13
+ oyaml==1.0
14
+ tensorboard==2.20.0
15
+ kornia==0.9.0
16
+ invisible-watermark==0.2.0
17
+ einops==0.8.2
18
+ accelerate==1.13.0
19
+ toml==0.10.2
20
+ albumentations==1.4.15
21
+ albucore==0.0.16
22
+ pydantic==2.13.2
23
+ omegaconf==2.3.0
24
+ k-diffusion==0.1.1.post1
25
+ open_clip_torch==3.3.0
26
+ timm==1.0.22
27
+ prodigyopt==1.1.2
28
+ controlnet_aux==0.0.10
29
+ python-dotenv==1.2.2
30
+ bitsandbytes==0.49.2
31
+ hf_transfer==0.1.9
32
+ lpips==0.1.4
33
+ pytorch_fid==0.3.0
34
+ optimum-quanto==0.2.4
35
+ sentencepiece==0.2.1
36
+ huggingface_hub==1.10.1
37
+ peft==0.18.1
38
+ python-slugify==8.0.4
39
+ opencv-python==4.13.0.92
40
+ pytorch-wavelets==1.3.0
41
+ setuptools==82.0.1
42
+ librosa==0.11.0
43
+ mutagen==1.47.0
44
+ pillow==12.2.0
45
+ numpy==2.4.4
@@ -0,0 +1,587 @@
1
+ #!/usr/bin/env bash
2
+ # Shared Colab window lifecycle, lock, live wall/cap/vitals/RAM guards, token scrubber, and EXIT trap.
3
+ set -euo pipefail
4
+
5
+ colab_resolve_python() {
6
+ if [ -n "${SDLAB_PYTHON:-}" ] && [ -x "${SDLAB_PYTHON}" ]; then
7
+ echo "${SDLAB_PYTHON}"
8
+ return 0
9
+ fi
10
+ if command -v python3 >/dev/null 2>&1; then
11
+ command -v python3
12
+ return 0
13
+ fi
14
+ command -v python
15
+ }
16
+
17
+ colab_refuse_nonempty_out_dir() {
18
+ local out_dir="$1"
19
+ if [ -d "${out_dir}" ] && [ -n "$(ls -A "${out_dir}" 2>/dev/null)" ]; then
20
+ echo "REFUSED: OUT_DIR_NOT_EMPTY: ${out_dir} already exists and is not empty" >&2
21
+ return 97
22
+ fi
23
+ }
24
+
25
+ colab_guard_fake_cli_if_test() {
26
+ local colab_bin
27
+ colab_bin="$(command -v colab || true)"
28
+ if [ -z "${colab_bin}" ]; then
29
+ echo "REFUSED: colab CLI not found in PATH" >&2
30
+ return 98
31
+ fi
32
+ if [ "${SDLAB_TEST_FAKE_COLAB:-0}" = "1" ]; then
33
+ if ! grep -q "^#SDLAB_FAKE_COLAB_CLI=1$" "${colab_bin}" 2>/dev/null; then
34
+ echo "REFUSED: REAL_COLAB_FORBIDDEN: colab resolved to ${colab_bin} without #SDLAB_FAKE_COLAB_CLI=1 marker" >&2
35
+ return 99
36
+ fi
37
+ fi
38
+ }
39
+
40
+ colab_verify_input_shas() {
41
+ local py_bin="$1"
42
+ local guards_py="$2"
43
+ shift 2
44
+ local args=()
45
+ for pair in "$@"; do
46
+ args+=("--verify-sha" "${pair}")
47
+ done
48
+ "${py_bin}" "${guards_py}" "${args[@]}" </dev/null
49
+ }
50
+
51
+ colab_poll_usage_and_ram() {
52
+ local py_bin="$1"
53
+ local out_dir="$2"
54
+ local fault_mode="${SDLAB_FAULT_POLL_HELPER:-}"
55
+ if [ -f "${out_dir}/start_balance.txt" ] && [ -n "${SDLAB_FAULT_POLL_HELPER_MIDRUN:-}" ]; then
56
+ fault_mode="${SDLAB_FAULT_POLL_HELPER_MIDRUN}"
57
+ fi
58
+ if [ "${fault_mode}" = "crash" ]; then
59
+ return 137
60
+ elif [ "${fault_mode}" = "empty" ]; then
61
+ return 0
62
+ elif [ "${fault_mode}" = "short" ]; then
63
+ echo "9.85 9.85"
64
+ return 0
65
+ fi
66
+ local default_rate="${RATE_CCU_PER_HOUR:-0.26}"
67
+ local guards_py
68
+ guards_py="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/launch_guards.py"
69
+
70
+ local usage_text=""
71
+ local usage_rc=0
72
+ usage_text="$(colab usage </dev/null 2>>"${out_dir}/colab.log")" || usage_rc=$?
73
+ printf '%s\n' "${usage_text}" >> "${out_dir}/usage.log"
74
+
75
+ "${py_bin}" - "${out_dir}" "${default_rate}" "${usage_rc}" "${usage_text}" "${guards_py}" << 'PYEOF'
76
+ import os
77
+ import re
78
+ import sys
79
+ from pathlib import Path
80
+
81
+ out_dir = Path(sys.argv[1])
82
+ default_rate = float(sys.argv[2])
83
+ usage_rc = int(sys.argv[3])
84
+ raw_usage = sys.argv[4]
85
+ guards_py = Path(sys.argv[5])
86
+
87
+ sys.path.insert(0, str(guards_py.parent))
88
+ import launch_guards as lg
89
+
90
+
91
+ def _read_float(p: Path) -> float | None:
92
+ if not p.exists():
93
+ return None
94
+ try:
95
+ return float(p.read_text(encoding="utf-8").strip())
96
+ except Exception:
97
+ return None
98
+
99
+
100
+ start_f = out_dir / "start_balance.txt"
101
+ end_f = out_dir / "end_balance.txt"
102
+ rate_f = out_dir / "rate_measured.txt"
103
+ rate_pre_f = out_dir / "rate_measured_pre_open.txt"
104
+ rate_post_f = out_dir / "rate_measured_post_open.txt"
105
+ rate_proj_f = out_dir / "rate_for_projection.txt"
106
+ rate_src_f = out_dir / "rate_source.txt"
107
+ err_f = out_dir / "usage_parse_error.txt"
108
+ opened_marker = out_dir / "session_opened.txt"
109
+
110
+ session_opened = opened_marker.exists()
111
+ existing_pre = _read_float(rate_pre_f)
112
+ existing_post = _read_float(rate_post_f)
113
+
114
+ parsed = lg.parse_usage_text(
115
+ raw_usage,
116
+ usage_rc=usage_rc,
117
+ configured_rate=default_rate,
118
+ session_opened=session_opened,
119
+ post_open_rate=existing_post,
120
+ pre_open_rate=existing_pre,
121
+ )
122
+
123
+ if parsed["ok"]:
124
+ bal = parsed["balance"]
125
+ if not start_f.exists():
126
+ start_f.write_text(f"{bal}\n", encoding="utf-8", newline="\n")
127
+ start_bal_str = start_f.read_text(encoding="utf-8").strip()
128
+ end_f.write_text(f"{bal}\n", encoding="utf-8", newline="\n")
129
+ cur_bal_str = f"{bal}"
130
+ if parsed["rate_measured_pre_open"] is not None:
131
+ rate_pre_f.write_text(f"{parsed['rate_measured_pre_open']}\n", encoding="utf-8", newline="\n")
132
+ if parsed["rate_measured_post_open"] is not None:
133
+ rate_post_f.write_text(f"{parsed['rate_measured_post_open']}\n", encoding="utf-8", newline="\n")
134
+ if parsed["rate_measured_post_open"] is not None:
135
+ rate_f.write_text(f"{parsed['rate_measured_post_open']}\n", encoding="utf-8", newline="\n")
136
+ elif parsed["rate_measured_pre_open"] is not None:
137
+ rate_f.write_text(f"{parsed['rate_measured_pre_open']}\n", encoding="utf-8", newline="\n")
138
+ rate_proj_f.write_text(f"{parsed['rate_for_projection']}\n", encoding="utf-8", newline="\n")
139
+ rate_src_f.write_text(f"{parsed['rate_source']}\n", encoding="utf-8", newline="\n")
140
+ err_tok = "NONE"
141
+ else:
142
+ err_msg = str(parsed["error"] or "COLAB_USAGE_PARSE_FAILED")
143
+ err_f.write_text(f"{err_msg}\n", encoding="utf-8", newline="\n")
144
+ if end_f.exists():
145
+ end_f.unlink()
146
+ start_bal_str = start_f.read_text(encoding="utf-8").strip() if start_f.exists() else "NONE"
147
+ cur_bal_str = "NONE"
148
+ rate_proj_f.write_text(f"{parsed['rate_for_projection']}\n", encoding="utf-8", newline="\n")
149
+ rate_src_f.write_text(f"{parsed['rate_source']}\n", encoding="utf-8", newline="\n")
150
+ err_tok = "COLAB_USAGE_PARSE_FAILED"
151
+
152
+ # Parse VM /proc/meminfo and cgroup memory from vitals.log when present
153
+ anon_mb = float(os.environ.get("CURRENT_ANON_MB", "512")) if "os" in sys.modules else 512.0
154
+ mem_total_mb = 12288.0
155
+ cgroup_max_mb = ""
156
+ vitals_f = out_dir / "vitals.log"
157
+ if vitals_f.exists():
158
+ vtxt = vitals_f.read_text(encoding="utf-8", errors="replace")
159
+ m_tot = re.findall(r"MemTotal:\s*(\d+)\s*kB", vtxt)
160
+ if m_tot:
161
+ mem_total_mb = round(int(m_tot[-1]) / 1024.0, 1)
162
+ m_anon = re.findall(r"AnonPages:\s*(\d+)\s*kB", vtxt)
163
+ if m_anon:
164
+ anon_mb = round(int(m_anon[-1]) / 1024.0, 1)
165
+ m_cg_cur = re.findall(r"CGROUP_MEM_CURRENT:\s*(\d+)", vtxt)
166
+ if m_cg_cur:
167
+ anon_mb = round(int(m_cg_cur[-1]) / (1024.0 * 1024.0), 1)
168
+ m_cg_max = re.findall(r"CGROUP_MEM_MAX:\s*(\d+)", vtxt)
169
+ if m_cg_max:
170
+ cgroup_max_mb = str(round(int(m_cg_max[-1]) / (1024.0 * 1024.0), 1))
171
+
172
+ print(f"{start_bal_str} {cur_bal_str} {parsed['rate_for_projection']} {err_tok} {anon_mb} {mem_total_mb} {cgroup_max_mb}")
173
+ PYEOF
174
+ }
175
+
176
+ colab_check_cap_and_ram() {
177
+ local py_bin="$1"
178
+ local guards_py="$2"
179
+ local mode="$3"
180
+ local elapsed_s="$4"
181
+ local wall_s="$5"
182
+ local grace_s="$6"
183
+ local cap="$7"
184
+ local margin="$8"
185
+ local out_dir="$9"
186
+
187
+ local metrics=""
188
+ local poll_rc=0
189
+ metrics="$(colab_poll_usage_and_ram "${py_bin}" "${out_dir}")" || poll_rc=$?
190
+ local start_bal="" cur_bal="" rate_val="" err_tok="" anon_mb="" mem_total_mb="" cgroup_max_mb=""
191
+ if [ "${poll_rc}" -ne 0 ]; then
192
+ err_tok="COLAB_USAGE_PARSE_FAILED"
193
+ printf 'COLAB_USAGE_PARSE_FAILED: usage poll helper exited with status %s\n' "${poll_rc}" > "${out_dir}/usage_parse_error.txt"
194
+ else
195
+ read -r start_bal cur_bal rate_val err_tok anon_mb mem_total_mb cgroup_max_mb <<< "${metrics}"
196
+ if [ -z "${metrics}" ] || [ -z "${start_bal}" ] || [ -z "${cur_bal}" ] || [ -z "${rate_val}" ] || [ -z "${err_tok}" ] || [ "${start_bal}" = "NONE" ] || [ "${cur_bal}" = "NONE" ]; then
197
+ err_tok="COLAB_USAGE_PARSE_FAILED"
198
+ if [ ! -s "${out_dir}/usage_parse_error.txt" ]; then
199
+ printf 'COLAB_USAGE_PARSE_FAILED: incomplete or missing usage metrics (%s)\n' "${metrics}" > "${out_dir}/usage_parse_error.txt"
200
+ fi
201
+ fi
202
+ fi
203
+
204
+ local guard_args=(
205
+ "--mode" "${mode}"
206
+ "--elapsed-s" "${elapsed_s}"
207
+ "--wall-s" "${wall_s}"
208
+ "--grace-s" "${grace_s}"
209
+ "--cap" "${cap}"
210
+ "--margin" "${margin}"
211
+ "--rate" "${rate_val:-0.26}"
212
+ )
213
+ if [ "${start_bal}" != "NONE" ] && [ -n "${start_bal}" ]; then
214
+ guard_args+=("--start-balance" "${start_bal}")
215
+ fi
216
+ if [ "${cur_bal}" != "NONE" ] && [ -n "${cur_bal}" ]; then
217
+ guard_args+=("--current-balance" "${cur_bal}")
218
+ fi
219
+ if [ "${err_tok}" != "NONE" ] && [ -n "${err_tok}" ]; then
220
+ local err_detail="${err_tok}"
221
+ if [ -f "${out_dir}/usage_parse_error.txt" ]; then
222
+ err_detail="$(cat "${out_dir}/usage_parse_error.txt")"
223
+ fi
224
+ guard_args+=("--usage-parse-error" "${err_detail}")
225
+ fi
226
+
227
+ local guard_rc=0
228
+ "${py_bin}" "${guards_py}" "${guard_args[@]}" </dev/null >> "${out_dir}/guards.log" || guard_rc=$?
229
+ if [ "${guard_rc}" -ne 0 ]; then
230
+ if [ "${err_tok}" != "NONE" ] && [ -n "${err_tok}" ]; then
231
+ echo "REFUSED: COLAB_USAGE_PARSE_FAILED" >&2
232
+ fi
233
+ return 1
234
+ fi
235
+
236
+ local ram_args=(
237
+ "--check-ram"
238
+ "--current-anon-mb" "${CURRENT_ANON_MB:-${anon_mb:-512}}"
239
+ "--projected-delta-mb" "${PROJECTED_DELTA_MB:-2048}"
240
+ "--mem-total-mb" "${MEM_TOTAL_MB:-${mem_total_mb:-12288}}"
241
+ )
242
+ if [ -n "${cgroup_max_mb:-}" ]; then
243
+ ram_args+=("--cgroup-max-mb" "${cgroup_max_mb}")
244
+ fi
245
+ local ram_rc=0
246
+ "${py_bin}" "${guards_py}" "${ram_args[@]}" </dev/null >> "${out_dir}/guards.log" || ram_rc=$?
247
+ if [ "${ram_rc}" -ne 0 ]; then
248
+ return "${ram_rc}"
249
+ fi
250
+ }
251
+
252
+ colab_run_exec_with_live_guards() {
253
+ local py_bin="$1"
254
+ local guards_py="$2"
255
+ local session="$3"
256
+ local runner_py="$4"
257
+ local start_epoch="$5"
258
+ local wall_s="$6"
259
+ local grace_s="$7"
260
+ local cap="$8"
261
+ local margin="$9"
262
+ local out_dir="${10}"
263
+ shift 10
264
+ local env_pairs=("$@")
265
+
266
+ local poll_s="${GUARD_POLL_INTERVAL_S:-2}"
267
+ local stall_s="${STALL_S:-300}"
268
+ local setup_stall_s="${SETUP_STALL_S:-600}"
269
+ local last_size=0
270
+ local last_progress_epoch
271
+ last_progress_epoch="$(date -u +%s)"
272
+ local last_setup_bytes=0
273
+ local last_setup_progress_epoch="${last_progress_epoch}"
274
+
275
+ local wall_int="${wall_s%.*}"
276
+ local grace_int="${grace_s%.*}"
277
+ local exec_timeout="$(( wall_int + grace_int + 60 ))"
278
+
279
+ local exec_args=("-s" "${session}" "-f" "${runner_py}" "--timeout" "${exec_timeout}")
280
+ for kv in "${env_pairs[@]}"; do
281
+ exec_args+=("--env" "${kv}")
282
+ done
283
+
284
+ colab exec "${exec_args[@]}" </dev/null > "${out_dir}/vitals.log" 2>&1 &
285
+ EXEC_PID="$!"
286
+
287
+ while kill -0 "${EXEC_PID}" 2>/dev/null; do
288
+ sleep "${poll_s}"
289
+ if ! kill -0 "${EXEC_PID}" 2>/dev/null; then
290
+ break
291
+ fi
292
+ local now_epoch elapsed_s cur_size idle_s cur_setup_bytes setup_idle_s run_only_bytes
293
+ now_epoch="$(date -u +%s)"
294
+ elapsed_s="$((now_epoch - start_epoch))"
295
+
296
+ local check_rc=0
297
+ colab_check_cap_and_ram "${py_bin}" "${guards_py}" "live" "${elapsed_s}" "${wall_s}" "${grace_s}" "${cap}" "${margin}" "${out_dir}" || check_rc=$?
298
+ if [ "${check_rc}" -ne 0 ]; then
299
+ pkill -P "${EXEC_PID}" 2>/dev/null || true
300
+ kill -9 "${EXEC_PID}" 2>/dev/null || true
301
+ wait "${EXEC_PID}" 2>/dev/null || true
302
+ EXEC_PID=""
303
+ return 91
304
+ fi
305
+
306
+ run_only_bytes="$(grep -Ev '^(===[[:space:]]*VITALS|CGROUP_MEM(_CURRENT|_MAX)?:|MEM:|SETUP_BYTES:|PROC:|HARNESS:|[[:space:]]*[0-9]+[[:space:]]+.*(comfy|main\.py|ai-toolkit|run\.py|llama-server|ollama|lane1_render\.py))' "${out_dir}/vitals.log" 2>/dev/null | wc -c | tr -d '[:space:]' || echo 0)"
307
+ cur_size="${run_only_bytes:-0}"
308
+ if [ "${cur_size:-0}" != "${last_size}" ]; then
309
+ last_size="${cur_size:-0}"
310
+ last_progress_epoch="${now_epoch}"
311
+ fi
312
+ idle_s="$((now_epoch - last_progress_epoch))"
313
+
314
+ cur_setup_bytes="$(grep -E '^SETUP_BYTES:[[:space:]]*[0-9]+[[:space:]]*$' "${out_dir}/vitals.log" 2>/dev/null | tail -n 1 | awk -F: '{gsub(/[[:space:]]/, "", $2); print $2}' || true)"
315
+ if [ -n "${cur_setup_bytes}" ] && [ "${cur_setup_bytes}" != "${last_setup_bytes}" ]; then
316
+ last_setup_bytes="${cur_setup_bytes}"
317
+ last_setup_progress_epoch="${now_epoch}"
318
+ fi
319
+ setup_idle_s="$((now_epoch - last_setup_progress_epoch))"
320
+
321
+ local vitals_rc=0
322
+ "${py_bin}" "${guards_py}" \
323
+ --vitals-file "${out_dir}/vitals.log" \
324
+ --idle-seconds "${idle_s}" \
325
+ --setup-idle-seconds "${setup_idle_s}" \
326
+ --stall-s "${stall_s}" \
327
+ --setup-stall-s "${setup_stall_s}" </dev/null >> "${out_dir}/guards.log" || vitals_rc=$?
328
+ if [ "${vitals_rc}" -ne 0 ]; then
329
+ pkill -P "${EXEC_PID}" 2>/dev/null || true
330
+ kill -9 "${EXEC_PID}" 2>/dev/null || true
331
+ wait "${EXEC_PID}" 2>/dev/null || true
332
+ EXEC_PID=""
333
+ return 93
334
+ fi
335
+ done
336
+
337
+ local exec_rc=0
338
+ wait "${EXEC_PID}" || exec_rc=$?
339
+ EXEC_PID=""
340
+
341
+ colab_scrub_all_logs "${py_bin}" "${guards_py}" "${out_dir}"
342
+
343
+ local marker_line=""
344
+ marker_line="$(grep -E '^SDLAB_REMOTE_RC=-?[0-9]+$' "${out_dir}/vitals.log" 2>/dev/null | tail -n 1 | tr -d '\r' || true)"
345
+ if [ -z "${marker_line}" ]; then
346
+ echo "REFUSED: SDLAB_REMOTE_RC marker missing from vitals.log (colab exec_rc=${exec_rc})" >> "${out_dir}/guards.log"
347
+ if [ "${exec_rc}" -ne 0 ]; then
348
+ return "${exec_rc}"
349
+ fi
350
+ return 94
351
+ fi
352
+ local remote_rc="${marker_line#SDLAB_REMOTE_RC=}"
353
+ if [ "${remote_rc}" -ne 0 ]; then
354
+ echo "REFUSED: remote runner reported SDLAB_REMOTE_RC=${remote_rc}" >> "${out_dir}/guards.log"
355
+ return "${remote_rc}"
356
+ fi
357
+
358
+ "${py_bin}" "${guards_py}" --vitals-file "${out_dir}/vitals.log" --idle-seconds 0 --setup-idle-seconds 0 --stall-s "${stall_s}" --setup-stall-s "${setup_stall_s}" </dev/null >> "${out_dir}/guards.log"
359
+ local end_epoch elapsed_final
360
+ end_epoch="$(date -u +%s)"
361
+ elapsed_final="$((end_epoch - start_epoch))"
362
+ colab_check_cap_and_ram "${py_bin}" "${guards_py}" "live" "${elapsed_final}" "${wall_s}" "${grace_s}" "${cap}" "${margin}" "${out_dir}"
363
+ }
364
+
365
+ colab_stop_and_log_closing() {
366
+ local py_bin="$1"
367
+ local session="$2"
368
+ local out_dir="$3"
369
+ colab stop -s "${session}" </dev/null >> "${out_dir}/colab.log" 2>&1 || true
370
+ colab sessions </dev/null >> "${out_dir}/colab.log" 2>&1 || true
371
+ colab_poll_usage_and_ram "${py_bin}" "${out_dir}" >/dev/null 2>&1 || true
372
+ }
373
+
374
+ colab_open_session_locked() {
375
+ local session="$1"
376
+ local card="$2"
377
+ local out_dir="$3"
378
+ local lock_file="${COLAB_OPEN_LOCK:-/tmp/colab-window-open.lock}"
379
+ mkdir -p "$(dirname "${lock_file}")"
380
+
381
+ local new_args=()
382
+ if [ "${card}" = "CPU-high-mem" ] || [ "${card}" = "CPU" ]; then
383
+ new_args=("new" "--high-mem" "-s" "${session}")
384
+ else
385
+ new_args=("new" "--gpu" "${card}" "--high-mem" "-s" "${session}")
386
+ fi
387
+
388
+ if command -v flock >/dev/null 2>&1; then
389
+ (
390
+ flock -x 9
391
+ printf '1\n' > "${lock_file}.held"
392
+ colab "${new_args[@]}" </dev/null >> "${out_dir}/colab.log" 2>&1
393
+ rm -f "${lock_file}.held"
394
+ ) 9>"${lock_file}"
395
+ else
396
+ local lock_dir="${lock_file}.lockdir"
397
+ while ! mkdir "${lock_dir}" 2>/dev/null; do
398
+ sleep 0.1
399
+ done
400
+ printf '1\n' > "${lock_file}.held"
401
+ local rc=0
402
+ colab "${new_args[@]}" </dev/null >> "${out_dir}/colab.log" 2>&1 || rc=$?
403
+ rm -f "${lock_file}.held"
404
+ rmdir "${lock_dir}" 2>/dev/null || true
405
+ if [ "${rc}" -ne 0 ]; then
406
+ return "${rc}"
407
+ fi
408
+ fi
409
+ printf '1\n' > "${out_dir}/session_opened.txt"
410
+ }
411
+
412
+ colab_scrub_all_logs() {
413
+ local py_bin="$1"
414
+ local guards_py="$2"
415
+ local out_dir="$3"
416
+ if [ ! -d "${out_dir}" ]; then
417
+ return 0
418
+ fi
419
+ local scrub_args=()
420
+ while IFS= read -r -d '' f; do
421
+ scrub_args+=("--scrub-file" "${f}")
422
+ done < <(find "${out_dir}" -type f \( -name "*.log" -o -name "*.json" -o -name "*.txt" -o -name "*.sh" \) -print0 2>/dev/null || true)
423
+ if [ "${#scrub_args[@]}" -gt 0 ]; then
424
+ "${py_bin}" "${guards_py}" "${scrub_args[@]}" </dev/null || true
425
+ fi
426
+ }
427
+
428
+ colab_write_window_and_outcome() {
429
+ local py_bin="$1"
430
+ local out_dir="$2"
431
+ local lane="$3"
432
+ local session="$4"
433
+ local card="$5"
434
+ local cap="$6"
435
+ local wall_s="$7"
436
+ local grace_s="$8"
437
+ local margin="$9"
438
+ local opened_at="${10}"
439
+ local closed_at="${11}"
440
+ local exit_code="${12}"
441
+ local shas_json="${13}"
442
+
443
+ "${py_bin}" - "${out_dir}" "${lane}" "${session}" "${card}" "${cap}" "${wall_s}" "${grace_s}" "${margin}" "${opened_at}" "${closed_at}" "${exit_code}" "${shas_json}" << 'PYEOF'
444
+ import hashlib
445
+ import json
446
+ import sys
447
+ import tarfile
448
+ from pathlib import Path
449
+
450
+ out_dir = Path(sys.argv[1])
451
+ lane = sys.argv[2]
452
+ session = sys.argv[3]
453
+ card = sys.argv[4]
454
+ cap = float(sys.argv[5])
455
+ wall_s = int(float(sys.argv[6]))
456
+ grace_s = int(float(sys.argv[7]))
457
+ margin = float(sys.argv[8])
458
+ opened_at = sys.argv[9]
459
+ closed_at = sys.argv[10]
460
+ exit_code = int(sys.argv[11])
461
+ shas_raw = json.loads(sys.argv[12])
462
+
463
+ out_dir.mkdir(parents=True, exist_ok=True)
464
+
465
+
466
+ def sha256_path(p: Path) -> str:
467
+ h = hashlib.sha256()
468
+ with p.open("rb") as f:
469
+ for chunk in iter(lambda: f.read(1 << 20), b""):
470
+ h.update(chunk)
471
+ return "sha256:" + h.hexdigest()
472
+
473
+
474
+ def read_float_file(p: Path) -> float | None:
475
+ if not p.exists():
476
+ return None
477
+ try:
478
+ return float(p.read_text(encoding="utf-8").strip())
479
+ except Exception:
480
+ return None
481
+
482
+
483
+ def read_text_file(p: Path) -> str | None:
484
+ if not p.exists():
485
+ return None
486
+ val = p.read_text(encoding="utf-8", errors="replace").strip()
487
+ return val if val else None
488
+
489
+
490
+ vm_facts: dict = {}
491
+ vm_win_file = out_dir / "vm_window.json"
492
+ if vm_win_file.exists():
493
+ try:
494
+ vm_facts.update(json.loads(vm_win_file.read_text(encoding="utf-8")))
495
+ except Exception:
496
+ pass
497
+
498
+ for tar_p in sorted(out_dir.glob("*.tar.gz")):
499
+ if tarfile.is_tarfile(tar_p):
500
+ try:
501
+ with tarfile.open(tar_p, "r:gz") as tf:
502
+ for member in tf.getmembers():
503
+ if Path(member.name).name == "vm_window.json":
504
+ ext = tf.extractfile(member)
505
+ if ext is not None:
506
+ vm_facts.update(json.loads(ext.read().decode("utf-8")))
507
+ except Exception:
508
+ pass
509
+
510
+ vitals_p = out_dir / "vitals.log"
511
+ if vitals_p.exists():
512
+ for line in vitals_p.read_text(encoding="utf-8", errors="replace").splitlines():
513
+ if line.startswith("WINDOW_JSON "):
514
+ try:
515
+ vm_facts.update(json.loads(line[len("WINDOW_JSON ") :].strip()))
516
+ except Exception:
517
+ pass
518
+
519
+ downloaded_files: dict[str, str] = {}
520
+ for artifact in sorted(list(out_dir.glob("*.tar.gz")) + list(out_dir.glob("*.safetensors"))):
521
+ if artifact.is_file():
522
+ downloaded_files[artifact.name] = sha256_path(artifact)
523
+
524
+ usage_parse_error = read_text_file(out_dir / "usage_parse_error.txt")
525
+ rate_measured_pre_open = read_float_file(out_dir / "rate_measured_pre_open.txt")
526
+ rate_measured_post_open = read_float_file(out_dir / "rate_measured_post_open.txt")
527
+ rate_measured = (
528
+ rate_measured_post_open
529
+ if rate_measured_post_open is not None
530
+ else read_float_file(out_dir / "rate_measured.txt")
531
+ )
532
+ rate_for_projection = read_float_file(out_dir / "rate_for_projection.txt")
533
+ rate_used_for_cap_and_projection = rate_for_projection
534
+ rate_source = read_text_file(out_dir / "rate_source.txt") or (
535
+ "post_open_measured"
536
+ if (rate_measured_post_open is not None and rate_measured_post_open > 0.0)
537
+ else (
538
+ "configured_pending_post_open_rate"
539
+ if (rate_measured_post_open is not None and rate_measured_post_open == 0.0)
540
+ else (
541
+ "max_of_pre_open_and_configured"
542
+ if rate_measured_pre_open is not None
543
+ else "configured"
544
+ )
545
+ )
546
+ )
547
+
548
+ window_payload = {
549
+ **vm_facts,
550
+ "window_id": session,
551
+ "lane": lane,
552
+ "session": session,
553
+ "card": card,
554
+ "cap": cap,
555
+ "wall_s": wall_s,
556
+ "grace_s": grace_s,
557
+ "margin": margin,
558
+ "start_balance": read_float_file(out_dir / "start_balance.txt"),
559
+ "end_balance": read_float_file(out_dir / "end_balance.txt"),
560
+ "rate_measured": rate_measured,
561
+ "rate_measured_pre_open": rate_measured_pre_open,
562
+ "rate_measured_post_open": rate_measured_post_open,
563
+ "rate_used_for_cap_and_projection": rate_used_for_cap_and_projection,
564
+ "rate_for_projection": rate_for_projection,
565
+ "rate_source": rate_source,
566
+ "usage_parse_error": usage_parse_error,
567
+ "input_shas": shas_raw.get("verified_shas", shas_raw),
568
+ "downloaded_files": downloaded_files,
569
+ "opened_at": opened_at,
570
+ "closed_at": closed_at,
571
+ "exit_code": exit_code,
572
+ }
573
+ (out_dir / "window.json").write_text(json.dumps(window_payload, indent=2) + "\n", encoding="utf-8", newline="\n")
574
+
575
+ outcome_payload = {
576
+ "window_id": session,
577
+ "lane": lane,
578
+ "session": session,
579
+ "card": card,
580
+ "status": "ok" if exit_code == 0 else "failed",
581
+ "exit_code": exit_code,
582
+ "opened_at": opened_at,
583
+ "closed_at": closed_at,
584
+ }
585
+ (out_dir / "outcome.draft.json").write_text(json.dumps(outcome_payload, indent=2) + "\n", encoding="utf-8", newline="\n")
586
+ PYEOF
587
+ }