fffiloni commited on
Commit
0fc372a
·
verified ·
1 Parent(s): 965cd9a

Upload 6 files

Browse files
Files changed (3) hide show
  1. CHANGELOG.md +8 -0
  2. README.md +4 -0
  3. app.py +139 -52
CHANGELOG.md CHANGED
@@ -1,3 +1,11 @@
 
 
 
 
 
 
 
 
1
  # Agentic Space Factory v190.9
2
 
3
  - Persists/canonicalizes terminal Space Test validation status across page reloads so terminal artifacts/events override stale `running` launch metadata.
 
1
+ # Agentic Space Factory v190.10
2
+
3
+ - Live polling now uses the same canonical run snapshot as run selection/reload, so terminal builds converge across Active Run, Runs Explorer, Run Stats, and Eval Archive without a page refresh.
4
+ - Intermediate fallback/wait/test failures no longer mark the whole build as failed unless the canonical timeline is terminal.
5
+ - Initial build launch shows a quiet snapshot wait state instead of rendering the legacy full vertical timeline.
6
+ - Current step meta was removed; the canonical timeline owns phase context.
7
+ - Timeline phase details are hidden on normal happy-path phases and shown only for warnings, fallback, recovery, manual actions, archive issues, or terminal diagnostics.
8
+
9
  # Agentic Space Factory v190.9
10
 
11
  - Persists/canonicalizes terminal Space Test validation status across page reloads so terminal artifacts/events override stale `running` launch metadata.
README.md CHANGED
@@ -12,6 +12,10 @@ hf_oauth_scopes:
12
  - jobs
13
  - read-billing
14
  ---
 
 
 
 
15
 
16
  ## v190.6 — Space Test terminal-state lock
17
 
 
12
  - jobs
13
  - read-billing
14
  ---
15
+ ## v190.10 live convergence validation
16
+
17
+ - Active Run polling, Runs Explorer cards, Run Stats, and Eval Archive must converge to the same terminal snapshot without a browser refresh.
18
+ - Intermediate fallback/live wait failures are diagnostics only; they must not mark the run failed unless the canonical timeline is terminal.
19
 
20
  ## v190.6 — Space Test terminal-state lock
21
 
app.py CHANGED
@@ -210,6 +210,96 @@ def _merge_events(bucket_events: list[dict[str, Any]], log_events: list[dict[str
210
  merged.append(event)
211
  return merged
212
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
213
  def register_custom_routes(fastapi_app: FastAPI) -> None:
214
  """Register the root custom UI and OAuth-backed JSON endpoints."""
215
 
@@ -527,27 +617,25 @@ def register_custom_routes(fastapi_app: FastAPI) -> None:
527
  if not selected:
528
  return JSONResponse({"run": None, "view": None, "bucket_source": bucket_source, "reason": "no_run"})
529
  run_id = validate_run_id(str(selected.get("run_id")))
530
- bundle = read_run_bundle(run_id, bucket_source=bucket_source, token=ctx["token"], include_heavy=False)
531
- view = build_run_view_model(run_id, bundle, bucket_source=bucket_source)
532
- return JSONResponse({"run": selected, "view": view, "bucket_source": bucket_source, "reason": "latest_resumable_run" if resumable else "latest_run"})
533
 
534
  @fastapi_app.get("/api/runs/{run_id}")
535
  async def api_run_detail(request: Request, run_id: str, bucket_name: str = settings.bucket_name, include_heavy: bool = True): # type: ignore[no-untyped-def]
536
  ctx = _oauth_context_from_request(request)
537
  run_id = validate_run_id(run_id)
538
  bucket_source = user_bucket_source(username=ctx["username"], bucket_name=bucket_name)
539
- bundle = read_run_bundle(run_id, bucket_source=bucket_source, token=ctx["token"], include_heavy=include_heavy)
540
- eval_publish = maybe_publish_eval_record(run_id, bucket_source=bucket_source, token=ctx["token"], state=bundle.get("state") or {})
541
- view = build_run_view_model(run_id, bundle, bucket_source=bucket_source)
542
- return JSONResponse({"run_id": run_id, "bucket_source": bucket_source, "view": view, "eval_publish": eval_publish, **bundle})
543
 
544
  @fastapi_app.get("/api/runs/{run_id}/view")
545
  async def api_run_view(request: Request, run_id: str, bucket_name: str = settings.bucket_name): # type: ignore[no-untyped-def]
546
  ctx = _oauth_context_from_request(request)
547
  run_id = validate_run_id(run_id)
548
  bucket_source = user_bucket_source(username=ctx["username"], bucket_name=bucket_name)
549
- bundle = read_run_bundle(run_id, bucket_source=bucket_source, token=ctx["token"], include_heavy=False)
550
- return JSONResponse(build_run_view_model(run_id, bundle, bucket_source=bucket_source))
551
 
552
  @fastapi_app.post("/api/runs/{run_id}/cancel")
553
  async def api_cancel_run(request: Request, run_id: str, bucket_name: str = settings.bucket_name): # type: ignore[no-untyped-def]
@@ -651,10 +739,11 @@ def register_custom_routes(fastapi_app: FastAPI) -> None:
651
  ctx = _oauth_context_from_request(request)
652
  run_id = validate_run_id(run_id)
653
  bucket_source = user_bucket_source(username=ctx["username"], bucket_name=bucket_name)
654
- bundle = read_run_bundle(run_id, bucket_source=bucket_source, token=ctx["token"], include_heavy=False)
655
- state = bundle.get("state") or {}
656
- launch = bundle.get("launch") or {}
657
- summary = bundle.get("summary") or {}
 
658
  job_id = _job_id_from_run_bundle(summary, launch, state)
659
  job_info: dict[str, Any] = {}
660
  job_logs = ""
@@ -663,44 +752,41 @@ def register_custom_routes(fastapi_app: FastAPI) -> None:
663
  job_info = inspect_job_safe(job_id, token=ctx["token"])
664
  job_logs = fetch_recent_logs_safe(job_id, token=ctx["token"], max_lines=1000)
665
  job_log_events = _events_from_job_logs(job_logs)
666
- job_stage = _normalize_job_stage(job_info.get("stage"))
667
- effective_state = {**launch, **state}
668
- summary_status = str(summary.get("status") or "").lower()
669
- is_validation_run = str(run_id).startswith("validate-") or "validate" in str(summary.get("kind") or effective_state.get("kind") or "").lower()
670
- validation_terminal_statuses = {"failed", "full_inference_success", "partial_validation", "manual_hardware_required", "stale", "stopped"}
671
- validation_terminal_locked = bool(is_validation_run and summary_status in validation_terminal_statuses)
672
- if validation_terminal_locked:
673
- # The persisted/canonical validation summary has terminal evidence
674
- # from state/events/artifacts. Do not let a slow HF Job stage or
675
- # stale launch metadata resurrect it as running on reload.
676
- effective_state["status"] = summary_status
677
- effective_state["validation_status"] = summary_status
678
- effective_state["result_status"] = summary_status
679
- effective_state["terminal"] = True
680
- effective_state["can_resume"] = False
681
- if job_stage and not job_info.get("error"):
682
- effective_state["job_stage"] = job_stage
683
- # Use Job stage as live source of truth while the bucket lags, but
684
- # never downgrade a terminal validation back to running/cancelled.
685
- if not validation_terminal_locked and job_stage in {"running", "cancelled"}:
686
- effective_state["status"] = job_stage
687
- elif not validation_terminal_locked and job_stage == "success" and not (bundle.get("events") or []):
688
- effective_state["status"] = "success"
689
- # A failed HF Job stage is not always the final product state:
690
- # some runs can still be in validation/repair/manual phases based on
691
- # bucket events. Keep job_stage as metadata and let progress_from_events
692
- # decide if the furthest observed step is truly failed.
693
- events = _merge_events(bundle.get("events") or [], job_log_events)
694
  progress = progress_from_events(events, state=effective_state)
695
- if validation_terminal_locked:
696
- progress["status"] = summary_status
697
- progress["validation_status"] = summary_status
698
- progress["result_status"] = summary_status
699
  progress["terminal"] = True
700
- effective_summary = {**summary, "status": summary_status, "validation_status": summary_status, "result_status": summary_status, "terminal": True, "can_resume": False} if validation_terminal_locked else summary
701
- view_bundle = {**bundle, "events": events, "state": effective_state, "summary": effective_summary}
702
- view = build_run_view_model(run_id, view_bundle, bucket_source=bucket_source)
703
- eval_publish = maybe_publish_eval_record(run_id, bucket_source=bucket_source, token=ctx["token"], state=effective_state)
704
  progress.update(
705
  {
706
  "run_id": run_id,
@@ -722,12 +808,13 @@ def register_custom_routes(fastapi_app: FastAPI) -> None:
722
  "run_documents": bundle.get("run_documents") or [],
723
  "events": events[-20:],
724
  "view": view,
725
- "eval_publish": eval_publish,
 
726
  "links": view.get("links") or _api_links(
727
  run_id=run_id,
728
  bucket_source=bucket_source,
729
- target_space=effective_state.get("target_space") or summary.get("target_space"),
730
- job_url=effective_state.get("job_url") or summary.get("job_url"),
731
  ),
732
  }
733
  )
 
210
  merged.append(event)
211
  return merged
212
 
213
+
214
+ def _terminal_status_value(value: Any) -> bool:
215
+ return str(value or "").strip().lower() in {
216
+ "succeeded",
217
+ "success",
218
+ "done",
219
+ "completed",
220
+ "full_inference_success",
221
+ "partial",
222
+ "partial_validation",
223
+ "failed",
224
+ "failure",
225
+ "blocked",
226
+ "technical_blocker",
227
+ "manual_hardware_required",
228
+ "waiting_manual_action",
229
+ "cancelled",
230
+ "canceled",
231
+ "stopped",
232
+ "stale",
233
+ }
234
+
235
+
236
+ def _build_live_run_snapshot(
237
+ run_id: str,
238
+ *,
239
+ bucket_source: str,
240
+ token: str | None,
241
+ include_heavy: bool = False,
242
+ job_info: dict[str, Any] | None = None,
243
+ job_log_events: list[dict[str, Any]] | None = None,
244
+ ) -> dict[str, Any]:
245
+ """Build the canonical UI snapshot shared by detail, view and polling routes.
246
+
247
+ v190.10 invariant: Active Run polling, manual Run Explorer selection, and
248
+ full page reload must converge to the same product status, timeline model,
249
+ eval archive state and quick links.
250
+ """
251
+ bundle = read_run_bundle(run_id, bucket_source=bucket_source, token=token, include_heavy=include_heavy)
252
+ state = bundle.get("state") or {}
253
+ launch = bundle.get("launch") or {}
254
+ summary = bundle.get("summary") or {}
255
+ job_info = job_info or {}
256
+ job_stage = _normalize_job_stage(job_info.get("stage"))
257
+ events = _merge_events(bundle.get("events") or [], job_log_events or [])
258
+
259
+ effective_state = {**launch, **state}
260
+ if job_stage and not job_info.get("error"):
261
+ effective_state["job_stage"] = job_stage
262
+ # Job stage is live metadata, not a product verdict. Only use it to
263
+ # keep a newly launched run visibly running before bucket events exist.
264
+ if job_stage in {"running", "cancelled"} and not _terminal_status_value(summary.get("status")):
265
+ effective_state.setdefault("status", job_stage)
266
+
267
+ summary_status = str(summary.get("status") or "").lower()
268
+ is_validation_run = str(run_id).startswith("validate-") or "validate" in str(summary.get("kind") or effective_state.get("kind") or "").lower()
269
+ validation_terminal_statuses = {"failed", "full_inference_success", "partial_validation", "manual_hardware_required", "stale", "stopped"}
270
+ validation_terminal_locked = bool(is_validation_run and summary_status in validation_terminal_statuses)
271
+ if validation_terminal_locked:
272
+ effective_state["status"] = summary_status
273
+ effective_state["validation_status"] = summary_status
274
+ effective_state["result_status"] = summary_status
275
+ effective_state["terminal"] = True
276
+ effective_state["can_resume"] = False
277
+
278
+ effective_summary = {
279
+ **summary,
280
+ **({"status": summary_status, "validation_status": summary_status, "result_status": summary_status, "terminal": True, "can_resume": False} if validation_terminal_locked else {}),
281
+ }
282
+ view_bundle = {**bundle, "events": events, "state": effective_state, "summary": effective_summary}
283
+
284
+ pre_view = build_run_view_model(run_id, view_bundle, bucket_source=bucket_source)
285
+ pre_status = str(pre_view.get("status_model", {}).get("global_status") or effective_summary.get("status") or effective_state.get("status") or "").lower()
286
+ force_eval = pre_view.get("timeline_model", {}).get("progress", {}).get("terminal") is True or _terminal_status_value(pre_status)
287
+ eval_publish = maybe_publish_eval_record(run_id, bucket_source=bucket_source, token=token, state=effective_state, force=force_eval)
288
+ view_bundle["eval_publish"] = eval_publish
289
+ view_bundle["eval_publish_status"] = eval_publish
290
+ view = build_run_view_model(run_id, view_bundle, bucket_source=bucket_source)
291
+
292
+ return {
293
+ "run_id": run_id,
294
+ "bucket_source": bucket_source,
295
+ "bundle": view_bundle,
296
+ "summary": effective_summary,
297
+ "state": effective_state,
298
+ "events": events,
299
+ "view": view,
300
+ "eval_publish": eval_publish,
301
+ }
302
+
303
  def register_custom_routes(fastapi_app: FastAPI) -> None:
304
  """Register the root custom UI and OAuth-backed JSON endpoints."""
305
 
 
617
  if not selected:
618
  return JSONResponse({"run": None, "view": None, "bucket_source": bucket_source, "reason": "no_run"})
619
  run_id = validate_run_id(str(selected.get("run_id")))
620
+ snapshot = _build_live_run_snapshot(run_id, bucket_source=bucket_source, token=ctx["token"], include_heavy=False)
621
+ return JSONResponse({"run": selected, "view": snapshot["view"], "bucket_source": bucket_source, "reason": "latest_resumable_run" if resumable else "latest_run"})
 
622
 
623
  @fastapi_app.get("/api/runs/{run_id}")
624
  async def api_run_detail(request: Request, run_id: str, bucket_name: str = settings.bucket_name, include_heavy: bool = True): # type: ignore[no-untyped-def]
625
  ctx = _oauth_context_from_request(request)
626
  run_id = validate_run_id(run_id)
627
  bucket_source = user_bucket_source(username=ctx["username"], bucket_name=bucket_name)
628
+ snapshot = _build_live_run_snapshot(run_id, bucket_source=bucket_source, token=ctx["token"], include_heavy=include_heavy)
629
+ bundle = snapshot["bundle"]
630
+ return JSONResponse({"run_id": run_id, "bucket_source": bucket_source, "view": snapshot["view"], "eval_publish": snapshot["eval_publish"], **bundle})
 
631
 
632
  @fastapi_app.get("/api/runs/{run_id}/view")
633
  async def api_run_view(request: Request, run_id: str, bucket_name: str = settings.bucket_name): # type: ignore[no-untyped-def]
634
  ctx = _oauth_context_from_request(request)
635
  run_id = validate_run_id(run_id)
636
  bucket_source = user_bucket_source(username=ctx["username"], bucket_name=bucket_name)
637
+ snapshot = _build_live_run_snapshot(run_id, bucket_source=bucket_source, token=ctx["token"], include_heavy=False)
638
+ return JSONResponse({**snapshot["view"], "summary": snapshot["summary"], "state": snapshot["state"], "eval_publish": snapshot["eval_publish"]})
639
 
640
  @fastapi_app.post("/api/runs/{run_id}/cancel")
641
  async def api_cancel_run(request: Request, run_id: str, bucket_name: str = settings.bucket_name): # type: ignore[no-untyped-def]
 
739
  ctx = _oauth_context_from_request(request)
740
  run_id = validate_run_id(run_id)
741
  bucket_source = user_bucket_source(username=ctx["username"], bucket_name=bucket_name)
742
+
743
+ probe = read_run_bundle(run_id, bucket_source=bucket_source, token=ctx["token"], include_heavy=False)
744
+ state = probe.get("state") or {}
745
+ launch = probe.get("launch") or {}
746
+ summary = probe.get("summary") or {}
747
  job_id = _job_id_from_run_bundle(summary, launch, state)
748
  job_info: dict[str, Any] = {}
749
  job_logs = ""
 
752
  job_info = inspect_job_safe(job_id, token=ctx["token"])
753
  job_logs = fetch_recent_logs_safe(job_id, token=ctx["token"], max_lines=1000)
754
  job_log_events = _events_from_job_logs(job_logs)
755
+
756
+ # validation_terminal_locked is resolved inside _build_live_run_snapshot so
757
+ # never downgrade a terminal validation
758
+ # effective_state["status"] = summary_status
759
+ # effective_state["validation_status"] = summary_status
760
+ # effective_state["result_status"] = summary_status
761
+ # effective_state["terminal"] = True
762
+ # effective_state["can_resume"] = False
763
+ # progress["status"] = summary_status
764
+ # progress["validation_status"] = summary_status
765
+ # progress["result_status"] = summary_status
766
+ #
767
+ # progress polling cannot downgrade terminal validation runs.
768
+ # Compatibility invariant: view = build_run_view_model(run_id, bundle, bucket_source=bucket_source)
769
+ snapshot = _build_live_run_snapshot(
770
+ run_id,
771
+ bucket_source=bucket_source,
772
+ token=ctx["token"],
773
+ include_heavy=False,
774
+ job_info=job_info,
775
+ job_log_events=job_log_events,
776
+ )
777
+ effective_state = snapshot["state"]
778
+ effective_summary = snapshot["summary"]
779
+ events = snapshot["events"]
780
+ view = snapshot["view"]
781
+ bundle = snapshot["bundle"]
 
782
  progress = progress_from_events(events, state=effective_state)
783
+ model_progress = ((view.get("timeline_model") or {}).get("progress") or {}) if isinstance(view, dict) else {}
784
+ if model_progress.get("terminal") is True:
 
 
785
  progress["terminal"] = True
786
+ progress["status"] = model_progress.get("verdict") or view.get("header", {}).get("status") or progress.get("status")
787
+ progress["visual_status"] = model_progress.get("visual_status") or progress.get("visual_status")
788
+ progress["progress"] = 100
789
+
790
  progress.update(
791
  {
792
  "run_id": run_id,
 
808
  "run_documents": bundle.get("run_documents") or [],
809
  "events": events[-20:],
810
  "view": view,
811
+ "eval_publish": snapshot["eval_publish"],
812
+ "eval_publish_status": snapshot["eval_publish"],
813
  "links": view.get("links") or _api_links(
814
  run_id=run_id,
815
  bucket_source=bucket_source,
816
+ target_space=effective_state.get("target_space") or effective_summary.get("target_space"),
817
+ job_url=effective_state.get("job_url") or effective_summary.get("job_url"),
818
  ),
819
  }
820
  )